Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,7 @@


# Fix syntax highlighting on GitHub to allow comments
.vscode/*.json linguist-language=JSON-with-Comments
.vscode/*.json linguist-language=JSON-with-Comments

# Git hooks must use LF line endings (Git Bash on Windows requires it)
.githooks/** text eol=lf
104 changes: 104 additions & 0 deletions .githooks/hooks/Invoke-PreCommit.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
[CmdletBinding()]
param()

$repoRoot = git rev-parse --show-toplevel
. "$PSScriptRoot/../lib/LintHelpers.ps1"

try {
# C# formatting (auto-fix + re-stage)
$csFiles = Get-StagedFiles -Extensions @('.cs')
if ($csFiles.Count -gt 0) {
$includePaths = ($csFiles | ForEach-Object { "--include", $_ })
Invoke-AutoFix -Files $csFiles -FixCommand {
dotnet format "$repoRoot/Moq.Analyzers.sln" --verbosity quiet @includePaths 2>&1 | Out-Null
}
$output = dotnet format "$repoRoot/Moq.Analyzers.sln" --verify-no-changes --verbosity quiet @includePaths 2>&1
if ($LASTEXITCODE -ne 0) {
Set-HookFailed -Check "dotnet format"
Write-Host $output

Check warning on line 18 in .githooks/hooks/Invoke-PreCommit.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PreCommit.ps1#L18

File 'Invoke-PreCommit.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
}
}

# Markdown linting (auto-fix + re-stage)
$mdFiles = Get-StagedFiles -Extensions @('.md')
if ($mdFiles.Count -gt 0) {
if (Test-ToolAvailable -Command "markdownlint-cli2" -InstallHint "npm install -g markdownlint-cli2") {
$fullPaths = $mdFiles | ForEach-Object { Join-Path $repoRoot $_ }
Invoke-AutoFix -Files $mdFiles -FixCommand {
markdownlint-cli2 --fix $fullPaths 2>&1 | Out-Null
}
$output = markdownlint-cli2 $fullPaths 2>&1
if ($LASTEXITCODE -ne 0) {
Set-HookFailed -Check "markdownlint-cli2"
Write-Host $output
}
}
}

# YAML linting (lint only)
$yamlFiles = Get-StagedFiles -Extensions @('.yml', '.yaml')
if ($yamlFiles.Count -gt 0) {
if (Test-ToolAvailable -Command "yamllint" -InstallHint "pip install yamllint") {
$fullPaths = $yamlFiles | ForEach-Object { Join-Path $repoRoot $_ }
$output = yamllint -c (Join-Path $repoRoot ".yamllint.yml") $fullPaths 2>&1
if ($LASTEXITCODE -ne 0) {
Set-HookFailed -Check "yamllint"
Write-Host $output
}
}
}

# JSON linting (lint only, exclude .verified.json)
$jsonFiles = Get-StagedFiles -Extensions @('.json')
$jsonFiles = $jsonFiles | Where-Object { $_ -notmatch '\.verified\.json$' }
if ($jsonFiles.Count -gt 0) {
if (Test-ToolAvailable -Command "python3" -InstallHint "https://www.python.org/downloads/") {
foreach ($file in $jsonFiles) {
$fullPath = Join-Path $repoRoot $file
$output = python3 -m json.tool $fullPath 2>&1
if ($LASTEXITCODE -ne 0) {
Set-HookFailed -Check "json: $file"
Write-Host $output
}
}
}
}

# Shell script linting (lint only)
$shellFiles = Get-StagedFiles -Extensions @('.sh', '.bash')
if ($shellFiles.Count -gt 0) {
if (Test-ToolAvailable -Command "shellcheck" -InstallHint "https://github.com/koalaman/shellcheck#installing") {
$fullPaths = $shellFiles | ForEach-Object { Join-Path $repoRoot $_ }
$output = shellcheck $fullPaths 2>&1
if ($LASTEXITCODE -ne 0) {
Set-HookFailed -Check "shellcheck"
Write-Host $output
}
}
}

# GitHub Actions linting (lint only)
$workflowFiles = Get-StagedFiles -Extensions @('.yml', '.yaml')
$workflowFiles = $workflowFiles | Where-Object { $_ -match '^\.github/workflows/' }
if ($workflowFiles.Count -gt 0) {
if (Test-ToolAvailable -Command "actionlint" -InstallHint "https://github.com/rhysd/actionlint#install") {
$fullPaths = $workflowFiles | ForEach-Object { Join-Path $repoRoot $_ }
$output = actionlint $fullPaths 2>&1
if ($LASTEXITCODE -ne 0) {
Set-HookFailed -Check "actionlint"
Write-Host $output
}
}
}
Comment on lines +39 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve efficiency, you can avoid calling Get-StagedFiles for YAML files twice. You can get the list of YAML files once, store it in a variable, and then use it for both the general YAML linting and the GitHub Actions linting. This avoids running an unnecessary git diff command.

    $allYamlFiles = Get-StagedFiles -Extensions @('.yml', '.yaml')
    if ($allYamlFiles.Count -gt 0) {
        Write-Section "YAML Lint"
        if (Test-ToolAvailable -Command "yamllint" -InstallHint "pip install yamllint") {
            $fullPaths = $allYamlFiles | ForEach-Object { Join-Path $repoRoot $_ }
            $output = yamllint -c (Join-Path $repoRoot ".yamllint.yml") $fullPaths 2>&1
            Write-Result -Check "yamllint" -Passed ($LASTEXITCODE -eq 0)
            if ($LASTEXITCODE -ne 0) {
                Write-Host $output
            }
        }
    }

    # --- JSON linting (lint only, exclude .verified.json) ---
    $jsonFiles = Get-StagedFiles -Extensions @('.json')
    $jsonFiles = $jsonFiles | Where-Object { $_ -notmatch '\.verified\.json$' }
    if ($jsonFiles.Count -gt 0) {
        Write-Section "JSON Lint"
        if (Test-ToolAvailable -Command "python3" -InstallHint "https://www.python.org/downloads/") {
            $jsonPassed = $true
            foreach ($file in $jsonFiles) {
                $fullPath = Join-Path $repoRoot $file
                $output = python3 -m json.tool $fullPath 2>&1
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "  Invalid JSON: $file"
                    Write-Host $output
                    $jsonPassed = $false
                }
            }
            Write-Result -Check "json validation" -Passed $jsonPassed
        }
    }

    # --- Shell script linting (lint only) ---
    $shellFiles = Get-StagedFiles -Extensions @('.sh', '.bash')
    if ($shellFiles.Count -gt 0) {
        Write-Section "Shell Lint"
        if (Test-ToolAvailable -Command "shellcheck" -InstallHint "https://github.com/koalaman/shellcheck#installing") {
            $fullPaths = $shellFiles | ForEach-Object { Join-Path $repoRoot $_ }
            $output = shellcheck $fullPaths 2>&1
            Write-Result -Check "shellcheck" -Passed ($LASTEXITCODE -eq 0)
            if ($LASTEXITCODE -ne 0) {
                Write-Host $output
            }
        }
    }

    # --- GitHub Actions linting (lint only) ---
    $workflowFiles = $allYamlFiles | Where-Object { $_ -match '^\.github/workflows/' }
    if ($workflowFiles.Count -gt 0) {
        Write-Section "GitHub Actions Lint"
        if (Test-ToolAvailable -Command "actionlint" -InstallHint "https://github.com/rhysd/actionlint#install") {
            $fullPaths = $workflowFiles | ForEach-Object { Join-Path $repoRoot $_ }
            $output = actionlint $fullPaths 2>&1
            Write-Result -Check "actionlint" -Passed ($LASTEXITCODE -eq 0)
            if ($LASTEXITCODE -ne 0) {
                Write-Host $output
            }
        }
    }

}
catch {
Write-Host $_ -ForegroundColor Red

Check warning on line 95 in .githooks/hooks/Invoke-PreCommit.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PreCommit.ps1#L95

File 'Invoke-PreCommit.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
Write-Host $_.ScriptStackTrace

Check warning on line 96 in .githooks/hooks/Invoke-PreCommit.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PreCommit.ps1#L96

File 'Invoke-PreCommit.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
$script:HookExitCode = 1
}

if ($script:HookExitCode -ne 0) {
Write-Host "Bypass: git commit --no-verify" -ForegroundColor Yellow

Check warning on line 101 in .githooks/hooks/Invoke-PreCommit.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PreCommit.ps1#L101

File 'Invoke-PreCommit.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
}

exit $script:HookExitCode
36 changes: 36 additions & 0 deletions .githooks/hooks/Invoke-PrePush.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[CmdletBinding()]
param()

$repoRoot = git rev-parse --show-toplevel
. "$PSScriptRoot/../lib/LintHelpers.ps1"

# Allow newer .NET runtimes to run tests targeting older TFMs
$env:DOTNET_ROLL_FORWARD = "LatestMajor"

try {
dotnet build (Join-Path $repoRoot "Moq.Analyzers.sln") /p:PedanticMode=true --verbosity quiet 2>&1
$buildPassed = $LASTEXITCODE -eq 0

if (-not $buildPassed) {
Set-HookFailed -Check "dotnet build"
Write-Host "Build failed. Skipping tests."

Check warning on line 16 in .githooks/hooks/Invoke-PrePush.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PrePush.ps1#L16

File 'Invoke-PrePush.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
}
else {
$runSettings = Join-Path $repoRoot "build/targets/tests/test.runsettings"
dotnet test (Join-Path $repoRoot "Moq.Analyzers.sln") --no-build --settings $runSettings --verbosity quiet 2>&1
if ($LASTEXITCODE -ne 0) {
Set-HookFailed -Check "dotnet test"
}
}
}
Comment on lines +10 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The output from dotnet build and dotnet test is currently being redirected but not captured or displayed on failure. This means if a build or test fails, the user will see the failure message from the hook but not the underlying error from dotnet. It's better to capture the output and print it when the command fails, similar to how it's handled in the Invoke-PreCommit.ps1 script. This provides immediate, actionable feedback to the developer.

try {
    # --- Build ---
    Write-Section "Build"
    $buildOutput = dotnet build (Join-Path $repoRoot "Moq.Analyzers.sln") /p:PedanticMode=true --verbosity quiet 2>&1
    $buildPassed = $LASTEXITCODE -eq 0
    Write-Result -Check "dotnet build" -Passed $buildPassed

    if (-not $buildPassed) {
        Write-Host $buildOutput
        Write-Host "  Build failed. Skipping tests." -ForegroundColor Yellow
    }
    else {
        # --- Tests ---
        Write-Section "Tests"
        $runSettings = Join-Path $repoRoot "build/targets/tests/test.runsettings"
        $testOutput = dotnet test (Join-Path $repoRoot "Moq.Analyzers.sln") --no-build --settings $runSettings --verbosity quiet 2>&1
        Write-Result -Check "dotnet test" -Passed ($LASTEXITCODE -eq 0)
        if ($LASTEXITCODE -ne 0) {
            Write-Host $testOutput
        }
    }
}

catch {
Write-Host $_ -ForegroundColor Red

Check warning on line 27 in .githooks/hooks/Invoke-PrePush.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PrePush.ps1#L27

File 'Invoke-PrePush.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
Write-Host $_.ScriptStackTrace

Check warning on line 28 in .githooks/hooks/Invoke-PrePush.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PrePush.ps1#L28

File 'Invoke-PrePush.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
$script:HookExitCode = 1
}

if ($script:HookExitCode -ne 0) {
Write-Host "Bypass: git push --no-verify" -ForegroundColor Yellow

Check warning on line 33 in .githooks/hooks/Invoke-PrePush.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/hooks/Invoke-PrePush.ps1#L33

File 'Invoke-PrePush.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
}

exit $script:HookExitCode
78 changes: 78 additions & 0 deletions .githooks/lib/LintHelpers.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Shared helper functions for git hook scripts.
# Dot-source this file: . "$PSScriptRoot/../lib/LintHelpers.ps1"

$script:HookExitCode = 0

function Test-ToolAvailable {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Command,

[Parameter()]
[string]$InstallHint
)

if (Get-Command $Command -ErrorAction SilentlyContinue) {
return $true
}

if ($InstallHint) {
Write-Warning "$Command not found. Install: $InstallHint"
}

return $false
}

function Get-StagedFiles {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string[]]$Extensions
)

$stagedFiles = git diff --cached --name-only --diff-filter=d
if (-not $stagedFiles) {
return @()
}

$patterns = $Extensions | ForEach-Object { [regex]::Escape($_) + '$' }
$combined = ($patterns -join '|')

$matched = $stagedFiles | Where-Object { $_ -match $combined }
if (-not $matched) {
return @()
}

return @($matched)
}

function Invoke-AutoFix {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock]$FixCommand,

[Parameter(Mandatory)]
[string[]]$Files
)
Comment on lines +50 to +58

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoke-AutoFix accepts a Label parameter but never uses it. Either use it for consistent output (e.g., in a section header / log line) or remove it to avoid dead parameters.

Copilot uses AI. Check for mistakes.

& $FixCommand

$modified = git diff --name-only -- $Files
if ($modified) {
Write-Host "Auto-fixed: $($modified -join ', ')"

Check warning on line 64 in .githooks/lib/LintHelpers.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/lib/LintHelpers.ps1#L64

File 'LintHelpers.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
git add $modified
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

function Set-HookFailed {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Check
)

Write-Host "FAIL: $Check" -ForegroundColor Red

Check warning on line 76 in .githooks/lib/LintHelpers.ps1

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.githooks/lib/LintHelpers.ps1#L76

File 'LintHelpers.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
$script:HookExitCode = 1
}
4 changes: 4 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
REPO_ROOT="$(git rev-parse --show-toplevel)"
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Clean shebang and repo discovery implementation.

The portable shebang and use of git rev-parse --show-toplevel follow bash best practices. Since this runs as a git hook, the command should always succeed.

Optional: Add defensive error handling

While unlikely to fail in a git hook context, defensive error handling would provide clearer diagnostics if git rev-parse unexpectedly fails:

 #!/usr/bin/env bash
-REPO_ROOT="$(git rev-parse --show-toplevel)"
+REPO_ROOT="$(git rev-parse --show-toplevel 2>&1)" || { echo "Error: Failed to determine repository root"; exit 1; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.githooks/pre-commit around lines 1 - 2, The shebang and REPO_ROOT
assignment are fine but add defensive error handling around the REPO_ROOT="$(git
rev-parse --show-toplevel)" command: test whether git rev-parse succeeded and if
not print a clear diagnostic and exit nonzero; update the hook to capture the
command exit status (or use a conditional assignment) and ensure REPO_ROOT is
non-empty before continuing so the script fails fast with a helpful message.

command -v pwsh >/dev/null 2>&1 || { echo "pwsh not found. Install PowerShell Core: https://aka.ms/powershell"; exit 1; }
exec pwsh -NoProfile -NonInteractive -File "$REPO_ROOT/.githooks/hooks/Invoke-PreCommit.ps1"
4 changes: 4 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
REPO_ROOT="$(git rev-parse --show-toplevel)"
command -v pwsh >/dev/null 2>&1 || { echo "pwsh not found. Install PowerShell Core: https://aka.ms/powershell"; exit 1; }
exec pwsh -NoProfile -NonInteractive -File "$REPO_ROOT/.githooks/hooks/Invoke-PrePush.ps1"
33 changes: 32 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,38 @@ This project adheres to the [Contributor Covenant Code of Conduct](CODE-OF-CONDU

5. **Install linting tools** (for local validation):
- [yamllint](https://yamllint.readthedocs.io/) (Python): `pip install yamllint`
- [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) (Node.js): `npm install -g markdownlint-cli`
- [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) (Node.js): `npm install -g markdownlint-cli2`

6. **Git hooks** auto-configure on first `dotnet build` or `dotnet restore`.
Hooks require [PowerShell Core](https://aka.ms/powershell) (`pwsh`).

**What each hook checks:**

| Hook | Check | Mode | Tool |
| ------ | ------- | ------ | ------ |
| pre-commit | C# formatting | Auto-fix + re-stage | `dotnet format` |
| pre-commit | Markdown lint | Auto-fix + re-stage | `markdownlint-cli2` |
| pre-commit | YAML lint | Lint only | `yamllint` |
| pre-commit | JSON validation | Lint only | `python3 -m json.tool` |
| pre-commit | Shell scripts | Lint only | `shellcheck` |
| pre-commit | GitHub Actions | Lint only | `actionlint` |
| pre-push | Build | Fail on error | `dotnet build` |
| pre-push | Tests | Fail on error | `dotnet test` |

**Optional tool installation:**

```bash
npm install -g markdownlint-cli2 # Markdown auto-fix
pip install yamllint # YAML lint
# shellcheck: apt install shellcheck / brew install shellcheck
# actionlint: go install github.com/rhysd/actionlint/cmd/actionlint@latest
```

Missing optional tools are skipped with a warning. C# formatting via `dotnet format` is always available.

Bypass hooks for WIP commits: `git commit --no-verify`

Manual setup (if auto-configure fails): `git config core.hooksPath .githooks`

## Universal Agent Success Principles for Project Maintainers

Expand Down
1 change: 1 addition & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
<Import Project="build/targets/tests/Tests.targets" />
<Import Project="build/targets/codeanalysis/CodeAnalysis.targets" />
<Import Project="build/targets/packaging/Packaging.targets" />
<Import Project="build/targets/githooks/GitHooks.targets" />
</Project>
22 changes: 22 additions & 0 deletions build/targets/githooks/GitHooks.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project>
<!--
Automatically configure git to use .githooks/ as the hooks directory.
Runs once per clone via sentinel file. Degrades gracefully in CI or non-git contexts.
-->
<Target
Name="ConfigureGitHooks"
BeforeTargets="Restore;Build"
Condition=" '$(DesignTimeBuild)' != 'true' AND Exists('$(RepoRoot).git') AND !Exists('$(RepoRoot).git/hooks/.githooks-configured') ">

<Exec
Command="git config core.hooksPath .githooks"

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git config core.hooksPath .githooks will overwrite any existing core.hooksPath the developer may already have for this clone. To avoid clobbering local configuration, consider checking the current value first and only setting it when it’s empty (or already .githooks).

Suggested change
Command="git config core.hooksPath .githooks"
Command="git config core.hooksPath || git config core.hooksPath .githooks"

Copilot uses AI. Check for mistakes.
WorkingDirectory="$(RepoRoot)"
IgnoreExitCode="true" />

<Touch
Files="$(RepoRoot).git/hooks/.githooks-configured"
AlwaysCreate="true" />

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentinel file is written under $(RepoRoot).git/hooks/…, but in git worktrees and submodules .git is often a file (pointing at the real gitdir), so $(RepoRoot).git/hooks is not a valid directory. This can make the Touch task fail and break dotnet build/restore. Consider placing the sentinel somewhere that is always a real directory in the working tree (e.g., under $(RepoRoot).githooks/) or resolve the hooks dir via git rev-parse --git-path hooks and write the sentinel there.

Copilot uses AI. Check for mistakes.

<Message Importance="high" Text="Git hooks configured. Run 'git config --unset core.hooksPath' to disable." />

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exec has IgnoreExitCode="true", but the target always writes the sentinel and prints the "configured" message afterward. If git config fails (e.g., git not on PATH, permissions, CI restrictions), this will permanently suppress future attempts while leaving hooks unconfigured. Capture the Exec exit code and only write the sentinel / show the message when it succeeds.

Suggested change
IgnoreExitCode="true" />
<Touch
Files="$(RepoRoot).git/hooks/.githooks-configured"
AlwaysCreate="true" />
<Message Importance="high" Text="Git hooks configured. Run 'git config --unset core.hooksPath' to disable." />
IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="GitConfigExitCode" />
</Exec>
<Touch
Condition=" '$(GitConfigExitCode)' == '0' "
Files="$(RepoRoot).git/hooks/.githooks-configured"
AlwaysCreate="true" />
<Message
Condition=" '$(GitConfigExitCode)' == '0' "
Importance="high"
Text="Git hooks configured. Run 'git config --unset core.hooksPath' to disable." />

Copilot uses AI. Check for mistakes.

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This target will run in CI as long as .git exists, which mutates the repo’s local git config and adds noise to build logs. Since the purpose is local developer workflow, consider skipping when $(ContinuousIntegrationBuild) is true (this repo already uses that property to drive CI-only behavior).

Copilot uses AI. Check for mistakes.
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
</Target>
</Project>
Loading