Git commit messages for the bold and the daring

Photo by Ankit Sinha on Unsplash

Good commit messages are like healthy eating habits: most people know they’re good for them, but few actually manage to follow through on a daily basis. Good commit messages take practice and discipline to get used to, but once you’re in the habit of writing them, you will learn to appreciate the many benefits that come along with them.

In this article, we’re going to talk about why proper commit messages are important, what kind of problems they solve, and how you can automate your client-side commit workflow to help contributors follow your guidelines.

If you are new to Git here’s a great tutorial to get you started.

But first, the benefits

Good commit guidelines provide more benefits than might first come to mind. These benefits are not just for yourself as a maintainer, but for your entire team as well as your future team members. A good commit message guideline will:

  • give context about the “what” and most importantly the “why” of your commits
  • make working with your commit history easier and more systematic(i.e. readability, search-ability, debugging, investigating issues, etc.)
  • help you spot commits that lack separation of concerns
  • lower the entry level for new contributors by making the commit history consistent and easy-to-follow
  • make you a more disciplined engineer and a more empathetic project maintainer
  • help you generate change logs

The developer community has plenty of great best-practices in place when it comes to what makes a good commit message and what doesn’t. But ultimately, it is up to you to decide if they work for your project or if you’d rather define your own set of rules. Whatever you decide, just remember that choosing a set of guidelines and announcing them to the world won’t guarantee that contributors will actually abide by them. This is not to say that people are ill-intentioned, but sometimes people forget or are unaware that such guidelines even exist. If you want to make sure that what ends up in the repository is exactly how you want, then you might want to consider automation.

There are a couple of ways you can automate your commit workflow to better support your guidelines. Let’s have a look at a few them and analyze what problems they solve, how restrictive they are and what advantages and disadvantages they come with.

BTW, did you know that there’s currently an ongoing effort by a few folks in the dev community to create a standardised specification for commit messages?

Git “commit.template”

Git gives you a LOT of options out of the box to configure your Git environment. The one that’s particularly interesting for the scope of this article is commit.template(*).

commit.template allows you to “specify the pathname of a file to use as the template for new commit messages”, which basically means that you can save your commit message template as a file in a project and then configure Git to use it every time you git commit your changes.

Let’s have a look at how we can set this up and what the benefits of using such a strategy are.

If you’re not sure whether you’re already using a Git template in your project, try running git config --get commit.template in your terminal. If no value is returned then you’re good to go.

– commit.template setting check –

Start off by creating the template file in your project. Keep in mind that Git will pre-populate the editor you’ve configured for your commit messages (see Git’s core.editor setting, which defaults to Vim) with the contents of this template. That content is what committers will see when prompted for the commit message at commit time. One example of such a template could look something like this:

# Hey there o/! 
# 
# We just wanted to let you know that we care a great deal about    
# making our git history clean, maintainable and easy to access for 
# all our contributors. Commit messages are very important to us,  
# which is why we have a strict commit message policy in place.     
# Please use the following guidelines to format all your commit     
# messages:
# 
#     <type>(<scope>): <subject>
#     <BLANK LINE>
#     <body>
#     <BLANK LINE>
#     <footer>
# 
# Please note that:
#  - The HEADER is a single line of max. 50 characters that         
#    contains a succinct description of the change. It contains a   
#    type, an optional scope, and a subject
#       + <type> describes the kind of change that this commit is   
#                providing. Allowed types are:
#             * feat (feature)
#             * fix (bug fix)
#             * docs (documentation)
#             * style (formatting, missing semicolons, …)
#             * refactor
#             * test (when adding missing tests)
#             * chore (maintain)
#       + <scope> can be anything specifying the place of the commit    
#                 change
#       + <subject> is a very short description of the change, in   
#                   the following format:
#             * imperative, present tense: “change” not             
#               “changed”/“changes”
#             * no capitalised first letter
#             * no dot (.) at the end
#  - The BODY should include the motivation for the change and      
#    contrast this with previous behavior and must be phrased in   
#    imperative present tense 
#  - The FOOTER should contain any information about Breaking       
#    Changes and is also the place to reference GitHub issues that   
#    this commit closes
# 
# Thank you <3

