--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Getopt::Long qw/:config no_ignore_case no_auto_abbrev gnu_compat/;
+
+my %color =
+(
+ '' => "\e[m",
+ 'outstanding' => "\e[1;33m",
+ 'unmerge' => "\e[1;31m",
+ 'merge' => "\e[32m",
+ 'base' => "\e[1;34m",
+ 'previous' => "\e[34m",
+);
+
+my %name =
+(
+ 'outstanding' => "OUTSTANDING",
+ 'unmerge' => "UNMERGED",
+ 'merge' => "MERGED",
+ 'base' => "BASE",
+ 'previous' => "PREVIOUS",
+);
+
+sub check_defined($$)
+{
+ my ($msg, $data) = @_;
+ return $data if defined $data;
+ die $msg;
+}
+
+sub backtick(@)
+{
+ open my $fh, '-|', @_
+ or return undef;
+ undef local $/;
+ my $s = <$fh>;
+ close $fh
+ or return undef;
+ return $s;
+}
+
+sub run(@)
+{
+ return !system @_;
+}
+
+my $width = ($ENV{COLUMNS} || backtick 'tput', 'cols' || 80);
+chomp(my $branch = backtick 'git', 'symbolic-ref', 'HEAD');
+ $branch =~ s/^refs\/heads\///
+ or die "Not in a branch";
+chomp(my $master = (backtick 'git', 'config', '--get', "branch-manager.$branch.master" or 'master'));
+chomp(my $datefilter = (backtick 'git', 'config', '--get', "branch-manager.$branch.startdate" or ''));
+my @datefilter = ();
+my $revprefix = "";
+if($datefilter eq 'mergebase')
+{
+ chomp($revprefix = check_defined "git-merge-base: $!", backtick 'git', 'merge-base', $master, "HEAD");
+ $revprefix .= "^..";
+}
+elsif($datefilter ne '')
+{
+ @datefilter = "--since=$datefilter";
+}
+
+our $do_commit = 1;
+my $logcache = undef;
+sub reset_to_commit($)
+{
+ my ($r) = @_;
+ #run 'git', 'merge', '-s', 'ours', '--no-commit', $r
+ # or die "git-merge: $!";
+ run 'git', 'checkout', $r, '--', '.'
+ or die "git-checkout: $!";
+ if($do_commit)
+ {
+ $logcache = undef;
+ run 'git', 'update-ref', 'MERGE_HEAD', $r
+ or die "git-update-ref: $!";
+ run 'git', 'commit', '--allow-empty', '-m', "::stable-branch::reset=$r"
+ or die "git-commit: $!";
+ }
+}
+
+sub merge_commit($)
+{
+ my ($r) = @_;
+ my $cmsg = "";
+ my $author = "";
+ my $email = "";
+ my $date = "";
+ if($do_commit)
+ {
+ $logcache = undef;
+ my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', $r
+ or die "git-log: $!";
+ for(split /\n/, $msg)
+ {
+ if(/^Author:\s*(.*) <(.*)>/)
+ {
+ $author = $1;
+ $email = $2;
+ }
+ elsif(/^AuthorDate:\s*(.*)/)
+ {
+ $date = $1;
+ }
+ elsif(/^ (.*)/)
+ {
+ $cmsg .= "$1\n";
+ }
+ }
+ open my $fh, '>', '.commitmsg'
+ or die ">.commitmsg: $!";
+ print $fh "$cmsg" . "::stable-branch::merge=$r\n"
+ or die ">.commitmsg: $!";
+ close $fh
+ or die ">.commitmsg: $!";
+ }
+ local $ENV{GIT_AUTHOR_NAME} = $author;
+ local $ENV{GIT_AUTHOR_EMAIL} = $email;
+ local $ENV{GIT_AUTHOR_DATE} = $date;
+ run 'git', 'cherry-pick', '-n', $r
+ or run 'git', 'mergetool'
+ or die "git-mergetool: $!";
+ if($do_commit)
+ {
+ run 'git', 'commit', '-F', '.commitmsg'
+ or die "git-commit: $!";
+ }
+}
+
+sub unmerge_commit($)
+{
+ my ($r) = @_;
+ my $cmsg = "";
+ my $author = "";
+ my $email = "";
+ my $date = "";
+ if($do_commit)
+ {
+ $logcache = undef;
+ my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', $r
+ or die "git-log: $!";
+ my $cmsg = "";
+ my $author = "";
+ my $email = "";
+ my $date = "";
+ for(split /\n/, $msg)
+ {
+ if(/^Author:\s*(.*)/)
+ {
+ $author = $1;
+ }
+ elsif(/^AuthorDate:\s*(.*)/)
+ {
+ $date = $1;
+ }
+ elsif(/^ (.*)/)
+ {
+ $cmsg .= "$1\n";
+ }
+ }
+ open my $fh, '>', '.commitmsg'
+ or die ">.commitmsg: $!";
+ print $fh "UNMERGE\n$cmsg" . "::stable-branch::merge=$r\n"
+ or die ">.commitmsg: $!";
+ close $fh
+ or die ">.commitmsg: $!";
+ }
+ local $ENV{GIT_AUTHOR_NAME} = $author;
+ local $ENV{GIT_AUTHOR_EMAIL} = $email;
+ local $ENV{GIT_AUTHOR_DATE} = $date;
+ run 'git', 'revert', '-n', $r
+ or run 'git', 'mergetool'
+ or die "git-mergetool: $!";
+ if($do_commit)
+ {
+ run 'git', 'commit', '-F', '.commitmsg'
+ or die "git-commit: $!";
+ }
+}
+
+sub rebase_log($$)
+{
+ my ($r, $log) = @_;
+
+ my @applied = (0) x @{$log->{order_a}};
+ my $newbase_id = $log->{order_h}{$r};
+
+ my @rlog = ();
+ my @outstanding = ();
+
+ for(0..$newbase_id)
+ {
+ if(!$log->{bitmap}[$_])
+ {
+ unshift @rlog, ['unmerge', $log->{order_a}[$_]];
+ }
+ }
+
+ for($newbase_id+1 .. @{$log->{order_a}}-1)
+ {
+ if($log->{bitmap}[$_])
+ {
+ push @rlog, ['merge', $log->{order_a}[$_]];
+ }
+ else
+ {
+ push @outstanding, ['outstanding', $log->{order_a}[$_]];
+ }
+ }
+
+ return
+ {
+ %$log,
+ base => $r,
+ log => [
+ @rlog,
+ @outstanding
+ ]
+ };
+}
+
+sub parse_log()
+{
+ return $logcache if defined $logcache;
+
+ my $base = undef;
+ my @logdata = ();
+
+ my %history = ();
+ my %logmsg = ();
+ my @history = ();
+
+ my %applied = ();
+ my %unapplied = ();
+
+ my $cur_commit = undef;
+ my $cur_msg = undef;
+ for((split /\n/, check_defined "git-log: $!", backtick 'git', 'log', '--topo-order', '--reverse', '--pretty=fuller', @datefilter, "$revprefix$master"), undef)
+ {
+ if(defined $cur_commit and (not defined $_ or /^commit (\S+)/))
+ {
+ $cur_msg =~ s/\s+$//s;
+ $history{$cur_commit} = scalar @history;
+ $logmsg{$cur_commit} = $cur_msg;
+ push @history, $cur_commit;
+ $cur_commit = $cur_msg = undef;
+ }
+ last if not defined $_;
+ if(/^commit (\S+)/)
+ {
+ $cur_commit = $1;
+ }
+ else
+ {
+ $cur_msg .= "$_\n";
+ }
+ }
+ $cur_commit = $cur_msg = undef;
+ my @commits = ();
+ for((split /\n/, check_defined "git-log: $!", backtick 'git', 'log', '--topo-order', '--reverse', '--pretty=fuller', @datefilter, "$revprefix"."HEAD"), undef)
+ {
+ if(defined $cur_commit and (not defined $_ or /^commit (\S+)/))
+ {
+ $cur_msg =~ s/\s+$//s;
+ $logmsg{$cur_commit} = $cur_msg;
+ push @commits, $cur_commit;
+ $cur_commit = $cur_msg = undef;
+ }
+ last if not defined $_;
+ if(/^commit (\S+)/)
+ {
+ $cur_commit = $1;
+ }
+ else
+ {
+ $cur_msg .= "$_\n";
+ }
+ }
+ my $lastrebase = undef;
+ for(@commits)
+ {
+ my $data = $logmsg{$_};
+ if($data =~ /::stable-branch::unmerge=(\S+)/)
+ {
+ push @logdata, ['unmerge', $1];
+ }
+ elsif($data =~ /::stable-branch::merge=(\S+)/)
+ {
+ push @logdata, ['merge', $1];
+ }
+ elsif($data =~ /::stable-branch::reset=(\S+)/)
+ {
+ @logdata = ();
+ $base = $1;
+ }
+ elsif($data =~ /::stable-branch::rebase=(\S+)/)
+ {
+ $lastrebase->[0] = 'ignore'
+ if defined $lastrebase;
+ push @logdata, ($lastrebase = ['rebase', $1]);
+ }
+ }
+
+ if(not defined $base)
+ {
+ warn 'This branch is not yet managed by git-branch-manager';
+ return
+ {
+ logmsg => \%logmsg,
+ order_a => \@history,
+ order_h => \%history,
+ };
+ }
+ else
+ {
+ my $baseid = $history{$base};
+ my @bitmap = map
+ {
+ $_ <= $baseid
+ }
+ 0..@history-1;
+ my $i = 0;
+ while($i < @logdata)
+ {
+ my ($cmd, $data) = @{$logdata[$i]};
+ if($cmd eq 'merge')
+ {
+ $bitmap[$history{$data}] = 1;
+ }
+ elsif($cmd eq 'unmerge')
+ {
+ $bitmap[$history{$data}] = 0;
+ }
+ elsif($cmd eq 'rebase')
+ {
+ # the bitmap is fine, but generate a new log from the bitmap
+ my $pseudolog =
+ {
+ order_a => \@history,
+ order_h => \%history,
+ bitmap => \@bitmap,
+ };
+ my $rebasedlog = rebase_log $data, $pseudolog;
+ my @l = grep { $_->[0] ne 'outstanding' } @{$rebasedlog->{log}};
+ splice @logdata, 0, $i+1, @l;
+ $i = @l-1;
+ $base = $data;
+ $baseid = $history{$base};
+ }
+ ++$i;
+ }
+
+ my @outstanding = ();
+ for($baseid+1 .. @history-1)
+ {
+ push @outstanding, ['outstanding', $history[$_]]
+ unless $bitmap[$_];
+ }
+
+ $logcache =
+ {
+ logmsg => \%logmsg,
+ order_a => \@history,
+ order_h => \%history,
+
+ bitmap => \@bitmap,
+ base => $base,
+ log => [
+ @logdata,
+ @outstanding
+ ]
+ };
+ return $logcache;
+ }
+}
+
+our $pebkac = 0;
+our $done = 0;
+
+sub run_script(@);
+sub run_script(@)
+{
+ ++$done;
+ my (@commands) = @_;
+ for(@commands)
+ {
+ my ($cmd, $r) = @$_;
+ if($pebkac)
+ {
+ $r = backtick 'git', 'rev-parse', $r
+ or die "git-rev-parse: $!"
+ if defined $r;
+ chomp $r
+ if defined $r;
+ }
+ print "Executing: $cmd $r\n";
+ if($cmd eq 'reset')
+ {
+ if($pebkac)
+ {
+ my $l = parse_log();
+ die "PEBKAC: invalid revision number, cannot reset"
+ unless defined $l->{order_h}{$r};
+ }
+ reset_to_commit $r;
+ }
+ elsif($cmd eq 'hardreset')
+ {
+ if($pebkac)
+ {
+ my $l = parse_log();
+ die "PEBKAC: invalid revision number, cannot reset"
+ unless defined $l->{order_h}{$r};
+ }
+ run 'git', 'reset', '--hard', $r
+ or die "git-reset: $!";
+ reset_to_commit $r;
+ }
+ elsif($cmd eq 'merge')
+ {
+ if($pebkac)
+ {
+ my $l = parse_log();
+ die "PEBKAC: invalid revision number, cannot reset"
+ unless defined $l->{order_h}{$r} and not $l->{bitmap}[$l->{order_h}{$r}];
+ die "PEBKAC: not initialized"
+ unless defined $l->{base};
+ }
+ merge_commit $r;
+ }
+ elsif($cmd eq 'unmerge')
+ {
+ if($pebkac)
+ {
+ my $l = parse_log();
+ die "PEBKAC: invalid revision number, cannot reset"
+ unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}];
+ die "PEBKAC: not initialized"
+ unless defined $l->{base};
+ }
+ unmerge_commit $r;
+ }
+ elsif($cmd eq 'outstanding')
+ {
+ }
+ else
+ {
+ die "Invalid command: $cmd $r";
+ }
+ }
+}
+
+sub opt_rebase($$)
+{
+ ++$done;
+ my ($cmd, $r) = @_;
+ if($pebkac)
+ {
+ $r = backtick 'git', 'rev-parse', $r
+ or die "git-rev-parse: $!"
+ if defined $r;
+ chomp $r
+ if defined $r;
+ my $l = parse_log();
+ die "PEBKAC: invalid revision number, cannot reset"
+ unless defined $l->{order_h}{$r};
+ die "PEBKAC: not initialized"
+ unless defined $l->{base};
+ }
+ my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', @datefilter, 'HEAD'
+ or die "git-log: $!";
+ $msg =~ /^commit (\S+)/s
+ or die "Invalid git log output";
+ my $commit_id = $1;
+ my $l = rebase_log $r, parse_log();
+ local $pebkac = 0;
+ local $do_commit = 0;
+ eval
+ {
+ reset_to_commit $r;
+ run_script @{$l->{log}};
+ run 'git', 'commit', '--allow-empty', '-m', "::stable-branch::rebase=$r"
+ or die "git-commit: $!";
+ 1;
+ }
+ or do
+ {
+ my $err = $@;
+ run 'git', 'reset', '--hard', $commit_id
+ or die "$err, and then git-reset failed: $!";
+ die $err;
+ };
+}
+
+my $histsize = 20;
+sub opt_list($$)
+{
+ ++$done;
+ my ($cmd, $r) = @_;
+ $r = undef if $r eq '';
+ if($pebkac)
+ {
+ ($r = backtick 'git', 'rev-parse', $r
+ or die "git-rev-parse: $!")
+ if defined $r;
+ chomp $r
+ if defined $r;
+ my $l = parse_log();
+ die "PEBKAC: invalid revision number, cannot reset"
+ unless !defined $r or defined $l->{order_h}{$r};
+ die "PEBKAC: not initialized"
+ unless defined $l->{base};
+ }
+ my $l = parse_log();
+ $l = rebase_log $r, $l
+ if defined $r;
+ my $last = $l->{order_h}{$l->{base}};
+ my $first = $last - $histsize;
+ $first = 0
+ if $first < 0;
+ my %seen = ();
+ for(@{$l->{log}})
+ {
+ ++$seen{$_->[1]};
+ }
+ my @l = (
+ (map { $seen{$l->{order_a}[$_]} ? () : ['previous', $l->{order_a}[$_]] } $first..($last-1)),
+ ['base', $l->{base}],
+ @{$l->{log}}
+ );
+ if($cmd eq 'chronology')
+ {
+ @l = map { [$_->[1], $_->[2]] } sort { $l->{order_h}{$a->[2]} <=> $l->{order_h}{$b->[2]} or $a->[0] <=> $b->[0] } map { [$_, $l[$_]->[0], $l[$_]->[1]] } 0..(@l-1);
+ }
+ elsif($cmd eq 'outstanding')
+ {
+ my %seen = ();
+ @l = reverse grep { !$seen{$_->[1]}++ && !$l->{bitmap}->[$l->{order_h}->{$_->[1]}] } reverse map { [$_->[1], $_->[2]] } sort { $l->{order_h}{$a->[2]} <=> $l->{order_h}{$b->[2]} or $a->[0] <=> $b->[0] } map { [$_, $l[$_]->[0], $l[$_]->[1]] } 0..(@l-1);
+ }
+ for(@l)
+ {
+ my ($action, $r) = @$_;
+ my $m = $l->{logmsg}->{$r};
+ my $m_short = join ' ', map { s/^ (?!git-svn-id)(.)/$1/ ? $_ : () } split /\n/, $m;
+ $m_short = substr $m_short, 0, $width - 11 - 1 - 40 - 1;
+ printf "%s%-11s%s %s %s\n", $color{$action}, $name{$action}, $color{''}, $r, $m_short;
+ }
+}
+
+sub opt_help($$)
+{
+ my ($cmd, $one) = @_;
+ print STDERR <<EOF;
+Usage:
+ $0 [{--histsize|-s} n] {--chronology|-c}
+ $0 [{--histsize|-s} n] {--chronology|-c} revision-hash
+ $0 [{--histsize|-s} n] {--log|-l}
+ $0 [{--histsize|-s} n] {--log|-l} revision-hash
+ $0 {--merge|-m} revision-hash
+ $0 {--unmerge|-u} revision-hash
+ $0 {--reset|-R} revision-hash
+ $0 {--hardreset|-H} revision-hash
+ $0 {--rebase|-b} revision-hash
+EOF
+ exit 1;
+}
+
+sub handler($)
+{
+ my ($sub) = @_;
+ return sub
+ {
+ my $r;
+ eval
+ {
+ $r = $sub->(@_);
+ 1;
+ }
+ or do
+ {
+ warn "$@";
+ exit 1;
+ };
+ return $r;
+ };
+}
+
+$pebkac = 1;
+my $result = GetOptions(
+ "chronology|c:s", handler \&opt_list,
+ "log|l:s", handler \&opt_list,
+ "outstanding|o:s", handler \&opt_list,
+ "rebase|b=s", handler \&opt_rebase,
+ "merge|m=s{,}", handler sub { run_script ['merge', $_[1]]; },
+ "unmerge|u=s{,}", handler sub { run_script ['unmerge', $_[1]]; },
+ "reset|R=s", handler sub { run_script ['reset', $_[1]]; },
+ "hardreset|H=s", handler sub { run_script ['hardreset', $_[1]]; },
+ "help|h", handler \&opt_help,
+ "histsize|s=i", \$histsize
+);
+if(!$done)
+{
+ opt_list("outstanding", "");
+}
+$pebkac = 0;