05 Norm Design How to Standardize Commit Message Styles That Are Odd and Hard to Read

05 Norm Design How to Standardize commit Message Styles that are Odd and Hard to Read #

Hello, I am Kong Lingfei. Today, we will continue to learn about the Commit Specifications in non-coding-related standards.

When we are developing code, we often need to submit our code, and when submitting code, we need to fill in the Commit Message, otherwise, it will not be allowed to submit.

In actual development, I found that the format of the Commit Message submitted by each developer can be quite varied. Some use Chinese, some use English, and some even directly write “11111”. Such Commit Messages, over time, may become difficult for the committer to understand the modifications being expressed, let alone others.

Therefore, for Go project development, a good Commit Message is crucial. It should:

  • Allow oneself or other developers to clearly understand the changes made by each commit, facilitating quick browsing of the change history. For example, one should be able to skip over code changes of document types or formatting types.
  • Enable filtering and searching based on these Commit Messages, for example, searching only for new features of a certain version: git log --oneline --grep "^feat|^fix|^perf".
  • Facilitate the generation of a Change Log based on standardized Commit Messages.
  • Trigger build or release processes based on certain types of Commit Messages. For example, we only trigger the CI process when the type is feat or fix.
  • Determine semantic version numbers. For example, the fix type can map to a PATCH version, and the feat type can map to a MINOR version. Commits with BREAKING CHANGE can be mapped to a MAJOR version. In this course, I use this approach to automatically generate version numbers.

In summary, a good Commit Message specification can improve the readability of Commit Messages and achieve automation. So how can we write a readable Commit Message?

Next, let’s look at how to standardize Commit Messages. In addition to Commit Messages, I will also introduce three key points related to Commits and how to ensure the standardization of Commit Messages through automated processes.

What are the specifications for Commit Message? #

There is no doubt that we can create our own Commit Message specifications according to our needs, but I would recommend using more mature specifications from the open-source community. On one hand, it can avoid reinventing the wheel and improve work efficiency. On the other hand, these specifications have been validated by a large number of developers and are scientific and reasonable.

Currently, there are various Commit Message specifications in the community, such as jQuery, Angular, etc. I have compiled these specifications and their formats into the following image for your reference:

Among these specifications, the Angular specification meets the needs of developers in terms of functionality, and it is clear and readable in terms of format. It is currently the most widely used.

The Angular specification is actually a semantic commit specification (Semantic Commit Messages), which includes the following elements:

  • The Commit Message is semantic: Each Commit Message is classified as a meaningful type to indicate the type of the commit.
  • The Commit Message is standardized: The Commit Messages follow a pre-defined specification, such as fixed format and belong to a specific type. These specifications can be recognized by both developers and tools.

To help you understand the Angular specification, let’s take a look at a commit history that follows the Angular specification in the image below:

Now let’s look at a complete Commit Message that complies with the Angular specification, as shown in the image below:

From the above two images, we can see that commits that comply with the Angular Commit Message specification have a specific format and semantic.

So how do we write a Commit Message that complies with the Angular specification?

In the Angular specification, a Commit Message consists of three parts: Header, Body, and Footer, with the following format:

<type>[optional scope]: <description>
// blank line
[optional body]
// blank line
[optional footer(s)]

The Header is required, while the Body and Footer are optional. In the above specifications, the <scope> must be enclosed in parentheses (), and <type>[<scope>] must be followed by a colon, and there must be two blank lines.

In actual development, to make the Commit Message more readable on GitHub or other Git tools, we often limit the length of each line. Depending on the needs, it can be limited to 50/72/100 characters. Here, I will limit the length to within 72 characters (some developers may limit it to 100, you can choose according to your needs).

The following is an example of a Commit Message that complies with the Angular specification:

fix($compile): couple of unit tests for IE9
# Please enter the Commit Message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# ...

Older IEs serialize html uppercased, but IE9 does not...
Would be better to expect case insensitive, unfortunately jasmine does
not allow to user regexps for throw expectations.

Closes #392
Breaks foo.bar api, foo.baz should be used instead

Next, let’s take a closer look at the three parts of the Commit Message in the Angular specification.

The Header section consists of only one line, including three fields: type (required), scope (optional), and subject (required).

Let’s start with the type, which is used to indicate the type of the commit. For easy memory, I have summarized these types, which mainly fall into two categories: Development and Production. They have the following meanings:

  • Development: This category includes changes related to project management that do not affect end users and the production environment, such as changes to the CI process or build method. When encountering such changes, it usually means that it can be released without testing.
  • Production: This category includes changes that affect end users and the production environment. Therefore, we must be cautious when making such changes and conduct sufficient testing before committing.

