开发者

Remove tracking branches no longer on remote

Is there a simple way to delete all tracking branches whose remote e开发者_StackOverflow社区quivalent no longer exists?

Example:

Branches (local and remote)

  • master
  • origin/master
  • origin/bug-fix-a
  • origin/bug-fix-b
  • origin/bug-fix-c

Locally, I only have a master branch. Now I need to work on bug-fix-a, so I check it out, work on it, and push changes to the remote. Next I do the same with bug-fix-b.

Branches (local and remote)

  • master
  • bug-fix-a
  • bug-fix-b
  • origin/master
  • origin/bug-fix-a
  • origin/bug-fix-b
  • origin/bug-fix-c

Now I have local branches master, bug-fix-a, bug-fix-b. The Master branch maintainer will merge my changes into master and delete all branches he has already merged.

So the current state is now:

Branches (local and remote)

  • master
  • bug-fix-a
  • bug-fix-b
  • origin/master
  • origin/bug-fix-c

Now I would like to call some command to delete branches (in this case bug-fix-a, bug-fix-b), which are no longer represented in the remote repository.

It would be something like the existing command git remote prune origin, but more like git local prune origin.


git remote prune origin prunes tracking branches not on the remote.

git branch --merged lists branches that have been merged into the current branch.

xargs git branch -d deletes branches listed on standard input.

Be careful deleting branches listed by git branch --merged. The list could include master or other branches you'd prefer not to delete.

To give yourself the opportunity to edit the list before deleting branches, you could do the following in one line:

git branch --merged >/tmp/merged-branches && \
  vi /tmp/merged-branches && xargs git branch -d </tmp/merged-branches


The safest way to do this is to use the "plumbing" command git for-each-ref with the interpolation variable %(upstream:track), which will be [gone] when the branch is no longer on the remote:

git fetch -p && for branch in $(git for-each-ref --format '%(refname) %(upstream:track)' refs/heads | awk '$2 == "[gone]" {sub("refs/heads/", "", $1); print $1}'); do git branch -D $branch; done

This approach is somewhat safer than using the "porcelain" command, because there is no risk of accidentally matching on part of the commit message. Here is a version using the "porcelain" git commands:

git fetch -p && for branch in $(git branch -vv | grep ': gone]' | awk '{print $1}'); do git branch -D $branch; done

The way this works is that after the command

git fetch -p

removes the remote references, when you run

git branch -vv

it will show 'gone' as the remote status. For example,

$ git branch -vv
  master                 b900de9 [origin/master: behind 4] Fixed bug
  release/v3.8           fdd2f4e [origin/release/v3.8: behind 2] Fixed bug
  release/v3.9           0d680d0 [origin/release/v3.9: behind 2] Updated comments
  bug/1234               57379e4 [origin/bug/1234: gone] Fixed bug

This is what the scripts iterate over.


Most of these answers do not actually answer the original question. I did a bunch of digging and this was the cleanest solution I found. Here is a slightly more thorough version of that answer:

  1. Check out your default branch. Usually git checkout master
  2. Run git fetch -p && git branch -vv | awk '/: gone]/{print $1}' | xargs git branch -d

Explanation:

Works by pruning your tracking branches then deleting the local ones that show they are "gone" in git branch -vv.

Notes:

If your language is set to something other than English you will need to change gone to the appropriate word. Branches that are local only will not be touched. Branches that have been deleted on remote but were not merged will show a notification but not be deleted on local. If you want to delete those as well change -d to -D.


I wouldn't normally answer a question that already has 16 answers, but all the other answers are wrong, and the right answer is so simple. The question says, "Is there a simple way to delete all tracking branches whose remote equivalent no longer exists?"

If "simple" means deleting them all in one go, not fragile, not dangerous, and without reliance on tools that not all readers will have, then the right answer is: no.

Some answers are simple, but they don't do what was asked. Others do what was asked, but are not simple: all rely on parsing Git output through text-manipulation commands or scripting languages, which may not be present on every system. On top of that, most of the suggestions use porcelain commands, whose output is not designed to be parsed by script ("porcelain" refers to the commands intended for human operation; scripts should use the lower-level "plumbing" commands).

Further reading:

  • why you shouldn't parse git branch output in a script.
  • tracking branches and the differences between git remote prune, git prune, git fetch --prune

If you want to do this safely, for the use case in the question (garbage-collect tracking branches which have been deleted on the server but still exist as local branches) and with high-level Git commands only, you have to

  • git fetch --prune (or git fetch -p, which is an alias, or git remote prune origin which does the same thing without fetching, and is probably not what you want most of the time).
  • Note any remote branches that are reported as deleted. Or, to find them later on, git branch -v (any orphaned tracking branch will be marked "[gone]").
  • git branch -d [branch_name] on each orphaned tracking branch

(which is what some of the other answers propose).

If you want to script a solution, then for-each-ref is your starting point, as in Mark Longair's answer here and this answer to another question, but I can't see a way to exploit it without writing a shell script loop, or using xargs or something.


