Git hook is the excellent alternative to Husky
Tags: git, javascript
Published: 2020-12-08 20:17:02

## 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`](https://github.com/typicode/husky) with [`lint-staged`](https://github.com/okonet/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`](https://git-scm.com/docs/githooks). 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`](https://git-scm.com/docs/githooks#_pre_commit) caught my attention, which is briefly described like this:

>*"This hook is invoked by [git-commit[1]](https://git-scm.com/docs/git-commit), 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**
```shell
#!/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 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**](https://typicode.github.io/husky/#/)~~.

## 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](https://github.com/kaczor6418/git-hooks-example/tree/master) 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.