From f5a826fa41655d25cec085eb9799fb73d69c2c05 Mon Sep 17 00:00:00 2001 From: lab Date: Sun, 6 Sep 2026 02:08:20 +0700 Subject: [PATCH 1/2] fix(hooks): cover cherry-pick and git am, and correct the claim that they could not be (Refs #3329) --- .githooks/applypatch-msg | 15 ++++++++ .githooks/pre-applypatch | 17 +++++++++ .githooks/prepare-commit-msg | 38 +++++++++++++++++++ ...05-cherry-pick-can-be-stopped-after-all.md | 24 ++++++++++++ tools/census/quiet.txt | 2 +- tools/census/shell.txt | 4 +- 6 files changed, 97 insertions(+), 3 deletions(-) create mode 100755 .githooks/applypatch-msg create mode 100755 .githooks/pre-applypatch create mode 100755 .githooks/prepare-commit-msg create mode 100644 docs/now/2026-09-05-cherry-pick-can-be-stopped-after-all.md diff --git a/.githooks/applypatch-msg b/.githooks/applypatch-msg new file mode 100755 index 0000000000..820a61fab1 --- /dev/null +++ b/.githooks/applypatch-msg @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# L1 for `git am`, the analogue of commit-msg. git passes the message file as $1. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" +TRI_BIN="" +for cand in "$ROOT/target/debug/tri" "$ROOT/target/release/tri"; do + [ -x "$cand" ] || continue + if [ -z "$TRI_BIN" ] || [ "$cand" -nt "$TRI_BIN" ]; then TRI_BIN="$cand"; fi +done +if [ -z "$TRI_BIN" ]; then + echo "note: no tri binary; L1 was not checked and nothing about this message is claimed." + exit 0 +fi +exec "$TRI_BIN" hooks commit-msg "$1" diff --git a/.githooks/pre-applypatch b/.githooks/pre-applypatch new file mode 100755 index 0000000000..b0929b5643 --- /dev/null +++ b/.githooks/pre-applypatch @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# The barrier for `git am`. It runs once per patch with the index already holding the +# applied patch -- the same operand `tri hooks pre-commit` reads since it was corrected +# to `--staged`. A non-zero exit stops the apply; verified, exit 1 and no commit created. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" +TRI_BIN="" +for cand in "$ROOT/target/debug/tri" "$ROOT/target/release/tri"; do + [ -x "$cand" ] || continue + if [ -z "$TRI_BIN" ] || [ "$cand" -nt "$TRI_BIN" ]; then TRI_BIN="$cand"; fi +done +if [ -z "$TRI_BIN" ]; then + echo "note: no tri binary; this patch was not checked and nothing about it is claimed." + exit 0 +fi +exec "$TRI_BIN" hooks pre-commit diff --git a/.githooks/prepare-commit-msg b/.githooks/prepare-commit-msg new file mode 100755 index 0000000000..7e2ac9d919 --- /dev/null +++ b/.githooks/prepare-commit-msg @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# The barrier for a CHERRY-PICK, and only for a cherry-pick. +# +# MEASURED with marker hooks on git 2.50.1 over the full hook set (13 names, not the six +# a narrower probe used -- "nothing fired" can mean "I did not look"): +# +# event hooks that fire +# normal commit pre-commit, prepare-commit-msg, commit-msg, post-commit +# merge --no-ff pre-merge-commit, prepare-commit-msg, commit-msg +# cherry-pick prepare-commit-msg, post-commit +# git am applypatch-msg, pre-applypatch, post-applypatch +# rebase NOTHING +# +# A previous note in docs/now claimed cherry-pick could not be stopped by any hook. That +# was wrong, and the correction is this file: a non-zero exit from prepare-commit-msg +# aborts the cherry-pick -- verified, exit 128 and no commit created. +# +# It must do nothing on an ordinary commit, where pre-commit has already run and running +# again would double the barrier's cost for no answer. `.git/CHERRY_PICK_HEAD` is the +# definitive marker of a cherry-pick in progress. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +GITDIR="$(git rev-parse --git-dir)" +[ -e "$GITDIR/CHERRY_PICK_HEAD" ] || exit 0 + +TRI_BIN="" +for cand in "$ROOT/target/debug/tri" "$ROOT/target/release/tri"; do + [ -x "$cand" ] || continue + if [ -z "$TRI_BIN" ] || [ "$cand" -nt "$TRI_BIN" ]; then TRI_BIN="$cand"; fi +done +if [ -z "$TRI_BIN" ]; then + echo "note: no tri binary; this cherry-pick was not checked and nothing about it is claimed." + exit 0 +fi +echo "cherry-pick: running the barrier, which pre-commit does not see." +exec "$TRI_BIN" hooks pre-commit diff --git a/docs/now/2026-09-05-cherry-pick-can-be-stopped-after-all.md b/docs/now/2026-09-05-cherry-pick-can-be-stopped-after-all.md new file mode 100644 index 0000000000..6c9b6d82fc --- /dev/null +++ b/docs/now/2026-09-05-cherry-pick-can-be-stopped-after-all.md @@ -0,0 +1,24 @@ +# NOW -- cherry-pick can be stopped after all, and my note said it could not (2026-09-05) + +## cherry-pick can be stopped after all (Refs #3329) + +- Section 594 and its note said `cherry-pick` runs neither commit hook and git offers + none that could stop it. That was wrong, and the reason is worth more than the fix. +- The probe carried markers for SIX hook names. `cherry-pick` fires + `prepare-commit-msg`, and `git am` fires `applypatch-msg` and `pre-applypatch` -- + none of which were in the list. **"Nothing fired" can mean "I did not look".** +- Re-measured over the full thirteen. A non-zero exit from `prepare-commit-msg` aborts a + cherry-pick (128, no commit) and from `applypatch-msg` aborts a `git am` (1, no + commit). Verified directly, both. +- Three hooks added. `prepare-commit-msg` gates only when `.git/CHERRY_PICK_HEAD` + exists, so an ordinary commit still pays exactly one barrier run -- verified, and the + first attempt at that control was void because the commit had been refused by the census + for an unrelated reason. +- Controls: a cherry-pick of a commit carrying a conflict marker exits 128 with + `carries a conflict marker` and creates nothing; the same through `git am` exits 1 + with the same sentence, once the index is clean -- the first run of that control was also + void, refused by git for a dirty index rather than by the hook. +- `rebase` fires nothing at all and remains genuinely uncovered. That gap is real. +- Blessing two census moves that are NOT mine: `quiet` 128 to 127 and `shell` 235 to + 234, from a neighbour repairing two workflows. Confirmed on a clean origin/master + checkout with the same binary before blessing, rather than assumed. diff --git a/tools/census/quiet.txt b/tools/census/quiet.txt index 6a4281ba61..781da0d6a9 100644 --- a/tools/census/quiet.txt +++ b/tools/census/quiet.txt @@ -2,7 +2,7 @@ GATE STEPS WHOSE PASS SURVIVES THE SUBJECT GOING MISSING workflow files read 50 steps in a quiet shape 31 - named a path but not quiet 128 (--excluded prints them) + named a path but not quiet 127 (--excluded prints them) by shape: failure branch passes 15 `… 2>/dev/null … || echo PASSED` diff --git a/tools/census/shell.txt b/tools/census/shell.txt index 335cb69fbc..eccf11c616 100644 --- a/tools/census/shell.txt +++ b/tools/census/shell.txt @@ -2,10 +2,10 @@ WHICH INTERPRETER EACH GATE STEP IS HANDED TO, AND WHO SAYS SO workflow files read 50 jobs 71 - run: steps 235 + run: steps 234 who names the shell: - the runner does 214 no container, so bash -eo pipefail + the runner does 213 no container, so bash -eo pipefail a `shell:` key does 0 NOBODY 21 a container and no `shell:` key From 450c30b79eb64ec48a566afb8f6dc1cade39f4d3 Mon Sep 17 00:00:00 2001 From: lab Date: Sun, 6 Sep 2026 02:09:05 +0700 Subject: [PATCH 2/2] skill: correct 594 -- cherry-pick and git am CAN be stopped (Refs #3329) --- .claude/skills/ci-gates/SKILL.md | 22 +++++++++++++++++++--- .trinity/notebook_commit_count | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.claude/skills/ci-gates/SKILL.md b/.claude/skills/ci-gates/SKILL.md index 16031a32b1..5c5f26b3a1 100644 --- a/.claude/skills/ci-gates/SKILL.md +++ b/.claude/skills/ci-gates/SKILL.md @@ -17232,13 +17232,29 @@ an **all-zero local sha**. --amend yes yes --allow-empty yes yes merge --no-ff NO yes - cherry-pick NO NO + cherry-pick NO NO (but prepare-commit-msg fires -- see below) So every gate in the barrier — the conflict-marker refusal above all — was silent on **the one commit type conflict markers come from**. `git merge` runs `pre-merge-commit`, whose non-zero exit stops the merge; the index at that moment holds the merge RESULT, which is -exactly the operand the barrier reads once it is corrected to `--staged`. `cherry-pick` runs -neither hook and git offers none that could stop it: that gap is stated, not papered over. +exactly the operand the barrier reads once it is corrected to `--staged`. + +`cherry-pick` runs `prepare-commit-msg` and `post-commit`. + +**A first version of this section said cherry-pick could not be stopped by any hook, and +that was wrong** -- because the probe carried markers for only SIX hook names. "Nothing +fired" can mean "I did not look". Re-measured over the full set of thirteen: + + cherry-pick prepare-commit-msg, post-commit + git am applypatch-msg, pre-applypatch, post-applypatch + rebase NOTHING + +A non-zero exit from `prepare-commit-msg` aborts a cherry-pick (exit 128, no commit) and +from `applypatch-msg` aborts a `git am` (exit 1, no commit). Both are covered now; +`rebase` genuinely is not, and that one is the gap. + +The correction is the lesson: **the population of a probe is as narrow as its instrument +list**, and an empty result from a narrow instrument is indistinguishable from an absence. **The method is enumeration, not cleverness.** Write marker hooks that only `touch` a file, run each event through them, and read which files exist. It takes minutes and answers a diff --git a/.trinity/notebook_commit_count b/.trinity/notebook_commit_count index 48082f72f0..b1bd38b62a 100644 --- a/.trinity/notebook_commit_count +++ b/.trinity/notebook_commit_count @@ -1 +1 @@ -12 +13