git branch -d -r remotename
I'm finding myself wanting to delete all local branch heads that are already present on a remote.
You wouldn't want it to be too eager about it, and certainly you wouldn't want to use it against any server that isn't considered completely "upstream" of the repository you're doing it on, otherwise if both ends run it and then "git remote prune" the refs will go away.
I think either "git remote prune" or "git branch -d" would be a good place for that.
Thoughts?
Sam.
Quoted 2 lines
I'm finding myself wanting to delete all local branch heads that are already present on a remote.
Something like this.
Subject: [PATCH] git-remote: implement prune -c
It would be nice to prune local refs which are irrelevant; add an option to git-remote prune, with documentation.
--- Documentation/git-remote.txt | 4 ++++ git-remote.perl | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-)
Documentation/git-remote.txt +4 -0
@@ -63,6 +63,10 @@ referenced by <name>, but are still locally available in With `-n` option, the remote heads are not confirmed first with `git ls-remote <name>`; cached information is used instead. Use with caution. ++ +With `-c` option, all *local* branches that can be found via this +remote are removed. Useful for cleaning up old style remote branches, +or temporary branches. 'fetch'::
git-remote.perl +31 -2
@@ -210,7 +210,7 @@ sub show_mapping { } sub prune_remote { - my ($name, $ls_remote) = @_; + my ($name, $ls_remote, $clean_local) = @_; if (!exists $remote->{$name}) { print STDERR "No such remote $name\n"; return;
@@ -224,6 +224,32 @@ sub prune_remote { my @v = $git->command(qw(rev-parse --verify), "$prefix/$to_prune"); $git->command(qw(update-ref -d), "$prefix/$to_prune", $v[0]); } + if ( $clean_local ) { + my @refs = map { (split " ")[0] } + $git->command(qw(for-each-ref), $prefix); + print "Saw ".@refs." remote refs\n"; + my %r = map { ($_ => undef) } + $git->command("rev-list", @refs); + print "Saw ".(keys %r)." revs\n"; + my %l = map { (split " ")[2,0] } + $git->command(qw(for-each-ref refs/heads)); + print "Saw ".(keys %l)." local heads\n"; + + # don't delete the current branch, but there must be a + # better way to find it out + chomp(my ($checked_out) = map { /^\* (.*)/ } + $git->command("branch")); + $checked_out = "refs/heads/$checked_out" + if $checked_out; + + while ( my ($ref, $rev) = each %l ) { + next if $checked_out and $ref eq $checked_out; + if ( exists $r{$rev} ) { + print "$ref is obsolete\n"; + $git->command(qw(update-ref -d), $ref, $rev); + } + } + } } sub show_remote {
@@ -344,6 +370,9 @@ elsif ($ARGV[0] eq 'prune') { if ($ARGV[$i] eq '-n') { $ls_remote = 0; } + elsif ($ARGV[$i] eq '-c') { + $clean_local = 1; + } else { last; }
@@ -353,7 +382,7 @@ elsif ($ARGV[0] eq 'prune') { exit(1); } for (; $i < @ARGV; $i++) { - prune_remote($ARGV[$i], $ls_remote); + prune_remote($ARGV[$i], $ls_remote, $clean_local); } } elsif ($ARGV[0] eq 'add') {
Signature · 1 line
1.5.0.4.210.gf8a7c-dirty
One time again, this time with the call to porcelain command git-branch replaced with the plumbing command symbolic-ref HEAD
I also changed the output to be a little less "Got here\n" style.
Subject: [PATCH] git-remote: implement prune -c
It would be nice to prune local refs which are irrelevant; add an option to git-remote prune, with documentation.
--- Documentation/git-remote.txt | 4 ++++ git-remote.perl | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-)
Documentation/git-remote.txt +4 -0
@@ -63,6 +63,10 @@ referenced by <name>, but are still locally available in With `-n` option, the remote heads are not confirmed first with `git ls-remote <name>`; cached information is used instead. Use with caution. ++ +With `-c` option, all *local* branches that can be found via this +remote are removed. Useful for cleaning up old style remote branches, +or temporary branches. 'fetch'::
git-remote.perl +27 -2
@@ -210,7 +210,7 @@ sub show_mapping { } sub prune_remote { - my ($name, $ls_remote) = @_; + my ($name, $ls_remote, $clean_local) = @_; if (!exists $remote->{$name}) { print STDERR "No such remote $name\n"; return;
@@ -224,6 +224,28 @@ sub prune_remote { my @v = $git->command(qw(rev-parse --verify), "$prefix/$to_prune"); $git->command(qw(update-ref -d), "$prefix/$to_prune", $v[0]); } + if ( $clean_local ) { + my @refs = map { (split " ")[0] } + $git->command(qw(for-each-ref), $prefix); + print "fetching revs for ".@refs." remote refs\n"; + my %r = map { ($_ => undef) } + $git->command("rev-list", @refs); + print "fetched ".(keys %r)." revs\n"; + my %l = map { (split " ")[2,0] } + $git->command(qw(for-each-ref refs/heads)); + print "checking ".(keys %l)." local heads\n"; + + # don't delete the current branch + my ($checked_out) = $git->command(qw(symbolic-ref HEAD)); + + while ( my ($ref, $rev) = each %l ) { + next if $checked_out and $ref eq $checked_out; + if ( exists $r{$rev} ) { + print "$ref is obsolete\n"; + $git->command(qw(update-ref -d), $ref, $rev); + } + } + } } sub show_remote {
@@ -344,6 +366,9 @@ elsif ($ARGV[0] eq 'prune') { if ($ARGV[$i] eq '-n') { $ls_remote = 0; } + elsif ($ARGV[$i] eq '-c') { + $clean_local = 1; + } else { last; }
@@ -353,7 +378,7 @@ elsif ($ARGV[0] eq 'prune') { exit(1); } for (; $i < @ARGV; $i++) { - prune_remote($ARGV[$i], $ls_remote); + prune_remote($ARGV[$i], $ls_remote, $clean_local); } } elsif ($ARGV[0] eq 'add') {
Signature · 1 line
1.5.0.4.210.gf8a7c-dirty
Quoted 9 lines · Sam Vilain
One time again, this time with the call to porcelain command
git-branchreplaced with the plumbing commandsymbolic-ref HEADI also changed the output to be a little less "Got here\n" style.
Subject: [PATCH] git-remote: implement prune -c
It would be nice to prune local refs which are irrelevant; add an option to git-remote prune, with documentation.
I do not understand what workflow you are assuming, so your use of the word "irrelevant" does not mean much to me. I suspect other readers of the patch and documentation wouldn't find it clear in what situation this option is useful.
Perhaps you are thinking about this scenario? I am only guessing because you are not clear enough:
$ git clone
... time passes ...
$ git checkout -b next origin/next
... build, install, have fun ...
$ git checkout master
... time passes ...
$ git branch
... notice that you do not hack on your copy of 'next'
... and want to remove it
$ git remote prune -cIn any case, are you checking irrelevancy? What if your foo branch has more changes to be sent upstream? Even when the remote has a bit older version doesn't your code remove yours? For example, if you did this, instead of the above, what happens?
$ git clone
... time passes ...
$ git checkout -b next origin/next
... build, install, have fun ...
... find an opportunity to improve ...
$ edit
$ git commit ;# on your 'next'.
... build, install, test ...
$ git checkout master
... time passes ...
$ git branch
... notice that you do not hack on your copy of 'next' anymore,
... and want to remove it
$ git remote prune -cIf the above is the usage scenario you are trying to help, then wouldn't it be helpful if you could also help removing 'my-next' in this slightly altered example?
$ git clone
... time passes ...
$ git checkout -b my-next origin/next
... build, install, have fun ...
$ git checkout master
... time passes ...
$ git branch
... notice that you do not hack on your copy of 'next'
... which is 'my-next', and want to remove it
$ git remote prune -cQuoted 11 lines · Junio C Hamano
Quoted 5 lines
Subject: [PATCH] git-remote: implement prune -c
It would be nice to prune local refs which are irrelevant; add an option to git-remote prune, with documentation.
I do not understand what workflow you are assuming, so your use of the word "irrelevant" does not mean much to me. I suspect other readers of the patch and documentation wouldn't find it clear in what situation this option is useful.
Bad choice of words. What I mean is to delete all local refs which are already reachable by a remote ref on the given remote.
Quoted 14 lines
Perhaps you are thinking about this scenario? I am only guessing because you are not clear enough:
$ git clone ... time passes ... $ git checkout -b next origin/next ... build, install, have fun ... $ git checkout master ... time passes ... $ git branch ... notice that you do not hack on your copy of 'next' ... and want to remove it $ git remote prune -c
Yes, that's it. Or clean up the references you already pushed because they are no longer of interest.
Quoted 5 lines
In any case, are you checking irrelevancy? What if your foo branch has more changes to be sent upstream? Even when the remote has a bit older version doesn't your code remove yours? For example, if you did this, instead of the above, what happens?
Quoted 15 lines
$ git clone
... time passes ...
$ git checkout -b next origin/next
... build, install, have fun ...
... find an opportunity to improve ...
$ edit
$ git commit ;# on your 'next'.
... build, install, test ...
$ git checkout master
... time passes ...
$ git branch
... notice that you do not hack on your copy of 'next' anymore,
... and want to remove it
$ git remote prune -cIt doesn't do that because the head doesn't match any revision that was given to us by rev-list refs/remotes/foo/*
Quoted 4 lines
If the above is the usage scenario you are trying to help, then wouldn't it be helpful if you could also help removing 'my-next' in this slightly altered example?
Quoted 10 lines
$ git clone
... time passes ...
$ git checkout -b my-next origin/next
... build, install, have fun ...
$ git checkout master
... time passes ...
$ git branch
... notice that you do not hack on your copy of 'next'
... which is 'my-next', and want to remove it
$ git remote prune -cYes, the idea was to "sweep" all branches that were just local branches of a remote and never worked on. This is most useful right now for people switching from Cogito or old-style remotes, who have a lot of branches that are remote tracking branches. Using this, they can just set up a new remote, fetch and prune -c and be left in a tidy state.
Sam.
Quoted 2 lines · Sam Vilain
It doesn't do that because the head doesn't match any revision that was given to us by
rev-list refs/remotes/foo/*
Gaah. That goes all the way down to the root commit.
+ print "fetching revs for ".@refs." remote refs\n";
Is this meant to be final message to the end-user, or debug?
+ my %r = map { ($_ => undef) }
+ $git->command("rev-list", @refs);
This traverses all the way down to the root commit, doesn't it?
That is probably good enough for a toy repository for testing, but is impractical in real repositories, I am afraid. See below for the standard ways to check ancestry.
+ # don't delete the current branch
+ my ($checked_out) = $git->command(qw(symbolic-ref HEAD));
When the HEAD is detached, does the error message go directly to the end user?
+ while ( my ($ref, $rev) = each %l ) {
+ next if $checked_out and $ref eq $checked_out;
+ if ( exists $r{$rev} ) {
+ print "$ref is obsolete\n";
+ $git->command(qw(update-ref -d), $ref, $rev);
+ }
+ }
The standard way to check if commit A is included in (i.e. is an ancestor of) commit B, without traversing the ancestry chain of B all the way down to the root commit, is to run:
git merge-base --all A B
and see if A appears in its output (if so, then A is an ancestor of B, otherwise it is not). This is a pair-wise check, and for your purpose the check would become N*M operation (Yuck).
The same check can be done in parallel with:
git show-branch --independent A B C D...
whose output would include A if the commit is not included in any of the other commits B C D... This parallel traversal has a limit --- you can only check 25 branches at a time.
Quoted 15 lines
Quoted 4 lines
If the above is the usage scenario you are trying to help, then wouldn't it be helpful if you could also help removing 'my-next' in this slightly altered example?
Quoted 10 lines
$ git clone ... time passes ... $ git checkout -b my-next origin/next ... build, install, have fun ... $ git checkout master ... time passes ... $ git branch ... notice that you do not hack on your copy of 'next' ... which is 'my-next', and want to remove it $ git remote prune -c
Quoted 5 lines
Yes, the idea was to "sweep" all branches that were just local branches of a remote and never worked on. This is most useful right now for people switching from Cogito or old-style remotes, who have a lot of branches that are remote tracking branches. Using this, they can just set up a new remote, fetch and prune -c and be left in a tidy state.
You make it sound like this is just a one-shot conversion issue, in which case I really doubt we would want that. But it appears to be a useful feature in general, provided if the assumed use case is described clearly so that new users know when to use it. In the form that was given to me, I think the documentation leaves the user in a "Huh?" state.
Quoted 10 lines · Junio C Hamano
The standard way to check if commit A is included in (i.e. is an ancestor of) commit B, without traversing the ancestry chain of B all the way down to the root commit, is to run:
git merge-base --all A Band see if A appears in its output (if so, then A is an ancestor of B, otherwise it is not). This is a pair-wise check, and for your purpose the check would become N*M operation (Yuck).
Quoted 7 lines
The same check can be done in parallel with:
git show-branch --independent A B C D...
whose output would include A if the commit is not included in any of the other commits B C D... This parallel traversal has a limit --- you can only check 25 branches at a time.
Well, I was silly. If you want to see if A is an ancestor of any of B C D..., the standard and most efficient way to do so is with rev-list.
git rev-list A --not B C D...
will show _nothing_ only when A is an ancestor of one (or more) of B C D..., so you invoke it and upon getting the first line of output you declare A cannot be removed without reading the remainder of the output.
show-branch --independent is an overkill for your purpose as it does not treat A any more special from others (iow, it checks if B is contained in A C D..., C is contained in A B D... all in parallel), and you are not interested in finding out how remote refs are related with each other.
(unknown)
Quoted 10 lines · Junio C Hamano
If you want to see if A is an ancestor of
any of B C D..., the standard and most efficient way to do so is with rev-list.git rev-list A --not B C D...
will show _nothing_ only when A is an ancestor of one (or more) of B C D..., so you invoke it and upon getting the first line of output you declare A cannot be removed without reading the remainder of the output.
Sure. I figured the commands I put would only use about 30MB for a 300,000 commit repository, so it wouldn't be too bad. But I didn't think of that.
Here we go, also based upon 'next' rather than the result of my previous patches of the day.
Subject: [PATCH] git-remote: implement prune -c
It would be nice to be able to prune local refs which just point to some place in the remote; add an option to git-remote prune, with documentation.
--- Documentation/git-remote.txt | 5 +++++ git-remote.perl | 25 +++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-)
Documentation/git-remote.txt +5 -0
@@ -54,6 +54,11 @@ Gives some information about the remote <name>. Deletes all stale tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in "remotes/<name>". ++ +With `-c` option, all *local* branches that exist in the history of +any of the mirrored heads from the remote are removed. This is useful +for discarding temporary local branches that you did not make any +commits to. DISCUSSION
git-remote.perl +23 -2
@@ -210,7 +210,7 @@ sub show_mapping { } sub prune_remote { - my ($name, $ls_remote) = @_; + my ($name, $ls_remote, $clean_local) = @_; if (!exists $remote->{$name}) { print STDERR "No such remote $name\n"; return;
@@ -224,6 +224,24 @@ sub prune_remote { my @v = $git->command(qw(rev-parse --verify), "$prefix/$to_prune"); $git->command(qw(update-ref -d), "$prefix/$to_prune", $v[0]); } + if ( $clean_local ) { + my @remote = map { (split " ")[0] } + $git->command(qw(for-each-ref), $prefix); + my %local = map { (split " ")[2,0] } + $git->command(qw(for-each-ref refs/heads)); + my %not_remote = map { ($_ => undef) } + $git->command("rev-list", keys %local, "--not", @remote); + # don't delete the current branch + my ($checked_out) = $git->command(qw(symbolic-ref HEAD)); + + while ( my ($ref, $rev) = each %local ) { + next if $checked_out and $ref eq $checked_out; + if ( not exists $not_remote{$rev} ) { + print "$ref is obsolete\n"; + $git->command(qw(update-ref -d), $ref, $rev); + } + } + } } sub show_remote {
@@ -310,6 +328,9 @@ elsif ($ARGV[0] eq 'prune') { if ($ARGV[$i] eq '-n') { $ls_remote = 0; } + elsif ($ARGV[$i] eq '-c') { + $clean_local = 1; + } else { last; }
@@ -319,7 +340,7 @@ elsif ($ARGV[0] eq 'prune') { exit(1); } for (; $i < @ARGV; $i++) { - prune_remote($ARGV[$i], $ls_remote); + prune_remote($ARGV[$i], $ls_remote, $clean_local); } } elsif ($ARGV[0] eq 'add') {
Signature · 1 line
1.5.0.2.214.gb318