chore: port the existing pre-push hook into .githooks before activating hooksPath
This checkout already had a pre-push in .git/hooks. Setting core.hooksPath redirects every hook to .githooks/, so activating the template's hooks would have silently stopped it running — which is the exact failure the versioned hooks directory exists to prevent, applied to a hook. Kept its two working-tree checks: a push that leaves edits behind is how docs end up one commit adrift of the code they describe. Dropped its third. It refused whenever the branch was ahead of its remote — the precondition for pushing at all — so it fired on every real push and its only advice was to re-run with --no-verify. A guard that can never pass teaches people to bypass the two beside it that can. It did exactly that on the push of the adoption commits. Both remaining checks proven to refuse a dirty tree and pass a clean one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa1d805e04
commit
8e186fdf3f
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Nothing uncommitted goes out with a push.
|
||||
#
|
||||
# ## Where this came from
|
||||
#
|
||||
# There was already a `pre-push` in `.git/hooks` on this checkout when the
|
||||
# template was adopted on 2026-08-18. Setting `core.hooksPath` to this directory
|
||||
# would have silently stopped it running — the whole class of failure these
|
||||
# hooks exist to prevent, applied to a hook — so its useful half was moved here,
|
||||
# where it is versioned and every clone gets it.
|
||||
#
|
||||
# ## What was kept, and what was dropped
|
||||
#
|
||||
# **Kept:** the two working-tree checks. A push that leaves edits behind is how
|
||||
# documentation ends up one commit adrift of the code it describes, and
|
||||
# `docs/WORK_CYCLE.md` is built on those travelling together.
|
||||
#
|
||||
# **Dropped:** its third check, which refused the push when the branch was ahead
|
||||
# of its remote. That is the precondition for pushing at all, so it fired on
|
||||
# every real push and its only instruction was to re-run with `--no-verify` — a
|
||||
# guard that can never pass teaches people to bypass the two beside it that can.
|
||||
#
|
||||
# ## Escape hatch
|
||||
#
|
||||
# git push --no-verify
|
||||
#
|
||||
# Deliberately not silent about it: the checks below print what they found
|
||||
# before refusing, so an intentional bypass is a decision with the evidence in
|
||||
# front of it.
|
||||
#
|
||||
# Exit 0 nothing outstanding, 1 something is.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)" || exit 1
|
||||
|
||||
say() { printf '\033[1mpre-push:\033[0m %s\n' "$*" >&2; }
|
||||
|
||||
fail=0
|
||||
|
||||
if ! git diff --quiet --exit-code; then
|
||||
say "uncommitted working-tree changes:"
|
||||
git diff --name-only | sed 's/^/ /' >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if ! git diff --cached --quiet --exit-code; then
|
||||
say "staged but uncommitted changes:"
|
||||
git diff --cached --name-only | sed 's/^/ /' >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
say ""
|
||||
say "push refused. Commit or stash the above first — a push that leaves edits"
|
||||
say "behind is how the documents end up describing a commit nobody is running."
|
||||
say "To push anyway: git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
|
@ -329,7 +329,7 @@ npm run dev # Vite on 5173, Express on 3001
|
|||
|
||||
### The git hooks
|
||||
|
||||
Three hooks live in `.githooks/` because `.git/hooks` is not versioned. **The
|
||||
Four hooks live in `.githooks/` because `.git/hooks` is not versioned. **The
|
||||
`core.hooksPath` line above is per clone**, so every checkout runs it once; an
|
||||
uninstalled hook fails silently.
|
||||
|
||||
|
|
@ -338,6 +338,7 @@ uninstalled hook fails silently.
|
|||
| `pre-commit` | scans the staged diff for credentials, then runs `npm run build` if source is staged |
|
||||
| `commit-msg` | refuses a message with no conventional type (`feat`, `fix`, `ui`, `docs`, `test`, `refactor`, `security`, `perf`, `chore`) |
|
||||
| `post-commit` | **pushes to `origin`** |
|
||||
| `pre-push` | refuses a push that leaves uncommitted or staged edits behind |
|
||||
|
||||
**`post-commit` pushes.** That is deliberate — work that exists on one laptop is
|
||||
one disk away from gone — but it has a consequence: whatever documentation was
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ of your own — how to write one that can actually fail.
|
|||
|
||||
## The hooks
|
||||
|
||||
Three, in `.githooks/`, because `.git/hooks` is not versioned and a hook living
|
||||
Four, in `.githooks/`, because `.git/hooks` is not versioned and a hook living
|
||||
there protects exactly one clone.
|
||||
|
||||
| Hook | What it runs here |
|
||||
|
|
@ -77,6 +77,16 @@ there protects exactly one clone.
|
|||
| `pre-commit` | `scripts/secrets.sh` on the staged diff, then `npm run build` when source is staged |
|
||||
| `commit-msg` | refuses a message with no conventional type |
|
||||
| `post-commit` | **pushes to `origin`** |
|
||||
| `pre-push` | refuses a push that leaves uncommitted or staged edits behind |
|
||||
|
||||
`pre-push` is not the template's — it is the useful half of a hook that was
|
||||
already sitting in this checkout's `.git/hooks` before adoption. Setting
|
||||
`core.hooksPath` would have silently stopped that one running, which is exactly
|
||||
the failure this directory exists to prevent, so it was moved here instead. Its
|
||||
third check went: it refused whenever the branch was ahead of its remote, which
|
||||
is the precondition for pushing at all, so it fired on every real push and told
|
||||
you to re-run with `--no-verify`. A guard that can never pass teaches people to
|
||||
bypass the ones beside it.
|
||||
|
||||
`git config core.hooksPath .githooks` is per clone, so every checkout runs it
|
||||
once. An uninstalled hook fails silently, which is the same class of problem the
|
||||
|
|
|
|||
Loading…
Reference in New Issue