Automating tasks with Grunt and Git hooks

Using Grunt and Git to speed up a more auto­mat­ic work­flow (Grunt and Git, sit­ting in a tree, K-I-S-S-I-N-G)

Requirements

To be able to use this arti­cle, you are expect­ed to have Grunt and Git installed.

Git Hooks

What are Git Hooks?

Git hooks are pre-defined scripts that run at par­tic­u­lar points in the git work­flow. They are stored in the fold­er .git/hooks, and each of them is named for the par­tic­u­lar action they hook into, like pre-commit, post-commit, post-merge, etc. The lan­guage they’re writ­ten in is some­what irrel­e­vant, as long as they’re a valid shell script of one flavour or anoth­er.

How can they be used?

How exact­ly each hook can be used depends, obvi­ous­ly, on where it gets trig­gered in the flow. If you look in the fold­er .git/hooks, there are sev­er­al scripts end­ing with .sample. Not too sur­pris­ing­ly, each of these is a sam­ple for a hook, with some­thing that the good peo­ple who work on Git feels might be use­ful. They are not acti­vat­ed by default, you would have to remove the .sample. Note that they are also not an exhaus­tive list of hooks.

The hooks come in two flavours: Some can stop the par­tic­u­lar action they hook into (such as pre-commit and commit-msg, which both will can­cel the com­mit if they exit with a non-zero exit code), while oth­ers react to the action they hook into, but can­not stop it from hap­pen­ing.

The main issue with git hooks is that not only are they on a per-repos­i­to­ry basis, they are on a local per-repos­i­to­ry basis, which means that you have to in some way auto­mate copy­ing the hooks over to the hooks direc­to­ry after cloning the project, and you also need to auto­mate ensur­ing that the hooks are up-to-date. Of course, it is pos­si­ble to leave it up to each user to remem­ber to acti­vate the hooks, but we can do bet­ter than that, no?

Enter Grunt, stage left!

Grunt Githooks

The Githooks task by Romar­ic Pas­cal takes a few con­fig options (includ­ing the pos­si­bil­i­ty of a spe­cial­ized tem­plate), and when you run grunt githooks, it cre­ates the hooks you’ve defined. By default they’re writ­ten in Node­JS, since that’s cross-OS and will be installed if you’re run­ning grunt tasks. You can define that it should be anoth­er lan­guage, how­ev­er, espe­cial­ly if you’re want­i­ng to add the hook to a script already defined.

As this arti­cle would be far too exhaus­tive with all options, details of the tasks can be found linked in the ref­er­ences below.

To install, run npm install --save-dev grunt-githooks in the ter­mi­nal, just like any oth­er grunt task.

Example git hooks

To get the full source code, refer to this github repos­i­to­ry. It runs three dif­fer­ent hooks, one of which is run­ning a grunt task.

All templates/config files for the tasks are expect­ed to reside in hooks, and out­side of that it fol­lows the stan­dard Webapp gen­er­a­tor of yeo­man, with some of the unnec­es­sary cruft cleaned away.

Pre-commmit

This is the most com­mon git hook I’ve used. I want to ensure that noth­ing sil­ly gets into the live site, where “sil­ly” can range from syn­tax errors, to white­space errors, to break­ing tests, all depend­ing on the par­tic­u­lar sit­u­a­tion of your project. Mine assumes you have a task in your Grunt­file named commit, with whichev­er tasks are rel­e­vant for your project.

grunt.initConfig({
    githooks: { // Definition of the task config
        stage: { // Name it something relevant
            options:{
                template: 'hooks/stage.js.hbs' // the Handlebars template the hook is rendered from
            },
            // Each hook can be defined here with whatever task is used
            'pre-commit': 'commit' 
        }
    }
)};

Now, based on the arti­cle by Gar­rett Mur­phey I came to a few con­clu­sions on how I want to build up the hook. In par­tic­u­lar, the pre-com­mit tasks should:

  • Not run on rebase
  • Only run on staged changes

With help from Romar­ic Pas­cal I found this arti­cle which shows how to accom­plish the sec­ond point, and with some exper­i­men­taion I accom­plished the first point in Node­JS (using execSync and the exec func­tion rather than the run func­tion).