The path and name of your template are really up to you, as long as you link the commit.template value to the correct file. Some folks seem to prefer .gitmessage for the name of the file, but really anything ranging from .git-message-template to gitmessage.txt will work.

Once you have the template in place, you’ll need to configure Git to use it

git config --local commit.template "path_to_template_file/filename"

and voilà! You’re ready to  git commit with style:

– committing with a Git commit.template –

commit.template shines the most in that it brings your commit formatting rules right in the context of editing the message. This saves contributors the hassle of changing context to figure out what the commit guidelines were.

On the downside, commit.template heavily relies on customizing one’s local Git environment, which contributors may or may not do. Good documentation can definitely help with that, but it will ultimately still leave you at the odds of contributors following that documentation. Furthermore, you need to keep in mind that the template applies to commits done via git commit ONLY. This means that any changes that are committed using git commit -m or via any other Git user interface or tool will not prompt the user with the commit message template.

The other important thing to remember is that commit.template is not a means of enforcing a certain commit etiquette on contributors but rather acts as a friendly, informative reminder of what that etiquette is for the given project. The commit.template setting alone will not abort a commit if the commit message ignores the guidelines and is therefore invalid.

If enforcing commit conventions is the kind of behavior you’re looking for, then git hooks might be what you’re looking for.

If you want to take it one step further and get the ULTIMATE commit experience with commit.template, consider spicing up your Git core.editor editor too. Harry Roberts wrote a great post about how you could do that for VIM.

Git Hooks

Git hooks are custom scripts that Git executes when certain important actions occur (think commitpushmerge, a.s.o.). Git supports two types of hooks: client-side and server-side hooks. For the scope of this article we’ll focus only on the client-side ones.

The reason why client-side Git hooks are so powerful is that they can catch invalid commits and provide feedback about them before changes are pushed to the remote repository.

The disadvantage, however, is that they cannot be version controlled. This is because they are stored in the .git/hooks folder, and, per definition, the .git folder is not subject to versioning. This not only means that your git hooks can reside strictly on your local machine, but that sharing them across a team is a bit of a more involved business.

One way around this is to create your Git hooks in a folder that can be versioned and then use custom scripts to make sure those hooks are installed in the .git directory. Alternatively, you could set the Git core.hooksPath to point to your custom hooks folder and have the hooks served directly from that folder.

The Angular project is a very good example of a project that uses custom scripts to validate commit messages. I highly recommend checking it out and learning from their experience.

Another, probably more elegant, way which we’ll go into more detail is husky.husky is an npm package that allows you to declaratively configure Git hooks either via a configuration file or using package.json configuration and takes care of installing them for you in the .git/hooks folder.

– .git/hooks before and after husky install –

The beauty of husky is that it comes with zero setup and maintenance overhead for both maintainers and contributors. Maintainers have their life made easier because all hook configuration is centralized in one place and shared across the team with version control. No need for extra scripts to have the hooks locally installed or special instructions to get everyone set up and no more relying on whether team members have their local Git environments configured in a specific way. As for contributors, the only thing they have to do is install husky and everything else will work out of the box.


// npm 
npm install --save-dev husky
// yarn
yarn add husky --dev

 

That said, let’s have a look at how we can set up a Git commit-msg hook with husky to enforce good commit messages.

Start by configuring husky and the hooks you want it to tap into. In this article, we’re using a .huskyrc configuration file, but you could just as well use package.json for your configuration.

{
  "hooks": {
    "commit-msg": "node ./my_commit_validation_script.js"
  }
}

 

With that in place, the next step is to figure out what script you want to run to validate the commit messages. I personally prefer using commitlint for its simple and declarative commit rules configuration, but you can also write your own custom script based on the needs of your project. Whichever you choose, the end workflow will look something like this:

– committing with husky and commitlint –

