The Branch Dumping Ground
If you're anything like us, you're not the best at deleting your local git branches after your pull requests have been merged.
After a while, the hundreds of dusty, stale git branches pile up. Running git branch
is useless. Any hope of finding that branch that you're looking for but forgot the name to is futile in the poorly labeled haystack.
Cleaning this mess up, even if you muster the willpower, is a daunting task — you fear accidentally deleting a branch with some useful, in-progress, but unmerged code that you were saving for that task on the backlog.
Eventually, the frustration and lack of usability spills over and you declare branch bankruptcy, going through and clearing all of your branches; the code can be re-written.
Finding A Programmatic Solution
As we developed the Graphite CLI, we found ourselves running into this situation more and more. This task is particularly important for stacked workflows, where there are more branches and where users frequently merge in the bottom of their stack and need to shift the remainder.
As a result, we found a better, more civil solution: a way to programmatically review your branches and delete any that are definitively merged (the exact contents of their commits have been merged into trunk).
The following snippet successfully does the job:
(skip ahead for the explanation)
function isMerged(branchName: string, trunk: string) {const tree = execSync(`git rev-parse "${branchName}^{tree}"`).toString();const mergeBase = execSync(`git merge-base ${trunk} ${branchName}`).toString();const commitObject = execSync(`git commit-tree ${tree} -p ${mergeBase} -m _`).toString();const cherryResult = execSync(`git cherry ${trunk} ${commitObject}`).toString();return cherryResult.startsWith('-');}
In English, we:
Capture the git tree from the tip of the branch. A git tree "corresponds to UNIX directory entries and blobs corresponding more or less to inodes or file contents."
Create a fake commit, whose parent is the merge base between the branch and trunk, and whose contents is the git tree from the tip of the branch.
Use the
git cherry
command to see if the contents of the fake commit have been incorporated upstream on the trunk from the branch's origination.
This sequence reliably finds branches that have been merged into trunks, including stacked branches. That being said, it's not perfect:
By comparing exactly the tip of the branch, the script won't flag a branch that had been merged into trunk but then advanced by a commit. We could improve the script by having it consider each commit in the branch one at a time, rather than just the tip.
It will also only flag branches whose merge base is behind their merge commit. This means the script would not flag a branch that had been squashed and merged on GitHub, and then later rebased the branch onto trunk's tip.
Try It Out
If you want to clean your old branches right now, run:
brew install screenplaydev/tap/graphite;gt repo sync;
In addition to deleting your branches with this logic, gt repo sync
performs a few other niceties:
pulls the latest code in from your trunk
detects partial sets of stacked branches that have been merged, deletes them, and rebases the remainder of the stack appropriately
removes any now-dead remotes
If you're interested a look behind the curtain, we're open source — take a look at how we do it here!