Skip to content

Commit abd5567

Browse files
committed
Blog post about cleaning merged branches
1 parent 33c86ee commit abd5567

File tree

1 file changed

+113
-0
lines changed

1 file changed

+113
-0
lines changed

_posts/2025/2025-04-17-git-gone.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
title: "Cleaning up gone branches"
3+
description: "A git alias to clean up gone branches. Even ones that have been squashed and merged."
4+
tags: [git]
5+
image: https://github.com/user-attachments/assets/4ac1c607-5b6f-40d8-8d93-554658fb80bf
6+
---
7+
8+
A long time ago, I wrote a [useful set of git aliases](https://haacked.com/archive/2014/07/28/github-flow-aliases/) to support the GitHub flow. My favorite alias was `bdone` which would:
9+
10+
1. Checkout the default branch.
11+
2. Run `git up` to make sure you're up to date.
12+
3. Run `git bclean` to delete all the branches that have been merged into the default branch.
13+
14+
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.
15+
16+
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.
17+
18+
![Image](https://github.com/user-attachments/assets/4ac1c607-5b6f-40d8-8d93-554658fb80bf)
19+
20+
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.
21+
22+
But here's the thing about Git. There's almost always a way.
23+
24+
## Solution
25+
26+
When you merge a PR on GitHub, it shows you a "Delete branch" button:
27+
28+
![Image](https://github.com/user-attachments/assets/b972f11c-985e-4586-874b-b57575ecebdc)
29+
30+
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:
31+
32+
![Image](https://github.com/user-attachments/assets/b73144f6-b457-4f13-b092-933a8aac27dd)
33+
34+
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:
35+
36+
```bash
37+
> git branch -vv
38+
39+
haacked/decide-v4 ba39408 [origin/haacked/decide-v4: gone] Fix demo to handle variants
40+
haacked/fix-sample-app ec15751 [origin/haacked/fix-sample-app] Handle variants
41+
* haacked/local-only e78d2f6 Do important stuff
42+
main ab885d0 [origin/main] chore: Bump to v1.0.2
43+
```
44+
45+
Notice that `haacked/decide-v4` is marked as `gone`.
46+
47+
We can use git's porcelain to list branches and their tracking information in a more easily parseable format.
48+
49+
```bash
50+
> git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/
51+
52+
haacked/decide-v4 [gone]
53+
haacked/fix-sample-app
54+
haacked/local-only
55+
master
56+
```
57+
58+
Let's make this an alias to list these gone branches:
59+
60+
```bash
61+
> git config --global alias.gone "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '\$2 == \"[gone]\" { print \$1 }'"
62+
```
63+
64+
Now we can use the alias to list gone branches:
65+
66+
```bash
67+
> git gone
68+
69+
haacked/decide-v4
70+
```
71+
72+
Next step is to update my old `bclean` alias to use the new `gone` alias.
73+
74+
```bash
75+
> git config --global alias.bclean "!git gone | xargs -r git branch -D"
76+
```
77+
78+
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`.
79+
80+
Here's the complete set of aliases mentioned in this post you can cut and paste into your `.gitconfig`:
81+
82+
```ini
83+
[alias]
84+
default = "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
85+
bclean = "!git gone | xargs -r git branch -D"
86+
# Switches to specified branch (or the default branch if no branch is specified), runs git up, then runs bclean.
87+
bdone = "!f() { DEFAULT=$(git default); git checkout ${1-$DEFAULT} && git up && git bclean; }; f"
88+
gone = "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '$2 == \"[gone]\" { print $1 }'"
89+
```
90+
91+
Or, if you want to use the `git` command line, you can use the following:
92+
93+
```bash
94+
git config --global alias.default "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
95+
git config --global alias.gone "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '\$2 == \"[gone]\" { print \$1 }'"
96+
git config --global alias.bclean "!git gone | xargs -r git branch -D"
97+
git config --global alias.bdone "!f() { DEFAULT=\$(git default); git checkout \${1:-\$DEFAULT} && git up && git bclean; }; f"
98+
```
99+
100+
> [!NOTE]
101+
> When using the `git default` alias, it's possible you'll encounter the following error:
102+
>
103+
> ```bash
104+
> fatal: ref refs/remotes/origin/HEAD is not a symbolic ref
105+
>```
106+
>
107+
> 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:
108+
>
109+
> ```bash
110+
> git remote set-head origin --auto
111+
> ```
112+
113+
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.

0 commit comments

Comments
 (0)