KonMari for your Git repositories

January is practically over, spring is just around the corner, it’s time to spring clean our Git repos! We’ll put together two Git aliases, delete-merged and delete-gone, that will help us keep our Git repositories clutter free and sparking joy.

A tree with branches fanning out in all directions.

I find it helpful to run git branch and see all my currently active branches. But unless we’re diligent and tidy up as we go, over time our Git repositories will tend to build up a bunch of crufty old branches that have long since served their purpose. Today we’ll look at some simple ways to keep our repositories clean and tidy.

I’ll be using the repo from a recent project I worked on to demonstrate.

Let’s start by seeing just how many branches we’re dealing with:

  $ git branch | wc -l

I think I can safely say I did not have 59 active branches when I finished working on that project…

First: Delete any merged branches

First up, Here’s a Git alias we’ll call git delete-merged that will get rid of any branches that have been merged into our main branch. Because if the branch has already been merged, it’s probably redundant, right?

  $ git config --global alias.delete-merged '!git branch --merged master | egrep -v "(^\*|^  master$)" | xargs git branch --delete'

Let’s break that down and see how it works:

1. Find the merged branches

First we get a list of all branches that have been merged:

  $ git branch --merged master
  * maintenance-page-relative-links

This outputs all the branches whose tips (latest commit) are reachable from the master branch, i.e. they’re part of the main history and therefore considered merged. Note the list includes the master branch, which we definitely don’t want to delete. So…

2. Strip out the branches we don’t want to delete

We’ll pipe this output to egrep to filter out a couple of branches that we don’t want to delete:

  egrep -v "(^\*|^  master$)"

This will give us a list of all the branch names, minus the current branch (which Git prefixes with a *) and the master branch. If you work with other long-lived branches that you want to exclude, say a development branch, you can add that to the list of exclusions by modifying the regular expression like so:

  egrep -v "(^\*|^  master$|^  development$)"

3. Delete the list of stale branches

Now we have a list of all the merged branches, we can use xargs to pass them through as arguments to git branch --delete and delete them!

So the full command looks like this:

  $ git branch --merged master | egrep -v "(^\*|^  master$)" | xargs git branch --delete

And now that’s configured as a Git alias, running git delete-merged dropped my branch count to a respectable 14.

But there are still branches in there that look suspiciously old. I reckon we can go even further…

Next: Delete “gone” branches

Here’s a second alias we’ll call git delete-gone that will help us get rid of even more branches, in this case any local branches where the remote tracking branch no longer exists. This will be useful if you work in a team that deletes remote branches after they are merged:

  $ git config --global alias.delete-gone '!git fetch --prune && git branch --verbose | awk '\''/\[gone]/{print $1}'\'' |  xargs git branch --delete --force'

Warning: this command comes with a health warning and you should make sure you understand what it does before you run it!

Let’s break it down.

1. Remove references to remote branches that no longer exist

The first part of our alias (git fetch --prune) fetches the latest remote state before deleting any refs to remote branches that no longer exist.

2. Collect branch names of “gone” branches

We can now see the local branches that point at a non-existent remote with the verbose output of git branch:

  git branch --verbose
  maintenance-page-tweaks 6b2bbd3 [gone] Remove redundant code related to submission files
* master                  7d8ba02 Release 58
  reduce-log-level        5f43458 [gone] Reduce log level for production to :info
  release-26              6f5dd2f [gone] Add RM1043.5 to list of frameworks we handle
  sign-in-filtering       fd3c246 Minor ApplicationController code tidy up

Every branch that is labelled with [gone] is pointing at a remote branch that no longer exists. In my example repo, 12 of the remaining 14 branches are labelled as gone. We pipe this output to awk to grab the names for those branches: awk '/\[gone]/{print $1}'.

3. Delete the “gone” branches (WARNING!)

Finally, we use xargs again to pass our “gone” branches to git branch --delete --force to delete them.

Note this time we’re include the --force option. This means branches will get deleted even if their tips aren’t reachable from master, i.e. as far as Git is concerned they’ve not been merged. The reason we need to do this is because after running our delete-merged alias, by definition the only branches left will be those that haven’t been merged, and git --branch --delete will fail with an error:

  error: The branch 'reduce-log-level' is not fully merged.
  If you are sure you want to delete it, run 'git branch -D reduce-log-level'.

So how have we ended up with a bunch of unmerged local branches that point to non-existent remotes? And isn’t it a bad idea to delete these unmerged branches?? Well yes, it might be. Technically, someone may (accidentally or otherwise) delete one of our branches from the upstream remote before it’s merged. In that scenario running this command would also delete our copy, meaning the branch would be lost! Hence why this alias comes with a warning. In reality though I think this scenario is unlikely, and if this were to happen, you always have access to Git’s reflog to help you recover the code from the deleted branch (more on the reflog in a future post).

The more likely scenario is that you work in a team that doesn’t shy away from rewriting the history on branches up to the point they get merged. Here’s a scenario:

  • you checkout someone else’s branch locally (the tip of the branch is 7d4d17c9)
  • the code gets reviewed and the author uses rebase to make amendments to the remote branch (the rebase results in a new tip on the remote: f47b078d)
  • the code is approved and the branch merged (f47b078d is now in master; 7d4d17c9, the old tip, is not)

Now because the tip on your local copy is the old commit, Git won’t find it in master’s history and will therefore report the branch as not fully merged, even though a revised version of that branch was actually merged.

If this scenario speaks to you and the way your team works, chances are you will have “gone” branches like these and the above command will help clean them up.

Last: Configure GitHub/GitLab to automatically delete merged branches

Lastly, you can help keep your local and remote repositories tidy by configuring your repo host to automatically delete branches after they are merged: GitHub has a setting to manage automatic deletion of branches and GitLab has a ‘Delete source branch’ setting for merge requests.

A clean and tidy Git repository

So having run our two new aliases my example repo has gone from 59 branches to a pleasingly de-cluttered two:

  $ git branch
  * master

How about you? How many stale branches did you sweep away? Drop me a line on Twitter and let me know!

Want more juicy Git tips like this straight to your inbox?

You'll get an email whenever I have a fresh insight or tip to share. Zero spam, and you can unsubscribe whenever you like with a single click.

More articles on Git

Authored by Published by