Backstory
Some time ago I was asked to introduce an automatization which would check if committed files fit linter rules responsible for uniform code formatting and code quality (e.g.: eslint, prettier, stylelint e.t.c.)
After I did some research it came out that the most common way to do that is to use husky with lint-staged. I installed and configured those tools. Everything worked as expected. If the file contained any errors which couldn't be auto-fixed by linter, committing process was interrupted and the error message was shown in the terminal. Unfortunately, this solution has one problem. Running husky and lint-staged takes much more time than I expected. Sometimes it even took more time than the committing process itself (including checking the files for any errors).
Git-hooks
As I had some time left after I completed this task I decided that I will look for another solution. I searched a little more and I found git-hooks. I read a bit more about git-hooks and it came out that git offer native solution to do some custom actions at certain points in git execution for example committing changes. pre-commit caught my attention, which is briefly described like this:
"This hook is invoked by git-commit[1], and can be bypassed with the
--no-verifyoption. It takes no parameters, and is invoked before obtaining the proposed commit log message and making a commit. ..."
From the above text it follows, that before commit becomes submitted we have some time to execute custom operations like linting and auto-fixing staged files. All files changed in this phase can be added and included in the same commit (it means that we do not have to create a separated commit to apply changes from linters auto-fixes). After I read some about shell scripting I was ready to create my first git-hook
pre-commit
#!/bin/sh
RED="\033[1;31m"
GREEN="\033[1;32m"
NC="\033[0m"
linter_exit_code=1
all_ts_files=$(git diff --cached --diff-filter=d --name-only | grep .ts$)
all_scss_files=$(git diff --cached --diff-filter=d --name-only | grep .scss$)
./node_modules/.bin/eslint $all_ts_files --quiet --fix && ./node_modules/.bin/stylelint $all_scss_files --stdin --quiet --fix
linter_exit_code=$?
git add -f $all_ts_files $all_scss_files
if [ $linter_exit_code -ne 0 ]
then
echo "${RED} ❌ Linter errors have occurred ( ͡ಥ ͜ʖ ͡ಥ)${NC}"
exit 1
else
echo "${GREEN} ✔ Eslint and Stylelint did not find any errors [̲̅$̲̅(̲̅ ͡° ͜ʖ ͡°̲̅)̲̅$̲̅]${NC}"
exit 0
fi
What is going on in above code:
-
git diff --cached --diff-filter=d --name-only | grep .ts$→ we are collecting all staged files, then we are filtering out deleted ones (if you do not do that your linter will throw an error for those files because this linter won't be able to resolve paths for deleted files) then I am usinggrepto take only files which I am interested in. In my case, I am collecting.tsfiles foreslintand.scssfor stylelint, -
linter_exit_code=$?→ save exit code of last executed action(0in case no errors or errors that can be auto-fixed by linter or1in case of errors not fixable by linters) -
git add -f $all_ts_files $all_scss_files→ add files auto-fixed by linters. We need to use-fflag to forcegit addin case of$all_ts_filesand$all_scss_filesare empty - At the end of this script I am displaying proper information basing on exit code value
After we create a git-hook we have to remember to update git configuration or create a symlink between git configuration and created git-hook:
-
gitcommand (should work for every operating system)
git config core.hooksPath ./git-hooks -
symlink (Linux)
ln -s -f ../../git-hooks/pre-commit .git/hooks/pre-commit
It is worth to add above scripts to npm postinstall, because of that every developer which will clone our repository and run npm install script will also configure git-hooks
Summary
Using git-hooks instead of husky and lint-staged came out to be an excellent idea because committing time was sped up about twice. In addition, I got rid of two additional dependencies in the project, what can become very useful especially in case of .husky because from Husky 5 documentation we can find out that Husky 5 will be free only for open-source projects
Seven steps to set up git-hooks
- In project directory create
git-hooksdirectory - Go to
.git/hooksdirectory - From the name of hook which you want to use remove
.sample - Move this hook into created
git-hooksdirectory - Create your
git-hookbody - Update
gitconfiguration or create a symlink fromgit-hooksto.git/hooksdirectory - Add the appropriate script to
npm postinstallcommand
Simple example
I prepared a simple repository git-hooks-example to prove that those git-hooks will work on Linux / Windows / Mac. In Redme.md I wrote how you can test this pre-commit hook.
Latest comments (32)
"Husky 5 will be free only for open-source projects." Is this still valid? It looks like they had second thoughts.
Thank you for the information :D I have updated my post
Good idea!
I also run
tscto check my TypeScript code. Here you go my git pre-commit hook (based on yours; thanks):I think this will fail if you're using
isolatedModulesThis is great as it brings awareness of the native git functionality around commit hooks. Love it 🤓
It might be worth checking this: it looks like you aren't checking for extensions, just end of strings with your TS and SCSS file grep (
/ts$vs/\.ts$/). Thus you could have commit hooks failing because a file called "starts" or "boots" for example would be linted and likely fail if there's any non-JS code in there.I am glad to hear that 😃.
You are right I should look for
.ts$(For grep you do not have to escape the.character 😉)Well git hook can't be the new husky because husky use git hooks ... (BTW
After the early access, husky v5 will be MIT again.)Husky is a simple way to have the hooks under versioning and shared between all the devs ... sure you can reinvente the wheel again but the whole post looks like a big misunderstanding of husky.
There's nothing wrong in manually create your link to emulate what husky already does but "Git hook SURELY ISN'T the new Husky"
There is one limitation which comes with this solution - all git hooks become source-controlled.
The whole idea having git-hooks out of source control under
.git/hooksfolder is to give flexibility for users/tools to install their own hooks without interfering with other repository contributors.While this is definitely a corner case and in most cases users don't install their own hooks and don't use dev tools which do so, still it can be an issue. And Husky solution doesn't have this limitation.
Husky was created AFTER git (and hooks) and is to facilitate work with git hooks
As someone else already said, husky uses git-hooks under the hood.
The main point is that when working in a team husky let you share the pre-commit hook with your team since it's in the repo,v defined in the package.json.
Going with your solution will require to share an additional file. I've used that in the past but I can assure you that not every team member will set up the pre-commit hook manually.
With husky it's automatic.
Lorenzo, please check an example which I added to this article. The team member only has to run
npm installthis will triggernpm postisntallwhich will set upgit-gooksautomatically. I think that every developer after downloading JavaScript project with npm will runnpm installbefore he starts to work with this codeSorry, i missed that part 😅 (i read the post and replied with my little son climbing on me)
Husky is a convenient way of auto-adding Git hooks during
npm installoryarn install. But more importantly, it is a way of providing platform independence for hook scripts. This blog post uses#!/bin/bash. But what if the developer does not havesh? (As is the case for most Windows users.) By contrast, we do have the guarantee that every user of husky has NodeJs.This is *
#!/bin/shnot#!/bin/bash. I added an example to this blog post. You can download it and check that this will work on Windows too: git-hooks-exampleI didn't claim it was
bash. The Windows command line interpreter definitely cannot run arbitraryshscripts, which are a Unix standard. Let alone certain basic commands - for examplecpis for copying files on Unixoids, butcopymust be used on Windows. It's interesting that your script runs on Windows. I see two possible explanations: Either your script is so parsimonious that its syntax is valid both inshand on Windows. Or somehow Git, or your Git installation, bringsshwith it. It would be interesting to figure out if the second explanation is true. It would also be interesting to know if, for example, you could use thecpcommand on Windows from ashscript like yours above.you can if you install winbash, which is different from msysgit bash in that winbash actually treats windows paths as windows paths, where as msysgit bash mutates them to some virtual-path-that-like-linux-but-not-really-linux.
If you can't sponsor Husky, that's okay, husky v4 is free to use in any project. During the early access, v4 will continue to receive maintenance updates.
After the early access, husky v5 will be MIT again.
"Git hook is the new Husky" makes it sound like Husky isn't git hook under the hood and no mention of that either in your article. Hmm.. JS is the new React when? I'm sure that would speed some apps up "about twice".