Save Yourself from Yourself

Using pre-commit hooks in git to avoid common mistakes and embarrassment

01-10-2019

We’ve all submitted a pull request to our team for review and at some point received feedback that we didn’t indent properly or missed a space or semi-colon. Not the type of feedback you want to hear. It can be embarrassing.

In recent projects I’ve added a few new tools to my workflow to help improve efficiency and consistency across the projects and teams I work on. Things like Prettier for code formatting, ESLint for code linting, and Jest for code testing are all great tools. I’ve used them or some variation of them for many years. But.. there’s still a time, every few months or so, that I forget to run one of them before committing my code and submitting a pull request.

To save myself and members of my team the embarrassment I’ve started using pre-commit GIT hooks and a package called Husky to ensure that before code is committed we are taking the appropriate steps to ensure code quality.

Here’s how it works. After I make code changes, it’s really a thing of beauty, and stage the files for committing, I run a command something like git commit -m "Saving the world with a new config file". Prior to GIT actually committing a file(s) to my local repository it first looks for any pre-commit hooks and executes those first. By leveraging a package like Husky we can define commands we want to run first. If everything is successful the commit will happen no problem. If everything isn’t fine, like I have a failing test, etc. then the power of Husky really shines.

For example, before I commit code I always want to make sure it meets our code formatting standards, no tabs v. spaces here, by running Prettier. I might have an npm command like:

package.json

1
2
3
4
5
"scripts": {
...
"format": prettier --write src/*
...
}

To enable a pre-commit hook with Husky is easy. First, we need to add Husky to our package.json so that everyone on the team has the save functionality.

1
npm install husky --save-dev

Next we need to setup our package.json to listen for commit commands and run our pre-commit hooks fist.

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
...
"scripts" : {
"format": prettier --write src/*
},
{
"husky": {
"hooks": {
"pre-commit": "format"
}
}
}
...
}

This works great and can help enforce code quality by having all of the processes run before a file can every be committed to the repository. But as our applications grow with more files and more tests this process can slow down developer productivity, not what we’re trying to do. Fortunately there is an npm package called lint-staged that helps solve this by identifying on the files that are “staged” with git and running commands against those files. So if your commits are small (please make them small!) then running code quality, linting, and tests should be pretty quick.

To add lint-staged to our pre-commit hook solution we need to first install it in our project.

1
npm install lint-staged --save-dev

Then in our package.json we need to make a couple changes. First we update our Husky object pre-commit hook to call “lint-staged”. This will allow lint-staged to take over and execute only the commands we want on just the staged files. In the below example I’m telling Jest to “bail” if any of the related tests for the staged files fail with the --bail and --findRelatedTests flags.

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
...
"scripts" : {
"format": prettier --write src/*
},
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
jest --bail --findRelatedTests
]
}
}
...
}

Lint-Staged allows us to pass in multiple commands into the array which is great for ensuring all of our steps run prior to committing our code and letting our team or continuous integration (CI) process do our jobs :).

The final package.json might looks something like this:

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
...
"scripts" : {
"format": prettier --write src/*
},
{
"husky": {
"hooks": {
"pre-commit": "format"
}
},
"lint-staged": {
"*.js": [
"npm run format",
"git add",
"jest --bail --findRelatedTests"
]
}
}
...
"devDependencies": {
"husky": "1.3.1",
"jest": "^22.4.3",
"lint-staged": "8.1.0",
"prettier": "1.15.3"
}
}

With everything configured, now when I run “git commit -m ‘I amaze myself sometimes’” I can be assured that everything will be up to snuff with my code before it’s sent to the main repository. And if it’s not, I will receive a fun little message like this:

A quick fix to a broken test and no ones the wiser :)

Using pre-commits like this can really save your bacon allowing you and your team to focus on the things that matter. Like saving the world one {} at a time!