Git Hooks for the Front End Developer


Git Hooks are powerful, and can really provide a smooth polish to your workflow. We use package managers, task runners, source control, and all kinds of other good stuff in our workflow; but what makes sure we follow all the steps and run all the tasks we should before committing code? It'd be nice if there was a way to do a final check of what's being committed to the repo at the time of committing... oh wait! There is! It's called a pre-commit hook! That alone makes git hooks worth looking into.

What are Git Hooks?

Git Hooks are essentially just scripts that get executed at various stages or checkpoints along the git process. For instance, a post-checkout hook will run right after a git checkout [branch|file] command is executed and the working tree updated. If you wanted to, you could write a simple script that logs a message to the console if a specific branch is checked out, i.e. if a branch is checked out and it's master, then log something like "IN MASTER BRANCH - NO TOUCHY!!"

These hooks live in the .git/hooks/ directory, as the below graphic shows.

![git-hooks file structure](/content/images/2015/08/git-hooks-location.png)

Git Hook script names match up exactly to the stage at which they run

How to Write Git Hooks

Git hooks are generally written in sh or bash scripts. Fortunately, it's super easy and not at all cryptic to read, write, and debug these scripts. Here's a [hyperbolic] example:

//post-checkout
if [ "" -neq "$(grep '^pew-pew' "$1" | sed -e '/[^wtf]!?:(omg|idonteven)/d')" ]; then
  echo >&2 You can't even!
  exit 1
fi

If only there were a way to use git hooks with the language and tools I'm already using in my front end application, like NodeJS and Gulp... Actually, there is!!

Using NodeJS for a Git Hook Script

Fortunately, there's a different way of doing this that, to me, is much more palatable. We can use JavaScript and put a shebang (#!) header on the file to let any OS with some sense know what program to use to interpret the script. From there, we can quickly come to the realization that we could execute tasks using a task-runner, perform some arbitrary custom logic on the modified files, or anything else we'd ever want to do - all using the NodeJS runtime! While an sh script would likely be a bit faster, using JavaScript we get the opportunity to make things much more readable and easier to maintain. Here's an example script that is for a pre-commit hook:

#!/usr/bin/env node

//pre-commit

'use strict';

//assumes node_modules is at root of repository

var spawn = require('child_process').spawn,
    gulp = (process.platform === 'win32' ? 'gulp.cmd' : 'gulp'),
    path = require('path'),
    cwd = process.env.PWD || process.cwd(),
    hookName = process.argv[1].split(path.sep).pop(); //will be pre-commit

//run "gulp pre-commit" as a child process, 
//but hold a ref to it so we can kill it if needed
var hook = spawn(gulp, [hookName], {
    stdio: 'inherit'
});

hook.on('close', function (code) {
    //anything other than 0 is failure
    if (code !== 0) {
        console.error("YOU CAN'T EVEN!!!");
    }
    process.exit(code);
});

// catch exceptions so node doesn't exit prematurely, leaving a runaway process
process.on('uncaughtException', function (err) {
    console.error('Uncaught Pre-Commit Exception', err.stack);
    //http://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html
    hook.kill('SIGTERM');
    process.exit(1);
});

The absolute most important lines in this script are:

  • The heading with a shebang (#!) - as this tells the OS what runtime to interpret the script with. (Have to do this a bit differently on Windows, there's an example in the demo I'll link to at the end.)
  • The lines that contain process.exit(...) as this is how you tell the caller (in this case git) whether it can move on to the next stage in the workflow or not. If you exit with anything but a 0 (zero), it's a failure. If the git process is one that can be stopped, i.e. a commit, then it will be exited if the exit code is a failure code and the commit will not go through.

Who loves demos?

I thought that this approach warranted a demo - so I created a small repo in github to show this in action. It should work in Windows also, and shows a pre-commit hook in action - so check it out!

As always, be free with your thoughts in the comments or hit me up on twitter!
-Bradley