Improve Your .NET Git Commits With Husky.Net
Do commit messages like "fixed linting errors", "fixed unit tests" or "format code" look familiar? These are typical commits to fix the little mistakes that inevitably creep into everyone's code.
They can be quick to fix on their own, but as our projects and teams scale, these little mistakes can routinely waste our teammates' time during pull request reviews and cause broken builds and failures that waste resources in our CI pipelines.
Git Hooks provide a mechanism to help us catch these small mistakes early in the development lifecycle by triggering custom scripts on our code changes as they move through the Git workflow.
In other words, when we git commit
or git push
new code, we can run linters and formatting tools or execute unit tests to ensure the updates are valid and meet expected standards. This improves consistency across the codebase by ensuring all developers apply the same linting and formatting rules as they push code.
In this post, I'll show you how to use Husky.Net to set up Git hooks in a .NET project and run some useful tasks on new code as part of your Git workflow.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Why Husky.Net?
Hooks are a native feature in your Git installation and can be configured and used directly without extra packages. Husky is a popular npm package used to simplify the management of Git hooks, and Husky.Net is a port of Husky for .NET developers!
Project Setup
You can find the complete source code for this tutorial in this repository.
The demo solution consists of a sample console app and an xUnit test project.
Installing Husky.Net
The Husky package ships as a .Net tool. I installed it locally from the command line in the root of the solution directory.
dotnet new tool-manifest
dotnet tool install Husky
Automating Setup for Others
One neat feature of Husky.Net is that it replicates the concept of devDependencies
from the NPM package system.
Running the command below attaches Husky to your projects. This ensures that others working on and updating the project will automatically run your configured hooks.
dotnet husky attach DotNetGitHooksApp/DotNetGitHooksApp.csproj
Adding Your First Hook
With Husky installed, we can add a simple hook to test it. This hook prints a message in the terminal.
dotnet husky add pre-commit -c "echo 'My first hook!'"
git add .
Run git commit to see it in action.
git commit -m "Commit powered with hooks ๐ช"
My first hook!
[main 4b9f620] Commit powered with hooks ๐ช
4 files changed, 52 insertions(+), 3 deletions(-)
...
Format Committed Code
Let's extend the hook to do something a little more interesting. We'll run the dotnet format
command on new code to ensure it follows our styling rules.
Hooks that run linting and formatting tasks take a little extra consideration. ๐ค
Ideally, we want to run this task before our code is committed and only on the files we have staged in the commit snapshot. ๐
Otherwise, we can end up with a poorly optimized hook that blindly runs on every file in the project and adds extra changes and commits beyond the files we've worked on. ๐
Husky has us covered here by providing a task-runner.json
file where we can set up the dotnet format
command to achieve the ideal result.
Run dotnet format as a Husky Task
The previous setup steps created the task-runner.json
file for us in the .husky
directory.
{
"tasks": [
{
"name": "welcome-message-example",
"command": "bash",
"args": [ "-c", "echo Husky.Net is awesome!" ],
"windows": {
"command": "cmd",
"args": ["/c", "echo Husky.Net is awesome!" ]
}
}
]
}
Replace the default content with a new task to run dotnet format
along with additional arguments to ensure it only runs on staged files.
{
"tasks": [
{
"name": "dotnet-format-staged-files",
"group": "pre-commit-operations",
"command": "dotnet",
"args": ["format", "--include", "${staged}"],
"include": ["**/*.cs"]
}
]
}
Here's a breakdown of the configuration:
name
: Allows Husky to run this task by name.husky run --name task-name
. We'll wire this up in thepre-commit
hook next.group
: Alternatively, Husky can run all tasks in this group.husky run --group group-name
command
: Command name or a path to the executable file or script to run. We're targeting the globaldotnet
CLI command executable.args
: The command arguments. Here, we specify we want to call theformat
command along with the--include
option to select the exact list of files to include in formatting. The${staged}
argument is the key to making all this work. This is a variable Husky provides listing the currently staged files to ensure only these files get formatted.include
: A glob pattern to further filter the set of files to include when running the task. We've set it**/*.cs
to indicate all C# source files.
Update the pre-commit Hook
The last step is to update the pre-commit
hook to call the format task. Replace the contents of the .husky/pre-commit
file with the Husky command to run the task by name. Alternatively, you could also use the --group
option to run a set of tasks.
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo 'Ready to commit changes!'
echo 'Formatting staged files...'
dotnet husky run --name dotnet-format-staged-files
echo 'Completed pre-commit changes'
With these changes in place, I ran git commit
, and the hook was successfully triggered! ๐
Running Unit Tests
A suite of passing tests gives us some confidence that new code changes won't break existing functionality. We can achieve this by adding a task to run the dotnet test
command at the end of the workflow before we git push
our changes.
In reality, you may want to execute this step earlier. Thanks to Husky's flexibility, it's easy to create and modify hooks to suit the needs of your project and team.
I used the same steps as written above to make the pre-push
hook, so there is no need to repeat them here. Here are my updates to the .husky/pre-push
and .husky/task-runner.json
files.
A new dotnet-test
task added to task-runner.json
with a filter used to run only a subset of the tests.
...
{
"name": "dotnet-test",
"group": "pre-push-operations",
"command": "dotnet",
"args": ["test", "--filter", "Category=Unit"]
}
The update to the pre-push
file to call it:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo 'Ready to push changes!'
echo 'Running unit tests...'
dotnet husky run --name dotnet-test
echo 'Completed pre-push changes'
When running git push,
the unit tests are executed before the code is pushed. ๐งช
A test failure or non-zero exit will abort the operation early.
Wrapping Up
Husky.Net makes dealing with hooks in your .NET project easy, helping you unlock the benefits of producing more consistent and better-quality code, which is particularly valuable in projects with multiple contributors.
Many thanks to the creator and contributors of Husky.Net for their work in making it available. ๐๐ค
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.
Get notified on new posts
Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.