oops, it's index, not work tree, here
[xonotic/div0-gittools.git] / git-branch-manager
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use Getopt::Long qw/:config no_ignore_case no_auto_abbrev gnu_compat/;
6
7 my %color =
8 (
9         '' => "\e[m",
10         'outstanding' => "\e[1;33m",
11         'unmerge' => "\e[1;31m",
12         'reject' => "\e[31m",
13         'unreject' => "\e[31m",
14         'merge' => "\e[32m",
15         'base' => "\e[1;34m",
16         'previous' => "\e[34m",
17 );
18
19 my %html_style =
20 (
21         '' => "color: black; background-color: black",
22         'outstanding' => "color: black; background-color: yellow",
23         'unmerge' => "color: black; background-color: lightred",
24         'reject' => "color: black; background-color: red",
25         'unreject' => "color: black; background-color: red",
26         'merge' => "color: black; background-color: green",
27         'base' => "color: black; background-color: lightblue",
28         'previous' => "color: black; background-color: blue",
29 );
30
31 my %name =
32 (
33         'outstanding' => "OUTSTANDING",
34         'unmerge' => "UNMERGED",
35         'reject' => "REJECTED",
36         'unreject' => "UNREJECTED",
37         'merge' => "MERGED",
38         'base' => "BASE",
39         'previous' => "PREVIOUS",
40 );
41
42 sub check_defined($$)
43 {
44         my ($msg, $data) = @_;
45         return $data if defined $data;
46         die $msg;
47 }
48
49 sub backtick(@)
50 {
51         open my $fh, '-|', @_
52                 or return undef;
53         undef local $/;
54         my $s = <$fh>;
55         close $fh
56                 or return undef;
57         return $s;
58 }
59
60 sub run(@)
61 {
62         return !system @_;
63 }
64
65 my $width = ($ENV{COLUMNS} || backtick 'tput', 'cols' || 80);
66 my $branch = $ENV{GIT_BRANCH};
67 if(not $branch)
68 {
69         chomp($branch = backtick 'git', 'symbolic-ref', 'HEAD');
70                 $branch =~ s/^refs\/heads\///
71                         or die "Not in a branch";
72 }
73 chomp(my $master = (backtick 'git', 'config', '--get', "branch-manager.$branch.master" or 'master'));
74 chomp(my $datefilter = (backtick 'git', 'config', '--get', "branch-manager.$branch.startdate" or ''));
75 my @datefilter = ();
76 my $revprefix = "";
77 if($datefilter eq 'mergebase')
78 {
79         chomp($revprefix = check_defined "git-merge-base: $!", backtick 'git', 'merge-base', $master, $branch);
80         $revprefix .= "^..";
81 }
82 elsif($datefilter ne '')
83 {
84         @datefilter = "--since=$datefilter";
85 }
86
87 # if set, don't actually merge/revert changes, just mark as such
88 my $skip = 0;
89
90 our $do_commit = 1;
91 my $logcache = undef;
92 sub reset_to_commit($)
93 {
94         my ($r) = @_;
95         #run 'git', 'merge', '-s', 'ours', '--no-commit', $r
96         #       or die "git-merge: $!";
97         run 'git', 'checkout', $r, '--', '.'
98                 or die "git-checkout: $!";
99         if($do_commit)
100         {
101                 $logcache = undef;
102                 run 'git', 'update-ref', 'MERGE_HEAD', $r
103                         or die "git-update-ref: $!";
104                 run 'git', 'commit', '--allow-empty', '-m', "::stable-branch::reset=$r"
105                         or die "git-commit: $!";
106         }
107 }
108
109 sub reject_commit($)
110 {
111         # reject == merge but skip
112         my ($r) = @_;
113         my $cmsg = "";
114         my $author = "";
115         my $email = "";
116         my $date = "";
117         if($do_commit)
118         {
119                 $logcache = undef;
120                 my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', $r
121                         or die "git-log: $!";
122                 for(split /\n/, $msg)
123                 {
124                         if(/^Author:\s*(.*) <(.*)>/)
125                         {
126                                 $author = $1;
127                                 $email = $2;
128                         }
129                         elsif(/^AuthorDate:\s*(.*)/)
130                         {
131                                 $date = $1;
132                         }
133                         elsif(/^    (.*)/)
134                         {
135                                 $cmsg .= "$1\n";
136                         }
137                 }
138                 open my $fh, '>', '.commitmsg'
139                         or die ">.commitmsg: $!";
140                 print $fh "REJECT! $cmsg" . "::stable-branch::reject=$r\n"
141                         or die ">.commitmsg: $!";
142                 close $fh
143                         or die ">.commitmsg: $!";
144         }
145         local $ENV{GIT_AUTHOR_NAME} = $author;
146         local $ENV{GIT_AUTHOR_EMAIL} = $email;
147         local $ENV{GIT_AUTHOR_DATE} = $date;
148         if($do_commit)
149         {
150                 run 'git', 'commit', '--allow-empty', '-F', '.commitmsg'
151                         or die "git-commit: $!";
152         }
153 }
154
155 sub unreject_commit($)
156 {
157         # reject == merge but skip
158         my ($r) = @_;
159         my $cmsg = "";
160         my $author = "";
161         my $email = "";
162         my $date = "";
163         if($do_commit)
164         {
165                 $logcache = undef;
166                 my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', $r
167                         or die "git-log: $!";
168                 for(split /\n/, $msg)
169                 {
170                         if(/^Author:\s*(.*) <(.*)>/)
171                         {
172                                 $author = $1;
173                                 $email = $2;
174                         }
175                         elsif(/^AuthorDate:\s*(.*)/)
176                         {
177                                 $date = $1;
178                         }
179                         elsif(/^    (.*)/)
180                         {
181                                 $cmsg .= "$1\n";
182                         }
183                 }
184                 open my $fh, '>', '.commitmsg'
185                         or die ">.commitmsg: $!";
186                 print $fh "UNREJECT! $cmsg" . "::stable-branch::unreject=$r\n"
187                         or die ">.commitmsg: $!";
188                 close $fh
189                         or die ">.commitmsg: $!";
190         }
191         local $ENV{GIT_AUTHOR_NAME} = $author;
192         local $ENV{GIT_AUTHOR_EMAIL} = $email;
193         local $ENV{GIT_AUTHOR_DATE} = $date;
194         if($do_commit)
195         {
196                 run 'git', 'commit', '--allow-empty', '-F', '.commitmsg'
197                         or die "git-commit: $!";
198         }
199 }
200
201 sub merge_commit($)
202 {
203         my ($r) = @_;
204         my $cmsg = "";
205         my $author = "";
206         my $email = "";
207         my $date = "";
208         if($do_commit)
209         {
210                 $logcache = undef;
211                 my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', $r
212                         or die "git-log: $!";
213                 for(split /\n/, $msg)
214                 {
215                         if(/^Author:\s*(.*) <(.*)>/)
216                         {
217                                 $author = $1;
218                                 $email = $2;
219                         }
220                         elsif(/^AuthorDate:\s*(.*)/)
221                         {
222                                 $date = $1;
223                         }
224                         elsif(/^    (.*)/)
225                         {
226                                 $cmsg .= "$1\n";
227                         }
228                 }
229                 open my $fh, '>', '.commitmsg'
230                         or die ">.commitmsg: $!";
231                 print $fh "$cmsg" . "::stable-branch::merge=$r\n"
232                         or die ">.commitmsg: $!";
233                 close $fh
234                         or die ">.commitmsg: $!";
235         }
236         local $ENV{GIT_AUTHOR_NAME} = $author;
237         local $ENV{GIT_AUTHOR_EMAIL} = $email;
238         local $ENV{GIT_AUTHOR_DATE} = $date;
239         if(!$skip)
240         {
241                 run 'git', 'cherry-pick', '-n', $r
242                         or run 'git', 'mergetool'
243                                 or die "git-mergetool: $!";
244         }
245         if($do_commit)
246         {
247                 run 'git', 'commit', '--allow-empty', '-F', '.commitmsg'
248                         or (run 'git', 'mergetool'
249                                 and run 'git', 'commit', '--allow-empty', '-F', '.commitmsg')
250                                         or die "git-commit: $!";
251         }
252 }
253
254 sub unmerge_commit($)
255 {
256         my ($r) = @_;
257         my $cmsg = "";
258         my $author = "";
259         my $email = "";
260         my $date = "";
261         if($do_commit)
262         {
263                 $logcache = undef;
264                 my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', $r
265                         or die "git-log: $!";
266                 for(split /\n/, $msg)
267                 {
268                         if(/^Author:\s*(.*) <(.*)>/)
269                         {
270                                 $author = $1;
271                                 $email = $2;
272                         }
273                         elsif(/^AuthorDate:\s*(.*)/)
274                         {
275                                 $date = $1;
276                         }
277                         elsif(/^    (.*)/)
278                         {
279                                 $cmsg .= "$1\n";
280                         }
281                 }
282                 open my $fh, '>', '.commitmsg'
283                         or die ">.commitmsg: $!";
284                 print $fh "UNMERGE! $cmsg" . "::stable-branch::unmerge=$r\n"
285                         or die ">.commitmsg: $!";
286                 close $fh
287                         or die ">.commitmsg: $!";
288         }
289         local $ENV{GIT_AUTHOR_NAME} = $author;
290         local $ENV{GIT_AUTHOR_EMAIL} = $email;
291         local $ENV{GIT_AUTHOR_DATE} = $date;
292         if(!$skip)
293         {
294                 run 'git', 'revert', '-n', $r
295                         or run 'git', 'mergetool'
296                                 or die "git-mergetool: $!";
297         }
298         if($do_commit)
299         {
300                 run 'git', 'commit', '--allow-empty', '-F', '.commitmsg'
301                         or (run 'git', 'mergetool'
302                                 and run 'git', 'commit', '--allow-empty', '-F', '.commitmsg')
303                                         or die "git-commit: $!";
304         }
305 }
306
307 sub rebase_log($$)
308 {
309         my ($r, $log) = @_;
310
311         my @applied = (0) x @{$log->{order_a}};
312         my $newbase_id = $log->{order_h}{$r};
313
314         my @rlog = ();
315         my @outstanding = ();
316
317         for(0..$newbase_id)
318         {
319                 if($log->{bitmap}[$_] < 0)
320                 {
321                         unshift @rlog, ['reject', $log->{order_a}[$_]];
322                 }
323                 elsif($log->{bitmap}[$_] == 0)
324                 {
325                         unshift @rlog, ['unmerge', $log->{order_a}[$_]];
326                 }
327         }
328
329         for($newbase_id+1 .. @{$log->{order_a}}-1)
330         {
331                 if($log->{bitmap}[$_] > 0)
332                 {
333                         push @rlog, ['merge', $log->{order_a}[$_]];
334                 }
335                 elsif($log->{bitmap}[$_] < 0)
336                 {
337                         push @rlog, ['reject', $log->{order_a}[$_]];
338                 }
339                 else
340                 {
341                         push @outstanding, ['outstanding', $log->{order_a}[$_]];
342                 }
343         }
344
345         return
346         {
347                 %$log,
348                 base => $r,
349                 log => [
350                         @rlog,
351                         @outstanding
352                 ]
353         };
354 }
355
356 sub parse_log()
357 {
358         return $logcache if defined $logcache;
359
360         my $base = undef;
361         my @logdata = ();
362
363         my %history = ();
364         my %logmsg = ();
365         my @history = ();
366
367         my %applied = ();
368         my %unapplied = ();
369
370         my $cur_commit = undef;
371         my $cur_msg = undef;
372         for((split /\n/, check_defined "git-log: $!", backtick 'git', 'log', '--topo-order', '--reverse', '--pretty=fuller', @datefilter, "$revprefix$master"), undef)
373         {
374                 if(defined $cur_commit and (not defined $_ or /^commit (\S+)/))
375                 {
376                         $cur_msg =~ s/\s+$//s;
377                         $history{$cur_commit} = scalar @history;
378                         $logmsg{$cur_commit} = $cur_msg;
379                         push @history, $cur_commit;
380                         $cur_commit = $cur_msg = undef;
381                 }
382                 last if not defined $_;
383                 if(/^commit (\S+)/)
384                 {
385                         $cur_commit = $1;
386                 }
387                 else
388                 {
389                         $cur_msg .= "$_\n";
390                 }
391         }
392         $cur_commit = $cur_msg = undef;
393         my @commits = ();
394         for((split /\n/, check_defined "git-log: $!", backtick 'git', 'log', '--topo-order', '--reverse', '--pretty=fuller', @datefilter, "$revprefix$branch"), undef)
395         {
396                 if(defined $cur_commit and (not defined $_ or /^commit (\S+)/))
397                 {
398                         $cur_msg =~ s/\s+$//s;
399                         $logmsg{$cur_commit} = $cur_msg;
400                         push @commits, $cur_commit;
401                         $cur_commit = $cur_msg = undef;
402                 }
403                 last if not defined $_;
404                 if(/^commit (\S+)/)
405                 {
406                         $cur_commit = $1;
407                 }
408                 else
409                 {
410                         $cur_msg .= "$_\n";
411                 }
412         }
413         my $lastrebase = undef;
414         for(@commits)
415         {
416                 my $data = $logmsg{$_};
417                 if($data =~ /::stable-branch::unmerge=(\S+)/)
418                 {
419                         next if not defined $history{$1};
420                         push @logdata, ['unmerge', $1];
421                 }
422                 elsif($data =~ /::stable-branch::merge=(\S+)/)
423                 {
424                         next if not defined $history{$1};
425                         push @logdata, ['merge', $1];
426                 }
427                 elsif($data =~ /::stable-branch::reject=(\S+)/)
428                 {
429                         next if not defined $history{$1};
430                         push @logdata, ['reject', $1];
431                 }
432                 elsif($data =~ /::stable-branch::unreject=(\S+)/)
433                 {
434                         next if not defined $history{$1};
435                         push @logdata, ['unreject', $1];
436                 }
437                 elsif($data =~ /::stable-branch::reset=(\S+)/)
438                 {
439                         next if not defined $history{$1};
440                         @logdata = ();
441                         $base = $1;
442                 }
443                 elsif($data =~ /::stable-branch::rebase=(\S+)/)
444                 {
445                         next if not defined $history{$1};
446                         $lastrebase->[0] = 'ignore'
447                                 if defined $lastrebase;
448                         push @logdata, ($lastrebase = ['rebase', $1]);
449                 }
450         }
451
452         if(not defined $base)
453         {
454                 warn 'This branch is not yet managed by git-branch-manager';
455                 return
456                 {
457                         logmsg => \%logmsg,
458                         order_a => \@history,
459                         order_h => \%history,
460                 };
461         }
462         else
463         {
464                 my $baseid = $history{$base};
465                 my @bitmap = map
466                 {
467                         $_ <= $baseid
468                 }
469                 0..@history-1;
470                 my $i = 0;
471                 while($i < @logdata)
472                 {
473                         my ($cmd, $data) = @{$logdata[$i]};
474                         if($cmd eq 'merge')
475                         {
476                                 $bitmap[$history{$data}] = 1;
477                         }
478                         elsif($cmd eq 'unmerge')
479                         {
480                                 $bitmap[$history{$data}] = 0;
481                         }
482                         elsif($cmd eq 'reject')
483                         {
484                                 $bitmap[$history{$data}] = -1;
485                         }
486                         elsif($cmd eq 'unreject')
487                         {
488                                 $bitmap[$history{$data}] = 0;
489                         }
490                         elsif($cmd eq 'rebase')
491                         {
492                                 # the bitmap is fine, but generate a new log from the bitmap
493                                 my $pseudolog =
494                                 {
495                                         order_a => \@history,
496                                         order_h => \%history,
497                                         bitmap => \@bitmap,
498                                 };
499                                 my $rebasedlog = rebase_log $data, $pseudolog;
500                                 my @l = grep { $_->[0] ne 'outstanding' } @{$rebasedlog->{log}};
501                                 splice @logdata, 0, $i+1, @l;
502                                 $i = @l-1;
503                                 $base = $data;
504                                 $baseid = $history{$base};
505                         }
506                         ++$i;
507                 }
508
509                 my @outstanding = ();
510                 for($baseid+1 .. @history-1)
511                 {
512                         push @outstanding, ['outstanding', $history[$_]]
513                                 unless $bitmap[$_];
514                 }
515
516                 $logcache =
517                 {
518                         logmsg => \%logmsg,
519                         order_a => \@history,
520                         order_h => \%history,
521
522                         bitmap => \@bitmap,
523                         base => $base,
524                         log => [
525                                 @logdata,
526                                 @outstanding
527                         ]
528                 };
529                 return $logcache;
530         }
531 }
532
533 our $pebkac = 0;
534 our $done = 0;
535
536 sub run_script(@);
537 sub run_script(@)
538 {
539         ++$done;
540         my (@commands) = @_;
541         for(@commands)
542         {
543                 my ($cmd, $r) = @$_;
544                 if($pebkac)
545                 {
546                         $r = backtick 'git', 'rev-parse', $r
547                                 or die "git-rev-parse: $!"
548                                         if defined $r;
549                         chomp $r
550                                 if defined $r;
551                 }
552                 print "Executing: $cmd $r\n";
553                 if($cmd eq 'reset')
554                 {
555                         if($pebkac)
556                         {
557                                 my $l = parse_log();
558                                 die "PEBKAC: invalid revision number, cannot reset"
559                                         unless defined $l->{order_h}{$r};
560                         }
561                         reset_to_commit $r;
562                 }
563                 elsif($cmd eq 'hardreset')
564                 {
565                         if($pebkac)
566                         {
567                                 my $l = parse_log();
568                                 die "PEBKAC: invalid revision number, cannot reset"
569                                         unless defined $l->{order_h}{$r};
570                         }
571                         run 'git', 'reset', '--hard', $r
572                                 or die "git-reset: $!";
573                         reset_to_commit $r;
574                 }
575                 elsif($cmd eq 'merge')
576                 {
577                         if($pebkac)
578                         {
579                                 my $l = parse_log();
580                                 die "PEBKAC: invalid revision number, cannot reset"
581                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] == 0;
582                                 die "PEBKAC: not initialized"
583                                         unless defined $l->{base};
584                         }
585                         merge_commit $r;
586                 }
587                 elsif($cmd eq 'unmerge')
588                 {
589                         if($pebkac)
590                         {
591                                 my $l = parse_log();
592                                 die "PEBKAC: invalid revision number, cannot reset"
593                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] > 0;
594                                 die "PEBKAC: not initialized"
595                                         unless defined $l->{base};
596                         }
597                         unmerge_commit $r;
598                 }
599                 elsif($cmd eq 'reject')
600                 {
601                         if($pebkac)
602                         {
603                                 my $l = parse_log();
604                                 die "PEBKAC: invalid revision number, cannot reset"
605                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] == 0;
606                                 die "PEBKAC: not initialized"
607                                         unless defined $l->{base};
608                         }
609                         reject_commit $r;
610                 }
611                 elsif($cmd eq 'unreject')
612                 {
613                         if($pebkac)
614                         {
615                                 my $l = parse_log();
616                                 die "PEBKAC: invalid revision number, cannot reset"
617                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] < 0;
618                                 die "PEBKAC: not initialized"
619                                         unless defined $l->{base};
620                         }
621                         unreject_commit $r;
622                 }
623                 elsif($cmd eq 'outstanding')
624                 {
625                 }
626                 else
627                 {
628                         die "Invalid command: $cmd $r";
629                 }
630         }
631 }
632
633 sub opt_rebase($$)
634 {
635         ++$done;
636         my ($cmd, $r) = @_;
637         if($pebkac)
638         {
639                 $r = backtick 'git', 'rev-parse', $r
640                         or die "git-rev-parse: $!"
641                         if defined $r;
642                 chomp $r
643                         if defined $r;
644                 my $l = parse_log();
645                 die "PEBKAC: invalid revision number, cannot reset"
646                         unless defined $l->{order_h}{$r};
647                 die "PEBKAC: not initialized"
648                         unless defined $l->{base};
649         }
650         my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', @datefilter, $branch
651                 or die "git-log: $!";
652         $msg =~ /^commit (\S+)/s
653                 or die "Invalid git log output";
654         my $commit_id = $1;
655         my $l = rebase_log $r, parse_log();
656         local $pebkac = 0;
657         eval
658         {
659                 if($cmd eq 'rebase')
660                 {
661                         local $do_commit = 0;
662                         reset_to_commit $r;
663                         run_script @{$l->{log}};
664                         run 'git', 'commit', '--allow-empty', '-m', "::stable-branch::rebase=$r"
665                                 or die "git-commit: $!";
666                 }
667                 elsif($cmd eq 'resetrebase')
668                 {
669                         run_script ['reset', $r], @{$l->{log}};
670                 }
671                 elsif($cmd eq 'hardresetrebase')
672                 {
673                         run_script ['hardreset', $r], @{$l->{log}};
674                 }
675                 1;
676         }
677         or do
678         {
679                 my $err = $@;
680                 run 'git', 'reset', '--hard', $commit_id
681                         or die "$err, and then git-reset failed: $!";
682                 die $err;
683         };
684 }
685
686 sub escapeHTML {
687          my ($toencode,$newlinestoo) = @_;
688          return undef unless defined($toencode);
689          $toencode =~ s{&}{&amp;}gso;
690          $toencode =~ s{<}{&lt;}gso;
691          $toencode =~ s{>}{&gt;}gso;
692          $toencode =~ s{"}{&quot;}gso;
693          return $toencode;
694 }
695
696
697 my $histsize = 20;
698 my $cgi_url = undef;
699 sub opt_list($$)
700 {
701         ++$done;
702         my ($cmd, $r) = @_;
703         $r = undef if $r eq '';
704         if($pebkac)
705         {
706                 ($r = backtick 'git', 'rev-parse', $r
707                         or die "git-rev-parse: $!")
708                                 if defined $r;
709                 chomp $r
710                         if defined $r;
711                 my $l = parse_log();
712                 die "PEBKAC: invalid revision number, cannot reset"
713                         unless !defined $r or defined $l->{order_h}{$r};
714                 die "PEBKAC: not initialized"
715                         unless defined $l->{base};
716         }
717         my $l = parse_log();
718         $l = rebase_log $r, $l
719                 if defined $r;
720         my $last = $l->{order_h}{$l->{base}};
721         my $first = $last - $histsize;
722         $first = 0
723                 if $first < 0;
724         my %seen = ();
725         for(@{$l->{log}})
726         {
727                 ++$seen{$_->[1]};
728         }
729         my @l = (
730                         (map { $seen{$l->{order_a}[$_]} ? () : ['previous', $l->{order_a}[$_]] } $first..($last-1)),
731                         ['base', $l->{base}],
732                         @{$l->{log}}
733                         );
734         if($cmd eq 'chronology')
735         {
736                 @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);
737         }
738         elsif($cmd eq 'outstanding')
739         {
740                 my %seen = ();
741                 @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);
742         }
743         if(defined $cgi_url)
744         {
745                 print "Content-Type: text/html\n\n<table border>\n";
746                 for(@l)
747                 {
748                         my ($action, $r) = @$_;
749                         my $m = $l->{logmsg}->{$r};
750                         my $m_short = join ' ', map { s/^    (?!git-svn-id)(.)/$1/ ? $_ : () } split /\n/, $m;
751                         printf "<tr style=\"%s\"><td>%s</td><td><a href=\"%s%s\">%s</a></td><td style=\"white-space: pre\">%s</td></tr>\n", $html_style{$action}, $name{$action}, escapeHTML($cgi_url), escapeHTML($r), escapeHTML($r), escapeHTML($m_short);
752                 }
753                 print "</table>\n";
754         }
755         else
756         {
757                 for(@l)
758                 {
759                         my ($action, $r) = @$_;
760                         my $m = $l->{logmsg}->{$r};
761                         my $m_short = join ' ', map { s/^    (?!git-svn-id)(.)/$1/ ? $_ : () } split /\n/, $m;
762                         $m_short = substr $m_short, 0, $width - 11 - 1 - 40 - 1;
763                         printf "%s%-11s%s %s %s\n", $color{$action}, $name{$action}, $color{''}, $r, $m_short;
764                 }
765         }
766 }
767
768 sub opt_help($$)
769 {
770         my ($cmd, $one) = @_;
771         print STDERR <<EOF;
772 Usage:
773         $0 [{--histsize|-s} n] {--chronology|-c}
774         $0 [{--histsize|-s} n] {--chronology|-c} revision-hash
775         $0 [{--histsize|-s} n] {--log|-l}
776         $0 [{--histsize|-s} n] {--log|-l} revision-hash
777         $0 {--merge|-m} revision-hash
778         $0 {--unmerge|-u} revision-hash
779         $0 {--reset|-R} revision-hash
780         $0 {--hardreset|-H} revision-hash
781         $0 {--rebase|-b} revision-hash
782         $0 {--resetrebase|-B} revision-hash
783         $0 {--hardresetrebase} revision-hash
784 EOF
785         exit 1;
786 }
787
788 sub handler($)
789 {
790         my ($sub) = @_;
791         return sub
792         {
793                 my $r;
794                 eval
795                 {
796                         $r = $sub->(@_);
797                         1;
798                 }
799                 or do
800                 {
801                         warn "$@";
802                         exit 1;
803                 };
804                 return $r;
805         };
806 }
807
808 $pebkac = 1;
809 my $result = GetOptions(
810         "chronology|c:s", handler \&opt_list,
811         "log|l:s", handler \&opt_list,
812         "outstanding|o:s", handler \&opt_list,
813         "rebase|b=s", handler \&opt_rebase,
814         "resetrebase|B=s", handler \&opt_rebase,
815         "hardresetrebase=s", handler \&opt_rebase,
816         "skip", handler \$skip,
817         "merge|m=s{,}", handler sub { run_script ['merge', $_[1]]; },
818         "unmerge|u=s{,}", handler sub { run_script ['unmerge', $_[1]]; },
819         "reject|r=s{,}", handler sub { run_script ['reject', $_[1]]; },
820         "unreject|U=s{,}", handler sub { run_script ['unreject', $_[1]]; },
821         "reset|R=s", handler sub { run_script ['reset', $_[1]]; },
822         "hardreset|H=s", handler sub { run_script ['hardreset', $_[1]]; },
823         "help|h", handler \&opt_help,
824         "histsize|s=i", \$histsize,
825         "cgi=s", \$cgi_url
826 );
827 if(!$done)
828 {
829         opt_list("outstanding", "");
830 }
831 $pebkac = 0;