Signed-off-by: Jan Engelhardt <jengelh@xxxxxxxxxxxxxxx> --- contrib/git-forest/git-forest | 422 +++++++++++++++++++++++++++++ contrib/git-forest/git-forest.txt | 53 ++++ 2 files changed, 475 insertions(+), 0 deletions(-) create mode 100755 contrib/git-forest/git-forest create mode 100644 contrib/git-forest/git-forest.txt diff --git a/contrib/git-forest/git-forest b/contrib/git-forest/git-forest new file mode 100755 index 0000000..c38b218 --- /dev/null +++ b/contrib/git-forest/git-forest @@ -0,0 +1,422 @@ +#!/usr/bin/perl +# +# git-森æ?? +# text-based tree visualisation +# Copyright © Jan Engelhardt <jengelh [at] gmx 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 encoding "utf8"; +my $Repo = Git->repository($ENV{"GIT_DIR"} || "."); +my $Show_all = 0; +my $Show_rebase = 1; +my $Style = 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; }, + "style=i" => \$Style, + "sha" => \$With_sha, + ); + if ($Show_all) { + unshift(@ARGV, "--all", "HEAD"); + } + &process(); +} + +sub process () +{ + my(@vine); + my $refs = &get_refs(); + my($fh, $fhc) = $Repo->command_output_pipe("log", "--date-order", + "--pretty=format:<%H><%h><%P>%s", @ARGV); + + while (defined(my $line = <$fh>)) { + chomp $line; + my($sha, $mini_sha, $parents, $msg) = + ($line =~ /^<(.*?)><(.*?)><(.*?)>(.*)/s); + 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}."â??".$Color{default})); + &ref_print($refs->{$sha}); + } else { + print &vis_post(&vis_commit($ra, " ")); + } + if ($With_sha) { + print $msg, $Color{at}, "â??â??(", $Color{sha}, $mini_sha, + $Color{at}, ")", $Color{default}, "\n"; + } else { + print $msg, "\n"; + } + + &vine_merge(\@vine, $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) { + my $up = $Repo->command("rev-parse", ".dotest-merge/upstream"); + my $old = $Repo->command("rev-parse", ".dotest-merge/head"); + chomp $up; + chomp $old; + unshift(@{$ret->{$up}}, "rebase/upstream"); + unshift(@{$ret->{$old}}, "rebase/old-HEAD"); + } + + my $head = $Repo->command("rev-parse", "HEAD"); + chomp $head; + if ($rebase) { + unshift(@{$ret->{$head}}, "rebase/inprogress"); + } + unshift(@{$ret->{$head}}, "HEAD"); + + return $ret; +} + +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}, "]â??â??", $Color{default}; + } +} + +sub vine_branch ($$) +{ + my($vine, $rev) = @_; + my $idx; + + my $left = "â? "; + my $matched = 0; + my $ret; + + for ($idx = 0; $idx < scalar(@$vine); ++$idx) { + if (!defined($vine->[$idx])) { + $ret .= "â??"; + next; + } elsif ($vine->[$idx] ne $rev) { + $ret .= "â?ª"; + next; + } + if ($matched == 0) { + $ret .= "â? "; + } else { + $ret .= "â?©"; + $vine->[$idx] = undef; + } + ++$matched; + } + + if ($matched < 2) { + return; + } + + while (!defined($vine->[$#$vine])) { + pop(@$vine); + } + + print &vis_post(&vis_branch($ret)), "\n"; +} + +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 .= "â??"; + } else { + $ret .= "â??"; + } + } + + if ($ret !~ /â??/) { + # Not having produced a â?? before means this is a HEAD + $ret .= "â??"; + push(@$vine, $rev); + } + + while (scalar(@$vine) > 0 && !defined($vine->[$#$vine])) { + pop(@$vine); + } + + if (scalar(@$parents) == 0) { + # tree root + $ret =~ s/â??/â??/g; + } + + return $ret; +} + +# +# Generate vine graphics for a merge +# +sub vine_merge ($$$) +{ + my($vine, $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]; + + while (scalar(@$vine) > 0 && !defined($vine->[$#$vine])) { + pop(@$vine); + } + return; + } + + # + # Find some good spots to split out into. + # + push(@slot, $orig_vine); + my $parent = 0; + + for (my $seeker = 2; $parent < $#$parents && + $seeker < 2 + 2 * $#$vine; ++$seeker) + { + my $idx = ($seeker % 2 == 0) ? -1 : 1; + $idx *= int($seeker / 2); + $idx += $orig_vine; + + if ($idx >= 0 && $idx <= $#$vine && !defined($vine->[$idx])) { + push(@slot, $idx); + ++$parent; + } + } + for (my $idx = $orig_vine + 1; $parent < $#$parents; ++$idx) { + 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) + scalar(@slot); + + for (my $i = 0; $i < $max; ++$i) { + if ($#slot >= 0 && $i == $slot[0]) { + shift @slot; + $vine->[$i] = shift @$parents; + $ret .= ($i == $orig_vine) ? "S" : "s"; + } elsif (defined($vine->[$i])) { + $ret .= "â??"; + } else { + $ret .= " "; + } + + } + + $ret =~ s/ +$//gs; + print &vis_post(&vis_merge($ret)), "\n"; +} + +sub vis_branch ($) +{ + # Sample input: â?¬â??â? â?¬â?¬â?¬â?©â?¬â??â?¬â?¬â?¬â?¬â?¬â?¬â?©â?¬â?©â??â?¬â?¬ + my $ra = shift @_; + my $i; + + $ra =~ s{^(.+?)â? }{ + $_ = $1; + $_ =~ tr/â?ªâ??/â?? /; + $_ =~ s/(.)/$1 /gs; + $_ .= 'â? '; + }es; + $ra =~ s{(â? .*)â?©}{ + $_ = $1; + $_ =~ s/(.)/$1â??/gs; + $_ .= 'â??'; + }es; + $ra =~ s{â??(.*)$}{ + $_ = $1; + $_ =~ tr/â?ªâ??/â?? /; + $_ =~ s/(.)/$1 /gs; + $_ = "â?? $_"; + }es; + return $ra; +} + +sub vis_commit ($$) +{ + my($ra, $sep) = @_; + my($l, $r) = ($ra =~ /^(.*?)([â??â??â??].*)/); + $l =~ s/(.)/$1 /gs; + $r =~ s/(.)/$1 /gs; + $r =~ s/ /$sep/gs; + return $l.$r; +} + +sub vis_merge ($) +{ + my $s = shift @_; + + if ($s =~ s/(s.*)S(.*s)/&vis_merge3($1, $2)/es) { + ; + } elsif ($s =~ /(?:s.*)S/s) { + while ($s =~ s/(s.*)â??(.*S)/$1â?ª$2/s) { + ; + } + $s =~ s/(s.*)S/&vis_merge2L($1)."â?£"/es; + } elsif ($s =~ /S(?:.*s)/s) { + while ($s =~ s/(S.*)â??(.*s)/$1â?ª$2/s) { + ; + } + $s =~ /S(.*s)/; + $s =~ s/S(.*s)/"â? ".&vis_merge2R($1)/es; + } else { + # $s =~ s/S/â??/s; + die "Should not come here"; + } + $s =~ s{(.)}{&vis_merge1($1)}egs; + return $s; +} + +sub vis_merge1 ($) +{ + if ($_[0] eq "â??" || $_[0] eq "â?¦" || $_[0] eq "â? " || $_[0] eq "â?ª") { + return $_[0]."â??"; + } else { + return $_[0]." "; + } +} + +sub vis_merge2L ($) +{ + my $l = shift @_; + $l =~ s/^s/â??/; + $l =~ s/s/â?¦/g; + return $l; +} + +sub vis_merge2R ($) +{ + my $r = shift @_; + $r =~ s/s$/â??/; + $r =~ s/s/â?¦/g; + return $r; +} + +sub vis_merge3 ($$) +{ + my($l, $r) = shift @_; + $l =~ s/^s/â??/; + $l =~ s/s/â?¦/g; + $r =~ s/s$/â??/; + $r =~ s/s/â?¦/g; + return "$lâ?ª$r"; +} + +# +# post-process vine graphic +# +sub vis_post ($) +{ + my $s = shift @_; + + if ($Style == 1) { + $s =~ tr/â??â?¦â??â? â?¬â?£â??â?©â??â??â??â??â??â??â?ª/â??â?¬â??â??â?¼â?¤â??â?´â??â??â??â??â??â??â?¼/; + } elsif ($Style == 2) { + $s =~ tr/â?ª/â?¬/; + } elsif ($Style == 3) { + $s =~ tr/â??â?¦â??â? â?¬â?£â??â?©â??â??â??â??â??/â??â?¤â??â??â?ªâ?¡â??â?§â??â??â??â??â??/; + } elsif ($Style == 4) { + $s =~ tr/â??â?¦â??â? â?¬â?£â??â?©â??â?ªâ??/â??â?¥â??â??â?«â?¢â??â?¨â??â?«â??/; + } + + if ($Color{default} ne "") { + $s =~ s{\Q$Color{default}\E}{$&$Color{tree}}g; + } + return $Color{tree}, $s, $Color{default}; +} diff --git a/contrib/git-forest/git-forest.txt b/contrib/git-forest/git-forest.txt new file mode 100644 index 0000000..5dfd478 --- /dev/null +++ b/contrib/git-forest/git-forest.txt @@ -0,0 +1,53 @@ + +Usage: git-forest [OPTIONS...] [REVLISTOPTIONS] [REFSPEC...] + +--no-rebase + Do not show rebase/ pseudo-refs +--style=1 + Use single-line visuals +--style=2 + Use double-line visuals (default) +--style=3 + Use single-line visuals in vertical direction, + and double-line ones in horizontal direction. +--style=4 + Use double-line visuals in vertical direction, + and single-line ones in horizontal direction. +--sha + Display SHAs for each commit + +All other options and arguments are passed down to git-log. +Commonly useful options are --all and --topo-order. + +Example: + git-forest --all | less -RS + git-forest origin/master | less -RS + +This tool does not try to minimize the empty space between branches +like gitk does. Take it as a feature. + +Notes on interpretation: + +Connections with four "legs" ('â?¬' or variants thereof, like 'â?ª') are +meant to be interpreted as being connected only in the horizontal and +vertical direction, NOT around-the-corner. + +Connections with three or less "legs" ('â? ', 'â?©', 'â?¦', etc.) imply +a connection to all connected directions. + +Consider the following example history. The merge at 070 has merged +together the branches with commits f21, 886 and 8f8, but NOT d3e. + + â??â??[HEAD]â??â??[master]â??â??-â??â??(0c6) + â? â??â?? + â?? â?? -â??â??(710) + â?? â?? -â??â??(070) + â? â??â?¬â??â?¦â??â?? + â?? â?? â?? â?? -â??â??(d3e) + â?? â?? â?? â?? -â??â??(886) + â?? â?? â?? â?? -â??â??(8f8) + â?? â? â??â?? â?? + â?? â?? â?? -â??â??(199) + â?? â?? â?? -â??â??(f21) + â? â??â?©â??â??â??â?? + â?? -â??â??(15f) -- 1.5.4.4 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html