#!/usr/bin/env bash
#
# Every commit goes to Forgejo, without anybody having to remember the push.
#
# ## Why a hook rather than a habit
#
# The commit that added `pre-commit` sat unpushed for a day. Nothing was wrong
# with it; it just never got the second command. That is the whole failure this
# closes — work that exists on one laptop and nowhere else is work that is one
# disk away from gone, and it is invisible to anybody reading the tracker.
#
# ## It runs after the commit, and cannot undo one
#
# git ignores this hook's exit code, which is the right shape for the job: a
# network that is down must not cost somebody a commit they already made. So a
# failed push is **reported loudly and left in place** — the commit stands, the
# branch is simply still ahead, and the next commit tries again.
#
# What it will never do is force. A rejected push means the remote has something
# this checkout has not seen, and the fix for that is a human running a pull, not
# a hook overwriting the difference.
#
# ## When it deliberately stays out of the way
#
#   - mid-rebase, mid-cherry-pick, mid-am: every step of a rebase fires this
#     hook, and pushing an intermediate commit publishes a history that is about
#     to be rewritten. Wait for the rebase to finish.
#   - detached HEAD: there is no branch to push, and guessing one is worse than
#     doing nothing.
#   - no `origin`: a clone with no remote is a legitimate state, not an error.
#
#   SKIP_PUSH=1 git commit ...     commits without publishing, loudly
#
# The counterpart to `SKIP_GUARDS` in the pre-commit hook, and loud for the same
# reason: an exception that leaves no trace becomes a habit.

set -uo pipefail

cd "$(git rev-parse --show-toplevel)" || exit 0

say() { printf '\033[1mpost-commit:\033[0m %s\n' "$*" >&2; }

if [ -n "${SKIP_PUSH:-}" ]; then
  say "SKIP_PUSH set — this commit was NOT pushed."
  exit 0
fi

git_dir=$(git rev-parse --git-dir)

# A rebase, cherry-pick or `git am` fires this hook once per replayed commit.
# Those commits are provisional by definition.
for marker in rebase-merge rebase-apply CHERRY_PICK_HEAD; do
  if [ -e "$git_dir/$marker" ]; then
    say "a rebase or cherry-pick is in progress — not pushing until it finishes."
    exit 0
  fi
done

branch=$(git symbolic-ref --quiet --short HEAD) || {
  say "detached HEAD — no branch to push."
  exit 0
}

if ! git remote get-url origin >/dev/null 2>&1; then
  say "no 'origin' remote — nothing to push to."
  exit 0
fi

say "pushing $branch to origin…"

# --porcelain keeps the output to one parseable line per ref rather than the
# usual banner, and the timeout is here because an unreachable SSH host
# otherwise hangs the terminal well past the point of being useful.
if timeout 60 git push --porcelain origin "$branch"; then
  exit 0
fi

status=$?

if [ "$status" -eq 124 ]; then
  say "push timed out after 60s. The commit is safe locally; push when the"
  say "      remote is reachable."
else
  say "push was refused. The commit is safe locally and the branch is now ahead."
  say "      If this is a non-fast-forward, pull and reconcile — this hook will"
  say "      not force, and should not."
fi

# Deliberately zero. git ignores it either way, and returning non-zero here reads
# as though the commit failed when it did not.
exit 0
