[announce+patch] git-forest 20080402

3 messages, 2 authors, 2016-08-13 · open the first message on its own page

[announce+patch] git-forest 20080402

From: Jan Engelhardt <hidden>
Date: 2016-08-13 23:22:47

Hi,


here is the current snapshot of git-forest, a text-based tree 
visualizer, as a patch to the git.git/contrib/ directory.

What's new since the last posting is that it now displays [HEAD]
along with a commit, since HEAD does not always need to be the
first commit that is graphed.
    The --all option now includes graphing a detached HEAD, as can
happen on switching to a non-branch and during rebases.
    Speaking of rebases, git-forest will now also show where you are in 
a rebase operation -- quite handy if you forget whether you actually are 
in one (happened to me time and again and when you finally notice, you 
need to play with HEAD@{x} to restore it) -- it can be suppressed 
with --no-rebase.

I updated the screenshot at 
http://jengelh.hopto.org/images/git-forest.png (URL unchanged).
Color-agnostic people may use the --no-color option. (Tag colors 
follow gitk as far as the 16 colors permit.)

The direct grab URL for the script is still
http://dev.computergmbh.de/gitweb.cgi?p=hxtools;a=blob_plain;f=bin/git-forest
but of course a patch is provided below with the intent of getting it 
into contrib/ at least.


thanks,
Jan

N.B.: This was a ridiculous command to get it right...
can't this be done easier?
`git-format-patch -C -M --stat=72 --summary -p --thread -n HEAD^..HEAD &&
git-send-email --suppress-from --no-chain --compose --to git@xxx 00*`

[PATCH 1/1] Import git-forest

From: Jan Engelhardt <hidden>
Date: 2016-06-15 22:44:27

Signed-off-by: Jan Engelhardt <redacted>
---
 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

Re: [announce+patch] git-forest 20080402

From: Jeff King <hidden>
Date: 2016-06-15 22:44:27

On Wed, Apr 02, 2008 at 10:58:14PM +0200, Jan Engelhardt wrote:
N.B.: This was a ridiculous command to get it right...
can't this be done easier?
`git-format-patch -C -M --stat=72 --summary -p --thread -n HEAD^..HEAD &&
git-send-email --suppress-from --no-chain --compose --to git@xxx 00*`
Most of the time you don't need -C -M (unless you are actually renaming
stuff). If you always want it, try:

  git config --global diff.renames copies

format-patch already defaults to "--stat --summary -p" if you give it no
format options. The default stat is 80, I believe; for format-patch it
should probably be bumped to 72 anyway. format-patch will also default
to ..HEAD if just given HEAD^.

So you should be able to do:

  git format-patch --thread -n HEAD^

For your send-email invocation, doing:

  git config --global sendemail.suppressfrom true
  git config --global sendemail.chainreplyto false
  # the --global makes the others user-wide; this
  # one should just be specific to your git repo
  git config sendemail.to git@vger.kernel.org

should allow:

  git sendemail --compose 00*

though if you are just sending a single patch, it is common to put the
cover letter material (if it is short) after the '---' but before the
diffstat.

HTH,
-Peff
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help