Simple pre-commit git hooks with Husky

Have you ever wondered if there was any way to run a script before you commit something? Who has the time to peruse our file changes nowadays anyway? If the answer is yes and you agree that time is a meaningless concept, then you have come to the correct place. What if you didn’t have to write those scripts in shell but in lovely JavaScript? Husky is the answer.

What is it?

Husky is in the author’s own words:

Git hooks made easy

Well, I’m sold. The README on GitHub says that I only need to install husky and add a husky object to package.json. It also states that all git hooks are supported. You can find a list of them all on the official git documentation here.

How to use it?

Everything seems to go smoothly so far, but as I know from experience, README’s seems fine and dandy when you read it. The problems occur when you are going to put it into practice. But, we shouldn’t jinx it, let’s keep calm and stay positive.

We are now entering a parallel universe with a ton of bugs to squash. Let’s say I have an important JSON document that I want to validate before I commit my changes. I do not want to commit an invalid version of that document because it will break my CI builds.

I start with creating a directory and running git init in my console to initialize a git repository. After that, I run npm init to create a package.json.

After that, I add the critically JSON document as described further up. I will name the document settings.json and it looks like this:

{
  "api_version": 1,
  "api_versions": [1, 2],
  "default_locale": "en-us"
}

Try not to focus on the content of this JSON file as this is purely foobar content. As we were saying earlier, the developer shouldn’t commit an invalid JSON file and we can also say they are not allowed to use an api_version that is not defined in the api_versions list.

After that, we should add and install husky like so npm install husky --save-dev in the console. When that is done we can add a property to our package.json so it looks like so:

{
  "name": "husky-tut",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "devDependencies": {
    "husky": "^1.3.1",
    "simple-git": "^1.107.0"
  },
  "husky": {
    "hooks": {
      "pre-commit": "node ./"
    }
  }
}

In this example I will use the simple-git package to retrieve the staged files.

Now we should start creating our script so we will create a new file index.js. First, we will import some dependencies and declare a variable:

'use-strict'

const git = require('simple-git/promise')
const fs = require('fs')

// Variables
let error = null

Followed by a helper method that simply validates the JSON:

function isJson (text){
  if (typeof text !== 'string') {
    return false
  }

  try{
    JSON.parse(text)
    return true
  }
  catch (error) {
    return false
  }
}

Then we will get to the core of our pre-commit logic; the check of our settings.json file:

async function validateChanges() {
  // Check if there are any files staged
  const diffSummary = await git().diffSummary(['--staged'])
  if (diffSummary.files.length === 0) {
    return
  }

  const settings = diffSummary.files.find(f => Object.is(f.file, 'settings.json'))
  if (settings === undefined) {
    error = 'The settings.json is invalid.'
    return
  }

  // Validate JSON document
  const settingsStr = fs.readFileSync(__dirname + '\\' + 'settings.json', { encoding: 'utf8' })
  if (!isJson(settingsStr)) {
    error = 'The settings.json is invalid.'
    return
  }

  // Check that the api_version exists
  const settingsJson = JSON.parse(settingsStr)
  if (!settingsJson.api_versions.includes(settingsJson.api_version)) {
    error = 'The api_version doesn\'t exist in the api_versions array.'
  }
}

validateChanges()
  .then(() => {
    if (error !== null) {
      console.log('Error: ' + error)
      process.exit(1)
    }

    console.log('Commit is ok.')
  })
  .catch((err) => {
    console.error(err)
  })

So if we have done everything correctly, we should now have a working git pre-commit hook.

That’s all for today folks. Happy pre-committing.