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 生成的可执行文件名

参考#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.