Add optional pre-commit hook for code formatting#5178
Add optional pre-commit hook for code formatting#5178jamescrosswell wants to merge 16 commits intomainfrom
Conversation
Adds a pre-commit git hook that verifies code formatting before allowing commits. This helps catch formatting issues early and reduces CI failures. Implementation: - `.githooks/pre-commit`: Runs `dotnet format --verify-no-changes` - `scripts/setup-hooks.sh`: One-time setup script to configure git hooks - Updated CONTRIBUTING.md with setup instructions The hook is optional but recommended. If formatting issues are detected, the commit is prevented and the developer is prompted to run `dotnet format` manually. The hook uses the same dotnet format command as CI, ensuring consistency. Addresses #4980 Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5178 +/- ##
==========================================
+ Coverage 74.06% 74.08% +0.02%
==========================================
Files 501 506 +5
Lines 18113 18247 +134
Branches 3521 3564 +43
==========================================
+ Hits 13415 13518 +103
- Misses 3838 3858 +20
- Partials 860 871 +11 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Previously the hook piped dotnet format output through grep to check for "formatted" string. This ignored the actual exit code, which meant: - If dotnet format failed for unrelated reasons (missing SDK, etc) but didn't output "formatted", the hook would pass incorrectly - The implementation diverged from how dotnet format is intended to work Now uses --verify-no-changes' exit code directly (non-zero when formatting is needed), which is more reliable and matches the tool's design. Addresses Warden feedback in review comment. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
ModuleInitializer is legitimately used in test projects for test setup (VerifyHttp initialization, EF provider registration, etc.). Test projects are effectively application code, not libraries, so this usage is appropriate. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Changed the hook to replicate CI's approach: - Stash unstaged changes with --keep-index - Run dotnet format (no --verify-no-changes, matches CI exactly) - Check git diff to detect formatting changes - Restore stashed changes This avoids false failures from analyzer warnings (IDE1006, CA2255, etc.) and only fails on actual formatting changes that would fail CI. The stash approach handles mixed staged/unstaged changes cleanly - we only check formatting on what's being committed. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Replace the manual stash-pop block with a `trap ... EXIT` handler so unstaged changes are restored even when `dotnet format` fails and `set -e` causes early termination. Also replace the silent `2>/dev/null || true` swallow with an explicit failure message so developers are notified when a stash-pop conflict leaves their changes in the stash. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
When git stash pop fails due to a conflict, git leaves conflict markers in the affected files and keeps the stash entry — running stash pop again won't help. Update the warning to tell the developer to resolve the conflict markers first, then drop the stash. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The stash/unstash dance was error-prone (data loss on format failure, silent conflicts on pop). Just run dotnet format and block the commit if it made changes — the developer handles their own working tree. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Adds `./dev.cs setup-hooks` as a discoverable way to configure git to use the repo's pre-commit hooks from .githooks/. Calling git config directly in C# means it works on Windows too, replacing the Unix-only scripts/setup-hooks.sh approach. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Superseded by `./dev.cs setup-hooks` which does the same thing cross-platform with no duplicate logic to keep in sync. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Use `--include` with the list of staged .cs files so the format run only touches what's actually being committed. Unstaged WIP is left completely alone. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
grep exits 1 on no matches, which set -e would treat as a failure. Adding || true to the assignment makes this robust regardless of pipefail settings. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Symmetric counterpart to setup-hooks — runs git config --unset core.hooksPath to restore default git hooks behaviour. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Replace the space-separated string + unquoted expansion with a bash array, passing each staged file as its own --include argument. This correctly handles paths containing spaces or shell metacharacters. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
dotnet format operates on the working tree, not the index, so partially staged files would have their unstaged hunks modified too. Skip with a warning instead — the check still runs on clean commits (the common case). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Replace removed scripts/setup-hooks.sh with ./dev.cs setup-hooks - Correct hook behaviour: auto-fixes formatting rather than verify-only - Document the unstaged changes skip behaviour - Add ./dev.cs remove-hooks opt-out instructions Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Redirecting all output to /dev/null meant build errors or analyzer failures silently blocked the commit with no explanation. Capture output and print it only when dotnet format exits non-zero. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d1f2687. Configure here.
| INCLUDE_ARGS=() | ||
| while IFS= read -r f; do | ||
| INCLUDE_ARGS+=(--include "$f") | ||
| done < <(git diff --cached --name-only --diff-filter=ACM | grep '\.cs$' || true) |
There was a problem hiding this comment.
Hook blocks non-solution C# files
Medium Severity
pre-commit sends every staged *.cs path to dotnet format Sentry.slnx --include. Files outside Sentry.slnx, such as dev.cs, make dotnet format fail, so hook users cannot commit those valid repo files without bypassing the hook.
Reviewed by Cursor Bugbot for commit d1f2687. Configure here.
| @@ -0,0 +1,45 @@ | |||
| #!/bin/bash | |||
There was a problem hiding this comment.
Hook may break on Windows checkouts
Medium Severity
pre-commit is an extensionless Bash script, while the repo only forces LF endings for *.sh. Windows checkouts with core.autocrlf can write CRLF here, making the shebang fail before the hook runs.
Reviewed by Cursor Bugbot for commit d1f2687. Configure here.
| INCLUDE_ARGS=() | ||
| while IFS= read -r f; do | ||
| INCLUDE_ARGS+=(--include "$f") | ||
| done < <(git diff --cached --name-only --diff-filter=ACM | grep '\.cs$' || true) |
There was a problem hiding this comment.
Renamed files skip formatting
Low Severity
--diff-filter=ACM omits staged renames, so a .cs file renamed with edits never reaches dotnet format. The hook can pass while committing unformatted renamed C# files, leaving CI to catch the formatting failure.
Reviewed by Cursor Bugbot for commit d1f2687. Configure here.
| public Task<int> SetupHooksAsync(GlobalOptions options = default!) | ||
| { | ||
| Console.WriteLine("[dev] Configuring git hooks path to .githooks/"); | ||
| return RunStepAsync("git config core.hooksPath", "git", "config core.hooksPath .githooks", options.DryRun); |
There was a problem hiding this comment.
Existing hook paths get lost
Low Severity
setup-hooks overwrites any existing local core.hooksPath, and remove-hooks only unsets it. Developers with custom hook paths lose that configuration when opting in and cannot restore it through the provided opt-out command.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d1f2687. Configure here.


Summary
Adds an optional pre-commit git hook that auto-fixes code formatting before allowing commits. This helps catch formatting issues early and reduces CI failures.
Implementation
New files:
.githooks/pre-commit: Hook script that runsdotnet formatagainst staged.csfiles onlyUpdated files:
dev.cs: Addssetup-hooksandremove-hookscommands to opt in/outBehavior:
./dev.cs setup-hooksonce to enable the hookdotnet formatruns against staged.csfiles only and auto-fixes any formatting issuesgit add -u) and commits againgit commit --no-verifyif needed./dev.cs remove-hooksto opt outDesign decisions:
git add -uand recommit rather than manually runningdotnet format--includescoped to staged.csfiles so only what's being committed gets formattedAddresses
Fixes #4980
Testing
.csfiles are staged./dev.cs setup-hooksenables the hook./dev.cs remove-hooksdisables the hookNext Steps
This PR makes the hook optional. After gathering feedback, we could:
🤖 Generated with Claude Code