* [PATCH 1/1] Import git-forest
2008-04-02 20:58 [announce+patch] git-forest 20080402 Jan Engelhardt
@ 2008-04-02 20:58 ` Jan Engelhardt
2008-04-02 21:49 ` Junio C Hamano
2008-04-03 10:14 ` [announce+patch] git-forest 20080402 Jeff King
1 sibling, 1 reply; 8+ messages in thread
From: Jan Engelhardt @ 2008-04-02 20:58 UTC (permalink / raw)
To: git; +Cc: jnareb, kzak
Signed-off-by: Jan Engelhardt <jengelh@computergmbh.de>
---
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
^ permalink raw reply related [flat|nested] 8+ messages in thread