Here are the common types in the Angular specification and their categories. When submitting a Commit Message, be sure to distinguish the category. For example, when performing Code Review, if encountering code of type Production, it is important to review it carefully because this type of code will affect the functionality of the live application and the experience of live users.

With so many types, how do we determine the type of a commit? Here is a flowchart to help us determine:

If we modify application code, such as a Go function code, then the modification belongs to the code category. In the code category, there are 4 types with clear intentions of changes: feat, fix, perf, and style. If our code modification does not belong to these 4 types, then we categorize it into the refactor type, which means optimizing code.

If we modify non-application code, such as changing documentation, then it belongs to the non-code category. In the non-code category, there are 3 types with clear intentions of changes: test, ci, docs. If our non-code modification does not belong to these 3 types, then we categorize it into the chore type.

The Angular Commit Message specification provides most of the types. In actual development, we can use some of these types or add our own types. Regardless of the chosen approach, we must ensure consistent types within a project. Next, let’s talk about the second field scope in the Header.

“Scope” is used to indicate the scope of the commit and must be a noun. Obviously, different projects will have different scopes. In the early stages of a project, we can set up some scopes with larger granularity, such as grouping them by component name or functionality. Later on, if there are changes or new features in the project, we can append new scopes.

In this course, we mainly set the scope based on component names and functionality. For example, scopes like “apiserver”, “authzserver”, and “user” are supported.

Here it is worth emphasizing that the scope should not be set to be too specific. If it is too specific, on one hand, it will cause the project to have too many scopes, making it difficult to maintain. On the other hand, developers will also find it difficult to determine which specific scope the commit belongs to, resulting in misplacement of scopes, which can make the scope lose its meaning of classification.

Of course, when specifying the scope, we also need to follow the pre-planned scope, so we need to document the scope and put it in documents similar to “devel”. You can refer to the scope document of the IAM project for this: IAM commit message scope.

Lastly, let’s talk about the subject.

The subject is a brief description of the commit and must start with a verb in the present tense. For example, we can use “change” but not “changed” or “changes”. Also, the first letter of the verb must be lowercase. Through this verb, we can clearly know the operation performed by the commit. Additionally, we should note that the subject should not have an English period at the end.

Body #

The Header gives a highly summarized view of the commit, making it easy for us to view the Commit Message. But how do we know specifically what changes have been made? The answer is through the Body section, which provides a more detailed description of the commit and is optional.

The Body section can be multiple lines and the format is relatively free. However, like the subject in the header, it must also start with a verb in the present tense. Additionally, it must include the motivation for the modification, as well as the points of change compared to the previous version.

I have provided an example below for you to refer to:

The body is mandatory for all commits except for those of scope "docs". When the body is required it must be at least 20 characters long.

The Footer section is optional and can be selected as needed. It is mainly used to explain the consequences of the commit. In practice, the Footer is usually used to explain incompatible changes and lists of closed issues, with the following format:

BREAKING CHANGE: <breaking change summary>
// empty line
<breaking change description + migration instructions>
// empty line
// empty line
Fixes #<issue number>

Next, I will give you a detailed explanation of these two scenarios:

  • Incompatible changes: If the current code is not compatible with the previous version, it needs to start with BREAKING CHANGE: in the Footer, followed by a summary of the incompatible changes. The other parts of the Footer need to explain the description of the changes, the reasons for the changes, and the migration methods. For example:
BREAKING CHANGE: isolate scope bindings definition has changed and
    the inject option for the directive controller injection was removed.

    To migrate the code follow the example below:

    Before:

    scope: {
      myAttr: 'attribute',
    }

    After:

    scope: {
      myAttr: '@',
    }
    The removed `inject` wasn't generally useful for directives, so there should be no code using it.
  • List of closed issues: Closed bugs need to be added as a new line in the Footer, starting with “Closes”, for example: Closes #123. If multiple issues are closed, they can be listed like this: Closes #123, #432, #886. For example:
Change pause version value to a constant for image

Closes #1137

Revert Commit #

In addition to the Header, Body, and Footer sections, there is also a special case for Commit Messages: if the current commit reverts a previous commit, it should start with revert:, followed by the Header of the commit being reverted. In the Body, it must be written as This reverts commit <hash>, where the hash is the SHA identifier of the commit being reverted. For example:

revert: feat(iam-apiserver): add 'Host' option

