Husky.Net: Git hooks made easy

Husky.Net: Git hooks made easy

During our day-to-day, usually, there are repetitive tasks we always do: formatting code, running unit tests, following commit policies, etc. Easy tasks, but we make them over and over again, which is a waste of time and effort. Luckily there is a way to automate those tasks, Git hooks. In this post, we will explore what they are and then review Husky.Net, a simple way to start using Git hooks under the .NET ecosystem.

Git hooks

Git hooks provide a way to fire off custom scripts on different events (after and before), such as during commit, push or rebase, etc. There are two types of hooks:

  • Client-side: Triggered by events in the local repository, such as when a developer commits code.
  • Server-side: Run under the remote repository and are triggered by events such as pushes received.

Useful client-side hooks

pre-commit

Invoked by git commit before Git asks the developer for a commit message or generates a commit object. It takes zero arguments and exiting with a non-zero status aborts the commit operation. Used for static analysis, linting, spell-checks, and code style checks or run tests.

prepare-commit-msg

Invoked by git commit after preparing the default commit message, just before launching the commit message editor. Arguments:

  • Name of the file that contains the commit log message.
  • The type of commit: message, template, merge, squash, or commit.
  • Commit SHA1.

It is useful for editing the default message before the commit author sees it (e.g., adding the branch name to the commit message).

commit-msg

Invoked by git commit after the user enters a commit message. This hook takes one parameter and is the path to a temporary file that contains the commit message. It is an appropriate place to validate the commit state or message to ensure compliance with a standard or to reject based on any criteria.

post-commit

Invoked by git commit. It takes no parameter and runs after the commit operation is completed. This hook can be used to provide notifications.

pre-push

Invoked by git push and can be used to prevent a push from taking place. The hook has two parameters, the remote name, and location(URL).

Husky. NET

Husky.Net offers a very simple way to start using git hooks or running certain tasks, write and run custom scripts and more ...

Let's start installing the tool:

dotnet tool install --global Husky

Now, we will create a library to sum two numbers:

dotnet new classlib -n calculator
dotnet new mstest -n test
dotnet new sln -n husky-sandbox
dotnet add tests/tests.csproj reference calculator/calculator.csproj
dotnet sln add --in-root calculator
dotnet sln add --in-root tests

Open the solution and go to the calculator project. Add a Calculator.cs file with the following content:

public class Calculator
{
    public int Sum(int a, int b)
    {
        return a + b;
    }
}

Go to the test project and add a CalculatorTests.cs file as follows:

[TestClass]
public class CalculatorTests
{
    [TestMethod]
    [DataRow(1, 2, 3)]
    [DataRow(2, 2, 4)]
    [DataRow(0, 0, 0)]
    [DataRow(-1, 1, 0)]
    public void sum_X_and_Y_should_be_Z(int x, int y, int z)
    {
        var sut = new Calculator();

        var r = sut.Sum(x, y);

        Assert.AreEqual(z, r);
    }
}

Run dotnet husky add pre-commit -c "echo 'running pre-commit'" to create our first git hook. The command is going to create a .husky/pre-commit file with the content below:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

## husky task runner examples -------------------
## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky'

## run all tasks
#husky run

### run all tasks with group: 'group-name'
#husky run --group group-name

## run task with name: 'task-name'
#husky run --name task-name

## pass hook arguments to task
#husky run --args "$1" "$2"

## or put your custom commands -------------------
#echo 'Husky.Net is awesome!'

echo 'running pre-commit'

Let's do some code formatting with dotnet-format. Add dotnet format -v detailed at the end of the file and run git commit -m "testing git hooks" --allow-empty. We will get an output like this:

running pre-commit
  The dotnet runtime version is '6.0.8'.
  Formatting code files in workspace 'C:\Source\husky-sandbox\husky-sandbox.sln'.
    Determining projects to restore...
  All projects are up-to-date for restore.
  Project calculator is using configuration from 'C:\Source\husky-sandbox\calculator\obj\Debug\net6.0\calculator.GeneratedMSBuildEditorConfig.editorconfig'.
  Project calculator is using configuration from 'C:\Program Files\dotnet\sdk\6.0.400\Sdks\Microsoft.NET.Sdk\analyzers\build\config\analysislevel_6_default.editorconfig'.
  Project tests is using configuration from 'C:\Source\husky-sandbox\tests\obj\Debug\net6.0\tests.GeneratedMSBuildEditorConfig.editorconfig'.
  Project tests is using configuration from 'C:\Program Files\dotnet\sdk\6.0.400\Sdks\Microsoft.NET.Sdk\analyzers\build\config\analysislevel_6_default.editorconfig'.
  Running 2 analyzers on calculator.
  Running 2 analyzers on tests.
  Running 134 analyzers on calculator.
  Running 134 analyzers on tests.
  Formatted 0 of 10 files.
  Format complete in 5035ms.
  Determining projects to restore...
  All projects are up-to-date for restore.
  calculator -> C:\Source\husky-sandbox\calculator\bin\Debug\net6.0\calculator.dll
  tests -> C:\Source\husky-sandbox\tests\bin\Debug\net6.0\tests.dll

Running C# scripts is another cool feature of Husky.Net. Let's try to validate the commit message to follow a fixed pattern. Run dotnet husky add commit-msg -c "echo 'running commit-msg'". Create a .husky/commit-lint.csx file with the following content:

using System.Text.RegularExpressions;
using Internal;

private var pattern = @"^[aA-zZ]+-[\d]+(\s).+";
private var msg = File.ReadAllLines(Args[0])[0];

if (Regex.IsMatch(msg, pattern))
   return 0;

Console.WriteLine("Invalid commit message");
Console.WriteLine("e.g: ABC-123 brief description of the commit");

return 1;

Open the .husky/commit-msg file and add husky exec .husky/commit-lint.csx --args "$1" at the end. Run git commit -m "wrong comment" --allow-empty, to see at the end of the output something like this:

Invalid commit message
e.g: ABC-123 brief description of the commit
script execution failed
husky - commit-msg hook exited with code 1 (error)

The last example will run the tests before make the push to the remote repository. Run dotnet husky add pre-push -c "echo 'running pre-push'". Open the file .husky/pre-push and add dotnet test at the end. Run git commit -m "ABC-1234 initial commit" --allow-empty and check the outputs.

There are other features like the Task Runner, and maybe more in the future. We suggest keeping an eye on this project. All the code is available here. Thanks, and happy coding.