#!/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. # It refuses any change to an existing tag, and an object name that isn't 40 # or 64 hex characters. 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 } idlen() { # $1: an object name. True at 40 (SHA-1) or 64 (SHA-256) characters. [ "${#1}" -eq 40 ] || [ "${#1}" -eq 64 ] } 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 ! idlen "$lsha" || ! idlen "$rsha"; then refuse "$rref: a ref line with an object name that isn't 40 or 64 hex characters (local $lsha, remote $rsha)" continue fi 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 # An existing tag never changes. A new tag object on the same commit, or # a tag moved to a descendant, would pass the ancestor check below. if [ "${rref#refs/tags/}" != "$rref" ] && [ "$lsha" != "$rsha" ]; then refuse "$rref: an existing tag can't be moved or replaced (remote $rsha, local $lsha); push a new tag instead" 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