Bo2SS

Bo2SS

The implementation principle of git commit -m

Functional Requirements#

[Function similar to git commit -m "msg"]

  • Directly print the message when using the -m option; automatically open vim for input when the -m option is not used.
  • Detailed Explanation
    • ① When there are options and option parameters -m "first commit"
      • Image
      • Directly print the relevant message.
    • ② When there is no -m option
      • Image
      • Automatically open vim, the user inputs "second commit" inside, and after saving and exiting, print the relevant message.
      • Image
      • If no valid message is entered in vim [comments do not count as valid messages]
      • Image
      • This operation will fail and provide a friendly prompt to the user.
  • [PS]
    • May require 3 processes: parent process, vim process, and process to delete files generated by vim.
    • Need to determine if the message entered in vim is valid.
    • The child process runs, the parent process waits, reads data, and prints it out [cat], rm.

Final Effect#

  • Input ./git_commit -m "first commit"
    • Image
    • Directly output the message.
  • Input ./git_commit
    • First pop up vim, allowing any message to be entered.
    • Image
    • ① If multiple lines of messages containing comments are entered
      • image
      • The final printed result is as follows.
      • image
      • Multiple lines are combined into one line [same as git design], and comments are suppressed.
    • ② If directly exit vim with
      • Image
      • Prompt that the message file cannot be opened.
    • ③ If all input is comments
      • Image
      • Prompt that the message is empty.

[PS]

  • When vim opens the file, can it specify its TYPE=GITCOMMIT? Currently, TYPE is empty.
    • 👉 Change the filename opened by vim to COMMIT_EDITMSG, so that the message code has some coloring.

Implementation Process#

Thought Process Flowchart#

  • Image

【Process Analysis】

  • Since it is necessary to create the rm process later, the parent process cannot be directly replaced by the vim process.
    • Otherwise, after exiting vim, the program will end.
  • The parent process needs to fork a child process, then exec family to replace that process with the vim process.
  • The parent process then forks another child process and replaces it with the rm process.
    • Directly replacing the parent process with the rm process is also feasible.
    • However, forking a new process allows the parent process to monitor the deletion status and possibly perform more operations.

Getting Command Line Parameters#

Capture the -m option; using this option must include a parameter.

#include "head.h"
int main(int argc, char **argv) {
    if (argc == 1) {
        // 1. No options: need vim to input msg.
        printf("no msg, need vim.\n");
    }
    int opt;
    while ((opt = getopt(argc, argv, "m:")) != -1) {
        switch (opt) {
            case 'm': {
                // 2. There is -m and option parameter: print parameter.
                printf("msg: %s\n", optarg);
            } break;
            default: {
                // 3. Invalid option: friendly prompt.
                fprintf(stderr, "Usage: %s [-m msg]\n", argv[0]);
                exit(1);
            }
        }
    }
    return 0;
}
  • There are only three cases: ① No options; ② There is -m option and option parameter; ③ Invalid option.
  • The header file head.h is at the end.
  • Use the test script test.sh [see end] for testing, the effect is as follows:
    • Image
    • Demonstrated the output in three cases.

Input Message#

Use vim to input the message.

  • The parent process uses fork to create a child process, then uses execlp to replace it with the vim process.
  • The parent process uses wait to monitor the child process status and avoid creating zombie processes.
// Input message
void input_msg() {
    pid_t pid;
    int status;
    // The parent process copies a child process.
    if ((pid = fork()) < 0) {
        perror("fork()");
        exit(1);
    }
    if (pid == 0) {
        // Replace the child process with the vim process.
        execlp("vim", "vim", "COMMIT_MSG", NULL);
    } else {
        wait(&status);  // Monitor child process status.
        // The parent process prompts vim error.
        if (WEXITSTATUS(status)) printf("vim error!\n");
        else printf("input msg completed!\n");
    }
    return ;
}
  • When there are no options, the vim process opens the COMMIT_MSG file.
    • Image
    • The user can input the message.
  • After exiting vim, the vim process terminates, and the parent process can sense it.
    • Image
    • The parent process prompts "input msg completed!"

Reading Messages#

The parent process attempts to read the message file.

  • 【Friendly Prompt】 The message file may not be able to open [e.g., file does not exist], or it may not contain valid messages [all comments].
  • 【Line Parsing】 Determine the existence of line comments; if multiple lines of valid messages are input, print the results combined into one line, with each line of messages separated by a space [same as git design].