Background explanation

To understand what's happening, you need to appreciate that, in the situation of tracking branches, you have not one branch, but three. (And recall that "branch" means simply a pointer to a commit.)

Given a tracking branch feature/X, the remote repository (server) will have this branch and call it feature/X. Your local repository has a branch remotes/origin/feature/X which means, "This is what the remote told me its feature/X branch was, last time we talked," and finally, the local repository has a branch feature/X which points to your latest commit, and is configured to "track" remotes/origin/feature/X, meaning that you can pull and push to keep them aligned.

At some point, someone has deleted the feature/X on the remote. From that moment, you are left with your local feature/X (which you probably don't want any more, since work on feature X is presumably finished), and your remotes/origin/feature/X which is certainly useless because its only purpose was to remember the state of the server's branch.

And Git will let you automatically clean up the redundant remotes/origin/feature/X -- that's what git fetch --prune does -- but for some reason, it doesn't let you automatically delete your own feature/X... even though your feature/X still contains the orphaned tracking information, so it has the information to identify former tracking branches that have been fully merged. (After all, it can give you the information that lets you do the operation by hand yourself.)


Windows Solution

For Microsoft Windows Powershell:

git checkout master; git remote update origin --prune; git branch -vv | Select-String -Pattern ": gone]" | % { $_.toString().Trim().Split(" ")[0]} | % {git branch -d $_}

Explaination

git checkout master switches to the master branch

git remote update origin --prune prunes remote branches

git branch -vv gets a verbose output of all branches (git reference)

Select-String -Pattern ": gone]" gets only the records where they have been removed from remote.

% { $_.toString().Trim().Split(" ")[0]} get the branch name

% {git branch -d $_} deletes the branch


I found the answer here: How can I delete all git branches which have been merged?

git branch --merged | grep -v "\*" | xargs -n 1 git branch -d

Make sure we keep master

You can ensure that master, or any other branch for that matter, doesn't get removed by adding another grep after the first one. In that case you would go:

git branch --merged | grep -v "\*" | grep -v "YOUR_BRANCH_TO_KEEP" | xargs -n 1 git branch -d

So if we wanted to keep master, develop and staging for instance, we would go:

git branch --merged | grep -v "\*" | grep -v "master" | grep -v "develop" | grep -v "staging" | xargs -n 1 git branch -d

Make this an alias

Since it's a bit long, you might want to add an alias to your .zshrc or .bashrc. Mine is called gbpurge (for git branches purge):

alias gbpurge='git branch --merged | grep -v "\*" | grep -v "master" | grep -v "develop" | grep -v "staging" | xargs -n 1 git branch -d'

Then reload your .bashrc or .zshrc:

. ~/.bashrc

or

. ~/.zshrc


TL;DR:

Remove ALL local branches that are not on remote

git fetch -p && git branch -vv | grep ': gone]' | awk '{print $1}' | xargs git branch -D

Remove ALL local branches that are not on remote AND that are fully merged AND that are not used as said in many answers before.

git fetch -p && git branch --merged | grep -v '*' | grep -v 'master' | xargs git branch -d

Explanation

  • git fetch -p will prune all branches no longer existing on remote
  • git branch -vv will print local branches and pruned branch will be tagged with gone
  • grep ': gone]' selects only branch that are gone
  • awk '{print $1}' filter the output to display only the name of the branches
  • xargs git branch -D will loop over all lines (branches) and force remove this branch

Why git branch -D and not git branch -d else you will have for branches that are not fully merged.

error: The branch 'xxx' is not fully merged.


You could do this:

git branch -vv | grep 'origin/.*: gone]' | awk '{print $1}' | xargs git branch -d

P.S.: as pointed out by Sam H.

execute this first:

git remote prune origin


The pattern matching for "gone" in most of the other solutions was a little scary for me. To be safer, this uses the --format flag to pull out each branch's upstream tracking status.

I needed a Windows-friendly version, so this deletes all branches that are listed as "gone" using Powershell:

git branch --list --format "%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)" | 
    ? { $_ -ne "" } | 
    % { git branch -D $_ }

The first line lists the name of local branches whose upstream branch is "gone". The next line removes blank lines (which are output for branches that aren't "gone"), then the branch name is passed to the command to delete the branch.


Yet another answer, because none of the solutions suit my needs on elegance and cross-platformness:

Command to delete local branches not on remote:

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

To integrate it with gitconfig so it can be run with git branch-prune:

Bash

git config --global alias.branch-prune '!git fetch -p && for b in $(git for-each-ref --format='\''%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)'\'' refs/heads); do git branch -d $b; done'

PowerShell

git config --global alias.branch-prune '!git fetch -p && for b in $(git for-each-ref --format=''%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)'' refs/heads); do git branch -d $b; done'

(Need help in finding a universal command for PowerShell and bash)

