This is a blog post I’ve refactored from a talk I gave at an engineering brown bag in June 2024.
I’m a developer with lots of bad habits. Those bad habits become a lot more annoying for other devs to have to deal with. Sometimes large code bases have A LOT of code with other people with some bad habits. I think using a small and lean set of git hooks could prevent bad habits making it to remote.
Git hooks
Git hooks allow you to fire off custom scripts when certain important actions occur. There are two groups of these hooks: client-side and server-side. Client-side hooks are triggered by operations such as committing and merging, while server-side hooks run on network operations such as receiving pushed commits.
Husky
Husky allows you to execute local commands before your client-side Git hooks run. If these commands finish with a nonzero exit code then the current git action will fail. A local command can be a JavaScript package manager command or a POSIX shell script. If your command finished with a nonzero exit code then the hook will fail and the current git action will not complete.
Lets install Husky.
bun add -D husky
bunx husky init
The husky init
command simplifies setting up husky in a project. It updates the prepare script in package.json as well as creating an example hook script. Running husky on the prepare lifecycle hook means it will be installed for other contributros to the project, and the same hooks and commands will run for them without any further setup required.
Lint-staged
I couldn’t write a better introduction to lint-staged than the authors themselves.
Run linters against staged git files and don’t let 💩 slip into your code base!
Linting and formatting and entire project can be slow and generally not necessary with every time you push code to a remote. Linting and formatting the files we are making changes to is sensible.
If the process is automated and should the linting check or formatting check fail lint-staged will finish with a nonzero exit code.
I think a clear benefit of something like this is that you can adopt it into an large existing codebase that has stood the test of time but might… smell a bit; and start gradually improving the quality of the code knowing that new logic follows the linting standards and is also well formatted.
Lets setup lint-staged
bun install -D lint-staged
add the following to package.json
{
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"prepare": "husky",
"lint-staged": "lint-staged",
"eslint": "eslint"
}
}
define the lint-staged
command in package.json
{
"lint-staged": {
"**/*": ["eslint", "prettier --write --ignore-unknown"]
}
}
Lets run lint-staged on the pre-commit
hook. To do this; add a new commit hook file in .husky
called pre-commit
. The content of that new file is going to tell bun to run the lint-staged
command
touch .husky/pre-commit
# .husky/pre-commit
bun run lint-staged
We now have a higher degree of assurace that code commited to the repository will be well formatted and follow the linting standards. I say higher degree
of assurance because these can of course all be bypassed locally using the force
.
Commitlint
Its also not a terrible idea to have some linting on our commit messages. Commitlint offers simple no-frills approach that does a lot for you. It comes with a nice way of using the conventional commits spec so you really don’t have to think about it too much.
bun install -D @commitlint/cli @commitlint/config-conventional
add a new commit hook file in .husky
called commit-msg
. The content of that new file is going to tell bun to run the commitlint command we’re about to setup in
our package.json
# .husky/commit-msg
bunx --no -- commitlint --edit "$1"
in your package json
{
"commitlint": {
"extends": ["./node_modules/@commitlint/config-conventional"]
}
}