Cleaning Up Old Git Branches
October 27, 2018At work, we typically delete remote branches after they are merged into master
. This often leaves me in a situation
where I have many local branches whose remote no longer exists.
Some folks like to keep those branches around just in case. Because of the power of git-reflog, I don’t think keeping old branches around is necessary.
In a perfect world, I’d git branch -D
everytime a remote branch was deleted. But things often
get too busy to clean up, and who wants to remember which branch was deleted? Can’t I just tell Git to clean up
after itself?
Yes, you can!
We can do so via this function in our shell profile of choice (mine is .zshrc
), which will “delete any local branches whose remote has been deleted”:
clean_branches() {
git remote prune origin
git branch -vv | grep "origin/.*: gone]" | awk '{print $1}' | xargs git branch -D
}
Let’s walk through it.
Pruning
git remote prune origin
The docs for this command tell us that it:
Deletes stale references associated with
<name>
In this case, we want to delete stale references associated with the remote origin
.
The best way to see what this command does is to compare before/after. Here is what git branch -vv
looks like before pruning:
> git branch -vv
bin-setup d135a8d bin/setup
* clean-branches f1fdebb Merge pull request #12 from speric/tweaks
gitignore 9fe5518 [origin/gitignore] Add jekyll-metadata to gitignore
master f1fdebb [origin/master] Merge pull request #12 from speric/tweaks
tweaks 5de39cc [origin/tweaks] Tweaks
This tells me:
- I am on the
clean-branches
branch right now (writing this post!) - I have a branch called
bin-setup
that is not tracking a remote branch - Both the
gitignore
andtweaks
branches do track remote branches, of the same name (as doesmaster
, obviously)
I know that the gitignore
and tweaks
branches’ remotes have been deleted, because I did so via the GitHub UI when I merged them into master
.
Now, I will run git remote prune origin
, and then git branch -vv
:
> git remote prune origin
Pruning origin
URL: git@github.com:speric/speric.github.com.git
* [pruned] origin/gitignore
* [pruned] origin/tweaks
> git branch -vv
bin-setup d135a8d bin/setup
* clean-branches f1fdebb Merge pull request #12 from speric/tweaks
gitignore 9fe5518 [origin/gitignore: gone] Add jekyll-metadata to gitignore
master f1fdebb [origin/master] Merge pull request #12 from speric/tweaks
tweaks 5de39cc [origin/tweaks: gone] Tweaks
We can see here that gitignore
and tweaks
have been pruned, which makes sense because their remote no longer exists.
Nothing happened to bin-setup
because it never had a remote. Nothing happened to master
either because it still exists at the remote.
Note that whereas, pre-prune, we saw the following for the gitignore
branch:
gitignore 9fe5518 [origin/gitignore] Add jekyll-metadata to gitignore
Now we see that it’s gone
:
gitignore 9fe5518 [origin/gitignore: gone] Add jekyll-metadata to gitignore
With this information we can now build up our command to delete those remote branches.
Deleting
git branch -vv | grep "origin/.*: gone]" | awk '{print $1}' | xargs git branch -D
Let’s take this set of commands apart.
git branch -vv
This command lists all local branches, verbosely. As we saw above, after we’ve pruned, it will tell us which branches are “gone”.
grep "origin/.*: gone]"
The output of git branch -vv
will be piped to the grep
command above. This command will grep
that output for any lines which contain origin/some-branch-name: gone]
.
> git branch -vv | grep "origin/.*: gone]"
gitignore 9fe5518 [origin/gitignore: gone] Add jekyll-metadata to gitignore
tweaks 5de39cc [origin/tweaks: gone] Tweaks
So now we have the two branches we want to delete: gitignore
and tweaks
.
awk '{print $1}'
The output of our grep
-ing is then piped to the above awk
command. awk is neat, and I confess I have
not grasped the full extent of its power.
In this context, we can think of the above code as saying: “For each line, in the piped-in text, print the first string”. We can see what it does:
> git branch -vv | grep "origin/.*: gone]" | awk '{print $1}'
gitignore
tweaks
We have isolated the actual branch names. Nearly there!
xargs git branch -D
The documentation for xargs
tells us:
xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines, and executes the command (default is /bin/echo) one or more times with any initial-arguments followed by items read from standard input.
xargs
is awesome, and you should take the time to learn it if you’re not already familiar with it.
Conceptually, we can think of our usage as “pass each item of the piped in text as the first argument to it’s own invocation of git branch -D
”. We
will wind up running git branch -D
twice in this case, once each for the gitignore
and tweaks
branches.
Put it all together, and we get:
> git branch -vv | grep "origin/.*: gone]" | awk '{print $1}' | xargs git branch -D
Deleted branch gitignore (was 9fe5518).
Deleted branch tweaks (was 5de39cc).
Done!
Wrap-up
Here’s the function again; stick it in your shell profile of choice:
clean_branches() {
git remote prune origin
git branch -vv | grep "origin/.*: gone]" | awk '{print $1}' | xargs git branch -D
}
I should also note that this kind of “workflow” only makes sense where you (or your team) get comfortable deleting remote branches after they’re merged.
Lastly, a shameless plug for my dotfiles.
UPDATE
Shortly after this post went live, @pengwynn pointed me to a post he wrote on Extending the Command Line. After digesting that post, I’ve tweaked my approach here.
The big idea is that any file in your PATH
that follows the git-
convention will become a new Git command. With that knowledge, I added my dotfiles bin
folder to the PATH
in my .zshrc
:
path+=(
${HOME}/dev/dotfiles/bin
)
(commit)
Then, I moved the body of clean_branches
to a file called git-cleanup in my dotfiles bin
folder:
# !/bin/sh
# Usage: git-cleanup
# Delete any local branches whose remote has been deleted
git remote prune origin
git branch -vv | grep "origin/.*: gone]" | awk '{print $1}' | xargs git branch -D
I then deleted the clean_branches
function from my .zshrc
.
Now, in any repository, I can do:
git cleanup
And the old branches will be deleted.