#!/usr/bin/env bash set -euo pipefail # # git pb: push branch # implicit: force with lease, set upstream if needed, etc # # Path coercion for platforms where git might be in multiple places and I can't # mess with the ordering "normally" but want to explicitly pick up newer git # here. [ -d /opt/local/bin ] && PATH="/opt/local/bin:$PATH" [ -d /opt/git/bin ] && PATH="/opt/git/bin:$PATH" # shellcheck disable=SC2034 SUBDIRECTORY_OK=true set +eu # shellcheck source=/dev/null . "$(git --exec-path)/git-sh-setup" set -eu # I like prefices in front of messages, so we also stomp of the normal git die() progname="$(basename "$0" .sh)" stderr() { printf >&2 '%s: %s\n' "$progname" "$*"; } die() { stderr "$@"; exit 1; } get_version_information() { local v="$( git --version )" v="${v#git version }" v="${v%% *}" git_version_full="$v" git_version_major="${v%%.*}" v="${v#*.}" git_version_minor="${v%%.*}" } cd_to_toplevel get_version_information # Should this respect core.hooksPath ? HOOKS_DIR="$(git rev-parse --git-dir)/hooks" readonly HOOKS_DIR # Remove all GIT_PDP_* variables, don't inherit any. # Provide a cleaner env for our hooks. unset "${!GIT_PDP_@}" should_force=true should_set_upstream=0 should_find_upstream=0 is_trunk=false disable_force_because_trunk() { stderr "force disabled for branch: ${1:?need a branch name}" should_force=false is_trunk=true } full_branch="$(git symbolic-ref HEAD)" || die "not on a branch, not pushing" branch="${full_branch#refs/heads/}" if [[ -z "${FORCE_TRUNK:-}" ]]; then case "$branch" in main | master | dev | v? ) disable_force_because_trunk "$branch" ;; esac if $should_force && protected_s="$(git config --local --get pdp.protect-branches)"; then for b in $protected_s; do [[ "$branch" == "$b" ]] || continue disable_force_because_trunk "$branch" break done; unset b fi fi if ! upstream="$(git for-each-ref --format='%(upstream:remotename)' "$full_branch")" || [[ -z "$upstream" ]] then stderr "missing a current upstream" should_set_upstream=1 should_find_upstream=1 if "$is_trunk"; then die "this is a trunk branch, not hunting for an upstream" fi fi bad_upstream='' if [[ -n "$upstream" ]]; then pu="$(git config --local --get "remote.$upstream.pushurl" || true )" need_new_up=0 case "${pu,,}" in (- | -- | nonexistant | non-existant | nonexistent | non-existent | no | none | off | 0 | disable | disabled | readonly | read-only | ro) need_new_up=1 ;; esac if (( need_new_up )); then stderr "upstream ${upstream@Q} unacceptable for push [${pu@Q}]" bad_upstream="$upstream" upstream='' # Do *NOT* _set_ upstream, just find it. # Upstream should be left on the repo which is read-only to us should_set_upstream=0 should_find_upstream=1 fi unset pu need_new_up fi if (( should_find_upstream )) && [[ -z "$upstream" ]]; then if candidate="$(git config --local --get remotes.push)"; then stderr "picking upstream ${candidate@Q} because is remotes.push" upstream="$candidate" fi fi if (( should_find_upstream )) && [[ -z "$upstream" ]]; then for B in main master; do candidate="$(git for-each-ref --format='%(upstream:remotename)' "refs/heads/$B")" [ "$candidate" != "" ] || continue stderr "picking upstream ${candidate@Q} as per branch ${B@Q}" upstream="$candidate" break done fi if [[ -n "$bad_upstream" ]] && [[ "$upstream" == "$bad_upstream" ]]; then stderr "oops, re-picked ${upstream@Q} after rejecting it" die "if repo declares our upstream as invalid to push to, repo should set 'remotes.push' too" fi # This is why we went to bash: when 2 conditionals & 4 patterns, was willing to stick to sh. # With three conditionals and 8 invocation patterns, time to use an array. declare -a subcmd=('push') if (( should_set_upstream )); then subcmd+=( -u "$upstream" "$branch" ) elif (( should_find_upstream )); then subcmd+=( "$upstream" "$branch" ) fi if $should_force; then # this checks that our ref for the remote matches subcmd+=( --force-with-lease ) if [[ $git_version_major -gt 2 ]] || [[ $git_version_major -eq 2 && $git_version_minor -ge 30 ]]; then # this guards against something else doing `git remote update` # and having matched our ref for the remote, by requiring that # the ref for the remote be reachable from a reflog entry for # the current branch. subcmd+=( --force-if-includes ) fi fi # Side-effect: sets cleanliness information set_hook_expensive() { if [[ -n "${GIT_PDP_DIRTY:-}" ]] || [[ -n "${GIT_PDP_CLEAN:-}" ]]; then return 0 fi local status status="$(git status --porcelain=v2)" if [[ "$status" == "" ]]; then declare -gxr GIT_PDP_CLEAN=true else declare -gxr GIT_PDP_DIRTY=true fi } # For the hooks run_checker() { local check_cmd="${1:?need a command to run}" declare -xr GIT_PDP_BRANCH="$branch" declare -xr GIT_PDP_UPSTREAM="$upstream" if (( should_set_upstream )); then declare -xr GIT_PDP_SET_UPSTREAM=true else declare -xr GIT_PDP_SET_UPSTREAM=false fi declare -xr GIT_PDP_FORCE_PUSH=$should_force set_hook_expensive "$check_cmd" } # TBD: should I set up anything like an easy way to get exactly the tree being pushed? # At this time, the tree visible to the hook is the current working tree, which might be bogus. # Or perhaps missing, if bare. found_check=false for checker in "$HOOKS_DIR/pdp.prepush"; do if [ -x "$checker" ]; then found_check=true stderr "invoking checker: $checker" run_checker "$checker" fi done if $found_check; then stderr "checkers complete"; fi git "${subcmd[@]}" "$@"