Bo2SS

Bo2SS

git commit的-m原理實現

功能要求#

[類似 git commit -m "msg" 的功能]

  • 使用 - m 選項時直接打印消息,未使用 - m 選項時自動打開 vim 供輸入消息
  • 詳細說明
    • ① 當含有選項和選項參數 - m "first commit" 時
      • 圖片
      • 直接打印相關消息
    • ② 當沒有 - m 選項時
      • 圖片
      • 自動打開 vim,用戶在裡面輸入 "second commit",並保存退出後,再打印相關消息
      • 圖片
      • 如果在 vim 中沒有輸入有效消息 [註釋不屬於有效消息]
      • 圖片
      • 該操作會失敗,並友好提示用戶
  • [PS]
    • 可能需要 3 個進程:父進程、vim 進程、刪除 vim 產生的文件的進程
    • 需要判斷 vim 裡輸入的消息是否有效
    • 子進程跑,父進程等,讀數據,打印出來 [cat],rm

最終效果#

  • 輸入./git_commit -m "first commit"
    • 圖片
    • 直接輸出消息
  • 輸入./git_commit
    • 先彈出 vim,可輸入任意消息
    • 圖片
    • ① 若輸入包含註釋的多行消息
      • image
      • 最終打印結果如下
      • image
      • 多行整合為一行 [同 git 設計],並屏蔽註釋
    • ② 若直接退出 vim
      • 圖片
      • 提示無法打開消息文件
    • ③ 若輸入全是註釋
      • 圖片
      • 提示消息為空

[PS]

  • vim 打開文件時,是否可以指定其 TYPE=GITCOMMIT?目前 TYPE 為空
    • 👉 vim 打開的文件名改為 COMMIT_EDITMSG 即可,這樣消息代碼就有些配色了

實現過程#

思路流程圖#

  • 圖片

【進程分析】

  • 因為後面還需要創建rm的進程,所以不能將父進程直接替換為vim進程
    • 否則退出 vim 後,程序就結束了
  • 父進程需要 fork 一個子進程,再 exec 族將該進程替換為vim進程
  • 父進程再 fork 一個子進程,並替換為rm進程
    • 直接將父進程直接替換為rm進程,也可行
    • 但 fork 一個新進程,可以讓父進程監控刪除狀態,並有可能進行更多的操作

獲取命令行參數#

捕捉 - m 選項,使用該選項必須帶參數

#include "head.h"
int main(int argc, char **argv) {
    if (argc == 1) {
        // 1.無選項:需要vim輸入msg
        printf("no msg, need vim.\n");
    }
    int opt;
    while ((opt = getopt(argc, argv, "m:")) != -1) {
        switch (opt) {
            case 'm': {
                // 2.有-m及選項參數:打印參數
                printf("msg: %s\n", optarg);
            } break;
            default: {
                // 3.選項不合法:友好提示
                fprintf(stderr, "Usage: %s [-m msg]\n", argv[0]);
                exit(1);
            }
        }
    }
    return 0;
}
  • 只有三種情況:① 無選項;② 有 - m 選項與選項參數;③ 選項不合法
  • 頭文件 head.h 見末尾
  • 使用測試腳本 test.sh [見末尾] 測試,效果如下:
    • 圖片
    • 展示了三種情況下的輸出

輸入消息#

利用 vim 供輸入消息

  • 父進程使用 fork 創建子進程,再利用 execlp 將其替換為 vim 進程
  • 父進程使用 wait 監控子進程狀態,並避免產生殭屍進程
// 輸入消息
void input_msg() {
    pid_t pid;
    int status;
    // 父進程複製一個子進程
    if ((pid = fork()) < 0) {
        perror("fork()");
        exit(1);
    }
    if (pid == 0) {
        // 將子進程替換為vim進程
        execlp("vim", "vim", "COMMIT_MSG", NULL);
    } else {
        wait(&status);  // 監控子進程狀態
        // 父進程提示vim出錯
        if (WEXITSTATUS(status)) printf("vim error!\n");
        else printf("input msg completed!\n");
    }
    return ;
}
  • 命令無選項時,用 vim 進程打開 COMMIT_MSG 文件
    • 圖片
    • 用戶可輸入消息
  • 退出 vim 後,vim 進程終止,父進程可感知
    • 圖片
    • 父進程提示 "input msg completed!"

讀取消息#

父進程嘗試讀取消息文件

  • 【友好提示】消息文件可能無法打開 [如文件不存在],也可能不包含有效消息 [全是註釋]
  • 【分行解析】判斷行註釋的存在;如果輸入了多行有效消息,打印結果時整合到一行,每行消息用空格隔開 [同 git 的設計]
