What’s a git hook and how we can manage them?
Let’s assume that you already know what git hook is, where they must be placed and how they can be managed. In case you don’t I would try to fill you in.
Git hook is a script that’s run when we perform specific git operations in our repository. Here’s a list of some git hooks:
- pre-merge-commit;
- prepare-commit-msg;
- commit-msg;
- post-commit.
The full list of hooks can be found here.
Git hooks must be placed in your repository in .git/hooks folder. So, if you want to set up, e.g., a pre-commit hook, you should create pre-commit file in that folder.
For instance, I might have a pre-commit git hook with the following script: echo "My favourite band is Red Hot Chili Peppers"
. And once I commit it's going to be triggered:
There are many libraries that help developers manage git hooks: Husky, Simple git hooks, Lefthook, etc..
The problem
Recently I needed to set up a pre-commit hook to lint staged files within the following git repository:
- Git repo contains of 100+ Node.js apps, the repository itself is not an app.
- Each project already contained its own eslint config, given different code owners all existing configs must be preserved.
- Files must be linted even if changes are made to multiple projects.
I bet you got the idea, but here's how its structure looked like:
project/
src/
project-1/
package.json
project-2/
package.json
project-3/
package.json
...
.gitingnore
README.md
I am not going to create 100 of demo apps just to show you my pain, a couple of demo projects should be enough, I believe.
This is our starting point, the project now looks like this:
project/
src/
demo-project-1/
package.json
demo-project-2/
package.json
.gitingnore
README.md
To solve the problem we are going to use:
- npm workspaces
- eslint
- lint-staged
- husky
Let’s get job done!
Npm workspaces
According to documentation:
Workspaces is a generic term that refers to the set of features in the npm cli that provides support to managing multiple packages from your local file system from within a singular top-level, root package.
Given that I want to have all listed packages installed only once, having workspaces set up in repository’s root with eslint, lint-staged & husky as dev dependencies would do the job.
- In order to quickly create package.json run
npm init
in the root. - Add workspaces property to the newly created package.json file, saying that all apps inside the src folder are now parts of workspaces.
At this point the file looks as follows:
{
"name": "monorepo-with-pre-commit-git-hook",
"version": "1.0.0",
"main": "index.js",
"workspaces": ["src/*"],
"author": "eduard mavliutov",
"license": "ISC",
"description": ""
}
Inner packages won’t even notice that each of them now is a part of workspaces.
Eslint
Let’s ensure all packages use the same version of eslint by installing eslint & eslint-config-airbnb-base dependencies in the root package.json.
Lint-staged
To lint staged files we’re going to use the lint-staged package: it’s easy to configure and supports different configurations per each project.
We are going to install it in the root project:
npm install --save-dev lint-staged
Ok, now let’s configure it for linting js files. There are multiple ways to do that, you can check it out yourself. We will go with lintstagedrc.json file per project. That option suits our needs the most:
- lintstagedrc.json can be updated separately per project;
- lintstagedrc.json can have different sets of tasks per project.
For demo purposes each project will use the same config:
{
"**/*.{js,ts}": [
"eslint --fix"
]
}
To run the package we need to use npx lint-staged
command, we’re going to use the command in the pre-commit hook, but we can give it a go now:
Husky 🐶
Again, we’re going to install it in the root project.
npm install --save-dev husky
Now we need 2 more things:
1) Create pre-commit file with npx lint-staged
command in it. We're going to use these commands in order to do that.
mkdir .husky
echo "npx lint-staged" > ./.husky/pre-commit
2) Create prepare command in package.json.
"scripts": {
"prepare": "husky"
}
Ok, we installed eslint, lint-staged and husky in the root project. Let’s clean up a bit to ensure that necessary dependencies are installed only on the top level. I am going to add a couple of new scripts right after the prepare one:
"scripts": {
"prepare": "husky”,
"clean": "bash -c \"rm -rf node_modules && rm -rf ./src/*/node_modules\"",
"reinstall": "npm run clean && npm install"
}
The final package.json looks like this:
{
"name": "monorepo-with-pre-commit-git-hook",
"version": "1.0.0",
"main": "index.js",
"workspaces": [
"src/*"
],
"scripts": {
"prepare": "husky",
"clean": "bash -c \"rm -rf node_modules && rm -rf ./src/*/node_modules\"",
"reinstall": "npm run clean && npm install"
},
"devDependencies": {
"eslint": "8.52.0",
"eslint-config-airbnb-base": "15.0.0",
"husky": "^9.1.7",
"lint-staged": "15.4.3"
},
"author": "eduard mavliutov",
"license": "ISC",
"description": ""
}
Run the reinstall command and we’re ready! Now let’s get mischievous!😈
Showtime
I am going to break some eslint rules in /src/demo-project-1/index.js
and in /src/demo-project-2/index.js
And now let’s try to commit that. For the sake of demo I am going to open my terminal in demo-project-1’s folder.
Thus, we have one eslint error in one of files and a couple of warnings in another, I tried to commit that and it didn’t work. Cool!
Let’s fix the error that’s blocking the commit and leave fixing the rest to eslint:
Yay!
Summing up
Here's what we have in the end:
- Our projects remain independent even if each of them is a part of workspaces.
- In each project staged code is linted in the pre-commit git hook.
- If developer works on multiple projects at once, all staged files are linted no matter the project.
- Each project may have its own eslint config OR all projects may use one eslint config.
Top comments (0)