Safely pruning merged topic branches locally 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/&lt;developer username&gt;/&lt;subtask ticket&gt;.

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/&lt;number&gt; 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 local master, 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 &amp;&amp; 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 &amp;&amp; delete_local_merged_branches

# Step 3
echo "Would you like to run the garbage collector?"
confirm &amp;&amp; \
 echo "Before garbage collecting: $(git count-objects)" &amp;&amp; \
 git gc

trap statistics EXIT
trap error SIGHUP SIGINT SIGTERM
The final line has no effect:

trap error SIGHUP SIGINT SIGTERM


As the program is about to exit, we're exceedingly unlikely to receive any of those signals between this point and the end of the program.

It's much more usual to set the trap handler as soon as you've defined the error function.

Similarly, the exit trap

trap statistics EXIT


is the same as simply

statistics


when we're about to exit.  I suggest that this line is moved to before Step 1, too.