Cleaning git branches
Aug 26, 2016The ubiquitous git (no, not a person) provides several tools for managing repositories, but most of them operate at a fairly low-level.This means it’s often necessary to hack together quick scripts when there’s something you need to get done. I needed to remove a load of old, but merged, branches from some remote (centrally hosted) repositories, so (in the words of the slingshot channel) “Let me show you what I came up with!”.
Origins
To get git to list merged branches, you can use the --merged
argument to git branch
, add -r
and you only get merged remote branches:
git branch -r --merged
To get general information on remote branches, you can use for-each-ref
, a
script which gives you information on objects defined in files within the
.git/refs
directory. Have a look at the documentation and you’ll see it can
easily be customised to display pretty much anything about a git object. In my
case, I want to display the name of the ref (so the branch name, basically),
and the date of the last commit:
git for-each-ref --sort=-committerdate refs/remotes --format="%(refname) %(committerdate:raw)"
Adding the :raw
parameter to the committer date gets it as a unix timestamp
(with a timezone we can probably just ignore), like this:
refs/remotes/origin/master 1469033580 +0100
So, to delete merged branches, whose last commit is older than a certain date,
I need to take this output, filter out the branches I’m not interested in, and
then join
the results with the output of the first command. This is pretty
simple, but there’s a little bit of faffing about needed to make the outputs
from each command match up.
The script
Here’s the script as it currently stands, I’ve kept things divided up into
functions to make everything easier. It works by running the two commands
I mentioned above (cleaning the output to make sure they’re the same).
Filtering by date is nice and easy, as both the GNU and BSD date functions let
you specify dates in the past, and I used the standard while read
idiom along
with a simple if
statement to decide what to keep and what to discard.
Next, I used the standard UNIX utility join
to find branches which appear in both
the merged list, and the list of branches older than the configured limit.
Finally, it removes the branches from the remote using git push
. I added
some basic argument parsing to allow setting the age of branches to remove and
only show what the script would like to do, rather than actually doing it,
and the result is shown below:
#! /bin/bash
# Deletes remote git branches which are older than 6 weeks and have been merged
#
# Author: Matt Foster <mpf@hackerific.net>
function process_args {
while getopts ":hlw:" opt; do
case $opt in
w)
WEEKS=$OPTARG
;;
l)
LIVE=1
;;
h)
usage
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
usage
exit 1
;;
esac
done
WEEKS=${WEEKS:-6}
LIVE=${LIVE:-0}
}
function usage {
echo "Usage: purge-branches [-w <weeks>] [-l]" >&2
echo " -w <weeks> - remove merged brances whose latest commit is as least <weeks> weeks old" >&2
echo " -l - actually remove the branches instead of just showing what to run" >&2
echo " -h - show this help" >&2
}
function get_limit {
unamestr=$(uname)
if [[ "$unamestr" == 'Darwin' ]]; then
LIMIT=$(date -j -v-${WEEKS}w +%s)
else
LIMIT=$(date --date="$WEEKS weeks ago" +%s)
fi
}
function get_branches {
git for-each-ref --sort=-committerdate refs/remotes --format="%(refname) %(committerdate:raw)"
}
function filter_by_date {
while read branch date zone; do
if [[ "$date" -le "$LIMIT" ]]; then
echo $branch
fi
done
}
function clean_branch_name {
sed -e 's~^\s\+~~' \
| sed -e 's~refs/~~' \
| sed -e 's~remotes/origin/~~' \
| sed -e 's~^origin/~~'
}
function merged_branches {
git branch -r --merged
}
function delete_branches {
while read branch; do
if [[ "$LIVE" -eq "1" ]]; then
git push origin --delete $branch
else
echo git push origin --delete $branch
fi
done
}
# Here's the main program!
process_args "$@"
get_limit
git fetch --all
old_branches=$(mktemp /tmp/purge-branches.XXXXXX)
merged_branches=$(mktemp /tmp/purge-branches.XXXXXX)
get_branches \
| filter_by_date \
| clean_branch_name \
| sort \
> $old_branches
merged_branches | fgrep -v ' -> ' | fgrep -v 'master' \
| clean_branch_name \
| sort \
> $merged_branches
join $old_branches $merged_branches \
| delete_branches
Hopefully it’ll prove useful to others too.