Git precommit hooks done right

Okay. We all know it. Git is a pain. I can never remember how to do anything slightly advanced, and as a result I and up asking google. Usually some nice fellows on Stack Overflow answered my question, but sometimes I have to dig deeper. Git commit hooks is such a case.

Before I continue, let me first show you what my precommit hooks looks like:

#! /bin/sh
git stash save -q --keep-index "Precommit stash"
make test
exit_code=$?
git reset -q --hard
git stash pop -q --index
exit $exit_code

Wow, is that really necessary? Well, to the best of my knowledge, yes it is. At least to cover my use cases. But let me explain it in more detail.

The reason for stashing is partial commits. If you are not doing a commit -a only part of your changes will end up in the commit, but simply running your tests will affect all your changes, so they are not testing your commit isolation. Stashing solves this problem, but you have to be careful.

The “-q” and “Precommit stash” are not that magic. They are merely there to avoid unnecessary output and giving you stash a useful name in case you have to recover it manually. Saving the exit code allows you execute more commands before telling git that the precommit hook is allowing or preventing the commit.

To avoid stashing the changes you have actually added for your commit, you need to specify the “ — keep-index” option. Even if you keep the index while stashing, git will unstage all you changes when popping the stash, including the changes in you didn’t stash, resulting in conflicts. To avoid this we need to do a hard reset to get a clean working tree. To actually split the stash up in its staged and unstaged parts, we need to add the “ — index” option when popping.

Doing the precommit hooks this way has the nice property that when fixing a rejected partial commit, it will continue to reject the commit until you actually remember to add your fixes to the index. Something that I usually forget.

A word of warning

This precommit hooks is nice, but it doesn’t prevent all errors from sneaking through. Doing a rebase will never trigger the precommit hook, so any resolutions will go unchecked into you commit and potentially out to others when you decide to push your changes.