#!/bin/sh
# Last gate before anything leaves the machine.
#
# Three checks, because a push publishes three different things:
#   (a) the TREE of every commit being pushed  — scanned via `git ls-tree`, not
#       the working tree: the working copy can be clean while the commit that
#       is actually travelling still carries the secret.
#   (b) the MESSAGE of every outgoing commit.
#   (c) runtime data files that must never be tracked at all — covered by
#       scrub_check's FORBIDDEN_PATHS / FORBIDDEN_IF_COMMITTED, which (a)
#       applies to the pushed tree (*.db, security.yaml, config.yaml,
#       trusted-devices.yaml, .env and .env.*, avatars except the placeholders).
#
# git feeds us "<local ref> <local sha> <remote ref> <remote sha>" on stdin.
set -e
cd "$(git rev-parse --show-toplevel)"

remote_name="$1"

# The all-zero sha means "no such ref". Its LENGTH differs between sha1 and
# sha256 repositories, so test the digits rather than comparing to 40 zeros.
is_zero() {
    case "$1" in
        '' | *[!0]*) return 1 ;;
        *)           return 0 ;;
    esac
}

# For a brand-new branch there is no remote sha to diff against. Walking the
# whole history instead would scan — and reject — every commit ever made, so
# exclude everything the remote already has.
if git remote get-url "$remote_name" >/dev/null 2>&1; then
    not_remotes="--not --remotes=$remote_name"
else
    # Pushed to a bare URL: no named remote to exclude, so exclude them all.
    not_remotes="--not --remotes"
fi

status=0
saw_ref=0
while read -r local_ref local_sha remote_ref remote_sha; do
    [ -n "$local_sha" ] || continue
    # Deleting a remote ref pushes nothing.
    if is_zero "$local_sha"; then continue; fi
    saw_ref=1

    python3 scripts/scrub_check.py --rev "$local_sha" || status=1

    if is_zero "$remote_sha"; then
        range="$local_sha $not_remotes"
    elif ! git cat-file -e "${remote_sha}^{commit}" 2>/dev/null \
         || ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
        # The remote tip is not an ancestor of what we are pushing: a force
        # push, or a remote whose history is unrelated to ours (e.g. a
        # repository created on the host with its own initial commit).
        # "$remote_sha..$local_sha" is then an invalid range, git log fails, and
        # a failed scan must never be read as a clean one — so fall back to
        # scanning everything the remote does not already have.
        echo "pre-push: remote tip $remote_sha is not an ancestor; scanning all outgoing commits"
        range="$local_sha $not_remotes"
    else
        range="$remote_sha..$local_sha"
    fi
    python3 scripts/scrub_check.py --commit-range "$range" || status=1
done

if [ "$saw_ref" = 0 ]; then
    echo "pre-push: nothing to scan (no refs being created or updated)"
fi

if [ "$status" != 0 ]; then
    echo "" >&2
    echo "pre-push: REFUSING to publish. Fix the commits above, then push again." >&2
    echo "          (History is already written: an amend or rebase is the fix," >&2
    echo "           not a new commit on top — the old one still carries it.)" >&2
fi
exit $status
