Get hooked on Git hooks
Last updated: 16.12.2022
If you're like me, you're crazy about automating boring stuff. One of the things I got hooked on (pun intended) during the last year, and which helps in that automation process, is Git Hooks. If you haven't heard of Git Hooks and want to see some cool ways of improving your daily git workflow stay tuned!
What are Git Hooks? 🎣
This page from Git documentation sums it up pretty well but in general Git Hooks are Git's answer on firing custom events when some Git related action occurs. We will focus on client-side pre-commit
and commit-msg
hooks today but the following options are available:
-
Client-Side Hooks
pre-commit
- runs before we even type the commit message.prepare-commit-msg
- runs before the commit message editor is opened up but after the default message is created.commit-msg
- a good place to validate the project state or the commit message before allowing the commit to proceed further.post-commit
- runs after the entire commit process is completed, used mostly for notifications.pre-rebase
- runs before the rebase.post-merge
- runs after the successful merge.pre-push
- runs during the Git push.pre-auto-gc
- runs before Git triggers a garbage collector.
-
Server-Side Hooks
pre-receive
- the first script that is run on the client-side push, if it exits non-zero, the push is not accepted.update
- pretty similar to thepre-receive
except it runs once for every branch that the client-side wants to update. For example, if we're pushing to five branches at the same time,pre-receive
will run once, andupdate
will run five times.post-receive
- similar to the client-sidepost-commit
just on the server side.
Talk is cheap, show me the code
Since Git hooks don't have the best out-of-the-box experience, we'll use the Husky library to make stuff easier:
npm install husky --save-dev npx husky installnpm pkg set scripts.prepare="husky install"npx husky add .husky/pre-commit "npm test"git add .husky/pre-commit
These scripts will install husky, enable git hooks, automatically have Git hooks enabled after install and add simple pre-commit hook that will run npm test on every commit
Alternatively, you can just run npx husky-init && npm install
which will do the same.
pre-commit
In most of cases we want to run the pre-commit
hook only on the staged files, lint-staged library helps us with that:
npm install lint-staged --save-dev
After we've added the lint-staged
we need to extend package.json
file like this:
To run scripts defined inside of the lint-staged
object we need to modify our pre-commit
file that was generated by Husky earlier:
Now that we know the basics, it's time to start adding scripts that will help our repository become a better place ✨.
First, let's add prettier - hope you've heard of it since it's the best thing that happened to code formatting in a while.
npm install prettier --save-dev
We can pass arguments to the prettier script directly but I'm a fan of config files, so we'll create a .prettierrc
file in the project root directory:
Prettier will format all staged files on the commit so they follow a code convention defined inside the .prettierrc
.
Time to lint our .js
files, we can easily do that with eslint.
npm install eslint --save-dev
We will define a config file again, this time the eslintrc.json
:
We need to define a special rule that will be triggered for .js
files only. eslint
will prevent committing if error is thrown.
As the final step I'll show you how to run relevant unit tests (relevant to committed files) and prevent commit if some of them are failing.
npm install jest --save-devnpm install eslint-plugin-jest --save-dev
We should add the previously installed jest plugin to our eslint config file so we eliminate eslint errors on .spec.js
files.
Now extend lint-staged
script:
--bail
will skip execution of other tests when the first test fails and --findRelatedTests
is pretty self-explanatory 😁.
To demonstrate how this works we can create two files test-file.js
and test-file.spec.js
We're intentionally making the unit test fail so we can see the commit failing:
commit-msg
There are only two hard things in Computer Science: cache invalidation and naming things
This rule applies to commit messages also, we've all seen or written commits like this in past:
git log --oneline7c1f5c5 final fix93393a0 aaaaa3626b1d TEST WIP45bc996 small css fix29b2993 css final final fixa2f6e18 lol3ae828c UNIT TESTS ADDED WOO
This is an extreme example but it perfectly shows how we can't make a clear conclusion about what is going on in a particular commit.
If we check the history of commit messages created during previous examples:
git log --oneline2c1f5c5 feat: add jest testing85bc9g6 refactor: reformat html file
Much cleaner right? These commits follow Conventional Commit convention created by Angular team.
In general, the pattern that a commit message should follow mostly look like this:
type(scope?): subject #scope is optional
Some of the common types are:
feat
- commit adds a new feature.fix
- commit fixes a bug.docs
- commit introduces documentation changes.style
- commit introduces code style change (indentation, format, etc.).refactor
- commit introduces code refactoring.perf
- commit introduces code performances.test
- commit adds test to an existing feature.chore
- commit updates something without impacting the user (ex: bump a dependency in package.json)
So, now that we know this, it's the perfect time to introduce commit-msg
hook where we'll check if the commit message respect these rules before we commit.
First, we want to install commitlint, something like eslint just for commit messages.
# install commitlint cli and conventional confignpm install --save-dev @commitlint/{config-conventional,cli}
Of course, we need to create another config file, .commitlintrc.json
, the last one I promise! 🤞
Last but not least, we need to add commit-msg
hook using husky add
command:
npx husky add .husky/commit-msg 'npx commitlint --edit "$1"'
Quick recap of what we learned today:
lint-staged
inside the pre-commit
hook will take care of:
- formatting all staged files via Prettier.
- check all staged
.js
files for syntax errors via Eslint - check if relevant
.spec.js
unit test files are failing before we commit via Jest
commitlint
inside the commit-msg
hook will take care of:
- enforce commit message to follow Conventional Commit rules via Commitlint.
See also
- cz-cli - The commitizen command line utility.
- husky-sandbox - Code samples from this post.