Cleaning up branches with Github’s squash-merge

Gordon Koo
Open House
Published in
4 min readDec 14, 2017

Last year, Github introduced squash-merging, a feature which squashes all of your commits into one commit before merging it into the base branch. This is great for keeping a clean, one-commit-per-change history in your master branch, but it complicates a crucial part of my day-to-day workflow.

One thing I like to do from time to time is delete local branches after they have been merged into the master branch in the remote repository (e.g. Github). With merge commits, this was as simple as writing a bash function using git branch --merged. With squash commits, we’ll need to get a little more creative.

First, let’s make use of the git remote prune origin command to determine branches that have been deleted on the remote repository. What this command does is remove stale remote-tracking branches for the origin remote. Quoting the Git help manual:

These stale branches have already been removed from the
remote repository referenced by <name>, but are still locally available in “remotes/<name>”.

Note that it doesn’t remove your local branches. However, it gives us the key information of what branches have been removed from origin (Github).

~/workspace/opendoor/web (master ✔) ᐅ git remote prune originPruning origin
URL: git@github.com:opendoor-labs/web.git
* [pruned] origin/11–29-newpartner
* [pruned] origin/add-comp-map
* [pruned] origin/agent-claim-auto
* [pruned] origin/atlas/minimum-standards
* [pruned] origin/change-offer-name
* [pruned] origin/create-update-customer-on-schedule-tour
* [pruned] origin/ft-rw
* [pruned] origin/gkoo__capitalize_lead_sellable_type
* [pruned] origin/gkoo__handle_ovm_error
* [pruned] origin/gkoo__is_seller_requested_inbound_channel
* [pruned] origin/gkoo__lead_supplemental_details
* [pruned] origin/gkoo__throttle_auction_requests
* [pruned] origin/pr-config-remove-local-resale
* [pruned] origin/pr-show-model-matches
* [pruned] origin/remove-unused-abad-compliances-column
* [pruned] origin/send-down-upcoming-listings-if-new-version
* [pruned] origin/support-for-lender-required-repairs
* [pruned] origin/ws-patch

Note: Once you run git remote prune origin, it will remove the remote-tracking branches and the same output won’t appear on successive calls. If you’d like to experiment with these commands, you can use the --dry-run option to avoid making any actual changes: git remote prune origin --dry-run.

Next, I want to filter for only my branches. One common technique to prevent branch naming collisions is to prefix branch names with some combination of your first and last name or initials. This trick also comes in handy for finding the specific branches we want to delete. In my case, I’m looking for branches prefixed with gkoo__, so I can pipe the output of git prune remote origin to a simple grep.

~/workspace/opendoor/web (master ✔) ᐅ git remote prune origin | grep gkoo__ * [pruned] origin/gkoo__capitalize_lead_sellable_type
* [pruned] origin/gkoo__handle_ovm_error
* [pruned] origin/gkoo__is_seller_requested_inbound_channel
* [pruned] origin/gkoo__lead_supplemental_details
* [pruned] origin/gkoo__throttle_auction_requests

Let’s use sed now to filter out everything except for the branch name.

~/workspace/opendoor/web (master ✔) ᐅ git remote prune origin | grep gkoo__ | sed ‘s/^.*origin\///g’gkoo__capitalize_lead_sellable_type
gkoo__handle_ovm_error
gkoo__is_seller_requested_inbound_channel
gkoo__lead_supplemental_details
gkoo__throttle_auction_requests

Almost there! For the actual branch deletion, we’ll use xargs to pass each of these branch names to the git branch -D command.

~/workspace/opendoor/web (master ✔) ᐅ git remote prune origin | grep gkoo__ | sed ‘s/^.*origin\///g’ | xargs -L1 -J % git branch -D %Deleted branch gkoo__capitalize_lead_sellable_type (was 615d68375b)
Deleted branch gkoo__handle_ovm_error (was ecaf757d21)
Deleted branch gkoo__is_seller_requested_inbound_channel (was cd2a26b32b)
Deleted branch gkoo__lead_supplemental_details (was a23fd5f59c)
Deleted branch gkoo__throttle_auction_requests (was 7d4e86acf2)

Lastly, we’ll add a command at the beginning to make sure that we’re on the master branch, in case we accidentally run this command while in a branch that we’ll end up wanting to delete.

~/workspace/opendoor/web (master ✔) ᐅ git checkout master && git remote prune origin | grep gkoo__ | sed ‘s/^.*origin\///g’ | xargs -L1 -J % git branch -D %Already on ‘master’
Your branch is up-to-date with ‘origin/master’.
Deleted branch gkoo__capitalize_lead_sellable_type (was 615d68375b)
Deleted branch gkoo__handle_ovm_error (was ecaf757d21)
Deleted branch gkoo__is_seller_requested_inbound_channel (was cd2a26b32b)
Deleted branch gkoo__lead_supplemental_details (was a23fd5f59c)
Deleted branch gkoo__throttle_auction_requests (was 7d4e86acf2)

Note that we need to use the more dangerous git branch -D instead of -d because the remote branch commits were squashed into a new commit, which doesn’t exist on your local branch, before merging into master. Using this command assumes that once a branch is pushed to and then deleted from the remote repository, the local copy of the branch is no longer needed. If this does not accurately describe your workflow, using this utility function may cause you to lose your changes!

Now throw this command in a bash or zsh function and enjoy your branch deletion! If you’re curious about any of the command line arguments, check out the man pages!

Do you have a different way of cleaning up local branches with Github’s squash-merge option enabled? Let me know!

EDIT: My fellow Opendoor engineer, Reid Roman, came up with a more generic command that compares pruned remote-tracking branches with the names of local branches, rather than relying on a branch naming convention:

alias gprunemerged='git checkout master && comm -12 <(git branch | sed "s/ *//g") <(git remote prune origin | sed "s/^.*origin\///g") | xargs -L1 -J % git branch -D %'

Give it a whirl!

--

--