Skip to content

Instantly share code, notes, and snippets.

@zmousm
Last active May 11, 2018 09:45
Show Gist options
  • Save zmousm/e4c5e5a047c11b205c3b2494bdd6a596 to your computer and use it in GitHub Desktop.
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.
#!/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