👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Automating Git Hook Setup in .NET Projects with MSBuild

Automating Git Hook Setup in .NET Projects with MSBuild

Author - Abdul Rahman (Bhai)

MSBuild

1 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

Git hooks are custom scripts that run at specific points in the Git workflow. Pre-commit hooks, for example, execute before a commit is made and can enforce coding standards or run tests. Automating the setup of these hooks ensures consistency across a team, removing the need for manual configuration.

In this article, we'll explore how to automate the setup of a pre-commit hook in a .NET project using MSBuild. The same principle can be applied to any git hook setup in dotnet apps.

sequence-diagram

Why we gonna do?

Setting up Git hooks manually can lead to errors and inconsistencies, especially in large team environments where its not possible to follow up with every developer or new joiner to setup git hooks because we cannot add default git hooks to source control. Automating this process within a .NET project using MSBuild ensures that the required pre-commit hook is tracked in source control and is copied before project build to git hooks directory and consistently installed and made executable for all developers. This enhances productivity and ensures that coding standards and checks are always applied.

How we gonna do?

The first step is to add a pre-commit hook script to the project. This script can include any logic you want to run before a commit is made.

pre-commit-in-source-control

By adding a custom target to the .csproj file, you can automate the setup of a pre-commit hook. Below is an example of a custom target named CopyGitHook and a detailed explanation of its components.


<Project Sdk="Microsoft.NET.Sdk">

    <Target Name="CopyGitHook" BeforeTargets="BeforeBuild" Condition="'$(Configuration)'!='Release'">
        <!-- Ensure the .git/hooks directory exists -->
        <MakeDir Directories="$(ProjectDir)/../.git/hooks" Condition="!Exists('$(ProjectDir)/../.git/hooks')" />

        <!-- Copy the pre-commit hook -->
        <Copy
        SourceFiles="$(ProjectDir)/../Scripts/git_hooks/pre-commit"
        DestinationFiles="$(ProjectDir)/../.git/hooks/pre-commit"
        SkipUnchangedFiles="true" />

        <!-- Make the pre-commit hook executable on Linux/macOS -->
        <Exec Command="chmod +x '$(ProjectDir)/../.git/hooks/pre-commit'" 
              Condition="Exists('$(ProjectDir)/../.git/hooks/pre-commit') AND '$(OS)' != 'Windows_NT'" />

        <!-- For Windows: Create a batch script to execute the pre-commit hook -->
        <WriteLinesToFile 
            File="$(ProjectDir)/../../.git/hooks/pre-commit.bat"
            Lines="@echo off&#xD;&#xA;bash .git/hooks/pre-commit"
            Overwrite="true"
            Condition="'$(OS)' == 'Windows_NT'" />
    </Target>

</Project>
        
  1. Define the pre-commit hook setup: The CopyGitHook target runs BeforeBuild, ensuring the pre-commit hook is set up before the build process starts. It executes only in non-release builds, as specified by the condition '$(Configuration)'!='Release'.
  2. Create the .git/hooks directory: The MakeDir task ensures the .git/hooks directory exists. It runs only if the directory is missing, as determined by the condition !Exists('$(ProjectDir)/../.git/hooks').
  3. Copy the pre-commit hook: The Copy task copies the pre-commit script from the ../Scripts/git_hooks folder to the .git/hooks directory. The SkipUnchangedFiles="true" attribute ensures the file is copied only if it has changed, avoiding redundant operations.
  4. Make the hook executable (Linux/macOS): The Exec task runs a shell command chmod +x to make the copied pre-commit script executable. This task runs only if the file exists and the system is not Windows, as determined by the condition Exists('$(ProjectDir)/../.git/hooks/pre-commit') AND '$(OS)' != 'Windows_NT'.
  5. Create a Windows-compatible batch script: Since Windows does not execute shell scripts natively, the WriteLinesToFile task generates a pre-commit.bat file. This file contains: @echo off bash .git/hooks/pre-commit This batch script ensures that the pre-commit hook runs correctly on Windows machines by executing it through Bash. It is created only on Windows, as specified by the condition '$(OS)' == 'Windows_NT'.

Now you can add pre commit logics like dotnet format, run test, measure code coverage, secret scanning, etc to the ../Scripts/git_hooks/pre-commit file and add it to source control. This will ensure that all developers have the same pre commit hooks setup. This also helps to add new logics or changes to pre commit in single place and track the changes.

Here is an example of ilovedotnet pre-commit hook file.


echo "Selecting modified .cs files"
FILES=$(git diff --cached --name-only --diff-filter=ACM "*.cs" | sed 's| |\\ |g')
# - `git diff --cached --name-only`: Lists the staged files.
# - `--diff-filter=ACM`: Filters only Added (A), Copied (C), or Modified (M) files.
# - `"*.cs"`: Restricts the list to C# files.
# - `sed 's| |\\ |g'`: Escapes spaces in file paths for proper handling.

if [ -n "$FILES" ]; then
  # Run dotnet format
  LC_ALL=C
  # Set locale to "C" to ensure consistent behavior for text processing, sorting, etc.

  # Initialize flag to check if any file was formatted
  formatted_files=0

  # Format all selected files
  # Loop through each file and run dotnet format
  # Build the --include arguments for dotnet format
  echo "Files to be formatted:"
  INCLUDE_ARGS=""
  for FILE in $FILES; do
    INCLUDE_ARGS="$INCLUDE_ARGS --include $FILE"
  done

  # Run dotnet format on all files at once
  dotnet format --no-restore --verbosity normal $INCLUDE_ARGS || {
    echo "dotnet format failed. Aborting commit." >&2
    exit 1
  }

  # Check if any files were modified by dotnet format
  # Stage the formatted files only if they are modified
  for FILE in $FILES; do
    git diff --exit-code "$FILE" > /dev/null || {
      git add "$FILE"  # Stage the formatted file
      formatted_files=1  # Set the flag to indicate that a file was formatted
    }
  done

  # Exit commit if any file was formatted
  if [ $formatted_files -eq 1 ]; then
    echo "One or more files were formatted. Please review the changes and commit again."
    exit 1
  else
    echo "No files were formatted."
  fi
fi

echo "Checking and installing dotnet report generator"
dotnet tool install --global dotnet-reportgenerator-globaltool

echo "Cleaning up previous test results inside *Tests directories..."
find . -type d -name "*Tests" -exec rm -rf {}/TestResults \;

echo "Running tests and collecting code coverage..."
dotnet test . --settings ./UITests/ilovedotnet.runsettings --collect:"XPlat Code Coverage"

echo "Generating aggregated code coverage report..."
reportgenerator -reports:"./**/TestResults/**/coverage.cobertura.xml" -targetdir:"test_coverage_report" -reporttypes:XmlSummary || {
    echo "Error: Failed to generate code coverage report. Commit aborted."
    exit 1
}

# Define the summary file path
SUMMARY_FILE="test_coverage_report/Summary.xml"

# Check if the summary file exists
if [ ! -f "$SUMMARY_FILE" ]; then
    echo "Error: Coverage summary file not found. Commit aborted."
    exit 1
fi

# Extract the Linecoverage percentage
COVERAGE=$(xmllint --xpath "string(//Summary/Linecoverage)" "$SUMMARY_FILE")

if [ -z "$COVERAGE" ]; then
    echo "Error: Failed to retrieve code coverage from summary. Commit aborted."
    exit 1
fi

# Set the required coverage threshold
THRESHOLD=70.0

# Round off coverage to one decimal place
COVERAGE_PERCENTAGE=$(printf "%.1f" "$COVERAGE")

echo "Aggregated Code Coverage: ${COVERAGE_PERCENTAGE}%"

# Check if the aggregated coverage meets the threshold
if (( $(echo "$COVERAGE_PERCENTAGE < $THRESHOLD" | bc -l) )); then
    echo "Code coverage (${COVERAGE_PERCENTAGE}%) is below the required threshold (${THRESHOLD}%). Commit aborted."
    exit 1
fi

echo "Code coverage ${COVERAGE_PERCENTAGE}% is sufficient. Proceeding with commit."
        
Windows Pre-Commit Output windows-pre-commit-output

Summary

Automating Git hook setup with MSBuild simplifies project configuration and ensures consistency across all developer machines. By leveraging a custom target in the .csproj file, you can efficiently set up pre-commit hooks without requiring manual intervention, improving both productivity and team collaboration.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Msbuild
  • Git Hook
  • Pre-Commit