Bootstrap

Improve Your .NET Git Commits With Husky.Net

Improve Your .NET Git Commits With Husky.Net
Photo by Annie Spratt on Unsplash

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 the pre-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 global dotnet CLI command executable.
  • args: The command arguments. Here, we specify we want to call the format 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. ๐Ÿ™๐ŸคŸ

Source code here

Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

Get notified on new posts
X

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.