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