Joachim Breitner

Spring cleaning: local git branches

Published 2019-03-03 in sections English, Digital World.

This blog post is not for everyone. Look at the top of your screen. Do you see dozens of open tabs? Then this blog post is probably not for you. Do you see a small number of open tabs, (and you use git and GitHub seriously)? Then this might be interesting to you.

In a typical development project using git and GitHub, I have a bunch of local git branches, in various categories:

  1. master, simply tracking upstream’s master branch
  2. Actively developed branches, already pushed to the remote repository.
  3. like 2., but already turned into a pull request.
  4. like 3., but already merged into master, using a merge commit,
  5. like 3., but already merged into master, using squash or rebase,
  6. like 2., but rejected and closed,
  7. like 6., and the corresponding remote branch already removed.
  8. Local commits that never made it remote.

This adds up to a fair number of local branches. Some of them are clearly mostly useless (in particular, those accepted into master, i.e. 4. and 5.). So I am motivate to delete as many branches as I can.

But I also do not want to lose any information. So I do not want to delete commits that I could not recover, if I had to.

A simple first thing to do, as recommended on StackExchange, is to delete all branches that have been merged into master:

git branch --merged | egrep -v "(^\*|master)" | xargs git branch -d

but this only catches category 4, and would not even delete branches that were merged using rebase or squash. Can we do better?

The crucial bit to know here is that Github treats each pull request like a branch, you just don’t see it, as they are hidden (they are in a different namespace, apart of the usual branches and tags). These branches stay around and, more importantly, when you merge a pull request using squash or rebase, they still reflect the “old” history of the PR.

In order to make use of that, we can instruct git to retch these pull request branches. To do so, edit .git/config in your repository, and add the following last line to the [remote "origin"] section:

[remote "origin"]
	url = …
	fetch = +refs/heads/*:refs/remotes/origin/*
        fetch = +refs/pull/*/head:refs/remotes/origin/pr/*

If you now run git fetch, you should see a lot of remotes/origin/pr/123 references being pushed.

And now that we see the state of all the pull requests that GitHub stores for us, we can delete all local branches that are contained in any of the remote branches:

for r in $(git branch -r --format='%(refname)'); do git branch --merged $r; done | sort -u | grep -v '^\*' | xargs -r git branch -D

Done! This deletes all categories mentioned above besides the last one.

A nice consequence of this is that I now know what to do with branches that did not lead anywhere, but which I did not simply want to remove: I push them to GitHub repository, create a new draft pull request, with a helpful comment for the future, immediately close the PR again and delete the remote and the local branch. If I ever want to come back to it, I find it in the list of closed pull requests, which is a much nicer attic than a ever growing list of branches of unclear status.

Comments

By opening and closing those pull requests for your stillborn branches, you’re really doing three things:

  1. picking a namespace to store such branches in

  2. adding a comment

  3. increasing your reliance on proprietary and centralized points of failure by another small amount

If you have any interest in only accomplishing a and b without c, you can instead check out the stillborn branch and use git commit --allow-empty and enter your comment about the branch there. Then git branch -M thebranch stillborn/thebranch to rename the branch into a namespace (feel free to pick a better name though) and then push the renamed branch to your remotes.

#1 Joey Hess am 2019-03-05

Have something to say? You can post a comment by sending an e-Mail to me at <mail@joachim-breitner.de>, and I will include it here.