#!/bin/sh
# ── LAW 6: ask the REPO which branch is canonical ───────────────────────────
#
# A push to the wrong trunk is not a mistake you notice. It leaves a plausible
# branch that nobody reads. THREE repositories here carry both a `main` and a
# `master` on the remote and they do not agree on which is canonical — znippy
# and gunnar are `master`, znippy-zoomies is `main` — but that list is a fact of
# today, not of this file: enumerate it with
#
# for r in ~/git/*/; do git -C "$r" ls-remote --heads origin main master; done
#
# Two lanes pushed to a non-canonical trunk in one week (znippy, then gunnar's
# `main` on 2026-08-30), which is why this file exists. The gunnar one was
# harmless only by luck — that tip was already an ancestor of master.
#
# WHAT IT REFUSES: a push whose destination is a TRUNK name that is not THIS
# repository's canonical trunk (main / master / trunk / develop). Nothing else.
# A lane branch, a feature branch, a tag — all pushed exactly as before. The
# trap is the second trunk, not branches in general.
#
# HOW IT KNOWS: `git symbolic-ref refs/remotes/<remote>/HEAD`, which is what the
# remote itself says its default branch is — never a name hardcoded here, or
# this file would be a fourth place for the two repositories to disagree. If the
# local ref is missing (a clone made with --no-checkout, or an old one), it asks
# the server directly with `git ls-remote --symref`, because a push is already
# talking to that server. If neither answers it says so and ALLOWS: a hook that
# blocked every push on a repository it could not resolve would be replaced
# within the day, and then nothing would guard anything.
#
# WHY SHELL, in a constellation whose LAW is pure Rust: git executes this file
# itself. It is not product code, it links nothing, and it ships in no binary.
#
# INSTALL: cp .githooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push
# (this file chains to `git lfs pre-push` itself, so it replaces an
# LFS hook rather than fighting it.)
set -u
remote_name="${1:-origin}"
remote_url="${2:-}"
trunk_names="main master trunk develop"
canonical=$(git symbolic-ref --quiet --short "refs/remotes/${remote_name}/HEAD" 2>/dev/null \
| sed "s|^${remote_name}/||")
if [ -z "${canonical}" ]; then
# Ask the server. `ls-remote --symref` prints `ref: refs/heads/<x>\tHEAD`.
canonical=$(git ls-remote --symref "${remote_name}" HEAD 2>/dev/null \
| sed -n 's|^ref: refs/heads/\([^[:space:]]*\)[[:space:]]*HEAD$|\1|p' | head -n 1)
fi
# stdin is `<local ref> <local sha> <remote ref> <remote sha>` per ref, and it
# is consumed here — buffer it so the LFS hook below gets the same bytes git
# would have given it.
refs_file=$(mktemp "${TMPDIR:-/tmp}/prepush-refs.XXXXXX") || exit 1
trap 'rm -f "${refs_file}"' EXIT INT TERM
cat > "${refs_file}"
if [ -z "${canonical}" ]; then
printf >&2 '%s\n' \
"pre-push: could not resolve ${remote_name}'s default branch, so the canonical-trunk guard is OFF for this push." \
" Fix it once with: git remote set-head ${remote_name} -a" >&2
else
refused=0
while read -r local_ref local_sha remote_ref remote_sha; do
[ -z "${remote_ref}" ] && continue
case "${remote_ref}" in
refs/heads/*) branch=${remote_ref#refs/heads/} ;;
*) continue ;;
esac
# A deletion (all-zero local sha) is not a push of content.
case "${local_sha}" in *[!0]*) ;; *) continue ;; esac
[ "${branch}" = "${canonical}" ] && continue
is_trunk=0
for t in ${trunk_names}; do
[ "${branch}" = "${t}" ] && is_trunk=1
done
[ "${is_trunk}" = 0 ] && continue
refused=1
printf >&2 '\n%s\n' "LAW 6: REFUSED — this push would put commits on \`${branch}\`, which is NOT this repository's canonical branch."
printf >&2 '%s\n' \
" canonical: ${canonical} (asked of the remote: git symbolic-ref refs/remotes/${remote_name}/HEAD)" \
" you pushed: ${branch} (${local_ref} -> ${remote_ref})" \
" remote: ${remote_name} ${remote_url}" \
"" \
" WHY: several repositories here carry BOTH a \`main\` and a \`master\` on the remote, and" \
" they do NOT agree on which is canonical — one repository's trunk is the next one's" \
" abandoned branch. A push to the wrong one is not rejected by the server and is not" \
" noticed by anyone: it quietly builds a second history that nobody reads. Enumerate" \
" them, never memorise them:" \
" for r in ~/git/*/; do git -C \"\$r\" ls-remote --heads origin main master; done" \
"" \
" DO THIS: git push ${remote_name} HEAD:${canonical}" \
" or, if you are deliberately maintaining the non-canonical trunk (retiring it, say):" \
" GIT_ALLOW_NONCANONICAL_TRUNK_PUSH=1 git push ${remote_name} ${branch}"
done < "${refs_file}"
if [ "${refused}" = 1 ]; then
if [ "${GIT_ALLOW_NONCANONICAL_TRUNK_PUSH:-0}" = 1 ]; then
printf >&2 '\n%s\n\n' " …allowed anyway: GIT_ALLOW_NONCANONICAL_TRUNK_PUSH=1 is set."
else
printf >&2 '\n'
exit 1
fi
fi
fi
# ── chain: whatever this repository needed a pre-push for before ────────────
# Git LFS installs its own pre-push; a repository with LFS-tracked paths must
# still upload them. Same stdin, same argv.
if [ -s .gitattributes ] && grep -q 'filter=lfs' .gitattributes 2>/dev/null; then
if command -v git-lfs >/dev/null 2>&1; then
git lfs pre-push "$@" < "${refs_file}"
exit $?
fi
printf >&2 '%s\n' "pre-push: this repository is configured for Git LFS but git-lfs is not on PATH."
exit 2
fi
exit 0