6

Is there any way to get the output of git branch -v as a plumbing command? To be exact I'm only interested in the state of the branch, i.e. whether it is [gone] or not.

For example given the following git branch -v output:

> git branch -v 
  master            32c59ad4 Some other comment
  someDeletedBranch 6aacba47 [gone] Some Comment

How could I get the someDeletedBranch ref?

Note that this is not the same as git branch --merged, if for example you're squashing pull requests into your master, so this solution won't do.

This is mostly in relation to this question, since this would be the missing part for being able to create a reliable script to remove local branches that don't exist on the remote any longer.

Voo
  • 29,040
  • 11
  • 82
  • 156
  • I am not sure if this is going to help you, but there is a way to list all local and remote branches. `git branch -a -v`. That way you can see which branches don't exist on remote anymore. Another option is to do diff between local and remote branch. If it throws and error, remote branch doesn't exist. `git diff /`. Sorry in advance if I didn't fully understand the question. – nemanja Nov 27 '18 at 16:36
  • 1
    @Nemanja The problem is that `git branch` is a porcelain command which should not be parsed in scripts, since your approach still uses it there's no improvement. And I don't have to do all those the manual comparisons between remotes and refs - which is not as trivial as you might think. `git branch -v` already does it for me and tells me which branches are gone and which aren't. – Voo Nov 27 '18 at 16:39
  • 2
    @NemanjaGlumac - IN addition to Voo's objection, a more pragmatic problem with those solutions is that they would get false positives for any local branch that has simply never yet been pushed. – Mark Adelsberger Nov 27 '18 at 17:07
  • @MarkAdelsberger - thanks for the update and clarification. – nemanja Nov 27 '18 at 17:14

2 Answers2

8

The plumbing substitute for git branch is usually git for-each-ref.

$ git fetch --prune
$ git for-each-ref --format '%(refname) %(upstream)' refs/heads refs/remotes/origin

This output will include an entry for each local branch, and an entry for each remote tracking ref pointing back to origin; so if run right after fetch --prune as shown here, it can show you what exists locally but not on the server.

Of course there's a gotcha: "exists locally but not on the server" might mean "removed from the server", or might mean "created locally and not yet pushed". To tell the difference, you need to also know whether your local branch "thinks" it has an upstream. (If it does, it's reasonably safe to assume it was removed from the server, in that git resists setting a nonexistent upstream; so for it to be wrong, someone would basically have to deliberately "trick" your script with a corrupt configuration.)

So that's what the --format option is for. You can process this output, looking for

refs/heads/somebranch refs/remotes/origin/somebranch

and, for each such entry, if there's not also a separate entry like

refs/remotes/origin/somebranch

then this is a branch that would be marked [gone]

Andrew Spencer
  • 15,164
  • 4
  • 29
  • 48
Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • Great solution. I was wondering how `git branch -v` decided which branches were gone (since the naive solution as you say wouldn't work), so it's nice to understand the background. Will accept as soon as I can. – Voo Nov 27 '18 at 17:42
  • 2
    `[gone]` is only part of the `%(upstream)` and `%(upstream:track)` output in v2.13.2+ – LightBender Nov 27 '18 at 17:43
  • @LightBender Nice example why parsing porcelain commands would be an exceedingly bad idea. – Voo Nov 27 '18 at 17:58
  • Adding `| grep 'refs/heads/.*\s$'` to the end helps filter out some noise – Snekse Jul 22 '22 at 13:48
5

Git v2.13.2+ only

Building off of Mark's answer, and this post, here's a pretty clean solution (split lines for readability):

git for-each-ref \
    --format='%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)' \
    refs/heads

Benefits:

Simple way to delete local branches not on remote

A simple way to use this is with command substitution with git branch -D. Note that git will complain if there are no branches to delete.

git branch -D $(git for-each-ref --format='%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)' refs/heads)

Here is a slightly longer version where git doesn't complain if there are no branches to delete.

for branch in $(git for-each-ref --format='%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)' refs/heads); do git branch -D $branch; done

Alternatively, you can save the output of git for-each-ref into a variable, then conditionally delete the branch if it's not empty, or show a message if it is.

Jerry Wu
  • 165
  • 3
  • 4
  • That if then syntax is interesting to say the least. This should definitely be the cannonical answer and I hope Mark won't be too upset that I changed the acceptance to put it on top :-) – Voo Apr 20 '20 at 08:21