I have had great luck with using git hooks to perform tool executions before commits or pushes. Running a linter on staged changes before the code is committed and verifying that tests run before the code is pushed makes it easier for developers to write clean code.
Doing this with heterogenous repositories, or repos which contain projects of different tech stacks, can be a bit daunting. The tools you want for one repository aren’t the tools you want for another.
How to “Hook”?
Hooks can be created directly in your repository following Git’s instructions. However, these scripts are seldom cross-OS compatible, so running your script will need some “help” in terms of compatibility. Additionally, the scripts themselves can be harder to find depending on your environment. VS Code, for example, hides the .git folder by default.
Having used NPM in the past, Husky has always been at the forefront of my mind when it comes to tooling around Git hooks. It helps by providing some cross-platform compatibility and easier visibility, as all scripts are in the .husky
folder in your repository. However, it requires some things that a pure .Net developer may not have (like NPM or some other package manager).
In my current position, though, our front ends rely on either Angular or React Native, so the chance that our developers have NPM installed are 100%. With that in mind, I put some automated linting and building into our projects.
Linting Different Projects
For this article, assume I have a repository with the following outline:
- docs/
- General Markdown documentation
- /source
- frontend/ – .Net API project which hosts my SPA
- ui/ – The SPA project (in my case, Angular)
I like lint-staged as a tool to execute linting on staged files. Why only staged files? Generally, large projects are going to have a legacy of files with formatting issues. Going all in and formatting everything all at once may not be possible. But if you format as you make changes, eventually most everything should be formatted well.
With the outline above, I want different tools to run based on which files need linted. For source/frontend
, I want to use dotnet format
, but for source/ui
, I want to use ESLint and prettier.
With lint-staged, you can configure individual folders using a configuration file. I was able to add a .lintstagedrc
file in each folder, and specify the appropriate linter for the folder. for the .Net project:
{
"*.cs": "dotnet format --include"
}
And for the Angular project:
{
"*": ["prettier", "eslint --fix"]
}
Also, since I do have some documentation files, I added a .lintstagedrc
file to the repository to run prettier on all my Markdown files.
{
"*.md": "prettier"
}
A Note on Settings
Each linter has its own settings, so follow the instructions for whatever linter you may be running. Yes, I know, for the .Net project, I’m only running it on *.cs
files. This may change in the future, but as of right now, I’m just getting to know what dotnet format
does and how much I want to use it.
Setting Up the Hooks
The hooks are, in fact, very easy to configure: follow the instructions on getting started from Husky. The configured hooks for pre-commit and pre-push are below, respectively:
npx lint-staged --relative
dotnet build source/mySolution.sln
The pre-commit hook utilizes lint-staged to execute the appropriate linter. The pre-push hook simply runs a build of the solution which, because of Microsoft’s .esproj
project type, means I get an NPM build and a .Net build in the same step.
What’s next?
I will be updating the pre-push hook to include testing for both the Angular app and the .Net API. The goal is to provide our teams with a template to write their own tests, and have those be executed before they push their code. This level of automation will help our engineers produce cleaner code from the start, alleviating the need for massive cleanup efforts down the line.