var exec = require('child_process').exec;
// https://npmjs.org/package/execSync
// Executes shell commands synchronously
var sh = require('execSync').run;
var branchName = require('execSync').exec('git branch | grep \'*\' | sed \'s/* //\'').stdout;

// Don't run on rebase
if (branchName !== '(no branch)') {
    exec('git diff --cached --quiet', function (err, stdout, stderr) {
        /*jshint unused: false*/
        'use strict';

        // 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);
           });
        }
    });
}

Note the /*jshint unused: false*/ in the first call to exec. It’s there because oth­er­wise jshint (yes, I lint my hooks as thor­ough­ly as I do my oth­er JavaScript files) would object to some of the vari­ables not being used.

Could you test for oth­er things in the tem­plate? Of course, this is a start to give you a feel for it. As long as there’s a spe­cif­ic pass/fail con­di­tion and a fail exits with a non-zero code you can test for oth­er things. The sam­ple hook also tests for non-ascii file names and white­space errors.

Running updates of dependencies when they’ve changed

Okay, so the title of this sec­tion might be slight­ly of a mis­nomer. It’s not tech­ni­cal­ly it run­ning updates when­ev­er a depen­den­cy has changed, but rather that when you pull from ori­gin (or, for that mat­ter, from upstream), you want it to auto­mat­i­cal­ly update any depen­den­cies if there have been changes.

As each project might have dif­fer­ent files that should be checked, the tasks reads a JSON-file, hooks/data/update.json, which holds sev­er­al objects with a file and a com­mand defined.

[
    {
        "file": "package.json",
        "command": "npm update"
    }
]

The tem­plate, inspired by this post on Stack­Over­flow uses git diff to check if the file has changed, and if it has, it will log a response to that effect before run­ning the update com­mand.

var object = require('../../hooks/data/update');
for (var i in object) {
    file = object[i].file;
    command = object[i].command;
    fileChanged = (shOutput('git diff HEAD@{1} --stat -- ' + file + ' | wc -l').stdout > 0); 

    if (fileChanged) {
        console.log(file + ' has changed, dependencies will be updated.');
        sh(command);
    }
}
process.exit(0);

Note that this doesn’t use any grunt tasks at all. There is no tech­ni­cal way to deal with that in the cur­rent task, but you don’t real­ly need to use the bound val­ues in your tem­plate unless you want to. The var­i­ous options for bind­ing val­ues to the tem­plate are based on using the default tem­plate sup­plied by this task.

Okay, so that deals with the options and the tem­plate. What about the hooks?Well, depending on how you call thegit pull‘, it will trig­ger one out of two hooks.

Git pull, no arguments

A git pull with­out argu­ments is a git fetch fol­lowed by a git merge, so the best place for an update hook in that case is the post-merge hook.

Git pull –rebase

If you’re using --rebase, on the oth­er hand, it runs git fetch fol­lowed by git rebase, which means that the post-checkout hook is the prop­er place for one. This, obvi­ous­ly, means we’ll need to bind two hooks to the same tem­plate. We can do that!

grunt.initConfig({
    githooks: { // Definition of the task config
       update: { // Name it something relevant
            options: {
                template: 'hooks/update.js.hbs' // the Handlebars template the hook is rendered from
            },
            // Each hook can be defined here with whatever task is used
            'post-merge': true,
            'post-checkout': true 
        }
    }
)};

Moving forward

Alright, so now we have three hooks that gets ran at dif­fer­ent points in the work­flow. From what I’ve read in the docs of grunt githooks you might be able to define sev­er­al snip­pet-tem­plates and mix-and-match, but I haven’t yet explored that.

One final thing to ease the use of this, how­ev­er:

Automatically running grunt githooks after npm is installed

So, we’ve auto­mat­ed it run­ning linting/tests/etc before every com­mit, and that it will check if depen­den­cies needs to be updat­ed after every pull. We still need to run grunt githooks to ini­ti­ate the hooks, yes?

Yes, but Node­JS is smart and can do it for us, by defin­ing a postin­stall script in package.json. Just paste the fol­low­ing lines into your package.json file, and run­ning npm install will auto­mat­i­cal­ly run grunt githooks.

"scripts": {
     "postinstall": "grunt githooks"
}

References


For the moment comments are not enabled, but feel free to reach out on Twitter.