There are not many disadvantages I can think of for this approach. One thing that might be inconvenient when you use husky is the extra third-party dependency(ies) you are adding to your project, including possibly the dependency on npm itself if you are working on non-Javascript projects and are relying on different package managers. Another downside is that a Git commit message hook can only validate the message after the fact which can introduce a considerable overhead in the feedback loop. If you want to make sure that contributors don’t have to wait until the hook passes or fails their commit, but rather give them feedback as they write the commit message, then Commitizen might be a good tool of choice.

Commitizen

Commitizen is an npm package that gives you instant feedback on your commit message formatting and prompts you for the required fields at commit time.

Just as with huskyCommitizen is configuration-driven and is incredibly easy to set up. Another great thing about it is its extensible and pluggable architecture. Rather than enforcing a certain commit message guideline, Commitizen acknowledges that each project has different requirements and, therefore, makes it possible for you to bring your own set of rules and plug them in into its core through what is called adapters.

To get the complete Commitizen experience all you need to do is make your repository Commitizen friendly, set up a commit script in package.json

{
  "scripts": {
    "commit": "git-cz"
  }
}

and run that script every time you want to commit your changes — npm run commit or yarn commit.

– committing with Commitizen and cz-conventional-changelog adapter –

The most important thing you need to remember about Commitizen is that all the prompting, message formatting, and validation are defined by the adapters and not Commitizen itself. Therefore, you need to pay extra attention when choosing the right adaptor for the job. For example: if you’re committing with cz-conventional-changelog you’ll notice that — due to some defaults the adapter is using — commit messages such as “feat:” will be considered valid.

On the other hand, if you’re committing with @commitlint/prompt, you’ll notice that not only is this adapter much more restrictive, but you’ll get step-by-step guidance and validation of your input. At the end of the day, it all boils down to what your needs are as a maintainer and what kind of user experience you want to provide your contributors with.

– committing with Commitizen and @commitlint/prompt adapter –

The main downside of this approach is that one can easily bypass Commitizenby using any other means of committing changes other than the npm custom script, be it git commit or other Git UI tools. The other thing is that — just like with using husky — Commitizen introduces extra third-party dependencies, including a dependency on the npm package manager for non-Javascript projects.

Despite all that, Commitizen can definitely be the ‘cherry on top’ of the commit user experience cake. By focusing on things like keeping all validation and validation feedback within the commit context or auto-”magically” formatting the commit message for you, it provides a great user experience that none of the other alternatives we discussed in this article does. On top of that, Commitizen is a fantastic fit for Git hooks. If you care about both compliant commit messages AND contributor experience, using the two together is definitely worth looking into.

Photo by Fredrick Kearney Jr on Unsplash

One last thing

Remember that at the end of the day, the person who knows best what your project needs, is you and your team. There is no one-size-fits-all solution that comes without trade-offs. The alternatives discussed in this article merely serve as examples of more established practices but are definitely not an exhaustive list of ways to automate and enforce your commit message guidelines.

One other important thing you should keep in mind is that all practices discussed here are client-side oriented and, therefore, won’t give you a 100% good commit guarantee. I personally see these as a way to enhance the contributor experience and to ensure that there’s some message validation in place on the client side before code gets pushed further to the remote repository. But ultimately, if you want to have absolute certainty that no random commit messages end up in your codebase you’ll want to look into server-side solutions. But that’s a story for another time.

And on a more personal note…I realize that commit message guidelines are not for everyone. I for one was inspired by the Angular community’s best practices and feel I’ve become a much more disciplined committer since I adopted them. I’ve definitely lived the benefits of good commit messages to tell the tale 😉

If you want to learn more about the subject, check out the resources section below and if you have any questions you can always ping me @CarmenPopoviciu on Twitter.

✧(◕‿◕✿)✧


If you’re still there

You can find a demo repository on Github that walks you through all the solutions described in this article.


Try Backlog for 30 days.

Join 800,000 developers running on Backlog. No credit card required.

Try It Free