Files
stack/scripts/git-hooks/pre-push
T

65 lines
2.7 KiB
Bash
Executable File

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