This reverts commit 079360c7cfc830ea8a6e13f4c8b8114febc9b48a.

To better comply with the Angular specification, it is recommended that you develop the habit of not using git commit -m when submitting code, that is, not using the -m option, but directly using git commit or git commit -a to enter the interactive interface to edit the Commit Message. This can help to format the Commit Message better.

However, in addition to the Commit Message specifications, there are three key points that we need to pay attention to when submitting code: commit frequency, merging commits, and modifying Commit Messages.

Let’s start by looking at commit frequency.

Commit Frequency #

In actual project development, casual commits may not have a big impact on individual projects, but for projects with multiple developers, casual commits not only make commit messages difficult to understand but also make other developers think you are not professional. Therefore, we need to establish commit frequency guidelines.

So when is the best time to commit?

I think there are two main scenarios. One scenario is to commit as soon as I make changes to the project, as long as it passes the testing phase. For example, after fixing a bug, completing a small feature, or finishing a complete feature and passing the testing phase, you can commit immediately. Another scenario is to set a specific time for regular commits. I recommend committing before leaving work and ensuring that any uncommitted local code does not exceed 1 day. This way, if local code is lost, the amount of lost code can be minimized as much as possible.

By following these two ways of committing code, you may feel that there are many code commits that seem arbitrary. Or, maybe we want to wait until we have completed a complete feature and submit it all in one commit. In this case, before merging the code or submitting a pull request, we can use git rebase -i to merge all previous commits.

So how do we merge commits? Let me explain in detail.

Merging Commits #

Merging commits means combining multiple commits into one commit. Here, I suggest keeping only 2-3 commit records when merging new commits into the main branch. So how do we do that?

In Git, we mainly use the git rebase command to merge commits. Git rebase is a command that we will often use in future development, so we must master its usage.

Introduction to the git rebase command

The biggest advantage of git rebase is that it can rewrite history.

We usually use git rebase -i <commit ID> to use the git rebase command, where the -i option stands for interactive. This command will enter an interactive interface, which is actually the Vim editor. In this interface, we can perform operations on the commits inside, as shown in the following figure:

This interactive interface will first list all the commits before the given <commit ID> (excluding it, the lower the newer), and each commit has an operation command in front of it, with pick as the default command. We can choose different commits and modify the command in front of the commit to perform different operations on that commit.

The following are the supported operations in git rebase:

Among the 7 commands above, squash and fixup can be used to merge commits. For example, to merge using squash, we just need to change the verb in front of the commit to squash (or s). Here is an example:

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
s 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

After rebasing, the commits in the second and third lines will be merged into the commit in the first line. At this point, the commit message will include the commit messages of all three commits:

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

# This is the 2ndCommit Message:
Fix PostChecker::Post#urls

# This is the 3rdCommit Message:
Hey kids, stop all the highlighting

If we change the squash command in the third line to fixup:

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
f 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

After rebasing, two commits will still be created. The commits in the second and third lines will be merged into the commit in the first line. However, in the new commit message, the commit message of the third line commit will be commented out:

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

# This is the 2ndCommit Message:
Fix PostChecker::Post#urls

This is the 3rd Commit Message: #

Hey kids, stop all the highlighting #

In addition, there are several points to note when using git rebase:

  • If you delete a commit line, that commit will be lost.
  • If you delete all commit lines, the rebase will be terminated.
  • Commits can be sorted, and git will merge them from top to bottom.

To deepen your understanding, let me give you a complete demonstration of how to merge commits.

Example of Merging Commits #

Let’s assume we need to develop a new module called “user” for registering, logging in, and logging out users on the platform. After the module is developed and tested, we need to merge it into the main branch. The specific steps are as follows:

First, we create a new branch. We need to create and switch to the feature/user branch based on the master branch:

$ git checkout -b feature/user
Switched to a new branch 'feature/user'

Here is our commit history:

$ git log --oneline
7157e9e docs(docs): append test line 'update3' to README.md
5a26aa2 docs(docs): append test line 'update2' to README.md
55892fa docs(docs): append test line 'update1' to README.md
89651d4 docs(doc): add README.md

Next, we develop and test the functionality on the feature/user branch and follow the standard commit format. After the development and testing are completed, the commit history of the Git repository will be as follows:

$ git log --oneline
4ee51d6 docs(user): update user/README.md
176ba5d docs(user): update user/README.md
5e829f8 docs(user): add README.md for user
f40929f feat(user): add delete user function
fc70a21 feat(user): add create user function
7157e9e docs(docs): append test line 'update3' to README.md
5a26aa2 docs(docs): append test line 'update2' to README.md
55892fa docs(docs): append test line 'update1' to README.md
89651d4 docs(doc): add README.md

