Cleaning up gone branches
A long time ago, I wrote a useful set of git aliases to support the GitHub flow. My favorite alias was bdone
which would:
- Checkout the default branch.
- Run
git up
to make sure you’re up to date. - Run
git bclean
to delete all the branches that have been merged into the default branch.
And this worked great for a long time. The way it worked was it would list all the branches that have been merged into the default branch and then delete them. In my case, I didn’t use git branch --merged
to list merged branches because I didn’t know about it at the time.
However, my aliases stopped working for me recently after joining PostHog. The main reason is on pretty much all of their repositories, they use Squash and Merge when merging PRs.
When you use git merge --squash
or GitHub’s “Squash and merge” feature, Git creates a new commit on the target branch that combines all the changes from the source branch into a single commit. This new commit doesn’t retain any reference to the original commits from the source branch. As a result, Git doesn’t consider the source branch as merged, and commands like git branch --merged
won’t show it.
But here’s the thing about Git. There’s almost always a way.
Solution
When you merge a PR on GitHub, it shows you a “Delete branch” button:
This is a great feature. It’s a good way to clean up branches that have been merged into the default branch. In fact, you can configure GitHub to “Automatically delete head branches” when merged:
I highly recommend you do the same. When the remote branch is deleted, Git will track it as “gone”. For example, if you run git branch -vv
you’ll see something like this:
> git branch -vv
haacked/decide-v4 ba39408 [origin/haacked/decide-v4: gone] Fix demo to handle variants
haacked/fix-sample-app ec15751 [origin/haacked/fix-sample-app] Handle variants
* haacked/local-only e78d2f6 Do important stuff
main ab885d0 [origin/main] chore: Bump to v1.0.2
Notice that haacked/decide-v4
is marked as gone
.
We can use git’s porcelain to list branches and their tracking information in a more easily parseable format.
> git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/
haacked/decide-v4 [gone]
haacked/fix-sample-app
haacked/local-only
master
Let’s make this an alias to list these gone branches:
> git config --global alias.gone "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '\$2 == \"[gone]\" { print \$1 }'"
Now we can use the alias to list gone branches:
> git gone
haacked/decide-v4
Next step is to update my old bclean
alias to use the new gone
alias.
> git config --global alias.bclean "!git gone | xargs -r git branch -D"
Unfortunately, since git doesn’t know it’s been merged, we have to do a force delete. That’s a bit scary, but this won’t touch local branches or any branches that are still tracking a remote branch. With this alias, you can run git bclean
to delete all the branches that have been merged into the default branch. Finally, we have the old bdone
alias to switch to the default branch, run git up
, and then run bclean
.
Here’s the complete set of aliases mentioned in this post you can cut and paste into your .gitconfig
:
[alias]
default = "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
bclean = "!git gone | xargs -r git branch -D"
# Switches to specified branch (or the default branch if no branch is specified), runs git up, then runs bclean.
bdone = "!f() { DEFAULT=$(git default); git checkout ${1-$DEFAULT} && git up && git bclean; }; f"
gone = "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '$2 == \"[gone]\" { print $1 }'"
Or, if you want to use the git
command line, you can use the following:
git config --global alias.default "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
git config --global alias.gone "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '\$2 == \"[gone]\" { print \$1 }'"
git config --global alias.bclean "!git gone | xargs -r git branch -D"
git config --global alias.bdone "!f() { DEFAULT=\$(git default); git checkout \${1:-\$DEFAULT} && git up && git bclean; }; f"
Note
When using the git default
alias, it’s possible you’ll encounter the following error:
fatal: ref refs/remotes/origin/HEAD is not a symbolic ref
This alias relies on the presence of the origin/HEAD symbolic reference. In some cases, especially with newly cloned repositories or certain configurations, this reference might not be set. To fix it:
git remote set-head origin --auto
So now, my old workflow is back. When I merge a PR, I can run git bdone
from the branch I merged to clean up the branches. All is right with the world again.
And as always, you can find these aliases and more in my dotfiles repo. See this blog post for more context on how you can include them in your dotfiles.
Comments
One response