|
| 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 | + |
| 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 | + |
| 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 | + |
| 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