Sam's Log

Git Hook Prepend Commit Message

At work we have to add the Jira issue number to each pull request, branch and commit. This is to help Management with planning. Unfortunately for me, adding the Jira ID to each commit often broke with my existing workflow or I often just simply forgot to do add it. To remove this as an issue, how about a script to automate prepending the git commit message with the Jira ticket ID?

Git Hook Script

For the impatient, here’s the script.

 1#!/usr/bin/env python3
 2
 3"""
 4Git hook to automatically prefix a git commit message with an issue number
 5(e.g. Jira ticket number) from the current branch name.
 6"""
 7
 8import re
 9import sys
10from subprocess import check_output
11
12
13commit_msg_filepath = sys.argv[1]
14branch = (
15    check_output(["git", "symbolic-ref", "--short", "HEAD"]).decode("utf-8").strip()
16)
17regex = r"(chore|feature|fix|hotfix)\/(\w+-\d+)"
18
19if re.match(regex, branch):
20    issue_number = re.match(regex, branch).group(2)
21    with open(commit_msg_filepath, "r+") as f:
22        commit_msg = f.read()
23        f.seek(0, 0)  # correctly positions issue_number when writing commit message
24        f.write(f"[{issue_number}] {commit_msg}")

First up, the script has some prerequisites and makes some assumptions:

With that out the way, let’s walk through the script and see what it does.

The shebang is important because we need to make sure this script executes using Python 3. Per the Python docs, “a good choice is usually #!/usr/bin/env python3”.

Next is a docstring to remind myself several months later what this script does. Then the imports required for the script. All imports are from the standard library so no need to pip install anything.

The variable commit_msg_filepath is a system argument passed into the script. We use this later on, opening it as a file to write the commit message.

We find the branch name by using subprocess.check_output

From the Python docs:

Run command with arguments and return its output. By default, this function will return the data as encoded bytes.

It runs the git command git symbolic-ref --short HEAD and the return is decoded to a string and stripped of whitespace.

The regex pattern for the git branch naming convention is declared as the regex variable. This allows the script to extract the Jira ID. This script is lenient so if there is no match, it assumes there is a valid reason for the branch not having a Jira ID. If the regex has a match we enter the if block. This is the code which amends the commit message.

The Jira issue number is group 2 in the match regex. It opens the commit message as a file and reads in the contents of the file.

seek() is an interesting one. If writing a commit message in vim then seek(0, 0) places the Jira ID as the first part of the commit message placeholder. Comment out seek(0, 0) and you’ll see the Jira ID half way down in the git commit message verbiage yet it will still get written as the first part of the commit message. That’s because the final line writes the actually git commit message e.g. after you type it, save and exit Vim the message gets written as [EX-100] example commit message.

Using the script

Finally, to make this script work there are two options.

Option one is to add to a single repo or to each repo you want this script to run. This wasn’t an option for me as we have many micro-services which I might work on and didn’t want to add the script to each repo.

Option two is to point global git config to a directory of your git hooks. This is what I went for and here’s how to do it.

Global Git Hooks

Save the script in a directory where you keep or plan to keep all your global git hooks. Make sure the script is executable chmod +x prepare-commit-msg.

Then run this to point the global hooks to your directory:

1git config --global core.hooksPath /path/to/git_hooks

Run cat .gitconfig to check the output:

       │ File: .gitconfig
───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1   │ [user]
   2   │     name = Sam Atkins
   ...
   5   │ [core]
   6   │     hooksPath = /path-to/git_hooks

Final thought

Here’s a link to the script on Github. Thanks for reading.