Created
March 8, 2017 16:53
-
-
Save andpol5/d07b8c6df50b6c4f84f8ea209c2fec77 to your computer and use it in GitHub Desktop.
Git-Forest script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment