Automate your commit messages

Clever scripting to the rescue!
The git logo with the title of the article superimposed next to it.

Aye, aye, cap'n!

Git has a lot of inbuilt functionality that you might not use all the time, but which is oh-so-handy when you need it. One such thing is hooks. These can be used to do stuff like formatting your code before committing or running tests before pushing to the remote. Today, though, let’s look at how we can use it to automatically fill in part of your commit messsage.

How did this come up?

The team I’m on at work uses a system for commit messages and branch names where they should both start with the issue id of whatever task we’re working on. Using GitLab, they end up looking something like #2 Adds sorting functionality to UI (we can argue about commit message tenses later) and #42/new-sorting-algorithm, respectively.

If you’re like me, your mind should be poking you right about now, saying that that’s a lot of duplication; surely we don’t need all of that? Well, you could argue that with feature branches scoped to issues and no fast-forward merges, the branch name should be enough, but it doesn’t really show up in condensed commit logs etc., so it’s nice to have the issue number available for when you need it.

But not to worry; we can make the computer fill it in for us!

Picking the right hook

Git has quite a few hooks available; if you check the .git/hooks directory of a tracked project, you should find a bunch of files named <hook name>.sample. Most of the hook names are fairly descriptive, but you could always look them up if you want to know more.

In our case we’re interested in hooking into the system right before the commit message buffer appears on our screen, so we’re going to go with the prepare-commit-msg hook.

Auto-filling the message

Now, my team has a template we use for commits, which starts with #[task number]. Using this knowledge, we can do some fancy shell magic and replace it with the task from the branch name.

The whole script looks like this:

Depending on whether you use BSD sed (macOS) or GNU sed (Linux), the sed command will behave slightly differently. For GNU sed, use the commented out line instead of the first one.

There is a slight difference in how you make the two versions of the programs write files in place.

The git rev-parse --abbrev-ref HEAD command gives you the name of the current branch, which we then split at the first / character using cut.

The next (and last) step is a simple sed replacement, replacing the #[task number] string with the branch’s task number.

The $1 variable at the very end of the last line is the name of the commit message file and is something the script receives automatically. We use this to tell sed what file to replace the text in.

And for the curious, these are all the variables you get access to in the pre-commit-msg hook (taken from the pre-commit-msg.sample file):

Caveats

There is a pretty significant exception to when this script does what you want it to: when you’re still on the master branch, where commit messages will start with the text master. I considered adding a special case for this, but then realized that it’s actually quite helpful in that it helps me remember when I should branch off, so I decided to keep it for now.

Another drawback is that because the commit message appears different from what git expects to find on disk (presumably), it will assume you put an actual message in and complete the commit. At least that’s what I found when using Vim. A simple workaround is deleting everything in the buffer (dae if you use the awesome text-obj-entire plugin) before exiting. Most of the time, though, I use Magit with Emacs, where canceling the commit works as expected.

Alternate approach

While I have opted for modifying the commit message before it’s shown to the user, you could potentially also have the machine add the id automatically in the pre-commit hook.

You would make the system check the message for a pattern and amend the commit with the expected issue id if it’s not present.

This solution doesn’t give you any way to verify whether it’s correct or not, though, so you’d probably want some sort of way to bypass it.