#!/usr/bin/env bash # Reads what .githooks/reference-transaction does in a primary: it tells the # mq at $HOME/.local/bin/mq that a ref moved, refuses no update, and carries # the line that keeps mq from starting it for mq's own writes. # # scripts/check-reference-transaction.sh .githooks/reference-transaction MQ # # MQ is the mq this tree builds. Nothing is contacted. set -euo pipefail usage='usage: check-reference-transaction.sh ' hook=${1:?$usage} mq_built=${2:?$usage} abs() { printf '%s\n' "$(cd -- "$(dirname -- "$1")" && pwd -P)/$(basename -- "$1")"; } hook=$(abs "$hook") mq_built=$(abs "$mq_built") tmp=$(mktemp -d "${TMPDIR:-/tmp}/check-reference-transaction.XXXXXX") tmp=$(cd -- "$tmp" && pwd -P) trap 'rm -rf -- "$tmp"' EXIT fail() { printf 'check-reference-transaction: %s\n' "$1" >&2 shift || true for line in "$@"; do printf ' %s\n' "$line" >&2; done exit 1 } unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \ GIT_COMMON_DIR GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_PREFIX \ GIT_QUARANTINE_PATH export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null # Where the hook calls mq, $HOME/.local/bin/mq, a wrapper around the mq this # tree builds: each call appends `== ` and the lines on its # standard input to $tmp/mq.log, then hands both to the real one, so what the # hook told mq is read and mq still does what it would. export HOME=$tmp/home mq_log=$tmp/mq.log mkdir -p "$HOME/.local/bin" : > "$mq_log" cat > "$HOME/.local/bin/mq" <> "$mq_log" printf '%s\n' "\$input" | "$mq_built" "\$@" WRAP chmod +x "$HOME/.local/bin/mq" primary=$tmp/primary mkdir -p "$primary/.githooks" cp "$hook" "$primary/.githooks/reference-transaction" # The rules beside it, as the primary has them. cp -R "$(dirname -- "$hook")/lib" "$primary/.githooks/lib" chmod +x "$primary/.githooks/reference-transaction" git -C "$primary" init --quiet --initial-branch=main git -C "$primary" config user.name 'hook check' git -C "$primary" config user.email 'check@invalid' git -C "$primary" config commit.gpgsign false git -C "$primary" config mq.pool "$tmp/pool" printf 'let x = 0\n' > "$primary/lib.ml" git -C "$primary" add -- lib.ml git -C "$primary" commit --quiet -m baseline out=$("$mq_built" -C "$primary" init 2>&1) || fail "mq init failed" "$out" # Absolute, as the primary is wired: git runs a push's hooks from the git # directory, where a relative hooks path names no hook at all. git -C "$primary" config core.hooksPath "$primary/.githooks" # mq's own writes start the hook for none of their phases: a land, a dozen # transactions, calls the wrapper not once, and mq stats counts no hook run. # A hook that does more than tell mq, or does not say so, is started for # every phase of each. wt=$("$mq_built" -C "$primary" new n1 2> "$tmp/new.err") || fail "mq new n1 failed" "$(cat "$tmp/new.err")" printf '1\n' > "$wt/n1" git -C "$wt" add -- n1 git -C "$wt" -c core.hooksPath=/dev/null commit --quiet -m 'add n1' : > "$mq_log" out=$("$mq_built" -C "$primary" land n1 2>&1) || fail "mq land n1 failed" "$out" [ ! -s "$mq_log" ] || fail "mq's own writes for a land started the hook" "$(grep -c '^==' "$mq_log") calls" [ "$(git -C "$primary" log -1 --format=%s main)" = 'add n1' ] || fail "the land did not reach main" "$(git -C "$primary" log --oneline -3 main)" stats=$("$mq_built" -C "$primary" stats 2>&1) || fail "mq stats failed" "$stats" case $stats in *"hook reference-transaction"*) fail "mq stats counts hook runs for its own lands" "$stats" ;; esac # A hand commit on main, made by cherry-pick, which runs no commit hook: the # hook tells mq, and mq wakes its scheduler. Nothing reads the message here, # so a message naming an issue moves main like any other; it is read at the # queue's entry and on its way to a remote (scripts/check-no-tracker-id.sh). out=$("$mq_built" -C "$primary" schedule start --scheduler=test 2>&1) || fail "mq schedule start failed" "$out" wake=$(git -C "$primary" rev-parse --path-format=absolute --git-common-dir)/mq/wake # The commit picked is made beside main, moving no ref. id=$(printf '%s-%s' space 1a2b3) side=$(git -C "$primary" hash-object -w --stdin <<< 'let y = 1') tree=$( (git -C "$primary" ls-tree main; printf '100644 blob %s\tside.ml\n' "$side") | git -C "$primary" mktree) picked=$(git -C "$primary" commit-tree -p main -m "side: y, for $id" "$tree") rm -f -- "$wake" : > "$mq_log" out=$(git -C "$primary" cherry-pick "$picked" 2>&1) || fail "a cherry-pick onto main was refused" "$out" [ "$(git -C "$primary" log -1 --format=%s main)" = "side: y, for $id" ] || fail "the cherry-pick did not move main" grep -q '^== hook reference-transaction committed$' "$mq_log" || fail "the hook did not tell mq that main moved" "$(cat "$mq_log")" grep -q ' refs/heads/main$' "$mq_log" || fail "the hook did not hand mq main's line" "$(cat "$mq_log")" [ -e "$wake" ] || fail "a hand commit on main did not wake mq's scheduler" # A commit starts mq once, in its committed phase: Git runs the hook at every # phase of every transaction, and the hook starts mq only for a committed # transaction naming a branch, or for a probe in any phase. : > "$mq_log" out=$(git -C "$primary" commit --quiet --allow-empty -m 'lib: one run' 2>&1) || fail "an empty commit on main was refused" "$out" calls=$(grep '^==' "$mq_log" || true) [ "$calls" = '== hook reference-transaction committed' ] || fail "a commit started mq other than once, in its committed phase" "$calls" # A probe comes in a transaction mq aborts, so the hook answers it in the # phases before: mq init then reads that the hook calls mq. out=$("$mq_built" -C "$primary" init 2>&1) || fail "mq init failed" "$out" case $out in *"the reference-transaction hook is installed"*) ;; *) fail "the hook left mq init's probe unanswered" "$out" ;; esac # Whatever mq answers, the update goes through. mkdir -p "$tmp/failing/.local/bin" printf '#!/bin/sh\ncat >/dev/null\necho "mq failed" >&2\nexit 1\n' > "$tmp/failing/.local/bin/mq" chmod +x "$tmp/failing/.local/bin/mq" before=$(git -C "$primary" rev-parse main) printf 'let x = 1\n' > "$primary/lib.ml" git -C "$primary" add -- lib.ml out=$(HOME=$tmp/failing git -C "$primary" commit --quiet -m 'lib: x is one' 2>&1) || fail "a commit was refused when mq failed" "$out" [ "$(git -C "$primary" rev-parse main)" != "$before" ] || fail "main did not move when mq failed" case $out in *"mq failed"*) fail "mq's output reached the committer" "$out" ;; esac # And with no mq at all. before=$(git -C "$primary" rev-parse main) out=$(HOME=$tmp/nobody git -C "$primary" commit --quiet --allow-empty -m 'lib: nothing' 2>&1) || fail "a commit was refused with no mq" "$out" [ "$(git -C "$primary" rev-parse main)" != "$before" ] || fail "main did not move with no mq" printf 'check-reference-transaction: ok\n'