DEV Community

Cover image for Git hook is the excellent alternative to Husky
Krzysztof Kaczyński
Krzysztof Kaczyński

Posted on • Edited on

Git hook is the excellent alternative to Husky

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
Enter fullscreen mode Exit fullscreen mode

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 using grep to take only files which I am interested in. In my case, I am collecting .ts files for eslint 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 or 1 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 force git 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

  1. In project directory create git-hooks directory
  2. Go to .git/hooks directory
  3. From the name of hook which you want to use remove .sample
  4. Move this hook into created git-hooks directory
  5. Create your git-hook body
  6. Update git configuration or create a symlink from git-hooks to .git/hooks directory
  7. 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)

Collapse
 
elninoisback profile image
Ε Γ И І И О

"Husky 5 will be free only for open-source projects." Is this still valid? It looks like they had second thoughts.

Collapse
 
krzysztofkaczy9 profile image
Krzysztof Kaczyński

Thank you for the information :D I have updated my post

Collapse
 
cpmech profile image
Dorival Pedroso

Good idea!

I also run tsc to check my TypeScript code. Here you go my git pre-commit hook (based on yours; thanks):

#!/bin/sh

staged_files=$(git diff --cached --diff-filter=d --name-only | grep  -E '\.(js|jsx|ts|tsx)$')

# skip if there are no js or ts files
if [ -z "$staged_files" ]; then
    exit 0
fi

# run type-check
yarn run --silent tsc
tsc_exit_code=$?

# check the tsc exit code
if [ $tsc_exit_code -ne 0 ]; then
    echo "🥵 tsc failed"
    exit 1
else
    echo "👍 tsc"
fi

# run linter on staged files => save exit code for later
yarn run --silent eslint $staged_files --quiet --fix
linter_exit_code=$?

# add files auto-fixed by the linter
git add $staged_files

# check linter exit code
if [ $linter_exit_code -ne 0 ]; then
    echo "🥵 lint failed"
    exit 1
else
    echo "👍 lint"
fi

# return 0-exit code
echo "🎉 all good to go"
exit 0
Enter fullscreen mode Exit fullscreen mode
Collapse
 
airtonix profile image
Zenobius Jiricek • Edited

I think this will fail if you're using isolatedModules

Collapse
 
tcarrio profile image
Tom

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.

Collapse
 
krzysztofkaczy9 profile image
Krzysztof Kaczyński

I am glad to hear that 😃.

You are right I should look for .ts$ (For grep you do not have to escape the . character 😉)

Collapse
 
edo78 profile image
Federico "Edo" Granata

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"

Collapse
 
zuzusik profile image
Viktor Zozuliak

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.

Collapse
 
milesq profile image
Miłosz Wiśniewski

Husky was created AFTER git (and hooks) and is to facilitate work with git hooks

Collapse
 
slackerzz profile image
Lorenzo

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.

Collapse
 
krzysztofkaczy9 profile image
Krzysztof Kaczyński

Lorenzo, please check an example which I added to this article. The team member only has to run npm install this will trigger npm postisntall which will set up git-gooks automatically. I think that every developer after downloading JavaScript project with npm will run npm install before he starts to work with this code

Collapse
 
slackerzz profile image
Lorenzo

Sorry, i missed that part 😅 (i read the post and replied with my little son climbing on me)

Collapse
 
cfuehrmann profile image
Carsten Führmann • Edited

Husky is a convenient way of auto-adding Git hooks during npm install or yarn 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 have sh? (As is the case for most Windows users.) By contrast, we do have the guarantee that every user of husky has NodeJs.

Collapse
 
krzysztofkaczy9 profile image
Krzysztof Kaczyński • Edited

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-example

Collapse
 
cfuehrmann profile image
Carsten Führmann

I didn't claim it was bash. The Windows command line interpreter definitely cannot run arbitrary sh scripts, which are a Unix standard. Let alone certain basic commands - for example cp is for copying files on Unixoids, but copy 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 in sh and on Windows. Or somehow Git, or your Git installation, brings sh 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 the cp command on Windows from a sh script like yours above.

Thread Thread
 
airtonix profile image
Zenobius Jiricek

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.

Collapse
 
ambroseus profile image
Eugene Samonenko

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.

Collapse
 
alevosia profile image
Ålex • Edited

"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".