build: add format targets and a pre-commit format hook - #32
Merged
Conversation
CI rejects unformatted C++ and Python, but nothing caught it before a push, so the first signal was a red PR. Three entry points now check the same things. tools/format.sh is the single implementation. CI calls it, the CMake format/format-check targets call it, and the pre-commit hook calls it. Sharing one script is the entire point: a local check that has drifted from CI is worse than no local check, because it is trusted. CI's inline clang-format and ruff blocks are replaced by a call to it; mypy stays inline, since type checking is not a formatting concern and is too slow for a commit hook. The hook checks staged content via `git show :file`, not the file on disk. A file can be staged in one state and edited further before committing, and checking the working tree would let a bad commit through -- or block a good one. Both directions are covered. It lives in .githooks rather than .git/hooks so it is version controlled, and the CMake configure step points core.hooksPath at it. That writes to the developer's local git config, so it is announced in the configure output rather than done silently, and -DINSTALL_GIT_HOOKS=OFF opts out. `git commit --no-verify` bypasses a single commit. clang-format is pinned to 18 to match CI and the dev container, with a warning if a different major version is found -- an unpinned binary reintroduces exactly the local/CI split this is meant to prevent. cmake/format.cmake is lowercase to match cmake/python_venv.cmake. CamelCase is the convention for CMake's own upstream and find-modules, not for a project's own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nehalkpatel
force-pushed
the
format-tooling
branch
from
August 27, 2026 07:00
806a920 to
c478a45
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
CI rejects unformatted C++ and Python, but nothing caught it before a push — the first signal was a red PR. That is exactly how #31 failed: scripted edits produced valid C++ that clang-format disagreed with, and it took a CI round trip plus a history rewrite to find out.
One implementation, three callers
tools/format.shis the single source of truth. CI calls it, the CMakeformat/format-checktargets call it, and the pre-commit hook calls it.Sharing one script is the entire point. A local check that has drifted from CI is worse than no local check, because it is trusted — you stop looking at CI, and CI is the one that's right. CI's inline clang-format and ruff blocks are replaced by a call to the script.
mypydeliberately stays inline in CI: type checking is not a formatting concern and is too slow to belong in a commit hook.The hook checks the index, not the working tree
git show :file, not the file on disk. A file can be staged in one state and edited further before committing; checking the working tree would let a bad commit through, or block a good one. Verified in both directions:Installation
The hook lives in
.githooksrather than.git/hooks, so it is version controlled and arrives with a clone. The CMake configure step pointscore.hooksPathat it.That writes to the developer's local git config, so it is announced in the configure output rather than done silently:
-DINSTALL_GIT_HOOKS=OFFopts out (verified: sets nothing, prints nothing).git commit --no-verifybypasses a single commit. The hook exits 0 iftools/format.shis missing, so a branch predating this change does not become uncommittable.Version pinning
clang-format is pinned to 18, matching CI and the dev container's
clang-format -> clang-format-18alias, and warns if a different major version is found. An unpinned binary reintroduces exactly the local/CI split this is meant to prevent.Verification
Every path was tested against deliberately broken input rather than assumed:
--checkrejects a bad file (rc=1) and passes a clean tree (rc=0)git commit— no commit object was created--fixrepairs the file and--checkthen passesformatandformat-checkboth exist as ninja targets and behave identically to the script-DINSTALL_GIT_HOOKS=OFFci.ymlparses as valid YAML; both scripts passbash -nhost-debugworkflow still green, 29/29The commit adding the hook was itself checked by the hook.
Note on naming
cmake/format.cmakeis lowercase to match the existingcmake/python_venv.cmake. CamelCase is CMake's convention for its own upstream and find-modules (FetchContent,GNUInstallDirs), not for a project's own modules — I had it CamelCased initially and corrected it.🤖 Generated with Claude Code