Why this answer is the best?

  • Offers a complete solution: adds a git branch-prune command to your git
  • Works fine from Windows PowerShell
  • The core idea is @jason.rickman's bulletproof method using git for-each-ref
  • Parsing and filtering is done with --filter so no external dependencies needed

Explanation:

  • Adds a new alias to your ~\.gitconfig. After executing this you can simply do git branch-prune
  • Inside this alias:
    • Fetches branches with the --prune flag, which "prunes remote-tracking branches no longer on remote"
    • Uses git for-each-ref and --filter, to get a list of the branches are [gone] (no remote)
    • Loops through this list and deletes the branch safely


Remove all branches that have been merged into master, but don't try to remove master itself:

git checkout master && git pull origin master && git fetch -p && git branch -d $(git branch --merged | grep master -v)

or add an alias:

alias gitcleanlocal="git checkout master && git pull origin master && git fetch -p && git branch -d $(git branch --merged | grep master -v)"

Explanation:

git checkout master checkout master branch

git pull origin master ensure local branch has all remote changes merged

git fetch -p remove references to remote branches that have been deleted

git branch -d $(git branch master --merged | grep master -v) delete all branches that have been merged into master, but don't try to remove master itself


git fetch -p

This will prune any branches that no longer exist on the remote.


While the above answers cover how to prune branches manually, this answer adds automation to solve this. git now has a new setting to prune stale branches that are no longer on the remote for every fetch action. This is great because we no longer have to manually call remote prune every time we delete branches (git pull also calls git fetch).

Enable prune behaviour for every fetch

To enable this in the global config:

git config --global fetch.prune true

Making the thing happen automatically means you can forget to add this setting on new machines. It just works.

Enable prune behaviour for every fetch on specific remotes

git config --global remote.<name>.prune true

Local automated pruning

We can apply the same command for local pruning as well without the --global flag.

.gitconfig

The commands above apply to the global and local .gitconfig as follows:

...
[fetch]
    prune = true

I can recommend adding this to an ansible configuration or to your dotfiles repository (.gitconfig) to automate the setup for the future.

The configuration setting calls the below command on every fetch:

git remote prune <remote name>

Summary

To prune references as part of your normal workflow without needing to remember to run that, set fetch.prune globally or remote.<name>.prune per-remote in the config. See git-config.


Yet-another-answer for the pile, drawing heavily from Patrick's answer (which I like because it seems to do away with any ambiguity about where gone] will match in the git branch output) but adding a *nix bent.

In its simplest form:

git branch --list --format \
  "%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)" \
  | xargs git branch -D

I have this wrapped up in a git-gone script on my path:

#!/usr/bin/env bash

action() {
  ${DELETE} && xargs git branch -D || cat
}

get_gone() {
  git branch --list --format \
    "%(if:equals=[gone])%(upstream:track)%(then)%(refname:short)%(end)"
}

main() {
  DELETE=false
  while [ $# -gt 0 ] ; do
    case "${1}" in
      (-[dD] | --delete) DELETE=true ;;
    esac
    shift
  done
  get_gone | action
}

main "${@}"

NB - The --format option seems to be fairly new; I needed to upgrade git from 2.10.something to 2.16.3 to get it.

EDIT: tweaked to include suggestion about refname:short from Benjamin W.

NB2 - I've only tested in bash, hence the hashbang, but probably portable to sh.


This will delete all the merged local branched except local master reference and the one currently being used:

git branch --merged | grep -v "*" | grep -v "master" | xargs git branch -d

And this will delete all the branches having already been removed from the remote repository referenced by "origin", but are still locally available in "remotes/origin".

git remote prune origin


Might be useful to some, simple one line to clear all local branches except master and develop

git branch | grep -v "master" | grep -v "develop" | xargs git branch -D


I don't think there is a built-in command to do this, but it is safe to do the following:

git checkout master
git branch -d bug-fix-a

When you use -d, git will refuse to delete the branch unless it is completely merged into HEAD or its upstream remote-tracking branch. So, you could always loop over the output of git for-each-ref and try to delete each branch. The problem with that approach is that I suspect that you probably don't want bug-fix-d to be deleted just because origin/bug-fix-d contains its history. Instead, you could create a script something like the following:

#!/bin/sh

git checkout master &&
for r in $(git for-each-ref refs/heads --format='%(refname:short)')
do
  if [ x$(git merge-base master "$r") = x$(git rev-parse --verify "$r") ]
  then
    if [ "$r" != "master" ]
    then
      git branch -d "$r"
    fi
  fi
done

Warning: I haven't tested this script - use only with care...


Because some answers don’t prevent accidental deletion

git fetch -p && LANG=c git branch -vv | awk '/: gone]/&&!/^\*/{print $1}' | xargs git branch -d

filtering out the branch having * in the first column is important.


You can use this:

git fetch --prune

then

git branch -vv | egrep -v "([origin/[a-zA-Z0-9/_-]+])" | awk "{print $1}" | xargs git branch -D

It removes all the local branches that are not linked to the remote

and I personally added an alias that does it for me

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