Skip to content

Instantly share code, notes, and snippets.

@philpennock
Last active January 5, 2023 22:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save philpennock/04c22c0e2bc74f87fb651b61c39eef6f to your computer and use it in GitHub Desktop.
Save philpennock/04c22c0e2bc74f87fb651b61c39eef6f to your computer and use it in GitHub Desktop.
git pb sub-command, "push branch"
#!/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