diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100755 index 000000000..0c7ef7e50 --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Install: ln -sf ../../scripts/git-hooks/pre-push .git/hooks/pre-push +# +# Mirror pushes to the tangled remote before allowing the origin push. +# +# Why: previously `origin` had two push URLs (github + tangled). Push fan-out +# was best-effort — if one side rejected, the other still succeeded and the +# rejected mirror silently drifted. This hook makes both sides atomic from the +# user's view: tangled push happens first, and if it fails the origin push +# aborts. No --no-verify on commits ever, so this is the trust boundary. +# +# Skipped when pushing to anything other than origin (so `git push tangled` +# directly does not recurse). + +set -euo pipefail + +remote_name="$1" + +if [ "$remote_name" != "origin" ]; then + exit 0 +fi + +# Read stdin (one line per ref being pushed) into refspec args. Skip deletions +# — let the user manage tangled deletions explicitly to avoid clobbering refs +# we forgot were only on the mirror. +ZERO=0000000000000000000000000000000000000000 +refspecs=() +while read -r local_ref local_sha remote_ref remote_sha; do + if [ "$local_sha" = "$ZERO" ]; then + continue + fi + refspecs+=("${local_sha}:${remote_ref}") +done + +if [ ${#refspecs[@]} -eq 0 ]; then + exit 0 +fi + +echo "pre-push: mirroring ${#refspecs[@]} ref(s) to tangled before origin..." +if ! git push tangled "${refspecs[@]}"; then + echo "pre-push: tangled rejected the push — aborting origin push to prevent drift." >&2 + echo "pre-push: reconcile tangled first (git fetch tangled; investigate), then retry." >&2 + exit 1 +fi +echo "pre-push: tangled OK, proceeding with origin."