As we can see, we have made 5 commits. Next, we need to merge the changes from the feature/user branch to the master branch, but 5 commits are too many. We want to merge these commits and then submit them to the master branch.

Next, we merge all the commits. In the previous step, we know that fc70a21 is the first commit ID of the feature/user branch, and its parent commit ID is 7157e9e. We need to merge all the branches before 7157e9e. We can do this by executing the following command:

$ git rebase -i 7157e9e

After running the command, we will enter an interactive interface. In this interface, we can perform the squash operation on the 4 commits that need to be merged. Here is an example:

![](../images/70ea34fa405042509813361acff34d49.jpg)

After making the modifications, we save by executing :wq and a new interactive page will appear. In this page, we can edit the commit message. The edited content will look like this:

![](../images/247a3454fd8246b1b05a4e8c0b79c5a0.jpg)

Lines starting with # are git comments which can be ignored. After the rebase, these lines will disappear. After making the modifications, we save by executing :wq, and the merge commit operation is completed.

In addition, there are two points to note:

  • The <commit ID> in git rebase -i <commit ID> must be the parent commit ID of the oldest commit that needs to be merged.
  • When we want to merge 5 commits from the feature/user branch into one commit, we need to ensure that the most recent commit is in the pick state during git rebase. Only then can we merge the other 4 commits into it.

Finally, we use the following command to check if the commits were successfully merged. As we can see, we successfully merged the 5 commits into one commit: d6b17e0.

$ git log --oneline
d6b17e0 feat(user): add user module with all function implements
7157e9e docs(docs): append test line 'update3' to README.md
5a26aa2 docs(docs): append test line 'update2' to README.md
55892fa docs(docs): append test line 'update1' to README.md
89651d4 docs(doc): add README.md

Finally, we can merge the changes from the “feature/user” branch into the main branch to complete the development of the new feature.

$ git checkout master
$ git merge feature/user
$ git log --oneline
d6b17e0 feat(user): add user module with all function implements
7157e9e docs(docs): append test line 'update3' to README.md
5a26aa2 docs(docs): append test line 'update2' to README.md
55892fa docs(docs): append test line 'update1' to README.md
89651d4 docs(doc): add README.md

Here’s a tip for you. If you have too many commits to merge, you can try this approach: undo the previous commits and create a new one.

$ git reset HEAD~3
$ git add .
$ git commit -am "feat(user): add user resource"

Note that except when there are too many commits, I generally do not recommend this method. It can be a bit rough and requires reorganizing all the previously submitted commit messages.

Modifying Commit Messages #

Even with Commit Message conventions in place, there may still be cases where a commit message does not conform to the conventions or needs to be corrected. In such cases, we can modify the Commit Message of a previous commit. There are two methods to achieve this, depending on different situations:

  1. git commit --amend: modifying the message of the most recent commit.
  2. git rebase -i: modifying the message of a specific commit.

Let’s discuss these two methods.

git commit --amend: modifying the message of the most recent commit

Sometimes, after committing a change, we realize that the commit message does not adhere to the conventions or needs correction. In such cases, we can use the git commit --amend command to modify the Commit Message of the most recent commit. Here are the steps to follow:

  1. View the log history of the current branch.
$ git log --oneline
418bd4 docs(docs): append test line 'update$i' to README.md
89651d4 docs(doc): add README.md

We can see that the most recent Commit Message is docs(docs): append test line 'update$i' to README.md, where update$i should be update1 instead.

  1. Update the Commit Message of the most recent commit.

In the Git repository, execute the command git commit --amend. This will open an interactive interface where you can modify the Commit Message of the most recent commit. After making the necessary changes, save and exit the editor. The command line will display the updated Commit Message, for example:

[master 55892fa] docs(docs): append test line 'update1' to README.md
 Date: Fri Sep 18 13:40:42 2020 +0800
 1 file changed, 1 insertion(+)
  1. Check if the Commit Message of the most recent commit has been updated.
$ git log --oneline
55892fa docs(docs): append test line 'update1' to README.md
89651d4 docs(doc): add README.md

We can see that the Commit Message of the most recent commit has been successfully modified.

git rebase -i: modifying the message of a specific commit

If we want to modify the Commit Message of a commit that is not the most recent one, we can use the git rebase -i <parent commit ID> command. This command is frequently used in actual development and it is important to understand how to use it. The process can be divided into 4 steps:

  1. View the log history of the current branch.
