todo-app/scripts/install_hooks.sh
2026-11-02 10:24:59 +01:00

92 lines
3.2 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
# The common git dir, not <toplevel>/.git: in a worktree .git is a file.
HOOKS_DIR="$(git rev-parse --path-format=absolute --git-common-dir)/hooks"
readonly HOOKS_DIR
readonly HOOK="$HOOKS_DIR/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="$HOOKS_DIR/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 common git dir, not $ROOT/.git: in a worktree .git is a file.
HOOKS_DIR="$(git rev-parse --path-format=absolute --git-common-dir)/hooks"
readonly HOOKS_DIR
# 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.
"$HOOKS_DIR/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 "$@"