150

Problem: I want a way of deleting all the local branches I have that do not have a remote. It's easy enough to pipe the names of the branches into a git branch -D {branch_name}, but how do I get that list in the first place?

For example:

I create a new branch without a remote:

$ git co -b no_upstream

I list all my branches, and there's only one with a remote

$ git branch -a
master
* no_upstream
remotes/origin/HEAD -> origin/master
remotes/origin/master

What command can I run to get no_upstream as an answer?

I can run git rev-parse --abbrev-ref --symbolic-full-name @{u} and that will show that it has no remote:

$ git rev-parse --abbrev-ref --symbolic-full-name @{u}
error: No upstream configured for branch 'no_upstream'
error: No upstream configured for branch 'no_upstream'
fatal: ambiguous argument '@{u}': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

But as this is an error, it won't let me use it or pipe it to other commands. I'm intending to use this as either a shell script alias'd to git-delete-unbranched or maybe make a super simple Gem like git-branch-delete-orphans

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
JC Denton
  • 1,515
  • 2
  • 10
  • 6
  • See also http://stackoverflow.com/questions/4950725/how-do-i-get-git-to-show-me-which-branches-are-tracking-what – Tomer Cohen Mar 17 '14 at 16:25
  • 1
    Possible duplicate of [Remove local branches no longer on remote](http://stackoverflow.com/questions/7726949/remove-local-branches-no-longer-on-remote) – ks1322 Jun 07 '16 at 15:37

11 Answers11

175

I recommend using git branch --format to specify the exact output you want from the git branch command. By doing that, you can pull out just the refname and the upstream, like this:

git branch --format "%(refname:short) %(upstream)"

It outputs the branches along with the remote branches if they exist, in the following format:

25-timeout-error-with-many-targets
31-target-suggestions refs/remotes/origin/31-target-suggestions
54-feedback-link refs/remotes/origin/54-feedback-link
65-digest-double-publish

Once you have this nicely formatted output, it's as easy as piping it through awk to get your list:

git branch --format "%(refname:short) %(upstream)" | awk '{if (!$2) print $1;}'

Results in the following output:

25-timeout-error-with-many-targets
65-digest-double-publish

The awk portion prints the first column if there is no second column.

Bonus: Create an alias

Make it easy to run by creating an alias in your global .gitconfig file (or wherever):

[alias]
  local-branches = "!git branch --format '%(refname:short) %(upstream:short)' | awk '{if (!$2) print $1;}'"

Bonus: Remote Filtering

If for some reason you have multiple tracking remotes for different branches, it's easy enough to specify which remote you want to check against. Just add the remote name to the awk pattern. In my case, it's origin so I can do this:

git branch --format "%(refname:short) %(upstream)" | awk '$2 !~/\/origin\// { print $1 }'

Important: The backslash needs to be escaped in the alias or else you will have an invalid gitconfig file.


Previous Answer

The previous answer was functionally similar, but used the following as it's starting point. Over time, commenters have pointed out that a regex is unreliable due to the variance possible in a commit message, so I no longer recommend this method. But, here it is for reference:

I recently discovered git branch -vv which is the "very verbose" version of the git branch command.

It outputs the branches along with the remote branches if they exist, in the following format:

  25-timeout-error-with-many-targets    206a5fa WIP: batch insert
  31-target-suggestions                 f5bdce6 [origin/31-target-suggestions] Create target suggestion for team and list on save
* 54-feedback-link                      b98e97c [origin/54-feedback-link] WIP: Feedback link in mail
  65-digest-double-publish              2de4150 WIP: publishing-state

Once you have this nicely formatted output, it's as easy as piping it through cut and awk to get your list:

git branch -vv | cut -c 3- | awk '$3 !~/\[/ { print $1 }'
Jeremy Baker
  • 3,986
  • 3
  • 24
  • 27
  • whats that `WIP:` anyway..? isn't that what a stash command creates..? – igrek Oct 26 '17 at 11:58
  • @igrek That's just a prefix I used in the commit message. Nothing specific to git. – Jeremy Baker Oct 27 '17 at 04:04
  • when i do `git branch -vv`, even if a remote branch exists and matches my local branch, i would not see the `[origin/branch-name]` alongside the result. I had to `diff` two lists of branches to actually figure out what was local only. – ryantuck Sep 18 '18 at 18:26
  • `git branch -vv | grep -v origin/` is enough for me – don't panic Aug 06 '19 at 07:34
  • 6
    Doesn't this miss the remote branches marked `gone`? – Reid Jan 22 '20 at 18:59
  • Any matching like that won't work if the commit message contains anything like that. For example, all our commit messages are "[ABC-nnnn] description", therefore every single one would match your regex. If matching "[origin" won't always produce the right results. – Aleks G Feb 06 '21 at 15:37
  • The updated answer does not work sorry. It returns the following for two branches with no upstream.... https://gist.github.com/rtfmoz2/587c0859a61936aa18133a34b62ea9fd – Kevin Davies Oct 07 '21 at 22:03
50

git branch (without any options) lists only local branches, but you don't know if they are tracking a remote branch or not.

Usually those local branches should be deleted once merged into main (as seen in this issue of git-sweep):

git branch --no-contains main --merged main| xargs git branch -d

Since 2020/2021, most repositories use main as a default branch instead of master.

This isn't as complete as you want, but it is a start.

With --merged, only branches merged into the named commit (i.e. the branches whose tip commits are reachable from the named commit) will be listed.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 1
    To also include remote branches without a local branch, use `git branch -a --merged`. – Asclepius May 11 '17 at 18:24
  • NOTE: a lot of repos have now switched from `master` to `main` as their primary branch so if that's the case, make sure to update it. – Joshua Pinter Jan 21 '23 at 16:38
  • 1
    @JoshuaPinter Good point, thank you. 10 years later, I have updated the answer accordingly. – VonC Jan 21 '23 at 20:49
34

I have a similar issue. I want to remove all local branches that were tracking remote branches that are now deleted. I am finding that git remote prune origin was insufficient to remove the branches that I want gone. Once the remote is deleted, I want the local to go away too. Here is what worked for me:

From my ~/.gitconfig:

[alias]
  prune-branches = !git remote prune origin && git branch -vv | grep ': gone]' | awk '{print $1}' | xargs -r git branch -d

Here is a git config --global ... command for easily adding this as git prune-branches:

git config --global alias.prune-branches '!git remote prune origin && git branch -vv | grep '"'"': gone]'"'"' | awk '"'"'{print $1}'"'"' | xargs -r git branch -d'

NOTE: I changed the -d to -D in my actual configuration, because I don't want to hear Git complain about unmerged branches. You may want this functionality as well. If so, simply use -D instead of -d at the end of that command.

Also, FWIW, your global configuration file would almost always be ~/.gitconfig.

(OS X Fix)

As written, this does not work on OS X because of the use of xargs -r (thanks, Korny).

The -r is to prevent xargs from running git branch -d without a branch name, which will result in an error message "fatal: branch name required". If you don't mind the error message, you can simply remove the -r argument to xargs and you're all set.

However, if you don't want to see an error message (and really, who could blame you) then you'll need something else that checks for an empty pipe. If you might be able to use ifne from moreutils. You would insert ifne before xargs, which will stop xargs from running with empty data. NOTE: ifne considers anything to be not empty, this includes blank lines, so you may still see that error message. I've not tested this on OS X.

Here is that git config line with ifne:

git config --global alias.prune-branches '!git remote prune origin && git branch -vv | grep '"'"': gone]'"'"' | awk '"'"'{print $1}'"'"' | ifne xargs git branch -d'
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Karl Wilbur
  • 5,898
  • 3
  • 44
  • 54
  • 1
    Is it possible to convert this into a "git config --global..." command? From an instruction/how-to-wiki-for-dummies point of view it is easy to pass on instead of saying "Find out your git config files, use editor to edit it then run commands" – Kannan Ekanath Sep 19 '17 at 09:04
  • Note that [xargs -r doesn't work on OSX](https://stackoverflow.com/questions/8803987/what-is-the-equivalent-to-xargs-r-under-osx) – Korny Apr 23 '18 at 12:53
  • Good to know. I don't know what the "no run if empty" option would be for OSX. – Karl Wilbur Apr 23 '18 at 12:56
21

This works for me:

git branch -vv | grep -v origin

(if your remote is named anything other than origin, substitute that).

This will list all the branches that aren't tracking a remote, which sounds like what you're looking for.

ebr
  • 606
  • 8
  • 13
20

Late edit:

Better is

git for-each-ref  --format='%(refname:short) %(upstream)'  refs/heads \
| awk '$2 !~/^refs\/remotes/'

On GNU/anything

for b in `git branch|sed s,..,,`; do
    git config --get branch.$b.remote|sed Q1 && echo git branch -D $b
done

If more than a handful of branches were likely, there'd be better ways, using comm -23 on the output of git branch|sed|sort and git config -l|sed|sort.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jthill
  • 55,082
  • 5
  • 77
  • 137
14

I synthetise my own Git command to get the origin/***: gone branches:

git remote prune origin && git branch -vv | cut -c 3- | grep ': gone]' | awk '{print $1}' | xargs -n1 -r echo git branch -d

git remote prune origin && git branch -vv will print branches in verbose mode.

cut -c 3- will remove very first characters.

grep ': gone]' will print only the gone remote branches.

awk '{print $1}' will print the branch name.

xargs -n1 -r echo git branch -d will print the git branch -d command to remove branches (-n1 will manage one command per time, -r to avoid issuing command if no branch is present).

HINT: remove the "echo" command to run the commands instead of print only, I left this in the command to check commands before issuing to git.

HINT 2: issue git branch -D if and only if you are sure you want to remove unmerged branches

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
fiorentinoing
  • 948
  • 12
  • 20
3

Here is something I have used in PowerShell with comments to explain what it's doing. In an attempt to make it clear what's going on, I've not used any abbreviated PowerShell commands (aliases). Feel free to compress it to your desired level of crypticness :)

$verboseList = @(git branch -vv)
foreach ($line in $verboseList)
{
    # Get the branch name
    $branch = [regex]::Match($line, "\s(\S+)\s").Captures.Groups[1].Value
    # Check if the line contains text to indicate if the remote is deleted
    $remoteGone = $line.Contains(": gone]")
    # Check if the line does not contain text to indicate it is a tracking branch (i.e., it's local)
    $isLocal = !($line.Contains("[origin/"))
    if ($remoteGone -or $isLocal)
    {
        # Print the branch name
        $branch
    }
}
grahamesd
  • 4,773
  • 1
  • 27
  • 27
3

The most voted answer here does not work on my system see gist

This following does work and is reasonably clean.

git branch -v | awk '$3 == "[gone]" { print $1 }' | xargs git branch -D

Use -d if you don't want to delete un-merged branches.

Kevin Davies
  • 155
  • 1
  • 4
1

None of the existing answers seems to work for me.

Here's my solution to list local branches that are not also remote

comm -13 <(git branch -r | sed 's/origin\/\(.*\)/\1/g' | cut -c 3-) <(git branch | cut -c 3-)

And here's an explanation of how it works:

  1. Consider the list of all remote branches (without 'origin/' or leading spaces) git branch -r | sed 's/origin\/\(.*\)/\1/g' | cut -c 3-

  2. Consider the list of all local branches (without asterisks or leading spaces) git branch | cut -c 3-

  3. Compare these two lists and show me anything in list 2 (my local branches) that isn't in list 1 (the list of remote branches) comm -13 <(git branch -r | sed 's/origin\/\(.*\)/\1/g' | cut -c 3-) <(git branch | cut -c 3-)

comm can take a combination of the flags -1, -2 and -3 indicating which file to suppress lines from (unique to file 1, unique to file 2 or common to both)

Darren Hicks
  • 4,946
  • 1
  • 32
  • 35
1

You can use %(upstream:track) formatting:

git branch --format "%(refname:short) %(upstream:track)" |
  awk '{if($2=="[gone]") print $1;}'

Output of git branch -v is unreliable for parsing.

Git version 2.37.1

Pavel Vlasov
  • 4,206
  • 6
  • 41
  • 54
  • Why am I the first to post this answer in 10 years? – Pavel Vlasov Jan 23 '23 at 11:45
  • Good point. Upvoted. When I wrote my answer in 2013, I did not have Git 2.5 and the `upstream:track` syntax I rediscovered [with `git for-each-ref`](https://stackoverflow.com/a/20499690/6309). – VonC Jan 23 '23 at 12:23
0

Combining a few of the existing answers:

[alias]
        branch-local = !git branch -vv | cut -c 3- | egrep '([0-9a-f]{7} [^[])|(: gone\\])' | sed -E 's/(^.+[0-9a-f]{7} (\\[.+\\])?).*$/\\1/'

E.g.:

$ git branch-local
autogen_autoreconf      fcfb1c6 [github/autogen_autoreconf: gone]
autotools_documentation 72dc477 [github/autotools_documentation: gone]
cray-mpich-messy        2196f4b
epel-sphinx             cfda770
lammps                  07d96a7 [github/lammps: gone]
lark-error              9ab5d6c [github/lark-error: gone]
no-root2                c3894d1
openmpi-cray            69326cf [github/openmpi-cray: gone]
shellcheck-cleanup      40d90ec
travis-python36         cde072e
update_vagrant_default  4f69f47 [github/update_vagrant_default: gone]
web-docs                e73b4a8
Reid
  • 1,999
  • 3
  • 17
  • 25