bin/git-forest 13.1 KiB raw
1
#!/usr/bin/perl
2
#
3
#	git-森林
4
#	text-based tree visualisation
5
#	Copyright © Jan Engelhardt <jengelh [at] medozas de>, 2008
6
#
7
#	This program is free software; you can redistribute it and/or modify
8
#	it under the terms of the GNU General Public License as published by
9
#	the Free Software Foundation; either version 2 or 3 of the license.
10
#
11
use Getopt::Long;
12
use Git;
13
use strict;
14
use utf8;
15
use Encode qw(encode);
16
my $Repo          = Git->repository($ENV{"GIT_DIR"} || ".");
17
my $Pretty_fmt    = "format:%s";
18
my $Reverse_order = 0;
19
my $Show_all      = 0;
20
my $Show_rebase   = 1;
21
my $Style         = 1;
22
my $Subvine_depth = 2;
23
my $With_sha      = 0;
24
my %Color         = (
25
	"default" => "\e[0m", # ]
26
	"at"      => "\e[1;30m", # ]
27
	"hhead"   => "\e[1;31m", # ]
28
	"head"    => "\e[1;32m", # ]
29
	"ref"     => "\e[1;34m", # ]
30
	"remote"  => "\e[1;35m", # ]
31
	"sha"     => "\e[0;31m", # ]
32
	"tag"     => "\e[1;33m", # ]
33
	"tree"    => "\e[0;33m", # ]
34
);
35
36
&main();
37
38
sub main
39
{
40
	&Getopt::Long::Configure(qw(bundling pass_through));
41
	&GetOptions(
42
		"all"       => \$Show_all,
43
		"no-color"  => sub { %Color = (); },
44
		"no-rebase" => sub { $Show_rebase = 0; },
45
		"a"         => sub { $Pretty_fmt = "format:\e[1;30m(\e[0;32m%an\e[1;30m)\e[0m %s"; }, # ]]]]
46
		"pretty=s"  => \$Pretty_fmt,
47
		"reverse"   => \$Reverse_order,
48
		"svdepth=i" => \$Subvine_depth,
49
		"style=i"   => \$Style,
50
		"sha"       => \$With_sha,
51
	);
52
	++$Subvine_depth;
53
	if (substr($Pretty_fmt, 0, 7) ne "format:") {
54
		die "If you use --pretty, it must be in the form of --pretty=format:";
55
	}
56
	$Pretty_fmt = substr($Pretty_fmt, 7);
57
	while ($Pretty_fmt =~ /\%./g) {
58
		if ($& eq "\%b" || $& eq "\%n" || ($&.$') =~ /^\%x0a/i) {
59
			die "Cannot use \%b, \%n or \%x0a in --pretty=format:";
60
		}
61
	}
62
	if ($Show_all) {
63
		#
64
		# Give --all back. And add HEAD to include commits
65
		# in the rev list that belong to a detached HEAD.
66
		#
67
		unshift(@ARGV, "--all", "HEAD");
68
	}
69
	if ($Reverse_order) {
70
		tie(*STDOUT, "ReverseOutput");
71
	}
72
	&process();
73
	if ($Reverse_order) {
74
		untie *STDOUT;
75
	}
76
}
77
78
sub get_line_block
79
{
80
	my($fh, $max) = @_;
81
82
	while (scalar(@h::ist) < $max) {
83
		my $x;
84
85
		$x = <$fh>;
86
		if (!defined($x)) {
87
			last;
88
		}
89
		push(@h::ist, $x);
90
	}
91
92
	my @ret = (shift @h::ist);
93
	foreach (2..$max) {
94
		push(@ret, $h::ist[$_-2]);
95
	}
96
	return @ret;
97
}
98
99
sub process
100
{
101
	my(@vine);
102
	my $refs = &get_refs();
103
	my($fh, $fhc) = $Repo->command_output_pipe("log", "--date-order",
104
	                "--pretty=format:<%H><%h><%P>$Pretty_fmt", @ARGV);
105
106
	while (my($line, @next_sha) = get_line_block($fh, $Subvine_depth)) {
107
		if (!defined($line)) {
108
			last;
109
		}
110
		chomp $line;
111
		my($sha, $mini_sha, $parents, $msg) =
112
			($line =~ /^<(.*?)><(.*?)><(.*?)>(.*)/s);
113
		my @next_sha = map { ($_) = /^<(.*?)>/ } @next_sha;
114
		my @parents = split(" ", $parents);
115
116
		&vine_branch(\@vine, $sha);
117
		my $ra = &vine_commit(\@vine, $sha, \@parents);
118
119
		if (exists($refs->{$sha})) {
120
			print &vis_post(&vis_commit($ra),
121
			      $Color{at}."m".$Color{default});
122
			&ref_print($refs->{$sha});
123
		} else {
124
			print &vis_post(&vis_commit($ra, " "));
125
		}
126
		if ($With_sha) {
127
			print $msg, $Color{at}, encode('UTF-8', "──("), $Color{sha}, $mini_sha,
128
			      $Color{at}, ")", $Color{default}, "\n";
129
		} else {
130
			print $msg, "\n";
131
		}
132
133
		&vine_merge(\@vine, $sha, \@next_sha, \@parents);
134
	}
135
	$Repo->command_close_pipe($fh, $fhc);
136
}
137
138
sub get_refs
139
{
140
	my($fh, $c) = $Repo->command_output_pipe("show-ref");
141
	my $ret = {};
142
143
	while (defined(my $ln = <$fh>)) {
144
		chomp $ln;
145
		if (length($ln) == 0) {
146
			next;
147
		}
148
149
		my($sha, $name) = ($ln =~ /^(\S+)\s+(.*)/s);
150
		if (!exists($ret->{$sha})) {
151
			$ret->{$sha} = [];
152
		}
153
		push(@{$ret->{$sha}}, $name);
154
		if ($name =~ m{^refs/tags/}) {
155
			my $sub_sha = $Repo->command("log", "-1",
156
			              "--pretty=format:%H", $name);
157
			chomp $sub_sha;
158
			if ($sha ne $sub_sha) {
159
				push(@{$ret->{$sub_sha}}, $name);
160
			}
161
		}
162
	}
163
164
	$Repo->command_close_pipe($fh, $c);
165
166
	my $rebase = -e $Repo->repo_path()."/.dotest-merge/git-rebase-todo" &&
167
	             $Show_rebase;
168
	if ($rebase) {
169
		if (open(my $act_fh, $Repo->repo_path().
170
		    "/.dotest-merge/git-rebase-todo")) {
171
			my($curr) = (<$act_fh> =~ /^\S+\s+(\S+)/);
172
			$curr = $Repo->command("rev-parse", $curr);
173
			chomp $curr;
174
			unshift(@{$ret->{$curr}}, "rebase/cn");
175
			close $act_fh;
176
		}
177
178
		chomp(my $up   = $Repo->command("rev-parse", ".dotest-merge/upstream"));
179
		chomp(my $onto = $Repo->command("rev-parse", ".dotest-merge/onto"));
180
		chomp(my $old  = $Repo->command("rev-parse", ".dotest-merge/head"));
181
		unshift(@{$ret->{$up}}, "rebase/upstream");
182
		unshift(@{$ret->{$onto}}, "rebase/onto");
183
		unshift(@{$ret->{$old}}, "rebase/saved-HEAD");
184
	}
185
186
	my $head = $Repo->command("rev-parse", "HEAD");
187
	chomp $head;
188
	if ($rebase) {
189
		unshift(@{$ret->{$head}}, "rebase/new");
190
	}
191
	unshift(@{$ret->{$head}}, "HEAD");
192
193
	return $ret;
194
}
195
196
#
197
# ref_print - print a ref with color
198
# @s:	ref name
199
#
200
sub ref_print
201
{
202
	foreach my $symbol (@{shift @_}) {
203
		print $Color{at}, "[";
204
		if ($symbol eq "HEAD" || $symbol =~ m{^rebase/}) {
205
			print $Color{hhead}, $symbol;
206
		} elsif ($symbol =~ m{^refs/(remotes/[^/]+)/(.*)}s) {
207
			print $Color{remote}, $1, $Color{head}, "/$2";
208
		} elsif ($symbol =~ m{^refs/heads/(.*)}s) {
209
			print $Color{head}, $1;
210
		} elsif ($symbol =~ m{^refs/tags/(.*)}s) {
211
			print $Color{tag}, $1;
212
		} elsif ($symbol =~ m{^refs/(.*)}s) {
213
			print $Color{ref}, $1;
214
		}
215
		print $Color{at}, encode('UTF-8', "]──"), $Color{default};
216
	}
217
}
218
219
#
220
# vine_branch -
221
# @vine:	column array containing the expected parent IDs
222
# @rev:		commit ID
223
#
224
# Draws the branching vine matrix between a commit K and K^ (@rev).
225
#
226
sub vine_branch
227
{
228
	my($vine, $rev) = @_;
229
	my $idx;
230
231
	my($matched, $master) = (0, 0);
232
	my $ret;
233
234
	# Transform array into string
235
	for ($idx = 0; $idx <= $#$vine; ++$idx) {
236
		if (!defined($vine->[$idx])) {
237
			$ret .= " ";
238
			next;
239
		} elsif ($vine->[$idx] ne $rev) {
240
			$ret .= "I";
241
			next;
242
		}
243
		if (!$master && $idx % 2 == 0) {
244
			$ret .= "S";
245
			$master = 1;
246
		} else {
247
			$ret .= "s";
248
			$vine->[$idx] = undef;
249
		}
250
		++$matched;
251
	}
252
253
	if ($matched < 2) {
254
		return;
255
	}
256
257
	&remove_trailing_blanks($vine);
258
	print &vis_post(&vis_fan($ret, "branch")), "\n";
259
}
260
261
#
262
# vine_commit -
263
# @vine:	column array containing the expected IDs
264
# @rev:		commit ID
265
# @parents:	array of parent IDs
266
#
267
sub vine_commit
268
{
269
	my($vine, $rev, $parents) = @_;
270
	my $ret;
271
272
	for (my $i = 0; $i <= $#$vine; ++$i) {
273
		if (!defined($vine->[$i])) {
274
			$ret .= " ";
275
		} elsif ($vine->[$i] eq $rev) {
276
			$ret .= "C";
277
		} else {
278
			$ret .= "I";
279
		}
280
	}
281
282
	if ($ret !~ /C/) {
283
		# Not having produced a C before means this is a tip
284
		my $i;
285
		for ($i = &round_down2($#$vine); $i >= 0; $i -= 2) {
286
			if (substr($ret, $i, 1) eq " ") {
287
				substr($ret, $i, 1) = "t";
288
				$vine->[$i] = $rev;
289
				last;
290
			}
291
		}
292
		if ($i < 0) {
293
			if (scalar(@$vine) % 2 != 0) {
294
				push(@$vine, undef);
295
				$ret .= " ";
296
			}
297
			$ret .= "t";
298
			push(@$vine, $rev);
299
		}
300
	}
301
302
	&remove_trailing_blanks($vine);
303
304
	if (scalar(@$parents) == 0) {
305
		# tree root
306
		$ret =~ tr/C/r/;
307
	}
308
309
	return $ret;
310
}
311
312
#
313
# vine_merge -
314
# @vine:	column array containing the expected parent IDs
315
# @rev:		commit ID
316
# @next_rev:	next commit ID in the revision list
317
# @parents:	parent IDs of @rev
318
#
319
# Draws the merging vine matrix between a commit K (@rev) and K^ (@parents).
320
#
321
sub vine_merge
322
{
323
	my($vine, $rev, $next_rev, $parents) = @_;
324
	my $orig_vine = -1;
325
	my @slot;
326
	my($ret, $max);
327
328
	for (my $i = 0; $i <= $#$vine; ++$i) {
329
		if ($vine->[$i] eq $rev) {
330
			$orig_vine = $i;
331
			last;
332
		}
333
	}
334
335
	if ($orig_vine == -1) {
336
		die "vine_commit() did not add this vine.";
337
	}
338
339
	if (scalar(@$parents) <= 1) {
340
		#
341
		# A single parent does not need a visual. Update and return.
342
		#
343
		$vine->[$orig_vine] = $parents->[0];
344
		&remove_trailing_blanks($vine);
345
		return;
346
	}
347
348
	#
349
	# Put previously seen branches in the vine subcolumns
350
	# Need to keep at least one parent for the slot algorithm below.
351
	#
352
	for (my $j = 0; $j <= $#$parents && $#$parents > 0; ++$j) {
353
		for (my $idx = 0; $idx <= $#$vine; ++$idx) {
354
			if ($vine->[$idx] ne $parents->[$j] ||
355
			    !grep { my $z = $vine->[$idx]; /^\Q$z\E$/ }
356
			    @$next_rev) {
357
				next;
358
			}
359
			if ($idx == $orig_vine) {
360
				die "Should not really happen";
361
			}
362
			if ($idx < $orig_vine) {
363
				my $p = $idx + 1;
364
				if (defined($vine->[$p])) {
365
					$p = $idx - 1;
366
				}
367
				if (defined($vine->[$p])) {
368
					last;
369
				}
370
				$vine->[$p] = $parents->[$j];
371
				str_expand(\$ret, $p + 1);
372
				substr($ret, $p, 1) = "s";
373
			} else {
374
				my $p = $idx - 1;
375
				if (defined($vine->[$p]) || $p < 0) {
376
					$p = $idx + 1;
377
				}
378
				if (defined($vine->[$p])) {
379
					last;
380
				}
381
				$vine->[$p] = $parents->[$j];
382
				str_expand(\$ret, $p + 1);
383
				substr($ret, $p, 1) = "s";
384
			}
385
			splice(@$parents, $j, 1);
386
			--$j; # outer loop
387
			last; # inner loop
388
		}
389
	}
390
391
	#
392
	# Find some good spots to split out into and record columns
393
	# that will be used soon in the @slot list.
394
	#
395
	push(@slot, $orig_vine);
396
	my $parent = 0;
397
398
	for (my $seeker = 2; $parent < $#$parents &&
399
	    $seeker < 2 + $#$vine; ++$seeker)
400
	{
401
		my $idx = ($seeker % 2 == 0) ? -1 : 1;
402
		$idx   *= int($seeker / 2);
403
		$idx   *= 2;
404
		$idx   += $orig_vine;
405
406
		if ($idx >= 0 && $idx <= $#$vine && !defined($vine->[$idx])) {
407
			push(@slot, $idx);
408
			$vine->[$idx] = "0" x 40;
409
			++$parent;
410
		}
411
	}
412
	for (my $idx = $orig_vine + 2; $parent < $#$parents; $idx += 2) {
413
		if (!defined($vine->[$idx])) {
414
			push(@slot, $idx);
415
			++$parent;
416
		}
417
	}
418
419
	if (scalar(@slot) != scalar(@$parents)) {
420
		die "Serious internal problem";
421
	}
422
423
	@slot = sort { $a <=> $b } @slot;
424
	$max  = scalar(@$vine) + 2 * scalar(@slot);
425
426
	for (my $i = 0; $i < $max; ++$i) {
427
		str_expand(\$ret, $i + 1);
428
		if ($#slot >= 0 && $i == $slot[0]) {
429
			shift @slot;
430
			$vine->[$i] = shift @$parents;
431
			substr($ret, $i, 1) = ($i == $orig_vine) ? "S" : "s";
432
		} elsif (substr($ret, $i, 1) eq "s") {
433
			; # keep existing fanouts
434
		} elsif (defined($vine->[$i])) {
435
			substr($ret, $i, 1) = "I";
436
		} else {
437
			substr($ret, $i, 1) = " ";
438
		}
439
	}
440
441
	print &vis_post(&vis_fan($ret, "merge")), "\n";
442
}
443
444
#
445
# vis_* - transform control string into usable graphic
446
#
447
# To cut down on code, the three vine_* functions produce only a dumb,
448
# but already unambiguous, control string which needs some processing
449
# before it is ready for public display.
450
#
451
452
sub vis_commit
453
{
454
	my $s = shift @_;
455
	my $f = shift @_;
456
	$s =~ s/ +$//gs;
457
	if (defined $f) {
458
		$s .= $f;
459
	}
460
	return $s;
461
}
462
463
sub vis_fan
464
{
465
	my $s = shift @_;
466
	my $b = shift(@_) eq "branch";
467
468
	$s =~ s{s.*s}{
469
		$_ = $&;
470
		$_ =~ tr/ I/DO/;
471
		$_;
472
	}ei;
473
474
	# Transform an ODODO.. sequence into a contiguous overpass.
475
	$s =~ s{O[DO]+O}{"O" x length($&)}eg;
476
477
	# Do left/right edge transformation
478
	$s =~ s{(s.*)S(.*s)}{&vis_fan3($1, $2)}es ||
479
	$s =~ s{(s.*)S}{&vis_fan2L($1)."B"}es ||
480
	$s =~ s{S(.*s)}{"A".&vis_fan2R($1)}es ||
481
	die "Should not come here";
482
483
	if ($b) {
484
		$s =~ tr/efg/xyz/;
485
	}
486
487
	return $s;
488
}
489
490
sub vis_fan2L
491
{
492
	my $l = shift @_;
493
	$l =~ s/^s/e/;
494
	$l =~ s/s/f/g;
495
	return $l;
496
}
497
498
sub vis_fan2R
499
{
500
	my $r = shift @_;
501
	$r =~ s/s$/g/;
502
	$r =~ s/s/f/g;
503
	return $r;
504
}
505
506
sub vis_fan3
507
{
508
	my($l, $r) = @_;
509
	$l =~ s/^s/e/;
510
	$l =~ s/s/f/g;
511
	$r =~ s/s$/g/;
512
	$r =~ s/s/f/g;
513
	return "${l}K$r";
514
}
515
516
sub vis_xfrm
517
{
518
	# A: branch to right
519
	# B: branch to right
520
	# C: commit
521
	# D:
522
	# e: merge visual left (╔)
523
	# f: merge visual center (╦)
524
	# g: merge visual right (╗)
525
	# I: straight line (║)
526
	# K: branch visual split (╬)
527
	# m: single line (─)
528
	# O: overpass (≡)
529
	# r: root (╙)
530
	# t: tip (╓)
531
	# x: branch visual left (╚)
532
	# y: branch visual center (╩)
533
	# z: branch visual right (╝)
534
	# *: filler
535
536
	my $s = shift @_;
537
	my $spc = shift @_;
538
	if ($spc) {
539
		$s =~ s{[Ctr].*}{
540
			$_ = $&;
541
			$_ =~ s{ }{\*}g;
542
			$_;
543
		}esg;
544
	}
545
546
	if ($Reverse_order) {
547
		$s =~ tr/efg.rt.xyz/xyz.tr.efg/;
548
	}
549
550
	if ($Style == 1) {
551
		$s =~ tr/ABCD.efg.IKO.mrt.xyz/├┤├─.┌┬┐.│┼≡.─└┌.└┴┘/;
552
	} elsif ($Style == 2) {
553
		$s =~ tr/ABCD.efg.IKO.mrt.xyz/╠╣╟═.╔╦╗.║╬═.─╙╓.╚╩╝/;
554
	} elsif ($Style == 10) {
555
		$s =~ tr/ABCD.efg.IKO.mrt.xyz/├┤├─.╭┬╮.│┼≡.─└┌.╰┴╯/;
556
	} elsif ($Style == 15) {
557
		$s =~ tr/ABCD.efg.IKO.mrt.xyz/┣┫┣━.┏┳┓.┃╋☰.━┗┏.┗┻┛/;
558
	}
559
	return $s;
560
}
561
562
#
563
# vis_post - post-process/transform vine graphic
564
# Apply user-specific style transformation.
565
#
566
sub vis_post
567
{
568
	my $s = shift @_;
569
	my $f = shift @_;
570
571
	$s = vis_xfrm($s, defined $f);
572
	$f =~ s/^([^\x1b]+)/vis_xfrm($1)/e;
573
	$f =~ s/(\x1b.*?m)([^\x1b]+)/$1.vis_xfrm($2)/eg;
574
	if (defined $f) {
575
		$s =~ s/\*/$f/g;
576
		$s =~ s{\Q$Color{default}\E}{$&.$Color{tree}}egs;
577
		$s .= $f;
578
	}
579
580
	return $Color{tree}, encode('UTF-8', $s), $Color{default};
581
}
582
583
sub remove_trailing_blanks
584
{
585
	my $a = shift @_;
586
587
	while (scalar(@$a) > 0 && !defined($a->[$#$a])) {
588
		pop(@$a);
589
	}
590
}
591
592
sub round_down2
593
{
594
	my $i = shift @_;
595
	if ($i < 0) {
596
		return $i;
597
	}
598
	return $i & ~1;
599
}
600
601
sub str_expand
602
{
603
	my $r = shift @_;
604
	my $l = shift @_;
605
606
	if (length($$r) < $l) {
607
		$$r .= " " x ($l - length($$r));
608
	}
609
}
610
611
package ReverseOutput;
612
require Tie::Handle;
613
@ReverseOutput::ISA = qw(Tie::Handle);
614
615
sub TIEHANDLE
616
{
617
	my $class = shift @_;
618
	my $self  = {};
619
620
	open($self->{fh}, ">&STDOUT");
621
622
	return bless $self, $class;
623
}
624
625
sub PRINT
626
{
627
	my $self = shift @_;
628
	my $fh   = $self->{fh};
629
630
	$self->{saved} .= join($\, @_);
631
}
632
633
sub UNTIE
634
{
635
	my $self = shift @_;
636
	my $fh   = $self->{fh};
637
638
	print $fh join($/, reverse split(/\n/s, $self->{saved}));
639
	print $fh "\n";
640
	undef $self->{saved};
641
}