Background
My team develops in a git monorepo. User stories are broken into very small discrete sub-tasks and merged directly into master
, rather than into a feature branch for the story.
Topic branches
Our topic branch names are all of the form issue/<developer username>/<subtask ticket>
.
Those are pushed to origin and merged via a code-reviewed pull-request process.
Release branches
We also maintain long-lived release branches named release/sprint/<number>
used for hot-patching.
Release branches are never re-merged to master
. The individual fixes, if relevant in the current release, are re-implemented in another branch or applied as a patch. There are few enough of them to manage manually.
Work
This works well for us, but it means we have hundreds of topic branches hanging around as developers often forget to delete them as they go.
I wrote a simple bash script to
- Sync up with origin (we have only one),
- Figure out which branches have been merged into
master
(or, optionally, a different branch, so far nobody has used that), - Delete locally-merged branches,
- Optionally run the gc
The verbose prompting at every stage is intentional as reassurance.
Concerns
Primarily, is this "safe", meaning:
- Have I used an appropriate method to list branches?
- Have I used an appropriate method for deleting branches?
- Have I accounted for all possible pathways that could result in destruction of a local branch?
- Have I adequately guarded against destruction of a local branch not merged into
master
? - Should I verify that topic branches have been merged into origin's
master
and not the localmaster
, in the event that a user mistakenly merges locally and loses their branch?
I'm very happy for any additional comments, no matter how nit-picky.
Code
#!/bin/bash
MERGED_INTO=${1:-"master"}
statistics() {
echo
echo "Remaining local branches:"
git branch
echo
echo "These local branches are not merged into ${MERGED_INTO}:"
git branch --no-merged ${MERGED_INTO}
echo
echo "Size of local repo: $(git count-objects)"
git status
}
error () {
echo "WARNING: Something went wrong. Aborting..."
if [[ ${1} ]]; then echo "The previous command exited with ${?}"; fi
echo "This is the current state of your working copy:"
git status
}
confirm () {
read -r -p "Are you sure? [y/N] " response
case ${response} in
[yY][eE][sS]|[yY])
true
;;
*)
echo "Ok, aborting..."
echo
false
;;
esac
}
check_on_main_branch() {
local current_branch;
current_branch=$(git symbolic-ref -q --short HEAD)
if [ "${current_branch}" != "${MERGED_INTO}" ]; then
echo "You should be on '${MERGED_INTO}' for this operation. I can switch branches for you."
confirm && git checkout "${MERGED_INTO}" || error $?
fi
}
delete_local_merged_branches() {
git branch --merged ${MERGED_INTO} | egrep -v "(^\*|${MERGED_INTO})" | xargs git branch --delete
}
# Step 1
echo "Fetching remote branch state"
git fetch --all
# Step 2
echo "The following local branches have been merged into ${MERGED_INTO} and will be deleted locally:"
git branch --merged ${MERGED_INTO} | egrep -v "(^\*|${MERGED_INTO})"
confirm && delete_local_merged_branches
# Step 3
echo "Would you like to run the garbage collector?"
confirm && \
echo "Before garbage collecting: $(git count-objects)" && \
git gc
trap statistics EXIT
trap error SIGHUP SIGINT SIGTERM