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-verify
option. 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 usinggrep
to take only files which I am interested in. In my case, I am collecting.ts
files foreslint
and.scss
for stylelint, -
linter_exit_code=$?
→ save exit code of last executed action(0
in case no errors or errors that can be auto-fixed by linter or1
in 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-f
flag to forcegit add
in case of$all_ts_files
and$all_scss_files
are 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
:
-
git
command (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-hooks
directory - Go to
.git/hooks
directory - From the name of hook which you want to use remove
.sample
- Move this hook into created
git-hooks
directory - Create your
git-hook
body - Update
git
configuration or create a symlink fromgit-hooks
to.git/hooks
directory - Add the appropriate script to
npm postinstall
command
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
tsc
to 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
isolatedModules
This 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/hooks
folder 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 install
this will triggernpm postisntall
which will set upgit-gooks
automatically. I think that every developer after downloading JavaScript project with npm will runnpm install
before 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 install
oryarn 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/sh
not#!/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 arbitrarysh
scripts, which are a Unix standard. Let alone certain basic commands - for examplecp
is for copying files on Unixoids, butcopy
must 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 insh
and on Windows. Or somehow Git, or your Git installation, bringssh
with 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 thecp
command on Windows from ash
script 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".