#define MAX_LENGTH 512
// Read message
void read_msg() {
    int fd, flag = 0;           // flag: whether valid message has been read.
    char buff[MAX_LENGTH] = {0}; // Read at most MAX_LENGTH bytes of message.
    ssize_t nread;
    // If unable to open the message file, friendly prompt.
    if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
        printf("aborting commit due to msg file can't open.\n");
        exit(1);
    }
    // Read the message file.
    if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
        char *line;
        // Line-by-line check for valid messages.
        line = strtok(buff, "\n");
        while (line != NULL) {
            // If it is not an invalid message [not a comment].
            if (line[0] != '#') {
                flag || printf("msg:");  // Print message prefix "msg:" when printing valid message for the first time.
                flag = 1;
                printf(" %s", line);
            }
            line = strtok(NULL, "\n");   // Keep calling until reaching buff's '\0'.
        }
        flag && printf("\n");
    }
    // If no valid message is read, friendly prompt.
    if (!flag) {
        printf("aborting commit due to empty commit msg.\n");
    }
    close(fd);
    return ;
}
  • Special case flag outputs message prefix: "msg:"
  • Read at most the first 512 bytes of the message file.
  • ⭐ String splitting function strtok——cplusplus
    • char* strtok(char* str, const char* delimiters)
    • Image
    • It needs to be called repeatedly, from the second time on, the str passed in should be replaced with NULL, each call returns a split string, returning NULL indicates reaching the end of str.
  • The effect is as follows:
    • When there are no options, input the following message in vim.
    • Image
    • After successful reading, combine into one line for printing.
    • Image
    • Comments will be ignored.

Deleting the Message File#

To allow the parent process to possibly do more things, a child process is forked here and replaced with the rm process.

  • The parent process can monitor the status of the rm process, although the rm process will also report failure information.
// Delete message file
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);
        // The parent process prompts rm failure [the rm process will also have a prompt].
        if (WEXITSTATUS(status)) printf("remove msg file error!\n");
    }
    return ;
}
  • After using the message file, do not leave the COMMIT_MSG file.

Complete Code#

git_commit.c#

#include "head.h"
#define MAX_LENGTH 512
// Input message
void input_msg() {
    pid_t pid;
    int status;
    // The parent process copies a child process.
    if ((pid = fork()) < 0) {
        perror("fork()");
        exit(1);
    }
    if (pid == 0) {
        // Replace the child process with the vim process.
        execlp("vim", "vim", "COMMIT_MSG", NULL);
    } else {
        wait(&status);  // Monitor child process status.
        // The parent process prompts vim error.
        if (WEXITSTATUS(status)) printf("vim error!\n");
    }
    return ;
}
// Read message
void read_msg() {
    int fd, flag = 0;           // flag: whether valid message has been read.
    char buff[MAX_LENGTH] = {0}; // Read at most MAX_LENGTH bytes of message.
    ssize_t nread;
    // If unable to open the message file, friendly prompt.
    if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
        printf("aborting commit due to msg file can't open.\n");
        exit(1);
    }
    // Read the message file.
    if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
        char *line;
        // Line-by-line check for valid messages.
        line = strtok(buff, "\n");
        while (line != NULL) {
            // If it is not an invalid message [not a comment].
            if (line[0] != '#') {
                flag || printf("msg:");  // Print message prefix "msg:" when printing valid message for the first time.
                flag = 1;
                printf(" %s", line);
            }
            line = strtok(NULL, "\n");   // Keep calling until reaching buff's '\0'.
        }
        flag && printf("\n");
    }
    // If no valid message is read, friendly prompt.
    if (!flag) {
        printf("aborting commit due to empty commit msg.\n");
    }
    close(fd);
    return ;
}
// Delete message file
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);
        // The parent process prompts rm failure [the rm process will also have a prompt].
        if (WEXITSTATUS(status)) printf("remove msg file error!\n");
    }
    return ;
}
int main(int argc, char **argv) {
    if (argc == 1) {
        // 1. No options: need vim to input msg.
        input_msg();
        read_msg();
        remove_file();  // If it can reach this step, the message file has been generated.
    }
    int opt;
    while ((opt = getopt(argc, argv, "m:")) != -1) {
        switch (opt) {
            case 'm': {
                // 2. There is -m and option parameter: print parameter.
                printf("msg: %s\n", optarg);
            } break;
            default: {
                // 3. Invalid option: friendly prompt.
                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 is the executable file name generated by gcc ... -o git_commit.

References#

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