#!/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", 'reject' => "\e[31m", 'unreject' => "\e[31m", 'merge' => "\e[32m", 'base' => "\e[1;34m", 'previous' => "\e[34m", ); my %html_style = ( '' => "color: black; background-color: black", 'outstanding' => "color: black; background-color: yellow", 'unmerge' => "color: black; background-color: lightred", 'reject' => "color: black; background-color: red", 'unreject' => "color: black; background-color: red", 'merge' => "color: black; background-color: green", 'base' => "color: black; background-color: lightblue", 'previous' => "color: black; background-color: blue", ); my %name = ( 'outstanding' => "OUTSTANDING", 'unmerge' => "UNMERGED", 'reject' => "REJECTED", 'unreject' => "UNREJECTED", '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); my $branch = $ENV{GIT_BRANCH}; if(not $branch) { chomp($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, $branch); $revprefix .= "^.."; } elsif($datefilter ne '') { @datefilter = "--since=$datefilter"; } # if set, don't actually merge/revert changes, just mark as such my $skip = 0; 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 reject_commit($) { # reject == merge but skip 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 "REJECT! $cmsg" . "::stable-branch::reject=$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; if($do_commit) { run 'git', 'commit', '--allow-empty', '-F', '.commitmsg' or die "git-commit: $!"; } } sub unreject_commit($) { # reject == merge but skip 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 "UNREJECT! $cmsg" . "::stable-branch::unreject=$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; if($do_commit) { run 'git', 'commit', '--allow-empty', '-F', '.commitmsg' 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; if(!$skip) { run 'git', 'cherry-pick', '-n', $r or run 'git', 'mergetool' or die "git-mergetool: $!"; } if($do_commit) { run 'git', 'commit', '--allow-empty', '-F', '.commitmsg' or (run 'git', 'mergetool' and run 'git', 'commit', '--allow-empty', '-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: $!"; 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 "UNMERGE! $cmsg" . "::stable-branch::unmerge=$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; if(!$skip) { run 'git', 'revert', '-n', $r or run 'git', 'mergetool' or die "git-mergetool: $!"; } if($do_commit) { run 'git', 'commit', '--allow-empty', '-F', '.commitmsg' or (run 'git', 'mergetool' and run 'git', 'commit', '--allow-empty', '-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}[$_] < 0) { unshift @rlog, ['reject', $log->{order_a}[$_]]; } elsif($log->{bitmap}[$_] == 0) { unshift @rlog, ['unmerge', $log->{order_a}[$_]]; } } for($newbase_id+1 .. @{$log->{order_a}}-1) { if($log->{bitmap}[$_] > 0) { push @rlog, ['merge', $log->{order_a}[$_]]; } elsif($log->{bitmap}[$_] < 0) { push @rlog, ['reject', $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$branch"), 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+)/) { next if not defined $history{$1}; push @logdata, ['unmerge', $1]; } elsif($data =~ /::stable-branch::merge=(\S+)/) { next if not defined $history{$1}; push @logdata, ['merge', $1]; } elsif($data =~ /::stable-branch::reject=(\S+)/) { next if not defined $history{$1}; push @logdata, ['reject', $1]; } elsif($data =~ /::stable-branch::unreject=(\S+)/) { next if not defined $history{$1}; push @logdata, ['unreject', $1]; } elsif($data =~ /::stable-branch::reset=(\S+)/) { next if not defined $history{$1}; @logdata = (); $base = $1; } elsif($data =~ /::stable-branch::rebase=(\S+)/) { next if not defined $history{$1}; $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 'reject') { $bitmap[$history{$data}] = -1; } elsif($cmd eq 'unreject') { $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 $l->{bitmap}[$l->{order_h}{$r}] == 0; 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}] > 0; die "PEBKAC: not initialized" unless defined $l->{base}; } unmerge_commit $r; } elsif($cmd eq 'reject') { 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}] == 0; die "PEBKAC: not initialized" unless defined $l->{base}; } reject_commit $r; } elsif($cmd eq 'unreject') { 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}] < 0; die "PEBKAC: not initialized" unless defined $l->{base}; } unreject_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, $branch 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; eval { if($cmd eq 'rebase') { local $do_commit = 0; reset_to_commit $r; run_script @{$l->{log}}; run 'git', 'commit', '--allow-empty', '-m', "::stable-branch::rebase=$r" or die "git-commit: $!"; } elsif($cmd eq 'resetrebase') { run_script ['reset', $r], @{$l->{log}}; } elsif($cmd eq 'hardresetrebase') { run_script ['hardreset', $r], @{$l->{log}}; } 1; } or do { my $err = $@; run 'git', 'reset', '--hard', $commit_id or die "$err, and then git-reset failed: $!"; die $err; }; } sub escapeHTML { my ($toencode,$newlinestoo) = @_; return undef unless defined($toencode); $toencode =~ s{&}{&}gso; $toencode =~ s{<}{<}gso; $toencode =~ s{>}{>}gso; $toencode =~ s{"}{"}gso; return $toencode; } my $histsize = 20; my $cgi_url = undef; 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); } if(defined $cgi_url) { print "Content-Type: text/html\n\n\n"; for(@l) { my ($action, $r) = @$_; my $m = $l->{logmsg}->{$r}; my $m_short = join ' ', map { s/^ (?!git-svn-id)(.)/$1/ ? $_ : () } split /\n/, $m; printf "\n", $html_style{$action}, $name{$action}, escapeHTML($cgi_url), escapeHTML($r), escapeHTML($r), escapeHTML($m_short); } print "
%s%s%s
\n"; } else { 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 <(@_); 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, "resetrebase|B=s", handler \&opt_rebase, "hardresetrebase=s", handler \&opt_rebase, "skip", handler \$skip, "merge|m=s{,}", handler sub { run_script ['merge', $_[1]]; }, "unmerge|u=s{,}", handler sub { run_script ['unmerge', $_[1]]; }, "reject|r=s{,}", handler sub { run_script ['reject', $_[1]]; }, "unreject|U=s{,}", handler sub { run_script ['unreject', $_[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, "cgi=s", \$cgi_url ); if(!$done) { opt_list("outstanding", ""); } $pebkac = 0;