Last active
January 5, 2023 22:38
-
-
Save philpennock/04c22c0e2bc74f87fb651b61c39eef6f to your computer and use it in GitHub Desktop.
git pb sub-command, "push branch"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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[@]}" "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment