mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 21:07:02 +01:00
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>
88 lines
3.0 KiB
Bash
Executable File
88 lines
3.0 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# ============================================================================
|
|
# Install this repo's git hooks.
|
|
#
|
|
# .git/hooks/ is not tracked, so a fresh clone has no hooks. Run this once
|
|
# after cloning.
|
|
#
|
|
# Two layers, deliberately:
|
|
#
|
|
# 1. pre-commit (the framework) runs the read-only gates in
|
|
# .pre-commit-config.yaml -- the crdt_sync override check and the
|
|
# 250-line file-length cap.
|
|
# 2. A thin wrapper hook installed here runs pre-commit first and, only if
|
|
# it passed, runs scripts/bump_patch_version.sh.
|
|
#
|
|
# The bump cannot live in .pre-commit-config.yaml. pre-commit stashes
|
|
# unstaged changes for the duration of its run and restores them with
|
|
# `git apply`; the bump rewrites pubspec.yaml inside that window, so an
|
|
# uncommitted edit anywhere near the `version:` line makes the restore
|
|
# conflict -- which aborts the commit and DROPS the developer's edit from the
|
|
# working tree. Running it after pre-commit has exited keeps the mutation
|
|
# outside the stash cycle, and gating it on pre-commit's exit status stops a
|
|
# rejected commit from leaving a stray bump staged for the next one.
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
readonly REPO_ROOT
|
|
readonly HOOK="$REPO_ROOT/.git/hooks/pre-commit"
|
|
readonly LEGACY_MARKER='bump_patch_version.sh'
|
|
|
|
# The old hand-written hook has to go before pre-commit will install over it;
|
|
# pre-commit refuses to clobber a foreign hook.
|
|
remove_legacy_hook() {
|
|
if [[ -e "$HOOK" ]] && grep -q "$LEGACY_MARKER" "$HOOK" \
|
|
&& ! grep -q 'pre-commit' "$HOOK"; then
|
|
rm -f "$HOOK"
|
|
echo "Removed the legacy hand-written pre-commit hook."
|
|
fi
|
|
}
|
|
|
|
ensure_pre_commit() {
|
|
if command -v pre-commit >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
echo "Installing pre-commit..."
|
|
if command -v pipx >/dev/null 2>&1; then
|
|
pipx install pre-commit
|
|
else
|
|
python3 -m pip install --user pre-commit
|
|
fi
|
|
}
|
|
|
|
# Wraps pre-commit's own generated hook so the version bump runs after it,
|
|
# and only on success. pre-commit install --hook-type pre-commit writes the
|
|
# generated script; we move it aside and call it from ours.
|
|
install_wrapper() {
|
|
local generated="$REPO_ROOT/.git/hooks/pre-commit.pre-commit-generated"
|
|
mv "$HOOK" "$generated"
|
|
cat > "$HOOK" <<'HOOK_BODY'
|
|
#!/bin/bash
|
|
# Installed by scripts/install_hooks.sh — see that file for why the version
|
|
# bump runs here rather than as a pre-commit hook.
|
|
set -euo pipefail
|
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
readonly ROOT
|
|
|
|
# The read-only gates. A non-zero exit here rejects the commit, and the bump
|
|
# below never runs, so a rejected attempt leaves no stray version change.
|
|
"$ROOT/.git/hooks/pre-commit.pre-commit-generated" "$@"
|
|
|
|
exec bash "$ROOT/scripts/bump_patch_version.sh"
|
|
HOOK_BODY
|
|
chmod 755 "$HOOK"
|
|
}
|
|
|
|
main() {
|
|
ensure_pre_commit
|
|
remove_legacy_hook
|
|
pre-commit install
|
|
install_wrapper
|
|
echo "Installed pre-commit hooks: $HOOK"
|
|
}
|
|
|
|
main "$@"
|