Smoothening your workflow with Git hooks

I'm a big fan of automation. If a computer can do a task as well as I could, why should I be the one to do it, especially if I'm going to have to do it over and over again. Git hooks provide a way to run scripts when some Git commands are executed. It sounds perfect to delegate a bit more to the computer. Let's see what we can do with them!

A quick overview of git hooks

Git hooks run when specific Git actions happen (here is a list of the available triggers), either on the server or on the client. They live in the .git/hooks directory of your project. Actually, if you have a Git project at hand and look into this folder, you'll see a few hooks sample there (remove the .sample extension to activate them).

The sample provided are shell scripts. However, if you're more comfortable with another language, you can definitely write them in Python, Ruby, PHP... The scripts will run in non-interactive environment, though, which makes getting user input a bit tricky. For this article, the hooks will be written in Javascript. While this might seem an odd choice, I first wrote them for a web project. The project used Grunt as a build tool, so I was sure node was available.

Now let's dive in and create a couple of client hooks:

  • a first one to prevent commit if the project cannot be built
  • another one to make sure commit messages contain certain piece of information

Both will require commits to be triggered. Obviously, we don't want to ruin the logs of our project. What we can do to avoid that is create a separate branch we'll destroy after we've created the hooks. Doing commits also means we'll need to make file changes. We're talking of automation, so we'll not do it by hand. We can use this little command for example:
echo $RANDOM > hook-dev && git add hook-dev && git commit (replace $RANDOM by %RANDOM% on Windows).

Building before committing

On to the first hook, now! Ideally, you'd run a build before every commit you make. It can be easy to forget it and let a commit that doesn't build slip past and get in the tree. Hopefully, you'd have some form of continuous integration running against one of your remote repository to notify you. But why wait for the code to be pushed to a remote repo to get this information?

Using the pre-commit hook, we can run the build every time we commit. No human interaction needed, the build won't be forgotten. If the build fails, we can abort the commit by returning a non-zero status, keeping the tree clean of code that doesn't build. Here's what we need for this first hook:

Asking for some input

The second hook will ensure that commit message contain specific informations, like the ID of the issue you're working on. We could use commit message template, but using the -m option when commtting will bypass this easily. Instead, we'll use the commit-msg hook to prompt the user for an issue ID. No issue ID provided, no commit (as for the pre-commit hook, a return status different than 0 will prevent the commit). Let's see how this one goes:

Note: This hooks rely on reading /dev/tty on Linux to get the user input, as hooks run in non interactive terminals. On Windows, there seems to be a 'CON' or 'CON:' stream to read from, but I couldn't get it to work. An alternative to prompting the user could be to check the commit message follows a given format.

Wrapping it up!

Much more can be done than these two examples. There are many other triggers you can create hook for, and many things you can do in the scripts you write. Watch out, though, commits could quickly become a burden if you run too many hooks (or just slow operations), so you don't want to overdo it. However, hooks are a really great tool to automate your workflow around Git.

comments powered by Disqus