#define MAX_LENGTH 512
// 讀取消息
void read_msg() {
    int fd, flag = 0;                    // flag:是否讀取到了有效消息
    char buff[MAX_LENGTH] = {0};         // 最多讀取MAX_LENGTH字節消息
    ssize_t nread;
    // 如果無法打開消息文件,友好提示
    if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
        printf("aborting commit due to msg file can't open.\n");
        exit(1);
    }
    // 讀取消息文件
    if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
        char *line;
        // 分行判斷消息是否有效
        line = strtok(buff, "\n");
        while (line != NULL) {
            // 如果不是無效消息 [不是註釋]
            if (line[0] != '#') {
                flag || printf("msg:");  // 第一次打印有效消息時打印消息前綴:"msg:"
                flag = 1;
                printf(" %s", line);
            }
            line = strtok(NULL, "\n");   // 不斷調用直到遇到buff的'\0'
        }
        flag && printf("\n");
    }
    // 如果沒有讀到有效消息,友好提示
    if (!flag) {
        printf("aborting commit due to empty commit msg.\n");
    }
    close(fd);
    return ;
}
  • 特判 flag 輸出消息前綴:"msg:"
  • 最多讀取消息文件的前 512 個字節
  • ⭐字符串分割函數 strtok——cplusplus
    • char* strtok(char* str, const char* delimiters)
    • 圖片
    • 需要不斷調用,從第二次起傳入的 str 替換為 NULL,每次調用返回一個被分割的字符串,返回 NULL 表示到達 str 末尾
  • 效果如下:
    • 命令無選項時,在 vim 中輸入下列消息
    • 圖片
    • 讀取成功後,整合到一行打印
    • 圖片
    • 註釋會被忽略

刪除消息文件#

為了讓父進程有可能做更多事,這裡 fork 了一個子進程,並替換為 rm 進程

  • 父進程可監控 rm 進程發揮狀態,雖然 rm 進程也會報失敗信息
// 刪除消息文件
void remove_file() {
    pid_t pid;
    int status;
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        execlp("rm", "rm", "COMMIT_MSG", NULL);
    } else {
        wait(&status);
        // 父進程提示rm失敗 [rm進程也會有提示]
        if (WEXITSTATUS(status)) printf("remove msg file error!\n");
    }
    return ;
}
  • 使用完消息文件後,不再留下 COMMIT_MSG 文件

完整代碼#

git_commit.c#

#include "head.h"
#define MAX_LENGTH 512
// 輸入消息
void input_msg() {
    pid_t pid;
    int status;
    // 父進程複製一個子進程
    if ((pid = fork()) < 0) {
        perror("fork()");
        exit(1);
    }
    if (pid == 0) {
        // 將子進程替換為vim進程
        execlp("vim", "vim", "COMMIT_MSG", NULL);
    } else {
        wait(&status);  // 監控子進程狀態
        // 父進程提示vim出錯
        if (WEXITSTATUS(status)) printf("vim error!\n");
    }
    return ;
}
// 讀取消息
void read_msg() {
    int fd, flag = 0;                    // flag:是否讀取到了有效消息
    char buff[MAX_LENGTH] = {0};         // 最多讀取MAX_LENGTH字節消息
    ssize_t nread;
    // 如果無法打開消息文件,友好提示
    if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
        printf("aborting commit due to msg file can't open.\n");
        exit(1);
    }
    // 讀取消息文件
    if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
        char *line;
        // 分行判斷消息是否有效
        line = strtok(buff, "\n");
        while (line != NULL) {
            // 如果不是無效消息 [不是註釋]
            if (line[0] != '#') {
                flag || printf("msg:");  // 第一次打印有效消息時打印消息前綴:"msg:"
                flag = 1;
                printf(" %s", line);
            }
            line = strtok(NULL, "\n");   // 不斷調用直到遇到buff的'\0'
        }
        flag && printf("\n");
    }
    // 如果沒有讀到有效消息,友好提示
    if (!flag) {
        printf("aborting commit due to empty commit msg.\n");
    }
    close(fd);
    return ;
}
// 刪除消息文件
void remove_file() {
    pid_t pid;
    int status;
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        execlp("rm", "rm", "COMMIT_MSG", NULL);
    } else {
        wait(&status);
        // 父進程提示rm失敗 [rm進程也會有提示]
        if (WEXITSTATUS(status)) printf("remove msg file error!\n");
    }
    return ;
}
int main(int argc, char **argv) {
    if (argc == 1) {
        // 1.無選項:需要vim輸入msg
        input_msg();
        read_msg();
        remove_file();  // 如果可以到這步,消息文件已產生
    }
    int opt;
    while ((opt = getopt(argc, argv, "m:")) != -1) {
        switch (opt) {
            case 'm': {
                // 2.有-m及選項參數:打印參數
                printf("msg: %s\n", optarg);
            } break;
            default: {
                // 3.選項不合法:友好提示
                fprintf(stderr, "Usage: %s [-m msg]\n", argv[0]);
                exit(1);
            }
        }
    }
    return 0;
}

head.h#

#ifndef _HEAD_H
#define _HEAD_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#endif

test.sh#

#!/bin/bash
cmd="./git_commit"
msg="first commit"
echo "1)${cmd}:" | lolcat
${cmd}
echo "2)${cmd} -m \"${msg}\":" | lolcat
${cmd} -m "${msg}"
echo "3)${cmd} -m:" | lolcat
${cmd} -m
echo "3)${cmd} -b:" | lolcat
${cmd} -b
  • ./git_commit 的 git_commit 是使用 gcc ... -o git_commit 生成的可執行文件名

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。