Client Side Git Hooks

Git Hook

Git Hooks can be very useful

You can’t stop a bad commit. One of those commits that was someones hurried attempt to solve a merge conflict. The commit that works on that developers box, but has no possibly of working anywhere else.

What you do have control over is HOW they commit this horrible code. You can do this if you have a git source code repository through something called Git Hooks.  This post will take a look at two git hooks.  Hopefully this will help you get your own hooks in place to prevent the guy who checks in at 4:58 and then leaves the building from not leaving you without a clue as to why he checked in.

Pre-Commit Hook

Let’s take a look at the first one, the Pre-Commit hook

#!/bin/bash

printf "Pre-Commit Hook Running...\n"

declare -a brokenRules

# Look for files you are not allowed to modify.
modifiedFiles=$(git status | grep 'modified:')
for fn in "$modifiedFiles"; do
if [[ -n $(printf "$fn" | grep -i 'app.config') ]];
then brokenRules+=("You cannot make modifications to the 'app.config' file"); fi
if [[ -n $(printf "$fn" | grep -i 'web.config') ]];
then brokenRules+=("You cannot make modifications to the 'web.config' file"); fi
done

COLOR_RED='\e[0;31m'
COLOR_YELLOW='\e[0;33m'
COLOR_NONE='\e[0m'

# Display any broken rules
if [ "${#brokenRules}" -gt 0 ]; then
printf "${COLOR_RED}[COMMIT ERROR(S)]${COLOR_RED}\n"
for rule in "${brokenRules[@]}"; do
printf " - ${rule}.\n"
done
printf "\a${COLOR_YELLOW}\n"
printf "You can override this warning with --no-verify, but you better be sure you know what you're doing."
printf "${COLOR_NONE}\n"
exit 1
fi

printf "Pre-Commit Hook finished.\n"
exit 0

The first line indicates that this is a bash script. I declare an array for the broken rules and then I look at the modified files to see if someone is accidentally trying to check in either the app.config or the web.config file. If they are; I add this to the broken rule and move on. Once I get to the bottom of the script I will look at the length of the array to see if it is > 0 If it is, then I will display a list of each broken rule.

Git Bash Console

File  Edit  View  Help

User@DISCOVERY ~/Projects/Sample (develop)
$ git commit -am ‘test commit’
Pre-Commit Hook Running…
[COMMIT ERROR(S)]
– You cannot make modifications to the ‘web.config’ file.

You can override this warning with –no-verify, but you better be sure you know what you’re doing.

User@DISCOVERY ~/Projects/Sample (develop)

As you can see, I will not be able to commit this without using the –no-verify option. This option turns off checking and allows me to commit the change anyhow.

Prepare-Commit-Msg Hook

This is most likely the hook that you will spend the most time perfecting. This hook will be your life saver because you can use it to enforce rules on your team on what their commit message should contain. Lets take a look.

#!/bin/bash

printf "Prepare-Commit-Msg Hook Running...\n"

#$1 = "Commit Message File 'COMMIT_EDITMSG'"
#$2 = "message"

OLD_IFS="$IFS"

declare -a brokenRules

commitMessage=$(cat "$1")

#	Prevent people putting in the same commit message multiple times by looking for an identical message in the last 20 commits
IFS=$'\n'
declare -a last20CommitMessages=($(git log --pretty=format:'%h|%an|%s' --max-count=20))
for rawMsg in "${last20CommitMessages[@]}"; do
	IFS="|"	read -r hash author message <<< "$rawMsg"
	if [[ $message == "$commitMessage" ]];
		then brokenRules+=("Unable to commit!  Commit message is not unique. Message duplicated in commit $hash authored by $author"); fi
done

#	Prevent people from committing without a story or defect number associated with the commit.
validStart=false

upperCaseCommitMsg=$(echo "$commitMessage" | tr '[:lower:]' '[:upper:]')

#	Check for story or defect number
if [[ $upperCaseCommitMsg == S* || $upperCaseCommitMsg == D* ]]; then
	rallyNum="${upperCaseCommitMsg:1:4}"
	rallyNumNoChars=${rallyNum//[^[:digit:]]}
	if [[ "${#rallyNum}" -eq ${#rallyNumNoChars} ]]
		then validStart=true; fi
fi

#	Make allowance for rebase and merge
if [[ $validStart == false && ($upperCaseCommitMsg == MERGE* || $upperCaseCommitMsg == REBASE*) ]];
	then validStart=true; fi

#	Finally if they still do not have a valid commit message, ask them for the information
if [ $validStart == false ];
	then brokenRules+=("Unable to commit!  Commit messages must start with the letter 'S####' or 'D####' for proper Rally integration"); fi

IFS="$OLD_IFS"

COLOR_RED='\e[0;31m'
COLOR_YELLOW='\e[0;33m'
COLOR_NONE='\e[0m'

#	Display any broken rules
if [ "${#brokenRules[@]}" -gt 0 ]; then
	printf "%b[COMMIT ERROR(S)]\n" "${COLOR_RED}"
	for rule in "${brokenRules[@]}"; do
		printf " - %s.\n" "${rule}"
	done
	printf "\a%b\n" "${COLOR_YELLOW}"
	printf "You can override this warning with --no-verify, but you better be sure you know what you're doing."
	printf "%b\n" "${COLOR_NONE}"
	exit 1
fi

printf "Prepare-Commit-Msg Hook finished.\n"

exit 0

As you can see this has the same broken rules logic from the previous one but is does several things differently than the last one. For one it will look at the git log 20 message back to make sure that you are not re-using a git commit message. How many times have we seen the same message 5 times in a commit chain. Defect 1234 – WIP. This kind of message is not useful as it really tells us nothing about the work that you have done since the last commit. This code block prevents this:

#	Prevent people putting in the same commit message multiple times by looking for an identical message in the last 20 commits
IFS=$'\n'
declare -a last20CommitMessages=($(git log --pretty=format:'%h|%an|%s' --max-count=20))
for rawMsg in "${last20CommitMessages[@]}"; do
	IFS="|"	read -r hash author message <<< "$rawMsg"
	if [[ $message == "$commitMessage" ]];
		then brokenRules+=("Unable to commit!  Commit message is not unique. Message duplicated in commit $hash authored by $author"); fi
done

The next part of this hook will examine what the user entered as a message. Does it follow the rules? In my case I want to ensure that every commit starts with an S or a D so that I can facilitate integration with my Application Lifecycle Management (ALM) and Project Portfolio Management (PPM) platform and products.

#	Check for story or defect number
if [[ $upperCaseCommitMsg == S* || $upperCaseCommitMsg == D* ]]; then
	rallyNum="${upperCaseCommitMsg:1:4}"
	rallyNumNoChars=${rallyNum//[^[:digit:]]}
	if [[ "${#rallyNum}" -eq ${#rallyNumNoChars} ]]
		then validStart=true; fi
fi

As you can see unless the user enters ‘s1234’ or ‘d1234’ in the commit message, it will not pass validation and the git hook will prevent you from committing to your local repository.

One that I don’t have a code sample for but is completely possible is to prevent someone from making commits after a certain time. You could write your script to create a blackout time that would prevent users from committing to their local repositories. While I don’t know about the effectiveness of a client side hook like this (since it can be disabled by the client); it should spark some ideas for you.

What git hooks would you create? Drop me a line and let me know. Until next time!

Advertisements

One thought on “Client Side Git Hooks

  1. Pingback: Git Searching | OutOfMemoryException

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s