Home Tools Blog Photos

Automatically add Jira issue to commit message from branch name

At FlightAware, we use Jira to track work and git for version control. To reference each other, git branch names start with the relevant issue number, taking the form

${project}_${ticketNumber}_${shortDescription}

Additionally, commit messages end with the same issue number in the form

${project}-${ticketNumber}

These conventions allow for easy referencing of issues to look for details and business decisions. Specifically, the issue number in the commit message is used by Zeitgit to attribute commit statistics to issues. It's also used by Jenkins to create useful links between tickets and pull requests.

It is, however, a little annoying to type the issue number in to every commit, especially when I know git already has this information in the branch name—git just doesn't know it. To automate this little task, I use a git hook to prepare the commit message with the issue number before it goes to $EDITOR (e.g. vim) for editing.

Git hooks

Git hooks are scripts—typically written in bash, zsh or another shell—which live in the .git/hooks/ directory of a project. These scripts provide a means of performing actions certain stages of git's behavior. By default, the hooks directory comes pre-populated with sample hooks, each ending with .sample. That file ending keeps them from running until the suffix is removed. Take note of their names though, git will look for these specific names (without the .sample ending) when looking for hooks to run.

The prepare-commit-msg hook

What we need to do is edit the commit message before it goes to your $EDITOR for regular editing; the one we want is called prepare-commit-msg. This hook is called by git commit and is given the name of the file which has the commit message, followed by the description of the commit message's source (discussed later), and the commit's SHA-1 hash.

For the script itself, we need to get the issue number from the branch name, format it according to FlightAware conventions, then prepend it onto the commit message file, preserving git's default message if it exists (the summary of changes in the commit). My script is in Zsh, but could be in any language available on the system; Bash, Python, plain old Bourne shell, etc. would be fine. I use Zsh as my login shell, so that's what I default to.

Hook script in full

#!/usr/bin/env zsh
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3

if [[ -z "$COMMIT_SOURCE" ]]
then
  branch=$(git symbolic-ref --short HEAD)
  if [[ "$branch" =~ ^([a-zA-Z]+)[_-]([0-9]+).* ]]
  then
    ticket="${match[1]:u}-${match[2]}"
    gitMsg=$(cat "$COMMIT_MSG_FILE")
    printf "\n\n%s\n" $ticket > "$COMMIT_MSG_FILE"
    printf "$gitMsg" >> "$COMMIT_MSG_FILE"
  fi
fi

This will handily extract issue names from branch names ranging from "web_14800_some_feature" to "NXT-1701". The first few letter are project codes, of which FlightAware has dozens like PREDICT, ADSB, OPS, NXT, WEB, etc., so we want to support as many as possible.

Now I'm a strong believer of not just executing code without understanding what's happening, so let's break it down.

Capturing commit information

COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3

To keep track of the information passed to the hook script, we store the arguments in variables. Here's what we get:

  1. The first argument is the name of the temporary commit message file, where git physically keeps the message until the commit is complete.
  2. The second argument is the commit source. We'll check this a couple lines below.
  3. The third argument is the commit's SHA-1 hash. This script won't use it, but it could be useful to capture if you extend this hook in the future.

Checking the commit source

if [[ -z "$COMMIT_SOURCE" ]]

Before doing any work, the script ensures there's no special commit source by ensuring it's empty with the -z test. This source gives us some info on why this commit is happening. It could be "merge" or "squash" or a number of other sources. In a "normal" commit, the source is just empty. We don't want to mess with any special commits (by FlightAware convention), so we can just check that it's empty.

Getting the branch name

  branch=$(git symbolic-ref --short HEAD)

We also need the branch name, which (may) contain an issue number. Git's symbolic-ref command lets us get info about a symbolic reference, in this case HEAD. The --short option shortens the symbolic ref's path to just its name. For example, this would shorten the full path refs/heads/main to just the name main.

Extracting the issue number

  if [[ "$branch" =~ ^([a-zA-Z]+)[_-]([0-9]+).* ]]

The next if condition performs two actions. First, it only resolves as true if there is an issue number in the branch name—at least of the form we use at FlightAware. Second, the () capture groups take the project code and ticket number and implicitly store them in the $match array. Any description text after the issue number is ignored.

Formatting the issue number

    ticket="${match[1]:u}-${match[2]}"

if we find an issue number, we'll need to format it. This is necessary only because of FlightAware conventions; branch names are snake_case, while Jira issue numbers are properly TRAIN-CASE. We simply create a new variable, ticket, built from the capture groups stored in $match. We upcase the project code portion with the :u expansion modifier to complete the transformation. There are other ways to upcase text, but this is the cleanest in my opinion.

Building the commit message

    gitMsg=$(cat "$COMMIT_MSG_FILE")

At this point, git has already created a message file and populated it with information including a diff summary. We capture all this pre-existing text into a variable, gitMsg. We'll need to add this back to the end of the file after overwriting the file to simulate prepending. There are other methods of prepending to a file, most interestingly by using a here-string, but this is the most straight-forward method.

    printf "\n\n%s\n" $ticket > "$COMMIT_MSG_FILE"
    printf "$gitMsg" >> "$COMMIT_MSG_FILE"

Now to build up our new message, we overwrite the existing message with a formatted string including the ticket number. This string starts with two newlines, providing space to put the substantive commit message, and ends with a newline to distance the ticket number from the git-supplied information. Finally, we append that git-supplied info to the message.

After this hook is complete, git will continue on it's merry way and (probably) open the user's $EDITOR as usual.

Usage

All we need to do to use this hook is to save it as .git/hooks/prepare-commit-msg and make it executable (chmod +x), git will handle the rest!

For security reasons, git doesn't allow committing hooks to a repository (nor anything inside .git/), so this script will have to be added to each project individually. If you'd like to add it to all projects, you can store it in a known directory and have git always look there for hooks:

git config --global core.hooksPath /path/to/your/hooks

However, this will stop git from looking at the local hooks directory. Unfortunately you can't have it both ways, at least as of git version 2.18.