Code Quality: Automate Linting, Formatting And More By Sharing Git Hooks

There he is. Bob. The new guy in the office. Time to on-board him onto the flagship project of the company. Sounds like a job for Kevin. Kevin helps out Bob to get setup. Providing him with the appropriate access rights, cloning the repository, and making sure Bob's seat is nice and comfy. After Bob has the project up and running, it is his time to shine and work on the first ticket. He fires up his IDE, touches a couple of files, resolves the issues, commits, pushes, and opens up a merge request for Kevin to review.

In this article:

yb
Yannick Baron is architecture consultant at Thinktecture and focuses on Angular and RxJS.

Kevin looks at the diff and quickly notices his blunder. He forgot to tell Bob about the code formatting guidelines and that the code should, of course, be free of linter errors before opening the MR. Thanks, Kevin.

Does the little story above sound familiar? Someone new joins the project, their IDE is not set up, and they are oblivious to linter errors? Would it not be great if there was a way to ensure that every commit only contains code that has been formatted to a project-wide setting and has no linter errors?

I feel that formatting code should not be something the developer has to think actively about. It should be set up once and be consistent for every person working on the project. Git hooks to the rescue.

Git Hooks

Git provides us with hooks, meaning, events upon which we can perform actions. One of the more prominent hooks is called pre-commit, and you might have guessed it, this hook helps us perform actions right before we commit our changes. So, it would be the perfect place to format our code and make sure there are no linter errors.

One drawback of git hooks is that they need to be set up locally in the git repository. If you inspect the .git folder of your project, you will find a folder called hooks in which we can place script files that git will execute once the respective event is fired. When you initialize your repository, you will find a few example scripts that git provides you with. *.sample files are ignored and will not be run.

				
					$ ls -la .git/hooks
total 112
drwxr-xr-x  14 ybaron  staff   448 Oct  9 13:34 .
drwxr-xr-x   9 ybaron  staff   288 Oct  9 13:34 ..
-rwxr-xr-x   1 ybaron  staff   478 Oct  9 13:34 applypatch-msg.sample
-rwxr-xr-x   1 ybaron  staff   896 Oct  9 13:34 commit-msg.sample
-rwxr-xr-x   1 ybaron  staff  4655 Oct  9 13:34 fsmonitor-watchman.sample
-rwxr-xr-x   1 ybaron  staff   189 Oct  9 13:34 post-update.sample
-rwxr-xr-x   1 ybaron  staff   424 Oct  9 13:34 pre-applypatch.sample
-rwxr-xr-x   1 ybaron  staff  1643 Oct  9 13:34 pre-commit.sample
-rwxr-xr-x   1 ybaron  staff   416 Oct  9 13:34 pre-merge-commit.sample
-rwxr-xr-x   1 ybaron  staff  1348 Oct  9 13:34 pre-push.sample
-rwxr-xr-x   1 ybaron  staff  4898 Oct  9 13:34 pre-rebase.sample
-rwxr-xr-x   1 ybaron  staff   544 Oct  9 13:34 pre-receive.sample
-rwxr-xr-x   1 ybaron  staff  1492 Oct  9 13:34 prepare-commit-msg.sample
-rwxr-xr-x   1 ybaron  staff  3635 Oct  9 13:34 update.sample
				
			

If you want a script to run on the pre-commit event, just place a file called pre-commit in that folder, and git will run it for you when you try to commit. Once the script ends without an error, the commit will be saved in the git history. In case of an error, committing will be aborted.

Making use of hooks, we can ensure for certain criteria to be fulfilled before we commit. The problem with the file-based system above is that this makes it hard to share git hooks for everyone in the project because they are stored locally and not on the remote.

In the case of npm, I can introduce a solution to that problem…

A Solution: Husky

Husky is an npm package that will set up the hooks folder in a way that every git hook will call husky’s script. It does so on an npm post-install, which means that everyone installing the project will have husky wire up your git hooks. That’s great!

Let’s see how Husky does it:

				
					$ ls -la .git/hooks
total 264
drwxr-xr-x  33 ybaron  staff  1056 Oct  9 13:58 .
drwxr-xr-x   9 ybaron  staff   288 Oct  9 13:34 ..
[...]
-rw-r--r--   1 ybaron  staff   234 Oct  9 13:58 husky.local.sh
-rw-r--r--   1 ybaron  staff  2500 Oct  9 13:58 husky.sh
[...]
-rwxr-xr-x   1 ybaron  staff   256 Oct  9 13:58 pre-applypatch
-rwxr-xr-x   1 ybaron  staff   424 Oct  9 13:34 pre-applypatch.sample
-rwxr-xr-x   1 ybaron  staff   256 Oct  9 13:58 pre-auto-gc
-rwxr-xr-x   1 ybaron  staff   256 Oct  9 13:58 pre-commit
-rwxr-xr-x   1 ybaron  staff  1643 Oct  9 13:34 pre-commit.sample
-rwxr-xr-x   1 ybaron  staff   256 Oct  9 13:58 pre-merge-commit
-rwxr-xr-x   1 ybaron  staff   416 Oct  9 13:34 pre-merge-commit.sample
-rwxr-xr-x   1 ybaron  staff   256 Oct  9 13:58 pre-push
-rwxr-xr-x   1 ybaron  staff  1348 Oct  9 13:34 pre-push.sample
[...]
				
			

Husky creates a script file for many git hooks and places its own scripts right beside them. Looking at the script files, we can see that all of them will just call the husky script:

				
					$ cat .git/hooks/pre-commit
#!/bin/sh
# husky

[...]

