Gunnar

nordisk/znippy

.githooks/pre-push

#!/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