If the answer is “never”, this article isn’t for you, if the answer is “sometimes” or “all the time”, keep reading.
At ResponseTap, in one of the first stages of our build pipelines we run some code scans (linting/code quality) and tests in parallel. Whilst this works pretty well for us, on occasion we see a failed pipeline that has halted due to one of these checks failing. This ultimately ends up in a commit like the one above, one that should have been caught before it made its way into the repository.
You may think it’s the responsibility of the Software Engineer to catch these things before they are committed, and you’d be right, so why does it happen?
The answer will vary massively, but could be as simple as not having the right tools set up on your machine. However, the “why?” doesn’t matter so much if we can put a stop to these types of issues before the code is even committed.
In this article we’ll explore how we can use Git Hooks, specifically the “pre-commit” hook to help us mitigate these issues.
How can the pre-commit hook help?
pre-commit script is executed every time you run
git commit. You can use this hook to inspect the changes that are about to be committed and act upon them.
Setting up a pre-commit hook is fairly simple and very powerful, let me show you.
This guide assumes you’re working in a project that uses NPM as a package tool. For us, that’s our front end projects or our APIs and services that we bootstrap with the Serverless framework. You can set up Git Hooks in any git project but using NPM gives us easy set-up for anyone who clones the project.
With that said, the following examples use husky, an NPM module that sets up the relevant Git Hooks after
npm install, and lint-staged, which in combination with husky allows you to run any command against any staged files in a commit.
To get started run the following command to add the packages to your project:
npm install --save-dev husky lint-staged
Run your linter against staged changes
To get going with a simple example we’ll start by setting up husky and lint-staged to run our linter on any commits. Add the following to your package.json.
Upon making a change and attempting a commit, the staged files are handed to the linter and checked against any linting rules you have defined.
Any issues cause the commit to be aborted, preventing poor quality code making its way into the code base.
Auto-fix linting issues
Similar to above, this config passes staged files to ESLint, but this time with the
--fix flag. This flag tells ESLint to try to fix any issues that are auto-fixable. We then re-stage the changes. Any issues that aren’t able to be fixed cause the commit to be aborted.
For such a quick change to your set up, this saves a bunch of time for those little formatting issues.
Fix all the things
This example uses Prettier (an opinionated code formatter). Prettier is great because you don’t have to worry about setting up your own lint rules for simple formatting. It can also fix a bunch of other formats, such as YAML, JSON, Markdown, etc.
Now on upon an attempted commit, the staged files will be formatted by Prettier and re-staged if successful.
If you want to use Prettier with ESLint to catch code issues and handle formatting, you can combine them together. I won’t go into to much detail here, but eslint-plugin-prettier allows you to apply Prettier formatting using
If you’re only interested in running Prettier against your staged files, you can use pretty-quick as an alternative to this set-up, which should be easier to get started with.
For python projects, you might want to have a look at Black.
Run tests against staged changes
This one may seem a little far fetched but can actually save you from a further set of wasted commits…
git commit -m “fix failing tests”
With Jest, the
--findRelatedTests flag allows you to find and run tests that are related to a list of files you pass in as arguments. In this example, lint-staged passes the list of staged files from the commit.
Woop! Before committing we can now confirm that we haven’t broken any existing functionality that you had covered with tests.
Putting it all together
There’s nothing new in this that we haven’t already discussed, but here we bring all of our examples together. For a project with all this bootstrapped check out the link at the end.
Each second saved adds up to more time to get your work done
Hopefully you can now see some of the benefits of using pre-commit hooks for checking the quality of your work.
No longer do you have to worry about the little formatting issues, nor the linting fails that escape your IDE linter, let alone the shame of your changes publicly breaking all the tests.
What you have achieved is a commitment to quality, where code is formatted consistently and tests are confirmed to be successful before code leaves your machine. Where failed commits aren’t actually commits, and you have only had to wait a matter of seconds to get some feedback on what’s gone wrong. Heck, you’ve even managed to save a minute or two by auto-fixing some of the issues. Those seconds and minutes add up.
However, had those things been committed, you might waste time discovering the issues in a code review, and then waste even more time arranging a follow up when you’ve fixed the issues.
Had your changes made it through to the build pipeline, you might sit and wait for the linting and testing checks to be run, which will fail, and then you’ll be shamed by your colleagues for pushing trash code again. 💩
You get my point, I’m not saying this is going to make you a better developer but it will make you faster and save you some shame.
The world’s your oyster when it comes to what you can set up to run on a pre-commit hook, do you have any ideas of what might be useful?