Re: best way to fastforward all tracking branches after a fetch
- From
Sitaram Chamarty <sitaramc@gmail.com>
- Date
- Dec 17, 2011, 10:11 UTC
- Message-ID
- <20111217101106.GB19248@sita-lt.atc.tcs.com>
- In-Reply-To
- <20111217101009.GA19248@sita-lt.atc.tcs.com>
oops; forgot the program...
On Sat, Dec 17, 2011 at 03:40:09PM +0530, Sitaram Chamarty wrote:
Show 62 quoted lines
> On Sat, Dec 10, 2011 at 01:26:32PM +0100, Gelonida N wrote: > > Hi, > > > > What is the best way to fastforward all fastforwardable tracking > > branches after a git fetch? > > I know this is a somewhat closed topic, but I took some time to > clean up a program I have been using for a while, including some > changes based upon ideas elsewhere in this thread. The program > "git-branch-check" is attached, and requires perl > 5.10.0. > > Note that this does a lot more than just fast-forward all > branches, although it can do that as well. > > I alias it (in ~/.gitconfig) to 'bc', so I just run "git bc". > Running with "-h" shows usage: > > Usage: /home/sitaram/bin/git-branch-check [options] [branches] > > Check or fast forward branches. Default: act upon all local branches if no > arguments supplied, or just the current branch if '-c' is passed. > -c act upon current branch only > -ff don't just check, try to fast forward also > -md max diff (default 100; see below for details) > -h help > 'max diff': > hide output for two branches different by more than so many commits > > My usual usage is just "git bc -c", which may give me: > > 1 pu...origin/pu > 1 pu...github/pu > 13 pu...master > 5 pu...q > 7 pu...vrs > > This quickly tells me my 'pu' is one ahead of both my own > gitolite server as well as github's copy, and that it is 13 > commits ahead of master. The (unreleased and frequently > rebased) feature branches 'q' and 'vrs' are ahead of pu, which > means a rebase is not pending. Without the "-c" I may see the > status of master versus its own upstream and other remotes, > etc., also. > > The purpose of the max diff limit (default 100) is to hide, for > example, the pair 'master' and 'man' from the git.git repo. > Otherwise you'd see something like: > > 27249 973 master...man > > which is pretty meaningless. The sum of those two numbers > should be less than the max. > > "git bc -ff" will attempt to fast forward all selected branches > that are ancestors of their respective upstreams. The current > branch will not be ff-ed if the tree is dirty, since you can't > do this by 'git branch -f'; it has to be an actual merge > command. > > The output is not (currently) pipable to other programs because > I use colors (obtained from 'git config --get-color') and > currently it is not conditional on STDOUT being a tty.
#!/usr/bin/perl -s use 5.10.0; use strict; use warnings;
# ----------------------------------------------------------------------
# bare-minimum subset of 'Tsh' (see github.com/sitaramc/tsh)
{
my($rc, $text);
sub rc { return $rc || 0; }
sub text { return $text || ''; }
sub lines { return split /\n/, $text; }
sub try {
my $cmd = shift; die "try: expects only one argument" if @_;
$text = `( $cmd ) 2>&1; echo -n RC=\$?`;
if ($text =~ s/RC=(\d+)$//) {
$rc = $1;
return (not $rc);
}
die "couldnt find RC= in result; this should not happen:\n$text\n\n...\n";
}
}# ----------------------------------------------------------------------
# options; the "-s" above sets one or more of these
# (the format of the lines below is special; it is used by usage() to generate
# help text for the options)
# BEGIN OPTIONS
our $c; # act upon current branch only
our $ff; # don't just check, try to fast forward also
our $md; # max diff (default 100; see below for details)
our $h; # help
# END OPTIONS
$md ||= 100;# get this over with; usage() exits so don't worry
usage() if $h;# get current branch
my $current = '';
try "git symbolic-ref HEAD" or die "DETACHED HEAD or no repo";
($current = (lines)[0]) =~ s(refs/heads/)();# get branch names
# first, all local branches as keys of a hash with upstream name if any, as the value
my %upstream;
try "git for-each-ref --perl '--format=\$upstream{%(refname:short)} = %(upstream:short);' refs/heads"
or die "for-each-ref 1 failed";
eval text; # local branches as a list; keep $current at the top, and the rest sorted
my @local = ($current, grep { $_ ne $current } sort keys %upstream); # remote branches as a list
try "git for-each-ref '--format=%(refname:short)' refs/remotes"
or die "for-each-ref 2 failed";
my @remote = lines;# decide what branches to act upon. Default: all local branches. If any
# arguments are given, then those. If '-c' is passed, only current branch.
my @branches = @local;
@branches = @ARGV if @ARGV;
@branches = ($current) if $c;# ----------------------------------------------------------------------
# show the tree state if it's dirty
print "dirty:\n", text if dirty();# process selected branches
for my $b (@branches) {
# attempt a fast-forward if -ff is passed
ff($b, $upstream{$b}, $current) if ($ff);
# check against its own upstream
check($b, $upstream{$b});
# then against all remote branches of the same name (I typically have
# my own gitolite server as 'upstream' but also have github and google
# code as additional remotes that I push my branches to)
check($b, grep(m(^[^/]+/$b$), @remote));
}
# ...then against all local branches. We do this in a separate loop
# so their output is kept separate from the remote compares above.
for my $b (@branches) {
check($b, @local);
}# DONE...
# ---------------------------------------------------------------------- # subroutines # ----------------------------------------------------------------------
sub ff {
# b=branch, u=upstream, c=current
my ($b, $u, $c) = @_; unless ($u) {
say "$b does not have an upstream";
return;
} if ($b eq $c and dirty()) {
say "working tree is dirty; skipping ff for (current branch) $b";
return;
} # $l = number of commits "l"eft side has over the "r"ight (similarly $r...)
my($l, $r) = compare($b, $u);
if ($r and not $l) {
# there is something to update, and ff is possible
if ($b eq $c) {
# current branch; needs an actual merge
try("git merge --ff-only $u") or die "$b: 'git merge --ff-only $u' failed:\n" . text;
} else {
# other branches can be forced
try("git branch -f $b $u") or die "$b: 'git branch -f $b $u' failed:\n" . text;
}
}
}sub check {
my ($b, @list) = @_;
state %seen; for my $u (@list) {
next unless $u;
next if $b eq $u or $seen{$b}{$u};
# seeing a...b is as good as seeing b...a also
$seen{$b}{$u} = 1;
$seen{$u}{$b} = 1;my ($l, $r) = compare($b, $u);
my $abs = $l + $r; next unless $abs; # if they're equal, don't show it
next if $abs >= $md; # if they're too far apart, don't show it print spacepad(4, $l) . color('green') . ($l || ' ');
print spacepad(4, $r) . color('red') . ($r || ' ');
say color('reset') . " $b...$u";
}
}sub compare {
my ($b, $u) = @_; try("git rev-list $u..$b") or die "'git rev-list $u..$b' failed:\n" . text;
my $l = lines; try("git rev-list $b..$u") or die "'git rev-list $b..$u' failed:\n" . text;
my $r = lines;return($l, $r); }
sub dirty {
try "git status -s -uno | cut -c1-2 | sort | uniq -c; /./";
}sub color {
my $color = shift;
return `git config --get-color "" $color`;
}sub spacepad {
return " " x ($_[0] - length($_[1]));
}sub usage {
print "
Usage: $0 [options] [branches]Check or fast forward branches. Default: act upon all local branches if no
arguments supplied, or just the current branch if '-c' is passed.
";
@ARGV=($0);
for ( grep { /BEGIN OPTIONS/../END OPTIONS/ and not /OPTION/ } <> ) {
s/our \$/\t-/;
s/; *#/\t/;
print;
}
say "\'max diff':\n hide output for two branches different by more than so many commits";
exit 1;
}