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.
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
59
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
lock-yarn-version
* maintenance-page-relative-links
master
reinstate-upload-amended-file-link
sign-in-filtering
...
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
sign-in-filtering
* master
How about you? How many stale branches did you sweep away? Drop me a line on Twitter and let me know!