Grunt: Getting Started with Git Hooks

Scenario: you work on a fairly large project with other developers. Getting the rest of the team to run tests before the build has been problematic. If only you could ensure that tests ran before code could even be committed...

Solution: Git Hooks to the rescue! Run anything you want before commits with the pre-commit hook. Like JSHint? Love unit testing? Run them before commits! The different hooks and potential uses are well documented, so let's take a look at their implentation and how we can improve it with Grunt.

The typical Git Hook implementation involves finding a hook (samples included in all repos) in .git/hooks, writing a shell script to do what you want, and then making it executable like chmod +x .git/hooks/pre-commit. This may not be appropriate for certain teams and could be difficult to distribute. Surely other people have already made this easier?

Enter grunt-githooks by Romaric Pascal. Getting up and running is simple. And most importantly, requirements easily transfer to collaborators.

 npm install grunt-githooks --save-dev
 // Gruntfile.js

grunt.loadNpmTasks('grunt-githooks');

grunt.initConfig({
 githooks: {
 all: {
 'pre-commit': 'test'
 }
 }
});
 grunt githooks

Customization: Improving pre-commit

By default, the pre-commit hook will run tests against the entire working tree. But we should only test what's staged for commit, right? Luckily, the template option allows us to easily override the default with our own.

Here's the default plugin template:

 var exec = require('child_process').exec;

exec('grunt {{task}}', function (err, stdout, stderr) {

 console.log(stdout);

 var exitCode = 0;
 if (err) {
 console.log(stderr);
 exitCode = -1;
 }{{#unless preventExit}}

 process.exit(exitCode);{{/unless}}
});

On commit, grunt-githooks runs the specified Grunt task and logs the output. If no error occurs, the process exits with 0 (success) and you can continue with your commit. The commit is aborted otherwise.

Expanding on this template with a few extra Git commands is not too difficult. First, install exec-sync to avoid some unnecessary callbacks.

 npm install execSync --save-dev

Here's the custom pre-commit template:

 // hooks/pre-commit.js

var exec = require('child_process').exec;
// https://npmjs.org/package/execSync
// Executes shell commands synchronously
var sh = require('execSync').run;

exec('git diff --cached --quiet', function (err, stdout, stderr) {

 // only run if there are staged changes
 // i.e. what you would be committing if you ran "git commit" without "-a" option.
 if (err) {

 // stash unstaged changes - only test what's being committed
 sh('git stash --keep-index --quiet');

 exec('grunt {{task}}', function (err, stdout, stderr) {

 console.log(stdout);

 // restore stashed changes
 sh('git stash pop --quiet');

 var exitCode = 0;
 if (err) {
 console.log(stderr);
 exitCode = -1;
 }
 process.exit(exitCode);
 });
 }

});

And further set up in your Gruntfile:

// Gruntfile.js

 githooks: {
 all: {
 options: {
 template: 'hooks/pre-commit.js'
 },
 'pre-commit': 'test'
 }
}

// Register githooks as a default grunt task
grunt.registerTask('default', ['githooks', 'test']);

I've put this all together in a sample application at https://github.com/vigetlabs/grunt-git-hooks-demo. Now you're ready to start playing with pre-commit and anything else you want to add. Don't forget to run githooks (grunt or grunt githooks) whenever you make changes to hooks in your Gruntfile!

I hope you've learned something new around improving your workflow. Please share how you use hooks and Grunt in the comments below!

Chris is a front-end developer who's passionate about web performance. He works in our Durham, NC, office for clients such as ESPN, Dick's Sporting Goods, and the Wildlife Conservation Society.

More posts by Chris