Skip to content

Add optional pre-commit hook for code formatting#5178

Open
jamescrosswell wants to merge 16 commits intomainfrom
feature/pre-commit-formatting-hooks
Open

Add optional pre-commit hook for code formatting#5178
jamescrosswell wants to merge 16 commits intomainfrom
feature/pre-commit-formatting-hooks

Conversation

@jamescrosswell
Copy link
Copy Markdown
Collaborator

@jamescrosswell jamescrosswell commented May 1, 2026

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 runs dotnet format against staged .cs files only

Updated files:

  • dev.cs: Adds setup-hooks and remove-hooks commands to opt in/out

Behavior:

  1. Developer runs ./dev.cs setup-hooks once to enable the hook
  2. On each commit, the pre-commit hook runs
  3. If unstaged changes are present → hook skips with a warning (dotnet format operates on the working tree, so partially staged files can't be handled safely)
  4. Otherwise, dotnet format runs against staged .cs files only and auto-fixes any formatting issues
  5. If fixes were applied → commit fails, developer stages the fixes (git add -u) and commits again
  6. Can bypass with git commit --no-verify if needed
  7. Run ./dev.cs remove-hooks to opt out

Design decisions:

  • Auto-fix: Hook applies formatting fixes rather than just reporting them — developer only needs to git add -u and recommit rather than manually running dotnet format
  • Staged files only: Uses --include scoped to staged .cs files so only what's being committed gets formatted
  • Skip on unstaged changes: If the working tree isn't clean, the hook exits with a warning rather than risking modification of unstaged hunks in partially staged files
  • Native git hooks: No external dependencies (Husky.NET not needed)
  • Opt-in via dev.cs: Discoverable through the existing dev CLI rather than a separate shell script

Addresses

Fixes #4980

Testing

  • ✅ Hook executes on commit
  • ✅ Hook passes when staged files are correctly formatted
  • ✅ Hook auto-fixes formatting and blocks commit when staged files need formatting
  • ✅ Hook skips with warning when unstaged changes are present
  • ✅ Hook skips cleanly when no .cs files are staged
  • ./dev.cs setup-hooks enables the hook
  • ./dev.cs remove-hooks disables the hook

Next Steps

This PR makes the hook optional. After gathering feedback, we could:

  • Document in onboarding materials
  • Consider making it required (auto-setup on clone)
  • Add other pre-commit checks (analyzers, etc.)

🤖 Generated with Claude Code

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]>
Comment thread .githooks/pre-commit Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.08%. Comparing base (32a55e3) to head (d1f2687).
⚠️ Report is 9 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

jamescrosswell and others added 3 commits May 4, 2026 09:40
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]>
Comment thread .githooks/pre-commit Outdated
Comment thread .githooks/pre-commit Outdated
Comment thread .githooks/pre-commit Outdated
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]>
Comment thread .githooks/pre-commit
jamescrosswell and others added 2 commits May 4, 2026 11:19
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]>
Comment thread .githooks/pre-commit Outdated
Comment thread .githooks/pre-commit Outdated
Comment thread .githooks/pre-commit Outdated
Comment thread .githooks/pre-commit Outdated
jamescrosswell and others added 3 commits May 4, 2026 12:22
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]>
Comment thread .githooks/pre-commit Outdated
Comment thread .githooks/pre-commit Outdated
jamescrosswell and others added 4 commits May 4, 2026 13:20
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]>
@jamescrosswell jamescrosswell marked this pull request as ready for review May 5, 2026 04:23
@jamescrosswell jamescrosswell requested a review from Flash0ver as a code owner May 5, 2026 04:23
Comment thread CONTRIBUTING.md
Comment thread CONTRIBUTING.md Outdated
Comment thread .githooks/pre-commit
jamescrosswell and others added 2 commits May 6, 2026 17:52
- 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]>
Comment thread .githooks/pre-commit
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ 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.

Comment thread .githooks/pre-commit
INCLUDE_ARGS=()
while IFS= read -r f; do
INCLUDE_ARGS+=(--include "$f")
done < <(git diff --cached --name-only --diff-filter=ACM | grep '\.cs$' || true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1f2687. Configure here.

Comment thread .githooks/pre-commit
@@ -0,0 +1,45 @@
#!/bin/bash
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1f2687. Configure here.

Comment thread .githooks/pre-commit
INCLUDE_ARGS=()
while IFS= read -r f; do
INCLUDE_ARGS+=(--include "$f")
done < <(git diff --cached --name-only --diff-filter=ACM | grep '\.cs$' || true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1f2687. Configure here.

Comment thread dev.cs
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1f2687. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can we run analysers anddotnet format as pre-commit hooks?

1 participant