git/list[1] front-page[2] threads[3] people[4] search[5] about
 

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;
}
Previous: Sitaram ChamartyNext: Nazri Ramliy
Message 24 of 27 in “best way to fastforward all tracking branches after a fetch”
  1. Gelonida NDec 10, 2011
  2. Sitaram ChamartyDec 11, 2011
  3. Gelonida NDec 11, 2011
  4. Jakub NarebskiDec 11, 2011
  5. Sitaram ChamartyDec 11, 2011
  6. Andreas SchwabDec 11, 2011
  7. Jakub NarebskiDec 11, 2011
  8. Gelonida NDec 11, 2011
  9. Andreas SchwabDec 11, 2011
  10. Martin LanghoffDec 11, 2011
  11. Stefan HallerDec 11, 2011
  12. Gelonida NDec 11, 2011
  13. Martin LanghoffDec 11, 2011
  14. Hallvard B FurusethDec 11, 2011
  15. Stefan HallerDec 12, 2011
  16. Jeff KingDec 12, 2011
  17. Stefan HallerDec 12, 2011
  18. Hallvard Breien FurusethDec 13, 2011
  19. Junio C HamanoDec 12, 2011
  20. Junio C HamanoDec 12, 2011
  21. Gelonida NDec 12, 2011
  22. Gelonida NDec 12, 2011
  23. Sitaram ChamartyDec 17, 2011
  24. Sitaram ChamartyDec 17, 2011
  25. Nazri RamliyDec 19, 2011
  26. Sitaram ChamartyJan 18, 2012
  27. Sitaram ChamartyJan 18, 2012

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.