Automatically Prefixing Git Commit Messages with an Issue Number From the Current Branch Name
Recently I’ve been working with a Git workflow that requires a specific format for branch names and commit messages: branch names are formatted to contain an issue number and each commit message is prefixed with the issue number it relates to.
It’s a new process to me from the way I normally branch and commit; I wanted to configure Git so that whenever I make a commit the branch name is checked and the issue number automatically prefixed to the commit message. Thankfully this is straightforward to do by implementing one of the numerous Git hooks that exist.
Git Hooks
Git hooks are scripts that reside within a repository’s .git/hooks directory. They provide a means of interacting with the default Git behaviour at various stages and add in custom functionality. If you take a look at a Git repository you’ve cloned or initiated you should find a few sample scripts in the .git/hooks directory. These sample scripts won’t currently do anything as they are suffixed with .sample, you can remove this for them to start working or create a new file with just the hook name.
If Git finds a file named after one of its hooks in the .git/hooks directory and the file is executable it will run at the relevant point in the Git processes.
The prepare-commit-msg Hook
We’re going to utilise a Git hook to check a branch name and automatically prefix commit messages with the current issue number.
In order to modify the commit messages we need to work with the prepare-commit-msg hook. This hook is invoked by git commit
; it is called straight after receiving the default commit message but before the editor is loaded so we can set up our message ready for editing before finally committing changes to a repository.
We’re going to use Python to script our desired functionality here and need to create the file prepare-commit-msg in our repository’s .git/hooks directory. It’s important to ensure that this file is executable otherwise it won’t run on git commit
. This can be done with chmod
from the command line.
$ chmod +x .git/hooks/prepare-commit-msg
Here’s the script in full.
#!/usr/bin/env python
import sys, re
from subprocess import check_output
commit_msg_filepath = sys.argv[1]
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
regex = '(feature|hotfix)\/(\w+-\d+)'
if re.match(regex, branch):
issue = re.match(regex, branch).group(2)
with open(commit_msg_filepath, 'r+') as fh:
commit_msg = fh.read()
fh.seek(0, 0)
fh.write('[%s] %s' % (issue, commit_msg))
elif branch != 'master' and branch != 'dev':
print 'Incorrect branch name'
sys.exit(1)
Let’s take a look at what’s going on in this script.
Getting the Temporary Commit Message File and Branch Name
One of the first things we need to do is determine the temporary file Git is going to use to write the commit message to. This will be something like .git/COMMIT_EDITMSG
. Git creates this file so that we can edit the commit message within an editor before finally committing the changes to the repository. We can get this file path from one of the hook parameters that Git will pass to our script. prepare-commit-msg
has three parameters we can use:-
- the temporary file for the commit message
- the source of the commit message
- the commit SHA-1
We want to grab the first parameter so that we can prefix the default commit message before the editor loads.
commit_msg_filepath = sys.argv[1]
We also need the branch name to check that it is using the correct format for our workflow and to determine the issue number. This can be retrieved by using the command git symbolic-ref --short HEAD
. We use Python’s check_output method to run this command and capture its return value.
branch = check_output(['git', 'symbolic-ref', '--short', ‘HEAD']).strip()
Determining the Issue Number
Once we’ve got the current branch name we want to check that it is in the expected format using a regular expression.
regex = '(feature|hotfix)\/(\w+-\d+)'
if re.match(regex, branch):
This will match branch names like:-
- feature/ISSUE-123
- hotfix/ISSUE-67
If the branch name is correct we grab the issue number from it.
issue = re.match(regex, branch).group(2)
Then we write this to the start of the temporary commit message file that we determined from the hook parameters.
with open(commit_msg_filepath, 'r+') as fh:
commit_msg = fh.read()
fh.seek(0, 0)
fh.write('[%s] %s' % (issue, commit_msg))
This prefixes the issue number in square brackets to the start of the commit message. For example, [ISSUE-123]
.
Rejecting Invalid Branches
The last part of my script is to prevent a commit taking place if the branch name is incorrect. I’ve included this so that I don’t break with the conventions of the Git workflow I am having to use.
elif branch != 'master' and branch != 'dev':
print 'Incorrect branch name'
sys.exit(1)
This adds an extra check as we’re still going to allow for a master
and dev
branch. Otherwise we print a warning to the terminal and then exit the script with a non-zero value. By exiting with the value 1
here we abort the commit process without adding anything to our repository if our branch name is incorrect.
Wrap-up
So there we have our script. As long as the file is executable every time we commit to our repository it will run and check the branch name, then prefix the commit message with our current issue number. If something isn’t right the commit will be aborted and a message written to the terminal alerting us to the problem. This is super useful.
For security reasons you can’t commit the hooks directory to the repository. You wouldn’t want people sneaking anything in that would run on a particular Git process without you expecting it! So you’re going to need to add this in to each repository manually. However, if you find yourself in a similar situation to me and needing it across all repositories you can use a set of global hooks instead of the ones residing in the individual repositories.
To use global hook scripts place them all outside of your repositories and then point Git at this new folder.
$ git config --global core.hooksPath /path/to/global/hooks
This will replace any hooks that exist in individual repositories.
This script has really helped me remember the process I am currently having to work with and is a great example of what can be achieved with Git hooks. I plan on sharing some further hook scripts I use to improve my development process at some point in the future, but for now I hope you’ve found this interesting.