diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e8aa342 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +# Runs the same checks CI does, before CI does — install once with `prek install` (or +# `pre-commit install`, prek reads this same file) and this runs automatically on every +# `git commit`. Skip it for one commit with `git commit --no-verify`; run it on demand +# against everything with `prek run --all-files`. +# +# Scoped to the files in the commit, not the whole tree: CI's `clang-format` job checks +# every source file on every push, but a commit here should live or die on the files it +# actually changed, not on some unrelated file elsewhere that happened to already be +# unformatted — that file is CI's problem to catch (on whatever commit touches it), not a +# reason to block this one. +# +# A local hook, not one of the upstream clang-format mirrors: those track whatever +# clang-format version they were packaged against, and scripts/format.sh already pins the +# exact version CI uses (22.1.8) so a contributor's hook and CI's job never disagree about +# what "formatted" means. Install that exact version with +# `pip install clang-format==22.1.8` — not `brew install clang-format`, whose formula +# tracks upstream's latest release rather than this pin, and will eventually drift ahead +# of it. +repos: + - repo: local + hooks: + - id: clang-format + name: clang-format (scripts/format.sh) + description: >- + Reformats the C/C++ source files in this commit in place with the pinned + clang-format, matching what CI's `clang-format` job would say about those + specific files. If it changes anything, the commit is blocked — `git add -u` + the reformatted files and commit again. + entry: scripts/format.sh + language: system + files: \.(c|h|cpp|hpp)$ + pass_filenames: true diff --git a/README.md b/README.md index 3aa30ca..179e15f 100644 --- a/README.md +++ b/README.md @@ -910,6 +910,26 @@ cd examples/wifi_basic && pio run # compile-check against every ESP32 varia Building the example on all four variants is the portability gate — a change that breaks RISC-V or single-core builds fails there rather than on someone's bench. +**Formatting runs in CI (`clang-format`, pinned to 22.1.8) and fails the build if a file +isn't formatted.** Catch it before pushing instead of after: + +```bash +pip install clang-format==22.1.8 # exact version — brew's formula tracks upstream + # latest and will eventually drift off this pin +brew install prek # or: pipx install prek — https://prek.j178.dev +prek install # one-time; also works with `pre-commit install` +``` + +This installs a git hook from [`.pre-commit-config.yaml`](.pre-commit-config.yaml) that +runs `scripts/format.sh` — the exact script and clang-format version CI uses — on the +C/C++ files in each `git commit`. Deliberately narrower than CI, which checks the whole +tree on every push: this only fails your commit over files *you* touched, not some +already-unformatted file elsewhere that CI would also catch on its own. If it reformats +anything, the commit is blocked; `git add -u` the reformatted files and commit again. Run +it on demand with `prek run --all-files`, or skip it for one commit with +`git commit --no-verify`. (Git hooks live in the repository, not the checkout — +installing from one `git worktree` of this repo enables it for all of them.) + **Touching anything that uses an Arduino API? Build C6 too:** ```bash diff --git a/scripts/format.sh b/scripts/format.sh index f0f422d..2f7d1fc 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,9 +1,16 @@ #!/usr/bin/env bash # -# Format (or check) every source file in the repo. +# Format (or check) source files — every one in the repo by default, or just the ones +# named on the command line. # -# scripts/format.sh rewrite files in place -# scripts/format.sh --check exit non-zero if anything is unformatted (what CI runs) +# scripts/format.sh rewrite every file in place +# scripts/format.sh --check exit non-zero if anything is unformatted (what CI runs) +# scripts/format.sh foo.c bar.h rewrite only those files +# scripts/format.sh --check foo.c check only that file +# +# The explicit-file form exists for the pre-commit hook (.pre-commit-config.yaml): it should +# format only what a commit actually touches, not fail it over some unrelated file elsewhere +# in the tree that CI's whole-repo check would also catch on its own. # # clang-format's output shifts between major versions, so a contributor on a different # version would otherwise "fix" formatting on every file they touch. CI pins 22.1.8 via pip; @@ -20,25 +27,36 @@ if ! command -v "$CLANG_FORMAT" >/dev/null 2>&1; then exit 1 fi -# Only our own sources: -prune keeps this out of .pio/, which holds the entire Arduino core -# and ESP-IDF once anything has been built. -# (A read loop rather than `mapfile`, which needs bash 4 — macOS still ships bash 3.2.) -files=() -while IFS= read -r file; do - files+=("$file") -done < <( - find . \ - \( -name .git -o -name .pio -o -name build \) -prune -o \ - \( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' \) -print | - sort -) +CHECK=0 +if [ "${1:-}" = "--check" ]; then + CHECK=1 + shift +fi + +if [ "$#" -gt 0 ]; then + # Explicit files, e.g. from pre-commit: format/check exactly those, in the order given. + files=("$@") +else + # Only our own sources: -prune keeps this out of .pio/, which holds the entire Arduino + # core and ESP-IDF once anything has been built. + # (A read loop rather than `mapfile`, which needs bash 4 — macOS still ships bash 3.2.) + files=() + while IFS= read -r file; do + files+=("$file") + done < <( + find . \ + \( -name .git -o -name .pio -o -name build \) -prune -o \ + \( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' \) -print | + sort + ) +fi if [ ${#files[@]} -eq 0 ]; then echo "no sources found" >&2 exit 1 fi -if [ "${1:-}" = "--check" ]; then +if [ "$CHECK" -eq 1 ]; then "$CLANG_FORMAT" --dry-run -Werror "${files[@]}" echo "${#files[@]} files are correctly formatted." else