#!/usr/bin/perl # # git-森林 # text-based tree visualisation # Copyright © Jan Engelhardt <jengelh [at] medozas de>, 2008 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 or 3 of the license. # use Getopt::Long; use Git; use strict; use utf8; use Encode qw(encode); my $Repo = Git->repository($ENV{"GIT_DIR"} || "."); my $Pretty_fmt = "format:%s"; my $Reverse_order = 0; my $Show_all = 0; my $Show_rebase = 1; my $Style = 1; my $Subvine_depth = 2; my $With_sha = 0; my %Color = ( "default" => "\e[0m", # ] "at" => "\e[1;30m", # ] "hhead" => "\e[1;31m", # ] "head" => "\e[1;32m", # ] "ref" => "\e[1;34m", # ] "remote" => "\e[1;35m", # ] "sha" => "\e[0;31m", # ] "tag" => "\e[1;33m", # ] "tree" => "\e[0;33m", # ] ); &main(); sub main { &Getopt::Long::Configure(qw(bundling pass_through)); &GetOptions( "all" => \$Show_all, "no-color" => sub { %Color = (); }, "no-rebase" => sub { $Show_rebase = 0; }, "a" => sub { $Pretty_fmt = "format:\e[1;30m(\e[0;32m%an\e[1;30m)\e[0m %s"; }, # ]]]] "pretty=s" => \$Pretty_fmt, "reverse" => \$Reverse_order, "svdepth=i" => \$Subvine_depth, "style=i" => \$Style, "sha" => \$With_sha, ); ++$Subvine_depth; if (substr($Pretty_fmt, 0, 7) ne "format:") { die "If you use --pretty, it must be in the form of --pretty=format:"; } $Pretty_fmt = substr($Pretty_fmt, 7); while ($Pretty_fmt =~ /\%./g) { if ($& eq "\%b" || $& eq "\%n" || ($&.$') =~ /^\%x0a/i) { die "Cannot use \%b, \%n or \%x0a in --pretty=format:"; } } if ($Show_all) { # # Give --all back. And add HEAD to include commits # in the rev list that belong to a detached HEAD. # unshift(@ARGV, "--all", "HEAD"); } if ($Reverse_order) { tie(*STDOUT, "ReverseOutput"); } &process(); if ($Reverse_order) { untie *STDOUT; } } sub get_line_block { my($fh, $max) = @_; while (scalar(@h::ist) < $max) { my $x; $x = <$fh>; if (!defined($x)) { last; } push(@h::ist, $x); } my @ret = (shift @h::ist); foreach (2..$max) { push(@ret, $h::ist[$_-2]); } return @ret; } sub process { my(@vine); my $refs = &get_refs(); my($fh, $fhc) = $Repo->command_output_pipe("log", "--date-order", "--pretty=format:<%H><%h><%P>$Pretty_fmt", @ARGV); while (my($line, @next_sha) = get_line_block($fh, $Subvine_depth)) { if (!defined($line)) { last; } chomp $line; my($sha, $mini_sha, $parents, $msg) = ($line =~ /^<(.*?)><(.*?)><(.*?)>(.*)/s); my @next_sha = map { ($_) = /^<(.*?)>/ } @next_sha; my @parents = split(" ", $parents); &vine_branch(\@vine, $sha); my $ra = &vine_commit(\@vine, $sha, \@parents); if (exists($refs->{$sha})) { print &vis_post(&vis_commit($ra), $Color{at}."m".$Color{default}); &ref_print($refs->{$sha}); } else { print &vis_post(&vis_commit($ra, " ")); } if ($With_sha) { print $msg, $Color{at}, encode('UTF-8', "──("), $Color{sha}, $mini_sha, $Color{at}, ")", $Color{default}, "\n"; } else { print $msg, "\n"; } &vine_merge(\@vine, $sha, \@next_sha, \@parents); } $Repo->command_close_pipe($fh, $fhc); } sub get_refs { my($fh, $c) = $Repo->command_output_pipe("show-ref"); my $ret = {}; while (defined(my $ln = <$fh>)) { chomp $ln; if (length($ln) == 0) { next; } my($sha, $name) = ($ln =~ /^(\S+)\s+(.*)/s); if (!exists($ret->{$sha})) { $ret->{$sha} = []; } push(@{$ret->{$sha}}, $name); if ($name =~ m{^refs/tags/}) { my $sub_sha = $Repo->command("log", "-1", "--pretty=format:%H", $name); chomp $sub_sha; if ($sha ne $sub_sha) { push(@{$ret->{$sub_sha}}, $name); } } } $Repo->command_close_pipe($fh, $c); my $rebase = -e $Repo->repo_path()."/.dotest-merge/git-rebase-todo" && $Show_rebase; if ($rebase) { if (open(my $act_fh, $Repo->repo_path(). "/.dotest-merge/git-rebase-todo")) { my($curr) = (<$act_fh> =~ /^\S+\s+(\S+)/); $curr = $Repo->command("rev-parse", $curr); chomp $curr; unshift(@{$ret->{$curr}}, "rebase/cn"); close $act_fh; } chomp(my $up = $Repo->command("rev-parse", ".dotest-merge/upstream")); chomp(my $onto = $Repo->command("rev-parse", ".dotest-merge/onto")); chomp(my $old = $Repo->command("rev-parse", ".dotest-merge/head")); unshift(@{$ret->{$up}}, "rebase/upstream"); unshift(@{$ret->{$onto}}, "rebase/onto"); unshift(@{$ret->{$old}}, "rebase/saved-HEAD"); } my $head = $Repo->command("rev-parse", "HEAD"); chomp $head; if ($rebase) { unshift(@{$ret->{$head}}, "rebase/new"); } unshift(@{$ret->{$head}}, "HEAD"); return $ret; } # # ref_print - print a ref with color # @s: ref name # sub ref_print { foreach my $symbol (@{shift @_}) { print $Color{at}, "["; if ($symbol eq "HEAD" || $symbol =~ m{^rebase/}) { print $Color{hhead}, $symbol; } elsif ($symbol =~ m{^refs/(remotes/[^/]+)/(.*)}s) { print $Color{remote}, $1, $Color{head}, "/$2"; } elsif ($symbol =~ m{^refs/heads/(.*)}s) { print $Color{head}, $1; } elsif ($symbol =~ m{^refs/tags/(.*)}s) { print $Color{tag}, $1; } elsif ($symbol =~ m{^refs/(.*)}s) { print $Color{ref}, $1; } print $Color{at}, encode('UTF-8', "]──"), $Color{default}; } } # # vine_branch - # @vine: column array containing the expected parent IDs # @rev: commit ID # # Draws the branching vine matrix between a commit K and K^ (@rev). # sub vine_branch { my($vine, $rev) = @_; my $idx; my($matched, $master) = (0, 0); my $ret; # Transform array into string for ($idx = 0; $idx <= $#$vine; ++$idx) { if (!defined($vine->[$idx])) { $ret .= " "; next; } elsif ($vine->[$idx] ne $rev) { $ret .= "I"; next; } if (!$master && $idx % 2 == 0) { $ret .= "S"; $master = 1; } else { $ret .= "s"; $vine->[$idx] = undef; } ++$matched; } if ($matched < 2) { return; } &remove_trailing_blanks($vine); print &vis_post(&vis_fan($ret, "branch")), "\n"; } # # vine_commit - # @vine: column array containing the expected IDs # @rev: commit ID # @parents: array of parent IDs # sub vine_commit { my($vine, $rev, $parents) = @_; my $ret; for (my $i = 0; $i <= $#$vine; ++$i) { if (!defined($vine->[$i])) { $ret .= " "; } elsif ($vine->[$i] eq $rev) { $ret .= "C"; } else { $ret .= "I"; } } if ($ret !~ /C/) { # Not having produced a C before means this is a tip my $i; for ($i = &round_down2($#$vine); $i >= 0; $i -= 2) { if (substr($ret, $i, 1) eq " ") { substr($ret, $i, 1) = "t"; $vine->[$i] = $rev; last; } } if ($i < 0) { if (scalar(@$vine) % 2 != 0) { push(@$vine, undef); $ret .= " "; } $ret .= "t"; push(@$vine, $rev); } } &remove_trailing_blanks($vine); if (scalar(@$parents) == 0) { # tree root $ret =~ tr/C/r/; } return $ret; } # # vine_merge - # @vine: column array containing the expected parent IDs # @rev: commit ID # @next_rev: next commit ID in the revision list # @parents: parent IDs of @rev # # Draws the merging vine matrix between a commit K (@rev) and K^ (@parents). # sub vine_merge { my($vine, $rev, $next_rev, $parents) = @_; my $orig_vine = -1; my @slot; my($ret, $max); for (my $i = 0; $i <= $#$vine; ++$i) { if ($vine->[$i] eq $rev) { $orig_vine = $i; last; } } if ($orig_vine == -1) { die "vine_commit() did not add this vine."; } if (scalar(@$parents) <= 1) { # # A single parent does not need a visual. Update and return. # $vine->[$orig_vine] = $parents->[0]; &remove_trailing_blanks($vine); return; } # # Put previously seen branches in the vine subcolumns # Need to keep at least one parent for the slot algorithm below. # for (my $j = 0; $j <= $#$parents && $#$parents > 0; ++$j) { for (my $idx = 0; $idx <= $#$vine; ++$idx) { if ($vine->[$idx] ne $parents->[$j] || !grep { my $z = $vine->[$idx]; /^\Q$z\E$/ } @$next_rev) { next; } if ($idx == $orig_vine) { die "Should not really happen"; } if ($idx < $orig_vine) { my $p = $idx + 1; if (defined($vine->[$p])) { $p = $idx - 1; } if (defined($vine->[$p])) { last; } $vine->[$p] = $parents->[$j]; str_expand(\$ret, $p + 1); substr($ret, $p, 1) = "s"; } else { my $p = $idx - 1; if (defined($vine->[$p]) || $p < 0) { $p = $idx + 1; } if (defined($vine->[$p])) { last; } $vine->[$p] = $parents->[$j]; str_expand(\$ret, $p + 1); substr($ret, $p, 1) = "s"; } splice(@$parents, $j, 1); --$j; # outer loop last; # inner loop } } # # Find some good spots to split out into and record columns # that will be used soon in the @slot list. # push(@slot, $orig_vine); my $parent = 0; for (my $seeker = 2; $parent < $#$parents && $seeker < 2 + $#$vine; ++$seeker) { my $idx = ($seeker % 2 == 0) ? -1 : 1; $idx *= int($seeker / 2); $idx *= 2; $idx += $orig_vine; if ($idx >= 0 && $idx <= $#$vine && !defined($vine->[$idx])) { push(@slot, $idx); $vine->[$idx] = "0" x 40; ++$parent; } } for (my $idx = $orig_vine + 2; $parent < $#$parents; $idx += 2) { if (!defined($vine->[$idx])) { push(@slot, $idx); ++$parent; } } if (scalar(@slot) != scalar(@$parents)) { die "Serious internal problem"; } @slot = sort { $a <=> $b } @slot; $max = scalar(@$vine) + 2 * scalar(@slot); for (my $i = 0; $i < $max; ++$i) { str_expand(\$ret, $i + 1); if ($#slot >= 0 && $i == $slot[0]) { shift @slot; $vine->[$i] = shift @$parents; substr($ret, $i, 1) = ($i == $orig_vine) ? "S" : "s"; } elsif (substr($ret, $i, 1) eq "s") { ; # keep existing fanouts } elsif (defined($vine->[$i])) { substr($ret, $i, 1) = "I"; } else { substr($ret, $i, 1) = " "; } } print &vis_post(&vis_fan($ret, "merge")), "\n"; } # # vis_* - transform control string into usable graphic # # To cut down on code, the three vine_* functions produce only a dumb, # but already unambiguous, control string which needs some processing # before it is ready for public display. # sub vis_commit { my $s = shift @_; my $f = shift @_; $s =~ s/ +$//gs; if (defined $f) { $s .= $f; } return $s; } sub vis_fan { my $s = shift @_; my $b = shift(@_) eq "branch"; $s =~ s{s.*s}{ $_ = $&; $_ =~ tr/ I/DO/; $_; }ei; # Transform an ODODO.. sequence into a contiguous overpass. $s =~ s{O[DO]+O}{"O" x length($&)}eg; # Do left/right edge transformation $s =~ s{(s.*)S(.*s)}{&vis_fan3($1, $2)}es || $s =~ s{(s.*)S}{&vis_fan2L($1)."B"}es || $s =~ s{S(.*s)}{"A".&vis_fan2R($1)}es || die "Should not come here"; if ($b) { $s =~ tr/efg/xyz/; } return $s; } sub vis_fan2L { my $l = shift @_; $l =~ s/^s/e/; $l =~ s/s/f/g; return $l; } sub vis_fan2R { my $r = shift @_; $r =~ s/s$/g/; $r =~ s/s/f/g; return $r; } sub vis_fan3 { my($l, $r) = @_; $l =~ s/^s/e/; $l =~ s/s/f/g; $r =~ s/s$/g/; $r =~ s/s/f/g; return "${l}K$r"; } sub vis_xfrm { # A: branch to right # B: branch to right # C: commit # D: # e: merge visual left (╔) # f: merge visual center (╦) # g: merge visual right (╗) # I: straight line (║) # K: branch visual split (╬) # m: single line (─) # O: overpass (≡) # r: root (╙) # t: tip (╓) # x: branch visual left (╚) # y: branch visual center (╩) # z: branch visual right (╝) # *: filler my $s = shift @_; my $spc = shift @_; if ($spc) { $s =~ s{[Ctr].*}{ $_ = $&; $_ =~ s{ }{\*}g; $_; }esg; } if ($Reverse_order) { $s =~ tr/efg.rt.xyz/xyz.tr.efg/; } if ($Style == 1) { $s =~ tr/ABCD.efg.IKO.mrt.xyz/├┤├─.┌┬┐.│┼≡.─└┌.└┴┘/; } elsif ($Style == 2) { $s =~ tr/ABCD.efg.IKO.mrt.xyz/╠╣╟═.╔╦╗.║╬═.─╙╓.╚╩╝/; } elsif ($Style == 10) { $s =~ tr/ABCD.efg.IKO.mrt.xyz/├┤├─.╭┬╮.│┼≡.─└┌.╰┴╯/; } elsif ($Style == 15) { $s =~ tr/ABCD.efg.IKO.mrt.xyz/┣┫┣━.┏┳┓.┃╋☰.━┗┏.┗┻┛/; } return $s; } # # vis_post - post-process/transform vine graphic # Apply user-specific style transformation. # sub vis_post { my $s = shift @_; my $f = shift @_; $s = vis_xfrm($s, defined $f); $f =~ s/^([^\x1b]+)/vis_xfrm($1)/e; $f =~ s/(\x1b.*?m)([^\x1b]+)/$1.vis_xfrm($2)/eg; if (defined $f) { $s =~ s/\*/$f/g; $s =~ s{\Q$Color{default}\E}{$&.$Color{tree}}egs; $s .= $f; } return $Color{tree}, encode('UTF-8', $s), $Color{default}; } sub remove_trailing_blanks { my $a = shift @_; while (scalar(@$a) > 0 && !defined($a->[$#$a])) { pop(@$a); } } sub round_down2 { my $i = shift @_; if ($i < 0) { return $i; } return $i & ~1; } sub str_expand { my $r = shift @_; my $l = shift @_; if (length($$r) < $l) { $$r .= " " x ($l - length($$r)); } } package ReverseOutput; require Tie::Handle; @ReverseOutput::ISA = qw(Tie::Handle); sub TIEHANDLE { my $class = shift @_; my $self = {}; open($self->{fh}, ">&STDOUT"); return bless $self, $class; } sub PRINT { my $self = shift @_; my $fh = $self->{fh}; $self->{saved} .= join($\, @_); } sub UNTIE { my $self = shift @_; my $fh = $self->{fh}; print $fh join($/, reverse split(/\n/s, $self->{saved})); print $fh "\n"; undef $self->{saved}; }