Subject: [PATCH] gitweb: Better symbolic link support in "tree" view

From: Jakub Narebsmi <[email-masked]>
Date: Mon, 4 Dec 2006 19:26:20 +0100

From: Jakub Narebski <[email-masked]>

In "tree" view (git_print_tree_entry subroutine), add for symbolic
links after file name " -> link_target", a la "ls -l".  Use
git_get_link_target_html to escape target name and make it into
hyperlink if possible.

Target of link is made into hyperlink when:
 * hash_base is provided (otherwise we cannot find hash of link
   target)
 * link is relative
 * in no place link goes out of root tree (top dir)
 * target of link exists for hash_base

Full path of symlink target from the root dir is provided in the title
attribute of hyperlink. If symlink target is a directory, '/' is added
to end of path in title attribute.

Currently symbolic link name uses ordinary file style (hidden
hyperlink), while the hyperlink to symlink target uses default
hyperlink style.

Signed-off-by: Jakub Narebski <[email-masked]>
---
 gitweb/gitweb.perl |   91 +++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 83 insertions(+), 8 deletions(-)

diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index ffe8ce1..3c1b75d 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -1989,12 +1989,83 @@ sub git_print_log ($;%) {
 	}
 }
 
+# print link tree entry (name of what link points to, possibly hyperlink)
+sub git_get_link_target_html {
+	my ($t, $basedir, $hash_base) = @_;
+
+	my $fd;
+	my %target = ();
+
+	# read link
+	open $fd, "-|", git_cmd(), "cat-file", "blob", $t->{'hash'};
+	{
+		local $/;
+		$target{'orig'} = <$fd>;
+	}
+	close $fd;
+
+	# we can make hyperlink out of link target only if $hash_base is provided
+	return esc_path($target{'orig'})
+		unless $hash_base;
+
+	# absolute links are returned as is, no hyperlink
+	if (substr($target{'orig'}, 0, 1) eq '/') {
+		return esc_path($target{'orig'});
+	}
+
+	# normalize link target to path from top (root) tree (dir)
+	if ($basedir) {
+		$target{'path'} = $basedir . '/' . $target{'orig'};
+	} else {
+		# we are in top (root) tree (dir)
+		$target{'path'} = $target{'orig'};
+	}
+	$target{'parts'} = [ ];
+	foreach my $part (split('/', $target{'path'})) {
+		# discard '.' and ''
+		next if (!$part || $part eq '.');
+		# handle '..'
+		if ($part eq '..') {
+			if (@{$target{'parts'}}) {
+				pop @{$target{'parts'}};
+			} else {
+				# link leads outside repository
+				return esc_path($target{'orig'});
+			}
+		} else {
+			push @{$target{'parts'}}, $part;
+		}
+	}
+	$target{'path'} = join('/', @{$target{'parts'}});
+
+	# check if path exists
+	open $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $target{'path'}
+		or return esc_path($target{'orig'});
+	my $line = <$fd>;
+	close $fd
+		or return esc_path($target{'orig'});
+	return esc_path($target{'orig'}) unless $line;
+
+	# parse ls-tree line to get type and hash of target of link
+	(undef, $target{'type'}, $target{'hash'}) =
+		($line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/);
+
+	# make a link if path exists
+	return $cgi->a({-href => href(action    => $target{'type'},
+	                              hash      => $target{'hash'},
+	                              file_name => $target{'path'},
+	                              hash_base => $hash_base),
+	                -title => $target{'path'} .
+	                          ($target{'type'} eq "tree" ? '/' : '')},
+	               esc_path($target{'orig'}));
+}
+
 # print tree entry (row of git_tree), but without encompassing <tr> element
 sub git_print_tree_entry {
 	my ($t, $basedir, $hash_base, $have_blame) = @_;
 
 	my %base_key = ();
-	$base_key{hash_base} = $hash_base if defined $hash_base;
+	$base_key{'hash_base'} = $hash_base if defined $hash_base;
 
 	# The format of a table row is: mode list link.  Where mode is
 	# the mode of the entry, list is the name of the entry, an href,
@@ -2005,16 +2076,20 @@ sub git_print_tree_entry {
 		print "<td class=\"list\">" .
 			$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
 			                       file_name=>"$basedir$t->{'name'}", %base_key),
-			        -class => "list"}, esc_path($t->{'name'})) . "</td>\n";
+			        -class => "list"}, esc_path($t->{'name'}));
+		if (S_ISLNK(oct $t->{'mode'})) {
+			print " -> " . git_get_link_target_html($t, $basedir, $hash_base);
+		}
+		print "</td>\n";
 		print "<td class=\"link\">";
 		print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
-					     file_name=>"$basedir$t->{'name'}", %base_key)},
-			      "blob");
+		                             file_name=>"$basedir$t->{'name'}", %base_key)},
+		              "blob");
 		if ($have_blame) {
 			print " | " .
 			      $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
-				                           file_name=>"$basedir$t->{'name'}", %base_key)},
-				            "blame");
+			                             file_name=>"$basedir$t->{'name'}", %base_key)},
+			              "blame");
 		}
 		if (defined $hash_base) {
 			print " | " .
@@ -2036,8 +2111,8 @@ sub git_print_tree_entry {
 		print "</td>\n";
 		print "<td class=\"link\">";
 		print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
-					     file_name=>"$basedir$t->{'name'}", %base_key)},
-			      "tree");
+		                             file_name=>"$basedir$t->{'name'}", %base_key)},
+		              "tree");
 		if (defined $hash_base) {
 			print " | " .
 			      $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
-- 
1.4.4.1