#!/bin/sh # Mosaic push guard (row 211, #426). Installed as pre-push in the common # .git/hooks by `scripts/install-git-hooks.sh`; never edit the installed copy. # For each ref git is about to push, it refuses a delete and any update whose # remote tip isn't an ancestor of the local tip: a force-push or a +refspec. # A new ref passes. It fetches nothing, so a remote tip that isn't in this # repository refuses. Any failure of the check refuses too. # `git push --no-verify` skips this hook; Gitea's branch protection is the # guard that holds (ops-01). Canon: never force-push. refused=0 refuse() { echo "mosaic push guard: refused: $*" >&2 refused=1 } zero() { case "$1" in *[!0]*) return 1 ;; esac return 0 } while read -r lref lsha rref rsha extra || [ -n "${lref:-}" ]; do if [ -z "${rsha:-}" ] || [ -n "${extra:-}" ]; then refuse "unreadable ref line from git: $lref ${lsha:-} ${rref:-} ${rsha:-} ${extra:-}" continue fi case "$lsha$rsha" in *[!0-9a-f]*) refuse "$rref: a ref line with a non-hex object name (local $lsha, remote $rsha)"; continue ;; esac if zero "$lsha"; then refuse "$rref: a delete (remote $rsha); seat branches are never deleted by push" continue fi if zero "$rsha"; then continue fi if ! git cat-file -e "$rsha^{commit}" 2>/dev/null; then refuse "$rref: the remote tip $rsha isn't in this repository; fetch first, then push again" continue fi git merge-base --is-ancestor "$rsha" "$lsha" 2>/dev/null rc=$? case $rc in 0) ;; 1) refuse "$rref: not a fast-forward; remote $rsha isn't an ancestor of local $lsha (a force-push or a rewritten branch)" ;; *) refuse "$rref: the ancestor check failed (exit $rc) for remote $rsha and local $lsha" ;; esac done if [ "$refused" != 0 ]; then echo "mosaic push guard: nothing was pushed; a pushed branch only moves forward (canon: never force-push)" >&2 exit 1 fi exit 0