$ git log --oneline
1d6289f docs(docs): append test line 'update3' to README.md
a38f808 docs(docs): append test line 'update$i' to README.md
55892fa docs(docs): append test line 'update1' to README.md
89651d4 docs(doc): add README.md

We can see that the Commit Message of the third most recent commit is docs(docs): append test line 'update$i' to README.md, where update$i should be update2 instead.

  1. Modify the Commit Message of the third most recent commit.

In the Git repository, execute the command git rebase -i 55892fa. This will open an interactive interface. In the interface, modify the Commit Message of the third most recent commit. We can use reword or r to preserve the changes made in the third commit but modify its message. After making the necessary changes, save and exit the editor. The command line will display the updated Commit Message, for example:

[detached HEAD 5a26aa2] docs(docs): append test line 'update2' to README.md
 Date: Fri Sep 18 13:45:54 2020 +0800
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/master.

Note: Make sure to provide the parent commit ID of the commit whose message you want to modify: git rebase -i <parent commit ID>.

  1. Check if the Commit Message of the third most recent commit has been updated.
$ git log --oneline
7157e9e docs(docs): append test line 'update3' to README.md
5a26aa2 docs(docs): append test line 'update2' to README.md
55892fa docs(docs): append test line 'update1' to README.md
89651d4 docs(doc): add README.md

We can see that the Commit Message of the third most recent commit has been successfully modified.

Here are two important points to note:

  • Commit Message is an attribute of the commit data structure, and if the Commit Message is changed, the commit ID will also change. git commit --amend only modifies the commit ID of the most recent commit, while git rebase -i modifies the commit IDs of all commits after the specified commit.
  • If there are uncommitted changes in the current branch, it is necessary to execute git stash first to temporarily stash the working state. After modifying the commit message, execute git stash pop to restore the previous working state.

Automation of Commit Message Standard #

In fact, we have realized one thing here: if we rely on documentation to enforce Commit Message standards, it will heavily depend on the developers’ coding literacy, and cannot guarantee that the committed commit messages are in compliance with the standards.

So, is there a way to ensure that our Commit Messages are always in accordance with the standards? Yes, we can use some tools to automatically generate and check if the Commit Messages are in compliance with the standards.

Furthermore, since Commit Messages are standardized, can we use these standards to implement some cooler features? The answer is yes, I have drawn a diagram below that illustrates some automation features that can be implemented around Commit Messages.

These automation features can be divided into the following two categories:

  • Commit Message generation and checking features: generating Commit Messages that comply with the Angular standard, pre-commit checks for Commit Messages, and historical Commit Message checks.
  • Tools for automatically generating CHANGELOG and SemVer based on Commit Messages.

We can use the following 5 tools to automatically accomplish the above features:

  • commitizen-go: allows you to enter interactive mode and generate Commit Messages based on prompts, and then commit them.
  • commit-msg: a githook that specifies the checking rules. commit-msg is a script that can be customized according to needs. The commit-msg in this course calls go-gitlint for checking.
  • go-gitlint: checks the Commit Messages in the commit history for compliance with the Angular standard. This tool can be added to the CI process to ensure that all Commit Messages are in compliance with the standards.
  • gsemver: a tool for automatically generating semantic versions.
  • git-chglog: generates CHANGELOG based on Commit Messages.

You can just have an overview of these tools. In the upcoming lessons, I will guide you through the actual usage to familiarize yourself with them.

Summary #

Today I introduced you to the Commit Message specification, mainly focusing on the widely used Angular convention.

In the Angular convention, a Commit Message consists of three parts: Header, Body, and Footer. The Header provides a highly summarized description of the commit, the Body provides a more detailed description of the commit, and the Footer is primarily used to explain the consequences of the commit. The format is as follows:

<type>[optional scope]: <description>
// empty line
[optional body]
// empty line
[optional footer(s)]

In addition, we also need to control the frequency of commit submission, such as submitting a commit after completing a feature, fixing a bug, or before leaving work.

Lastly, we also need to master some common commit operations, such as merging commit messages using git rebase -i, and modifying commit messages using git commit --amend or git rebase -i.

Exercise #

  1. Create a new git repository and make 4 commits that follow the Angular convention for commit messages. Then, merge the first 2 commits.
  2. Use the git-chglog tool to generate the CHANGELOG and the gsemver tool to generate a semantic version number.

I look forward to seeing your thoughts and answers in the comments. I also welcome a discussion about specification design. See you in the next lesson!