92 lines
3.2 KiB
Bash
Executable File
92 lines
3.2 KiB
Bash
Executable File
#!/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
|