todo-app/.pre-commit-config.yaml
Krzysztof kuhy Rudnicki 34b2c1c87c Fix three gate defects found reviewing the cap refactor
Moving bump_patch_version.sh into .pre-commit-config.yaml introduced three
bugs, all reproduced in clean clones of HEAD before fixing and re-verified
after:

1. DATA LOSS. pre-commit stashes unstaged changes and restores them with
   `git apply`. The bump rewrites pubspec.yaml inside that window, so an
   uncommitted edit near the `version:` line made the restore conflict:
   the commit aborted AND the developer's edit vanished from the working
   tree, recoverable only from $PRE_COMMIT_HOME. Only non-mutating checks
   belong in that file.

2. A commit rejected by the file-length gate still had its version bumped
   and staged, riding into the next successful commit. `always_run: true`
   with no `fail_fast` meant "ordered last" did not mean "runs last only
   on success" -- which is exactly what the comment above it claimed.

3. `files: ^pubspec\.yaml$` narrowed the crdt_sync override check from
   unconditional to staged-only, so a poisoned pubspec arriving by pull or
   merge was invisible to every later commit that did not touch pubspec.
   The hand-written hook this replaced checked it every time; that
   narrowing quietly widened the hole the check exists to close.

The fix keeps the framework for the read-only gates and puts the mutation
back outside it: install_hooks.sh now writes a wrapper that runs
pre-commit's generated hook first and bump_patch_version.sh only if it
passed. Plus fail_fast, and always_run on the crdt check.

Also fixes three stale references this refactor created: a load-bearing
layout comment citing the renamed _buildRaw, bin/todo_desktop.dart naming
two split-away test files, and CLAUDE.md giving two different suite
runtimes.

Verified: unstaged pubspec edit preserved through a commit; a rejected
commit leaves nothing staged; a merged-in override is caught on an
unrelated commit. 313 tests, 100% coverage, gate exits 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 17:13:31 +02:00

53 lines
2.4 KiB
YAML

# Pre-commit hooks for todo.
#
# This repo previously used a hand-written .git/hooks/pre-commit installed by
# scripts/install_hooks.sh. It moved to the pre-commit framework when the
# 250-line file-length gate was added, so the *checks* live in one place with
# one install path.
#
# Only non-mutating checks belong here. pre-commit stashes unstaged changes
# for the duration of the run and restores them with `git apply` afterwards;
# a hook that rewrites a tracked file inside that window makes the restore
# conflict, which aborts the commit AND drops the developer's uncommitted
# edit. scripts/bump_patch_version.sh rewrites pubspec.yaml, so it stays out
# of this file and runs from the wrapper hook instead -- see
# scripts/install_hooks.sh.
#
# fail_fast so a later check cannot run after an earlier one has already
# rejected the commit.
#
# Install with: scripts/install_hooks.sh
# The hooks are `language: system` because they are plain bash/python already
# on this machine; pre-commit must not build an isolated env for them.
fail_fast: true
repos:
- repo: local
hooks:
# Gate first: a local crdt_sync override left in place makes CI build a
# library this checkout has never tested against, silently and in both
# directions. Blocking the commit is the only thing that prevents it.
#
# always_run/pass_filenames rather than `files:`: a poisoned pubspec can
# arrive by pull, merge or --no-verify, and a staged-only trigger would
# then never look at it again for any commit that does not happen to
# touch pubspec.yaml. The old hand-written hook checked it every time;
# narrowing that to staged-only silently widened the hole this gate
# exists to close.
- id: no-crdt-sync-override
name: no local crdt_sync dependency_override
entry: scripts/check_no_crdt_sync_override.sh
language: system
pass_filenames: false
always_run: true
# The shared 250-line cap. Checks only the files in the commit, so an
# unrelated commit never breaks; exits 1 rather than warning. No
# baseline and no allowlist -- those are suppressions. The exemption
# list (generated/vendored/data files) lives with the checker in
# ~/utils/file_length/config.py so the gate and the survey cannot
# disagree.
- id: file-length
name: file length <= 250 lines
entry: scripts/check_file_length.sh
language: system