Photo by Pixabay from Pexels

Often times companies will have a standard template for their commit messages. Something like “issue number: short message, details”. Just like coding styles these help make the history legible. But just like coding styles they mean nothing if they are not enforced.

A typical standard commit message could be:

Your git commits should start with a short single line that begins with: DE: For defects followed by the defect number US: For user stories of new features followed by a story number

For example a good commit would look like:

DE175: Fix defect short description Here is a longer description of the changes that went into fixing the defect.

You could (and should) enforce these rules on your git server. It is possible to do this with pre-receive hooks. Setting up a pre-receive hook is fairly easy. Pre-receive hooks will check to ensure all commits coming to the server pass any commit message rules you have.

What happens when they don’t?

The git server will reject the push from the client. It is up to the client to fix any commit messages and then attempt the push again.

Now if the offending commit is at the head this isn’t too difficult. A simple commit amend command will fix it.

git commit --amend

This will allow you to quickly and easily modify the last commit.

But what if the offending commit isn’t at the head, and what if there is more than one offending commit?

Now the user needs to perform an interactive rebase.

git rebase -i

This can be a tedious long process. And if the commit is very old and merges have occurred between the HEAD and the offending commit then fixing the commit is even more difficult. Sometimes, I have had to abandon a whole change histories and simply done a merge squash in order to fix and remove any offending commits. Obviously, this isn’t ideal as I have lost all commits on the offending branch.

But these are just solutions to cleaning up my mess.

What if I stopped myself from creating the mess to begin with?

Photo by Pixabay from Pexels

Git Client Side Hooks

Just like server side git hooks that we saw earlier to enforce commit messages, we can employ a similar mechanism locally to ensure we don’t create any bad commits.

Git hooks are special programs that are executed at certain stages of git execution. There are several different hooks we can use but for our purposes we will focus on:

commit-msg: used to refuse the commit after inspecting the message file.

The hook itself must be an executable named commit-msg. To make things quick let’s just use a simple bash script.

When you run:

git commit

You are actually running a standalone program called git-commit. This program will then create a temporary file with the contents of the commit message. This file is then passed into our commit-msg script. If the script returns a non zero status code then the commit is considered invalid and rejected.

Here is a sample small commit-msg script

#!/bin/bash MSG_FILE=$1

FILE_CONTENT="$(cat $MSG_FILE)" # Initialize constants here

export REGEX='^(DE[0-9]+:|US[0-9]+|Merge) .+'

export ERROR_MSG="Commit message format must match regex \"${REGEX}\"" if [[ $FILE_CONTENT =~ $REGEX ]]; then

echo "Good commit!"

else

echo "Bad commit \"$FILE_CONTENT\""

echo $ERROR_MSG

exit 1

fi exit 0

Notice the regular expression we also allowed lines beginning with “Merge”. This is to allow the default merge commit message to also be valid.

Automatic Prefix

We can take this strategy one step further by having git automatically prefix any of our commits with the appropriate defect or story based on branch names. This way we don’t have to remember the details for the tracker of what we are working on and just focus writing good commit messages.

To do this we need to use another git hook:

prepare-commit-msg This hook is invoked by git-commit right after preparing the default log message, and before the editor is started. It takes one to three parameters. The first is the name of the file that contains the commit log message. The second is the source of the commit message, and can be: message (if a -m or -F option was given); template (if a -t option was given or the configuration option commit.template is set); merge (if the commit is a merge or a .git/MERGE_MSG file exists); squash (if a .git/SQUASH_MSG file exists); or commit , followed by a commit SHA-1 (if a -c , -C or --amend option was given). If the exit status is non-zero, git commit will abort.

(Git Docs)

This script is a bit more complicated but stay with me. The basic premise is we will modify the incoming commit and prefix the issue number from the current branch name.

To do this we follow a branch naming scheme of:

${type}/${userid}/${issue#}_${message}

For example:

defect/efenglu/de253_BadComments

Again we need a small bash script:

#!/bin/bash MSG_FILE=$1

MSG_TYPE=$2

FILE_CONTENT="$(cat $MSG_FILE)" export REGEX='^(DE[0-9]+:|US[0-9]+:|Merge) .+'

# If commit is already good, take it

if [[ $FILE_CONTENT =~ $REGEX ]]; then

exit 0

fi # Skip files

skip_list=`git rev-parse --git-dir`"/hooks/pre-commit.skip"

if [[ -f $skip_list ]]; then

if grep -E "^$BRANCH$" $skip_list; then

exit 0

fi

fi # Get curent branch

BRANCH="$(git rev-parse --abbrev-ref HEAD)" # Create prefix based off parsing branch name

STORY=$(echo ${BRANCH} | awk 'match($0, /US[0-9]+|DE[0-9]+/) {print toupper(substr($0, RSTART, 1)) substr($0, RSTART + 1, RLENGTH - 1)}') # If unable to parse story information from branch abort with zero

# Our other commit hook will catch bad commits for exit 0

if [[ ! $STORY ]]; then

exit 0

fi # Add story to commit in attempt to make it good

echo "Prepending branch information \"$STORY\"..."

echo "Message Type: $MSG_TYPE"

echo "$STORY: $FILE_CONTENT" > $MSG_FILE exit 0

Now when I run from branch defect/efenglu/de253:

git commit -m "Sample message."

The log will contain:

DE253: Sample message.

Controlling Scripts

Now its likely that you checkout from multiple different repositories with potentially different rules. For example, I use our corporate repository and public github. I don’t want the corporate rules enforced on my public github repositories.

To do this I added a check to the beginning of our commit hooks:

#!/bin/bash if [[ $(git remote -v | grep github.com) ]]; then

echo "Commit Rules NOT enforced"

exit 0

else

echo "Commit Rules enforced"

fi

Filename checks

There are several other hooks you can extend and provide all kinds of other functionality. We also wanted to enforce case-insenitive filenames. Since our developers use different filesystems, some that are case insensitive (OSX), and others where it is not (Linux). There were times when git wouldn’t be able to checkout a file due to file name collisions.

This produced lots of weird errors for our developers and were quite difficult to resolve.

Although we added the check to our git repository we also wanted a client side check.

pre-commit: verify what is about to be committed

#!/bin/bash if git rev-parse --verify HEAD >/dev/null 2>&1

then

against=HEAD

else

# Initial commit: diff against an empty tree object

against=4b825dc642cb6eb9a060e54bf8d69288fbee4904

fi # Redirect output to stderr.

exec 1>&2 TF=$(mktemp)

trap "rm -f $TF" 0 1 2 3 15

checkstdin() {

sort -f | uniq -di > $TF

cat $TF

test -s $TF || return 0 # if $TF is empty, we are good

echo "non-unique (after case folding) names found!" 1>&2

cat $TF 1>&2

return 1

} git ls-files | checkstdin || {

echo "ERROR - file name collision, check case of names, stopping commit" 1>&2

exit 1

}

Photo by Pixabay from Pexels

Setup

Unfortunately, for clients to use the client side hooks they must be setup by the client.

Fortunately, that is a very easy one time process. Simply clone the repo that contains your shared git client scripts.

Then enable the scripts in git:

git config --global core.hooksPath ~/git/git-hooks

Sample Code

Checkout my repository with full code examples from above.