josuah.net

Git Hooks

Git hooks permit to run commands on a range of git events, mainly: a git commit.

I use 3 shell scripts of roughly 10 lines each to configure git hooks, calling scripts from within the repository itself: .githooks/<hookname>

git-hooks-run

This is what runs on every event, to put on /bare-repo.git/hook/<hookname>. There is no point in running it by hand.

#!/bin/sh -e
hookname=$1 ref=${2:-master}

echo "${0##*/}: running '$1' on '$ref'"
git cat-file blob "$ref:.git$hookname" | {
	IFS='! ' read -r _ cmd args
	exec "$cmd" "$args" "/dev/stdin" "$ref"
}

It checks if there is a file called .githooks/<hookname> (git ls-tree "$ref" ...), and if so, extract this file from git (git cat-file blob ...), read the shebang, and execute the rest with the command of the shebang ("| { ... }").

git-hooks-install

This setups the command above for a bare git repository:

#!/bin/sh -e
for x; do
	echo "#!/usr/bin/env git-hooks-run" >"$x/hooks/post-upate"
	chmod +x "$x/hooks/post-update"
done

It replace selected hooks at repo.git/hooks/post-update with only this shebang:

#!/usr/bin/env git-hooks-run

This has the effect of calling the git-hooks-run from above with hook/post-update as argument, along with the extra arguments providedd by git, which is all we need for our hook.

git-hooks-workdir

With only git-hooks-run, we lack a way to use the content of the repository to interact with the hooks (it is not always needed to use the content).

In case this is needed, this command extract the workdir of the commit pushed into a new directory in /var/cache/git (that it delete in case of failure), and print it out so that the hook script can use it:

#!/bin/sh -e
ref=$1
commit=$(git rev-parse "$ref")
workdir="/var/cache/git/$commit"

mkdir -p "$workdir"
trap 'rm -rf "$workdir"' INT EXIT TERM HUP
git archive --prefix="$workdir/" --format="tar" "$ref" | (cd / && tar -xf -)
exec echo "$workdir"

To use it from within the hook, to catch the workdir and make sure there is no remaining file even in case of failure, thanks to the trap internal shell command:

#!/bin/sh -ex
tmp=$(git-hooks-workdir "$@")
trap 'rm -rf "$tmp"' INT TERM EXIT HUP
cd "$tmp"

This might be the top of your hook script.

The (optional) -x flags in the shebang, which will print every command as it is executed. It will be printed on commiter side as "remote: $line".

The (optional) -e flag is there to die if any command fails, such as the initial cd to the "$tmp" directory.

At that point, we will be in a workind directory containing the content of the state at the commit pushed, and we can run commands, such as running unit test (make test), send an email or anything we need.