. "$(dirname "$0")/husky.sh"
				
			

But, what can husky do for us?

Being a npm package, we can configure husky to run npm executables upon a git hook. Let’s say we find an opinionated code formatting npm package…

Prettier

Prettier is an opinionated code formatter that “supports many languages”, “integrates with most editors”, and only “has few options”.

We can use prettier to format a lot of different file types, my favorites being TypeScript files and JSON files, but go ahead and check out their website to find out more about what plugins there are.

Being an npm package, prettier is easy to share, and it does not matter which IDE or editor you are using. Everyone can easily format their code using the configuration placed in your project root.

Lastly, it would be great only to format the files that we have staged for our current commit.

Lint-Staged

lint-staged helps us with that.

We can tell lint-staged what actions to perform with certain file types. Let’s say we want to format TypeScript and JSON files but only run the linter for TypeScript. lint-staged can do that.

Putting It all Together: The Setup

Now that I have introduced a couple of tools, it is playtime. Let’s set this up!

First, install the packages:

				
					$ npm i --save-dev husky prettier lint-staged
				
			

Next, let’s configure our tools. There are a lot of formats Prettier accepts for their configuration. For this example, I will place a .prettierrc file in JSON format in the root of my project:

				
					// .prettierrc
{
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true
}
				
			

Husky and lint-staged also offer us several ways of configuration. I chose to set them up in my package.json:

				
					// package.json
{
  [...]
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,json}": "prettier --write"
  }
}
				
			

This will tell husky to run lint-staged in git’s pre-commit hook and configure lint-staged to format TypeScript and JSON files with prettier.

So once we stage a couple of files and try to commit, it might look like this:

				
					$ git commit -m "changes to AppComponent"
husky > pre-commit (node v10.13.0)
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up...
[feature/app-component 975337a] changes to AppComponent
 1 file changed, 13 insertions(+), 3 deletions(-)
				
			

We can see that modifications have been applied to files that we were about to commit. We tried committing a file that did not have the proper formatting. But we do not have to worry about that anymore, as the combination of the tools above saved us from embarrassment. The file has been formatted according to the project’s configuration, without us having to think about it.

The beauty of this setup is that no matter who is going to join the project, and no matter which IDE they are using, the code they commit will be formatted before being written into the git log. Nice.

But why stop there? In our story above, Kevin also found linting errors. While some of them are probably the result of the lack of proper formatting, others could have easily been avoided by preventing the commit. Let’s hook up the linting right after the formatting.

Let’s modify our package.json:

				
					// package.json
{
  [...]
  "lint-staged": {
    "*.ts": ["prettier --write", "tslint"],
    "*.json": "prettier --write"
  }
}
				
			

Now it is set up in a way that TypeScript files will first be formatted and then linted using tslint. In the future, tslint will be incorporated into eslint, and we should use eslint instead.

Let’s see what happens when we try to commit now:

				
					$ git commit -m "linting test"
husky > pre-commit (node v10.13.0)
✔ Preparing...
✔ Hiding unstaged changes to partially staged files...
⚠ Running tasks...
  ❯ Running tasks for *.{ts,json}
    ✔ prettier --write
    ✖ tslint [FAILED]
↓ Skipped because of errors from tasks. [SKIPPED]
↓ Skipped because of errors from tasks. [SKIPPED]
✔ Reverting to original state because of errors...
✔ Cleaning up...

✖ tslint:
ERROR: src/app/app.component.ts:16:13 - The selector should be prefixed by "app" (https://angular.io/guide/styleguide#style-02-07)

				
			

As we can see, the commit has been aborted, and we get an error message telling us first to fix the linting error before we can commit. This will enforce code quality on the commit level!

As a side note: we could also use the command tslint --fix and have certain errors that have auto-fixers be fixed automatically. However, it would be hard to argue whether you first want to run the fixer and then the formatter, or the other way round. The auto-fix might break the formatting. For that reason, I do not like using the auto-fix here.

A Word on Commit Message Linting

We can go beyond formatting and file linting! What about linting commit messages? Yes, you read that right. There is a tool called commitlint we can use to make sure that git messages have a certain format.

While this might sound very limiting, a lot of teams I have introduced this to enjoy it. You can easily extend the setup above to do that as well.

Conclusion

Having all files in our project formatted correctly is the minimum for code quality we can do in a project team. There should be no unreadable diffs, or discussion about one person formatting the code a different way, as it makes changes to the code hard to track. Furthermore, the developer should not have to actively think about formatting a file or remembering to format the file on save and whatnot.

Here, we looked at a surefire way to have every participant format in the same way, without the hassle of setting up their IDE.

Furthermore, we set up file linting as well. We do not tolerate linting errors anymore, which improves code quality and also makes sure that Kevin does not have to tell Bob about running the linter, or waiting for the CI pipeline to remind him to do so.

Lastly, I do have to admit that sometimes prettier formatting can be a bit awkward. Personally, I have found to enjoy it, and consistency is key, but hear me out. One advantage of the above system is that technically every developer can use their own formatting in their IDE if it helps to read and understand the code better. Because as soon as they commit changes, the files will be formatted using prettier.

Conventions are key in a team working together on a project. Make following them as easy as possible. This is one step and the least we can do for our code quality.

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
Angular
SL-rund
If you previously wanted to integrate view transitions into your Angular application, this was only possible in a very cumbersome way that needed a lot of detailed knowledge about Angular internals. Now, Angular 17 introduced a feature to integrate the View Transition API with the router. In this two-part series, we will look at how to leverage the feature for route transitions and how we could use it for single-page animations.
15.04.2024
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023