Last active
May 11, 2018 09:45
-
-
Save zmousm/e4c5e5a047c11b205c3b2494bdd6a596 to your computer and use it in GitHub Desktop.
Notify committers about stale branches in a repository. This is mostly useful in environments where collaborators push to a shared repository.
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
#!/bin/bash | |
usage () { | |
cat >&2 <<EOF | |
Usage: $0 (-r /path/to/git/repo | -u scheme:///git/repo/to/clone) [OPTIONS] | |
Detect potentially stale branches in a git repository and notify committers | |
or a designated party. | |
Mandatory arguments to long options are mandatory for short options too. | |
Required parameters: | |
-r, --repo-path=/some/path path to bare git repository | |
-u, --repo-url=scheme://uri URL for git repository to be shallow cloned | |
One of the above parameters must be provided, but not both. | |
Optional parameters: | |
-d, --days=days Ignore refs where the last commit is older than | |
this number of days (default: 30) | |
-x, --exclude-refs=ref,ref Refs to exclude, comma separated (default: | |
master) | |
-n, --repo-name=name Repository short name (default: read first line | |
from git "description") | |
--email-whitelist=pat,pat Whitelist committer e-mails matching this comma | |
separated list of patterns | |
--email-blacklist=pat,pat Blacklist committer e-mails matching this comma | |
separated list of patterns | |
-e, --email-other=mail Where to send e-mail for committers who don't | |
pass the white/black-list | |
-f, --email-from=mail Sender e-mail; requires (implies) heirloom mailx | |
(which accepts -r) | |
--heirloom-mailx Use options specific to heirloom mailx | |
EOF | |
} | |
warn() { | |
typeset formatstr | |
case "$1" in | |
-|/dev/stdin) | |
formatstr=$(cat -) | |
;; | |
*) | |
formatstr="$1" | |
;; | |
esac | |
shift 2>/dev/null | |
printf "$formatstr" "$@" | |
} | |
die() { warn "$@"; exit 1; } | |
test_getopt="$(getopt -T)" | |
if [ $? -eq 4 -a -z "$test_getopt" ]; then | |
: | |
else | |
die "enhanced getopt(1) was not found" | |
fi | |
params="$(getopt -o hr:u:d:x:n:e:f: \ | |
-l help,repo-path:,repo-url:,days:,exclude-refs: \ | |
-l email-whitelist:,email-blacklist:,email-other: \ | |
-l email-from:,repo-name:,heirloom-mailx \ | |
-n "$0" -- "$@")" | |
eval set -- "$params" | |
declare -A opts; | |
while true; do | |
case "$1" in | |
-h|--help) | |
usage | |
exit 0 | |
;; | |
-r|--repo-path) | |
opts[repo_path]="$2" | |
shift 2 | |
;; | |
-u|--repo-url) | |
opts[repo_url]="$2" | |
shift 2 | |
;; | |
-d|--days) | |
opts[days]="$2" | |
shift 2 | |
;; | |
-x|--exclude-refs) | |
opts[exclude_refs]="$2" | |
shift 2 | |
;; | |
-n|--repo-name) | |
opts[repo_name]="$2" | |
shift 2 | |
;; | |
--email-whitelist) | |
opts[email_whitelist]="$2" | |
shift 2 | |
;; | |
--email-blacklist) | |
opts[email_blacklist]="$2" | |
shift 2 | |
;; | |
-e|--email-other) | |
opts[email_other]="$2" | |
shift 2 | |
;; | |
-f|--email-from) | |
opts[email_from]="$2" | |
shift 2 | |
;; | |
--heirloom-mailx) | |
opts[heirloom_mailx]=yes | |
shift | |
;; | |
--) | |
shift | |
break | |
;; | |
*) | |
usage | |
exit 1 | |
;; | |
esac | |
done | |
if [ -n "${opts[repo_path]}" -a -n "${opts[repo_url]}" ]; then | |
die "Both repo-path and repo-url parameters should not be provided" | |
elif [ -n "${opts[repo_path]}" ]; then | |
repo_path="${opts[repo_path]}" | |
if [ -d "$repo_path" ]; then | |
cd "$repo_path" || \ | |
die "Could not access repo-path" | |
else | |
die "repo-path does not exist" | |
fi | |
elif [ -n "${opts[repo_url]}" ]; then | |
repo_url="${opts[repo_url]}" | |
clone_path=$(mktemp -u --tmpdir "$(basename "$0").XXXXXX") | |
trap 'popd 2>/dev/null' USR1 | |
trap 'kill -USR1 $$; rm -rf "$clone_path"' HUP INT QUIT TERM KILL EXIT | |
git clone --quiet --bare --depth 1 --no-single-branch \ | |
"$repo_url" "$clone_path" && \ | |
pushd "$clone_path" >/dev/null || \ | |
die "Could not clone repo" | |
else | |
die "One of repo-path or repo-url parameters should be provided" | |
fi | |
if [ -n "${opts[email_from]}" -o -n "${opts[heirloom_mailx]}" ]; then | |
assume_heirloom=yes | |
fi | |
if [ -n "${opts[repo_name]}" ]; then | |
repo_name="${opts[repo_name]}" | |
else | |
repo_name=$(head -1 description 2>/dev/null | xargs echo -n) | |
if [ -z "$repo_name" ] || [[ "$repo_name" = Unnamed\ repository* ]]; then | |
repo_name='unnamed' | |
fi | |
fi | |
subject="potentially stale branches in ${repo_name} repository" | |
if [ -z "${opts[days]}" ]; then | |
opts[days]=30 | |
fi | |
if [ -z "${opts[exclude_refs]}" ]; then | |
opts[exclude_refs]=master | |
fi | |
csv2array() { | |
typeset -n _ref=$1 | |
shift | |
typeset IFS=, | |
_ref=($1) | |
} | |
any_match() { | |
typeset -n _haystack=$1 | |
shift | |
typeset _needle=$1 | |
shift | |
typeset _i _needle_count=0 _haystack_count=${#_haystack[@]} | |
for _i in "${_haystack[@]}"; do | |
if [[ $_needle = $_i ]]; then | |
break | |
fi | |
((_needle_count++)) | |
done | |
[ $_needle_count -ne $_haystack_count ] | |
} | |
get_set_array() { | |
typeset -n _arr="$1" | |
shift | |
typeset _val="$1" | |
shift | |
typeset _i | |
for ((_i=0; _i < ${#_arr[@]}; _i++)); do | |
if [ "${_arr[$_i]}" = "$_val" ]; then | |
[ -n "$RETVAR" ] && eval $RETVAR="'$_i'" | |
return 0 | |
fi | |
done | |
_arr[$_i]="$_val" | |
[ -n "$RETVAR" ] && eval $RETVAR="'$_i'" | |
return 1 | |
} | |
get_set_array_create() { | |
typeset _arr="$1" | |
shift | |
eval "typeset -g -a $_arr" | |
get_set_array "$_arr" "$@" | |
} | |
cutoff_ts=$(date -d "${opts[days]} days ago" "+%s") | |
git_output=$(git for-each-ref --sort=committerdate \ | |
--format='%(refname:short)' | \ | |
xargs -I{} git log -1 --pretty="%ct|{}|%cr|%cN|%cE|%h" \ | |
--use-mailmap {}) | |
kill -USR1 $$ | |
typeset -a email_whitelist email_blacklist exclude_refs | |
csv2array email_whitelist "${opts[email_whitelist]}" | |
csv2array email_blacklist "${opts[email_blacklist]}" | |
csv2array exclude_refs "${opts[exclude_refs]}" | |
OLDIFS="$IFS" | |
IFS='|' | |
while read cdatets ref cdaterel cname cemail hash; do | |
if [ $cdatets -ge $cutoff_ts ]; then | |
break | |
fi | |
if any_match exclude_refs "$ref"; then | |
continue | |
fi | |
if any_match email_whitelist "$cemail" && | |
! any_match email_blacklist "$cemail"; then | |
recipient="$cemail" | |
else | |
recipient=__other__ | |
fi | |
RETVAR=recipient_id get_set_array_create recipients "$recipient" | |
RETVAR=committer_id get_set_array_create "committers_${recipient_id}" "${cname} <${cemail}>" | |
RETVAR=relcdate_id get_set_array_create "relcdates_${recipient_id}_${committer_id}" "$cdaterel" | |
get_set_array_create "refs_${recipient_id}_${committer_id}_${relcdate_id}" "$ref" | |
get_set_array_create "commits_${recipient_id}_${committer_id}_${relcdate_id}" "$hash" | |
done <<<"$git_output" | |
IFS="$OLDIFS" | |
unset OLDIFS | |
for ((recipient_id=0; recipient_id < ${#recipients[@]}; recipient_id++)); do | |
recipient="${recipients[$recipient_id]}" | |
committers="committers_${recipient_id}"[@] | |
committers=("${!committers}") | |
if [ ${#committers[@]} -eq 0 ]; then | |
continue | |
fi | |
if [ "$recipient" = __other__ ]; then | |
if [ -z "${opts[email_other]}" ]; then | |
continue | |
fi | |
recipient="${opts[email_other]}" | |
email_to="$recipient" | |
indentother=" " | |
unset subjprefix | |
phrasing=$(cat <<EOF | |
The listed branches are grouped by last committer and time to last commit. \ | |
These committers are not notified directly. | |
EOF | |
) | |
else | |
email_to=${committers[0]} | |
unset indentother | |
subjprefix=yes | |
phrasing='The listed branches are grouped by time to last commit.' | |
fi | |
if [ -n "$assume_heirloom" ]; then | |
mailx_args=(-t) | |
if [ -n "${opts[email_from]}" ]; then | |
mailx_args+=(-r "${opts[email_from]}") | |
fi | |
mail_headers=$"To: ${email_to} | |
Subject: ${subjprefix:+your (?) }${subject}" | |
else | |
mailx_args=(-s "${subjprefix:+your (?) }${subject}" | |
"${email_to}") | |
fi | |
{ | |
if [ -n "$mail_headers" ]; then | |
cat <<EOF | |
${mail_headers} | |
EOF | |
fi | |
{ | |
cat <<EOF | |
This is a report for ${repo_name} repository. What follows is a list of \ | |
branches where the last commit is older than ${opts[days]} days ago. The \ | |
commit hash is shown next to each branch. \ | |
${phrasing} | |
EOF | |
for ((committer_id=0; committer_id < ${#committers[@]}; committer_id++)); do | |
if [ -n "$indentother" ]; then | |
cat <<EOF | |
${committers[${committer_id}]}: | |
EOF | |
fi | |
relcdates="relcdates_${recipient_id}_${committer_id}"[@] | |
relcdates=("${!relcdates}") | |
for ((relcdate_id=0; relcdate_id < ${#relcdates[@]}; relcdate_id++)); do | |
cat <<EOF | |
${indentother}${relcdates[${relcdate_id}]} | |
EOF | |
refs="refs_${recipient_id}_${committer_id}_${relcdate_id}"[@] | |
refs=("${!refs}") | |
commits="commits_${recipient_id}_${committer_id}_${relcdate_id}"[@] | |
commits=("${!commits}") | |
for ((ref_id=0; ref_id < ${#refs[@]}; ref_id++)); do | |
cat <<EOF | |
${indentother} ${refs[${ref_id}]} -> ${commits[${ref_id}]} | |
EOF | |
done | |
done | |
done | |
cat <<EOF | |
Please remove any stale branches like so: git push <remote> :branch_name | |
EOF | |
} | \ | |
fmt -s | |
} | \ | |
mailx "${mailx_args[@]}" | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment