diff --git a/.githooks/pre-merge-commit b/.githooks/pre-merge-commit new file mode 100755 index 0000000000..34038a5b28 --- /dev/null +++ b/.githooks/pre-merge-commit @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# The merge analogue of pre-commit, and the barrier had no such thing. +# +# MEASURED on git 2.50.1, marker hooks over five commit-creating events: +# +# event pre-commit commit-msg +# normal commit yes yes +# --amend yes yes +# --allow-empty yes yes +# merge --no-ff NO yes +# cherry-pick NO NO +# +# So every gate in `tri hooks pre-commit` -- the conflict-marker refusal above all -- +# was silent on the one commit type conflict markers actually come from. `git merge` +# runs `pre-merge-commit` instead, and a non-zero exit from it stops the merge: verified, +# HEAD stayed on the pre-merge commit. +# +# The index at this moment holds the merge RESULT, which is exactly the operand +# `tri hooks pre-commit` reads since it was corrected to `--staged`. +# +# cherry-pick runs neither hook and git offers no hook that could stop it. That gap is +# stated in docs/now rather than papered over. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +# Newest, not first: a stale binary prints PASSED for gates it does not carry. +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; the merge was not checked and nothing about it is claimed." + exit 0 +fi +exec "$TRI_BIN" hooks pre-commit diff --git a/docs/now/2026-09-05-the-barrier-does-not-run-on-merges.md b/docs/now/2026-09-05-the-barrier-does-not-run-on-merges.md new file mode 100644 index 0000000000..f3deb06ad4 --- /dev/null +++ b/docs/now/2026-09-05-the-barrier-does-not-run-on-merges.md @@ -0,0 +1,25 @@ +# NOW -- The barrier does not run on merges (2026-09-05) + +## The barrier does not run on merges (Refs #3318) + +- Measured with marker hooks on git 2.50.1, five commit-creating events: a normal commit, + `--amend` and `--allow-empty` run both `pre-commit` and `commit-msg`; + `merge --no-ff` runs `commit-msg` but NOT `pre-commit`; `cherry-pick` runs neither. +- So every gate in `tri hooks pre-commit` was silent on the one commit type conflict + markers actually come from. +- `git merge` runs `pre-merge-commit`, and a non-zero exit stops the merge. Verified: + HEAD stayed on the pre-merge commit. +- The index at that moment holds the merge RESULT, which is exactly the operand the barrier + reads since it was corrected to `--staged`. +- Controls, both sides: a merge whose result carries a marker exits 1 and does not happen; + a clean merge exits 0 and does. +- `cherry-pick` runs neither hook and git offers none that could stop it. Recorded as a + known gap rather than papered over. +- The class is the POPULATION OF EVENTS, not the predicate and not the operand. Yesterday + it was a push that could be a deletion; today a commit that could be a merge. Both were + found by asking "what else comes through here", and neither by any control. +- Two self-inflicted hazards while measuring, both caught and reverted: `git config` in a + worktree writes to the SHARED config, so setting `core.hooksPath` for a probe disabled + the real hooks in all 148 worktrees; and a `cd` that fails inside a subshell does not + stop it, so a probe ran its commits in my own tree. Nothing was pushed and master was + untouched.