]> de.git.xonotic.org Git - xonotic/div0-gittools.git/blob - git-branch-manager
add a way to permanently reject an upstream commit
[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 "$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 "$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', '-F', '.commitmsg'
248                         or (run 'git', 'mergetool'
249                                 and run 'git', 'commit', '-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\n$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', '-F', '.commitmsg'
301                         or (run 'git', 'mergetool'
302                                 and run 'git', 'commit', '-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}[$_])
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                         push @logdata, ['unmerge', $1];
420                 }
421                 elsif($data =~ /::stable-branch::merge=(\S+)/)
422                 {
423                         push @logdata, ['merge', $1];
424                 }
425                 elsif($data =~ /::stable-branch::reject=(\S+)/)
426                 {
427                         push @logdata, ['reject', $1];
428                 }
429                 elsif($data =~ /::stable-branch::unreject=(\S+)/)
430                 {
431                         push @logdata, ['unreject', $1];
432                 }
433                 elsif($data =~ /::stable-branch::reset=(\S+)/)
434                 {
435                         @logdata = ();
436                         $base = $1;
437                 }
438                 elsif($data =~ /::stable-branch::rebase=(\S+)/)
439                 {
440                         $lastrebase->[0] = 'ignore'
441                                 if defined $lastrebase;
442                         push @logdata, ($lastrebase = ['rebase', $1]);
443                 }
444         }
445
446         if(not defined $base)
447         {
448                 warn 'This branch is not yet managed by git-branch-manager';
449                 return
450                 {
451                         logmsg => \%logmsg,
452                         order_a => \@history,
453                         order_h => \%history,
454                 };
455         }
456         else
457         {
458                 my $baseid = $history{$base};
459                 my @bitmap = map
460                 {
461                         $_ <= $baseid
462                 }
463                 0..@history-1;
464                 my $i = 0;
465                 while($i < @logdata)
466                 {
467                         my ($cmd, $data) = @{$logdata[$i]};
468                         if($cmd eq 'merge')
469                         {
470                                 $bitmap[$history{$data}] = 1;
471                         }
472                         elsif($cmd eq 'unmerge')
473                         {
474                                 $bitmap[$history{$data}] = 0;
475                         }
476                         elsif($cmd eq 'reject')
477                         {
478                                 $bitmap[$history{$data}] = -1;
479                         }
480                         elsif($cmd eq 'unreject')
481                         {
482                                 $bitmap[$history{$data}] = 0;
483                         }
484                         elsif($cmd eq 'rebase')
485                         {
486                                 # the bitmap is fine, but generate a new log from the bitmap
487                                 my $pseudolog =
488                                 {
489                                         order_a => \@history,
490                                         order_h => \%history,
491                                         bitmap => \@bitmap,
492                                 };
493                                 my $rebasedlog = rebase_log $data, $pseudolog;
494                                 my @l = grep { $_->[0] ne 'outstanding' } @{$rebasedlog->{log}};
495                                 splice @logdata, 0, $i+1, @l;
496                                 $i = @l-1;
497                                 $base = $data;
498                                 $baseid = $history{$base};
499                         }
500                         ++$i;
501                 }
502
503                 my @outstanding = ();
504                 for($baseid+1 .. @history-1)
505                 {
506                         push @outstanding, ['outstanding', $history[$_]]
507                                 unless $bitmap[$_];
508                 }
509
510                 $logcache =
511                 {
512                         logmsg => \%logmsg,
513                         order_a => \@history,
514                         order_h => \%history,
515
516                         bitmap => \@bitmap,
517                         base => $base,
518                         log => [
519                                 @logdata,
520                                 @outstanding
521                         ]
522                 };
523                 return $logcache;
524         }
525 }
526
527 our $pebkac = 0;
528 our $done = 0;
529
530 sub run_script(@);
531 sub run_script(@)
532 {
533         ++$done;
534         my (@commands) = @_;
535         for(@commands)
536         {
537                 my ($cmd, $r) = @$_;
538                 if($pebkac)
539                 {
540                         $r = backtick 'git', 'rev-parse', $r
541                                 or die "git-rev-parse: $!"
542                                         if defined $r;
543                         chomp $r
544                                 if defined $r;
545                 }
546                 print "Executing: $cmd $r\n";
547                 if($cmd eq 'reset')
548                 {
549                         if($pebkac)
550                         {
551                                 my $l = parse_log();
552                                 die "PEBKAC: invalid revision number, cannot reset"
553                                         unless defined $l->{order_h}{$r};
554                         }
555                         reset_to_commit $r;
556                 }
557                 elsif($cmd eq 'hardreset')
558                 {
559                         if($pebkac)
560                         {
561                                 my $l = parse_log();
562                                 die "PEBKAC: invalid revision number, cannot reset"
563                                         unless defined $l->{order_h}{$r};
564                         }
565                         run 'git', 'reset', '--hard', $r
566                                 or die "git-reset: $!";
567                         reset_to_commit $r;
568                 }
569                 elsif($cmd eq 'merge')
570                 {
571                         if($pebkac)
572                         {
573                                 my $l = parse_log();
574                                 die "PEBKAC: invalid revision number, cannot reset"
575                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] == 0;
576                                 die "PEBKAC: not initialized"
577                                         unless defined $l->{base};
578                         }
579                         merge_commit $r;
580                 }
581                 elsif($cmd eq 'unmerge')
582                 {
583                         if($pebkac)
584                         {
585                                 my $l = parse_log();
586                                 die "PEBKAC: invalid revision number, cannot reset"
587                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] > 0;
588                                 die "PEBKAC: not initialized"
589                                         unless defined $l->{base};
590                         }
591                         unmerge_commit $r;
592                 }
593                 elsif($cmd eq 'reject')
594                 {
595                         if($pebkac)
596                         {
597                                 my $l = parse_log();
598                                 die "PEBKAC: invalid revision number, cannot reset"
599                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] == 0;
600                                 die "PEBKAC: not initialized"
601                                         unless defined $l->{base};
602                         }
603                         reject_commit $r;
604                 }
605                 elsif($cmd eq 'unreject')
606                 {
607                         if($pebkac)
608                         {
609                                 my $l = parse_log();
610                                 die "PEBKAC: invalid revision number, cannot reset"
611                                         unless defined $l->{order_h}{$r} and $l->{bitmap}[$l->{order_h}{$r}] < 0;
612                                 die "PEBKAC: not initialized"
613                                         unless defined $l->{base};
614                         }
615                         unreject_commit $r;
616                 }
617                 elsif($cmd eq 'outstanding')
618                 {
619                 }
620                 else
621                 {
622                         die "Invalid command: $cmd $r";
623                 }
624         }
625 }
626
627 sub opt_rebase($$)
628 {
629         ++$done;
630         my ($cmd, $r) = @_;
631         if($pebkac)
632         {
633                 $r = backtick 'git', 'rev-parse', $r
634                         or die "git-rev-parse: $!"
635                         if defined $r;
636                 chomp $r
637                         if defined $r;
638                 my $l = parse_log();
639                 die "PEBKAC: invalid revision number, cannot reset"
640                         unless defined $l->{order_h}{$r};
641                 die "PEBKAC: not initialized"
642                         unless defined $l->{base};
643         }
644         my $msg = backtick 'git', 'log', '-1', '--pretty=fuller', @datefilter, $branch
645                 or die "git-log: $!";
646         $msg =~ /^commit (\S+)/s
647                 or die "Invalid git log output";
648         my $commit_id = $1;
649         my $l = rebase_log $r, parse_log();
650         local $pebkac = 0;
651         local $do_commit = 0;
652         eval
653         {
654                 reset_to_commit $r;
655                 run_script @{$l->{log}};
656                 run 'git', 'commit', '--allow-empty', '-m', "::stable-branch::rebase=$r"
657                         or die "git-commit: $!";
658                 1;
659         }
660         or do
661         {
662                 my $err = $@;
663                 run 'git', 'reset', '--hard', $commit_id
664                         or die "$err, and then git-reset failed: $!";
665                 die $err;
666         };
667 }
668
669 sub escapeHTML {
670          my ($toencode,$newlinestoo) = @_;
671          return undef unless defined($toencode);
672          $toencode =~ s{&}{&amp;}gso;
673          $toencode =~ s{<}{&lt;}gso;
674          $toencode =~ s{>}{&gt;}gso;
675          $toencode =~ s{"}{&quot;}gso;
676          return $toencode;
677 }
678
679
680 my $histsize = 20;
681 my $cgi_url = undef;
682 sub opt_list($$)
683 {
684         ++$done;
685         my ($cmd, $r) = @_;
686         $r = undef if $r eq '';
687         if($pebkac)
688         {
689                 ($r = backtick 'git', 'rev-parse', $r
690                         or die "git-rev-parse: $!")
691                                 if defined $r;
692                 chomp $r
693                         if defined $r;
694                 my $l = parse_log();
695                 die "PEBKAC: invalid revision number, cannot reset"
696                         unless !defined $r or defined $l->{order_h}{$r};
697                 die "PEBKAC: not initialized"
698                         unless defined $l->{base};
699         }
700         my $l = parse_log();
701         $l = rebase_log $r, $l
702                 if defined $r;
703         my $last = $l->{order_h}{$l->{base}};
704         my $first = $last - $histsize;
705         $first = 0
706                 if $first < 0;
707         my %seen = ();
708         for(@{$l->{log}})
709         {
710                 ++$seen{$_->[1]};
711         }
712         my @l = (
713                         (map { $seen{$l->{order_a}[$_]} ? () : ['previous', $l->{order_a}[$_]] } $first..($last-1)),
714                         ['base', $l->{base}],
715                         @{$l->{log}}
716                         );
717         if($cmd eq 'chronology')
718         {
719                 @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);
720         }
721         elsif($cmd eq 'outstanding')
722         {
723                 my %seen = ();
724                 @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);
725         }
726         if(defined $cgi_url)
727         {
728                 print "Content-Type: text/html\n\n<table border>\n";
729                 for(@l)
730                 {
731                         my ($action, $r) = @$_;
732                         my $m = $l->{logmsg}->{$r};
733                         my $m_short = join ' ', map { s/^    (?!git-svn-id)(.)/$1/ ? $_ : () } split /\n/, $m;
734                         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);
735                 }
736                 print "</table>\n";
737         }
738         else
739         {
740                 for(@l)
741                 {
742                         my ($action, $r) = @$_;
743                         my $m = $l->{logmsg}->{$r};
744                         my $m_short = join ' ', map { s/^    (?!git-svn-id)(.)/$1/ ? $_ : () } split /\n/, $m;
745                         $m_short = substr $m_short, 0, $width - 11 - 1 - 40 - 1;
746                         printf "%s%-11s%s %s %s\n", $color{$action}, $name{$action}, $color{''}, $r, $m_short;
747                 }
748         }
749 }
750
751 sub opt_help($$)
752 {
753         my ($cmd, $one) = @_;
754         print STDERR <<EOF;
755 Usage:
756         $0 [{--histsize|-s} n] {--chronology|-c}
757         $0 [{--histsize|-s} n] {--chronology|-c} revision-hash
758         $0 [{--histsize|-s} n] {--log|-l}
759         $0 [{--histsize|-s} n] {--log|-l} revision-hash
760         $0 {--merge|-m} revision-hash
761         $0 {--unmerge|-u} revision-hash
762         $0 {--reset|-R} revision-hash
763         $0 {--hardreset|-H} revision-hash
764         $0 {--rebase|-b} revision-hash
765 EOF
766         exit 1;
767 }
768
769 sub handler($)
770 {
771         my ($sub) = @_;
772         return sub
773         {
774                 my $r;
775                 eval
776                 {
777                         $r = $sub->(@_);
778                         1;
779                 }
780                 or do
781                 {
782                         warn "$@";
783                         exit 1;
784                 };
785                 return $r;
786         };
787 }
788
789 $pebkac = 1;
790 my $result = GetOptions(
791         "chronology|c:s", handler \&opt_list,
792         "log|l:s", handler \&opt_list,
793         "outstanding|o:s", handler \&opt_list,
794         "rebase|b=s", handler \&opt_rebase,
795         "skip", handler \$skip,
796         "merge|m=s{,}", handler sub { run_script ['merge', $_[1]]; },
797         "unmerge|u=s{,}", handler sub { run_script ['unmerge', $_[1]]; },
798         "reset|R=s", handler sub { run_script ['reset', $_[1]]; },
799         "hardreset|H=s", handler sub { run_script ['hardreset', $_[1]]; },
800         "help|h", handler \&opt_help,
801         "histsize|s=i", \$histsize,
802         "cgi=s", \$cgi_url
803 );
804 if(!$done)
805 {
806         opt_list("outstanding", "");
807 }
808 $pebkac = 0;