#!/usr/bin/env bash
# shadow-rs pre-push hook.
# Install: ./hooks/install.sh
#
# Runs the workspace test suite once, on Debian, before a push that carries new
# commits.
#
# It deliberately does *not* reproduce CI. CI runs fmt, clippy and the tests
# with and without `pam`, the MSRV build, cargo-deny, the static musl archive,
# the Debian/Alpine/Fedora matrix as root and the end-to-end deployment image,
# in parallel and with retries. A hook that tried to match that took minutes on
# every push, so every push used --no-verify -- and a hook that is always
# skipped protects nothing while still costing time whenever someone forgets to
# skip it.
#
# What is left is the one check worth having before the code leaves the
# machine: the tests actually run. `make test` is the single definition of
# what that means -- with and without `pam`, since the feature changes which
# code paths exist.

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

ZERO='0000000000000000000000000000000000000000'

# Does this push carry any commit that is not already on the remote?
#
# git feeds the hook one line per ref: <local ref> <local sha> <remote ref>
# <remote sha>. Pushing a tag, deleting a branch, or re-pushing a branch the
# remote already has moves no commits, and running a test suite for it is pure
# waste -- pushing the 0.2.2 tag moved one object and blocked for two minutes.
carries_commits() {
    local local_ref local_sha remote_ref remote_sha
    local found=1

    while read -r local_ref local_sha remote_ref remote_sha; do
        [ -z "${local_ref:-}" ] && continue
        # A tag names commits that are already being pushed on their branch.
        case "$local_ref" in
        refs/tags/*) continue ;;
        esac
        # Deleting a ref pushes nothing.
        [ "$local_sha" = "$ZERO" ] && continue

        local range=()
        if [ "$remote_sha" = "$ZERO" ]; then
            # A new branch: everything on it that no other remote branch has.
            range=("$local_sha" --not --remotes=origin)
        else
            range=("$remote_sha..$local_sha")
        fi

        if [ -n "$(git rev-list -n 1 "${range[@]}" 2>/dev/null)" ]; then
            found=0
        fi
    done

    return "$found"
}

# No stdin means the hook was invoked by hand; take the cautious path and run.
if [ -t 0 ]; then
    :
elif ! carries_commits; then
    echo "pre-push: no new commits in this push, skipping the test suite"
    exit 0
fi

echo -e "${YELLOW}pre-push${NC}: make test (debian)"
if ! docker compose run --rm -T debian make test 2>&1; then
    echo -e "${RED}FAILED${NC}: make test"
    echo "The full matrix -- fmt, clippy, ±pam, MSRV, musl, three distros,"
    echo "end-to-end -- runs in CI on the pull request."
    exit 1
fi

echo -e "${GREEN}pre-push: tests passed${NC}"
