Skip to content

Instantly share code, notes, and snippets.

@sambrightman
Last active November 12, 2020 20:00
Show Gist options
  • Save sambrightman/aec125cd1f4ccbab916cdbbe1ed0050a to your computer and use it in GitHub Desktop.
Save sambrightman/aec125cd1f4ccbab916cdbbe1ed0050a to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# This is a significant extension of the sample script intended to be
# more precise and robust than most Gists that attempt to prevent
# destructive/unintended operations on protected branches. Modify
# protected_branches with full symbolic refs or incomplete_prefixes
# with commit message prefixes which indicate work-in-progress.
# Probably should be written in another language.
# Original commentary:
# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local sha1> <remote ref> <remote sha1>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).
function not_protected_branch() {
local branch="$1" && shift
local protected_branches=(refs/heads/master refs/heads/develop)
element_in "${branch}" "${protected_branches[@]}" && return 1
return 0
}
function commits_complete() {
local range="$1" && shift
local incomplete_prefixes=(WIP fixup squash)
local patterns commit
patterns=($(printf -- "--grep ^%s " "${incomplete_prefixes[@]}"))
commit=$(git rev-list -n 1 "${patterns[@]}" "${range}")
if [ -n "${commit}" ]
then
echo >&2 "${commit} is incomplete"
return 1
fi
return 0
}
function not_forcing() {
local branch="$1" && shift
local remote="$1" && shift
local push_command
push_command=($(ps -ocommand= -p $PPID))
local offset=1
[ "$(basename "${push_command[0]}")" == "git-push" ] \
|| { [ "${push_command[1]}" == "push" ] && ((offset++)); } \
|| { echo >&2 "${push_command[*]}: not a push?"; return 2; }
local push_args=("${push_command[@]:${offset}}")
local num_options=0
while [ $OPTIND -le ${#push_args[@]} ]; do
local optspec=":f-:"
local optchar
while getopts "${optspec}" optchar "${push_args[@]}"; do
((num_options++))
case "${optchar}" in
-)
case "${OPTARG}" in
force) ;&
force-with-lease*) ;&
mirror)
return 1;
;;
esac;;
f)
return 1
;;
esac
done
# first non-option argument is the remote, next ones are refspecs
local current_arg="${push_args[((OPTIND - 1))]}"
# echo >&2 "current_arg ${current_arg}"
if [ $((OPTIND - num_options)) -gt 1 ]
then
# refspec in command
is_force_refspec_for "${current_arg}" "${branch}" && return 1
fi
# skip any non-option argument that fails getopts
((OPTIND++))
done
# relying on configuration
if [ $((OPTIND - 1 - num_options)) -eq 1 ]
then
# push.default cannot turn on force but remote.name.push can
local refspec
refspec=$(git config "remote.${remote}.push")
is_force_refspec_for "${refspec}" "${branch}" && return 1
fi
return 0
}
function is_force_refspec_for() {
local refspec="$1" && shift
local branch="$1" && shift
# echo >&2 "refspec $refspec branch $branch"
if [ "${refspec:0:1}" == "+" ]
then
refspec=${refspec:1}
# force push enabled for this refspec, does it match?
local full_remote_target
full_remote_target=$(git rev-parse --symbolic-full-name "${refspec##*:}" 2>/dev/null)
{ [ -z "${full_remote_target}" ] || [ "${full_remote_target}" = "${branch}" ]; } && return 0
fi
return 1
}
function element_in() {
local element="$1" && shift
local arr="$*"
local e
for e in $arr; do [[ "${e}" == "${element}" ]] && return 0; done
return 1
}
function sha_not_present() {
local sha="$1" && shift
local z40=0000000000000000000000000000000000000000
[ "${sha}" = "${z40}" ]
}
function main() {
local remote="$1" && shift
local url="$1" && shift
local local_ref local_sha remote_ref remote_sha
while read -r local_ref local_sha remote_ref remote_sha
do
# echo >&2 "got: ${remote} ${url} ${local_ref} ${local_sha} ${remote_ref} ${remote_sha}"
if sha_not_present "${local_sha}"
then
# Handle delete
not_protected_branch "${remote_ref}" || { echo >&2 "${remote_ref} is a protected branch, not deleting"; return 1; }
else
not_protected_branch "${remote_ref}" || not_forcing "${remote_ref}" "${remote}" || { echo >&2 "${remote_ref} is a protected branch, not force-pushing"; return 1; }
if sha_not_present "${remote_sha}"
then
# New branch, examine all commits
local range="${local_sha}"
else
# Update to existing branch, examine new commits
local range="${remote_sha}..${local_sha}"
fi
commits_complete "${range}" || { echo >&2 "in-progress commit in ${local_ref}, not pushing"; return 1; }
fi
done
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment