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