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.
Top comments (32)
husky uses githooks and just uses the convention of using like-named npm run scripts as the hook script. For example, this local repo with husky has the following pre-commit script (as does all the husky installed hooks):
husky.sh then checks for config files which package manager you're using and what not but eventually just executes the script of the same hook name you have defined. husky is ux sugar on top of git hooks.
Yes, but it is an additional layer of abstraction which maybe you do not need. Another difference is that
Husky
in version < 5 (I do not know how it will work in version 5 but for this version you have to pay unless your project is open-source) withoutlint-staged
will, lint all files not only staged files and I think it is not that good because in my opinion if you are committing 6 files you want to run your linter only on those 6 files no whole repo (it will also make committing process much longer if your project is big)Seems like there are several things getting conflated. You can run your shell script in an npm run script named, 'pre-commit' and not use lint-staged at all. lint-staged and husky are two different tools.
The temporary patron license window is kinda funny but I'm glad there are experimentation with open source funding models. After the funding drive / early access 5.x also will be licensed as MIT (according to the top of the project readme). The important thing is that we do not overreact to FUD. But this is a completely different issue than 'git-hook is the new husky'.
As for the unnecessary abstraction concern. Does husky, lefthook, overcommit, etc add value over using git-hooks directly? I would say in nearly all cases yes but each team/project should prob do what works best for them. git-hooks were not designed to be part of the collaborative workflow - .git/hooks/* are not nodes in the git DAG. Using the git-hook manager abstraction to hook into git but run scripts that are in other workflow tools and part of the versioned code helps the work flow code be more manageable, visible, and consistent.
In expressions-calculator you attempt to make git hooks collaborative with:
"postinstall": "ln -s -f ../../git-hooks/pre-commit .git/hooks/pre-commit",
I understand its an opinion, but I dont think this is cleaner than husky. Also, what is this doing, forcing symlinks to scripts from a grandparent dir from outside the repo?
On one had I dig a git-hooks dir in the repo root idea. If I wanted an alternative to husky I might have a deps module that on install symlinked the scripts from git-hooks/ to .git/hooks BUT, git hooks are just hooks. Not a special classification of script, why should they have their own folder in the root? Perhaps a ci dir could have a pre-commit script that gets symlinked .... or perhaps we just use husky and be done with it.
The reason I am spending this much time replying is that the rejection of husky seems to be an over reacting to the licensing FUD which hurts us all. Rather than dog the project we should help end the funding drive early, and get an MIT GA release out.
"You can run your shell script in an npm run script named, 'pre-commit'" → yes I can but then I will not have access to only staged files and
husky
will lint all files in your project what is probably not something what you want. Probably you can try to create a script which will pull out all staged files but then why do not usegit-hooks
?"it-hooks were not designed to be part of the collaborative workflow" → I agree in 100%, but
husky 5
will also usegit-hooks
under the hood, so not the best argument not to usegit-hooks
.git-hook
is the best way to control commits and ultimately perform custom operations that I know of. If you have a better idea, how can I control commits in JS projects let me know?"I understand its an opinion, but I dont think this is cleaner than husky. Also, what is this doing, forcing symlinks to scripts from a grandparent dir from outside the repo?" → as I mentioned in my article you do not have to use
symlinks
there is an alternative which I will paste below:And that's all, paste this line into
postinstall
script and this will work the same as symlink which I am using in my repository."If I wanted an alternative to husky I might have a deps module that on install symlinked the scripts from git-hooks/ to .git/hooks BUT, git hooks are just hooks. Not a special classification of script, why should they have their own folder in the root?" →I think that code splitting is always better than keeping everything in one file. If there is a possibility to move those hooks into a separated folder which will keep only those
git-hooks
rather than put everything intopackage.json
than I think this is the way to go."The reason I am spending this much time replying is that the rejection of husky seems to be an over reacting to the licensing FUD which hurts us all" → I didn't say: "Do not use husky it is bad". I show an alternative to
husky
. I think many people don't know about the existence ofgit-hooks
becausehusky
+lint-staged
is always the first result in the search engine if you are looking for information on controlling commits in JavaScript projects, so I wanted to shear my experience withgit-hooks
and show an alternative for this duo.I think the whole point of using husky is for anyone who clones the repo to have the hooks auto created (and stay up to date with whatever is confgured in package.json) without having to take the extra steps of creating links/scripts under .git.
It works the same for
git-hooks
after you clone the repo and runnpm install
your hooks will be ready for you. You can check this repository for example: git-hooks-example. After you clone this repo and runnpm install
and try to commit sth which doesn't fit my linter rules you will get an error if linter won't be able to auto-fix errorsVery nice! I didn't know you could do that
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.
This is good info...but, it sure is simple to do: npx mrm lint-staged
Hmm but if I use this
npx mrm lint-staged
, then I have to remember to run this script every time before I commit any changes.git-hoos
will do it automatically. Correct me if I am wrongWell, not necessarily.
npx mrm lint-staged
will add that to 'package.json' so it's all included withnpm i
. I use it for 'student repos' in my classes.As you say though, we are adding dependencies. So, maybe convenience for some additional package.
So, it adds something like this one.
If it's 'new project' - not 'template repo,' then, yes, you add it manually each time.
Yes, but now you are adding
husky
andlint-staged
and this article is about that maybe you do not need it because there is actually no so much work to set up git hooks in my opinion.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 😉)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)
Thank you for this remarkable post about git-hooks!
I was looking for a tool to run a script to generate a new site map on my blogs repo every time I added a new post (new md-file in posts directory). I stumbled upon git-hooks then, but didn't dive into the shell scripting.. With your article I am going to set it up this week 😄
I'm glad to hear that 😄
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.
"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".
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
So, going straight through Githooks instead of using Githooks through Husky is faster! Mind blown 😂
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"
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.
"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
Husky was created AFTER git (and hooks) and is to facilitate work with git hooks