Subject: [PATCH 16/19] gitweb: Use git-diff-tree or git-diff patch output for blobdiff

From: Jakub Narebski <[email-masked]>
Date: Fri, 25 Aug 2006 21:13:34 +0200

This is second part of removing gitweb dependency on external
diff (used in git_diff_print).

Get rid of git_diff_print invocation in git_blobdiff, and use either
git-diff-tree (when both hash_base and hash_parent_base are provided)
patch format or git-diff patch format (when only hash and hash_parent
are provided) for output.

Supported URI schemes, and output formats:
* New URI scheme: both hash_base and hash_parent_base (trees-ish
  containing blobs versions we want to compare) are provided.
  Also either filename is provided, or hash (of blob) is provided
  (we try to find filename then).

  For this scheme we have copying and renames detection, mode changes,
  file types etc., and information extended diff header is correct.

* Old URI scheme: hash_parent_base is not provided, we use hash and
  hash_parent to directly compare blobs using git-diff. If no filename
  is given, blobs hashes are used in place of filenames.

  This scheme has always "blob" as file type, it cannot detect mode
  changes, and we rely on CGI parameters to provide name of the file.

Added git_to_hash subroutine, which transforms symbolic name or list
of symbolic name to hash or list of hashes using git-rev-parse.

To have "blob" instead of "unknown" (or "file" regardless of the type)
in "gitweb diff header" for legacy scheme, file_type function now
returns its argument if it is not octal string.

Added support for fake "2" status code in git_patchset_body. Such code
is generated by git_blobdiff in legacy scheme case.

ATTENTION: The order of arguments (operands) to git-diff is reversed
(sic!) to have correct diff in the legacy (no hash_parent_base) case.
$hash_parent, $hash ordering is commented out, as it gives reversed
patch (at least for git version 1.4.1.1) as compared to output in new
scheme and output of older gitweb version.

Signed-off-by: Jakub Narebski <[email-masked]>
---
P.S. This is the place to mention that it would be nice to have in 
gitweb/README specification what minimal version of git is needed
for some gitweb features; e.g. "history" relies on having --full-history
option to git-rev-list, "blobdiff" (and later "blobdiff_plain") relies
on git-diff which accepts blob hashes, and relies on bug in git-diff...

 gitweb/gitweb.perl |  149 ++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 132 insertions(+), 17 deletions(-)

diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index a866922..9be2b2c 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -454,7 +454,13 @@ sub mode_str {
 
 # convert file mode in octal to file type string
 sub file_type {
-	my $mode = oct shift;
+	my $mode = shift;
+
+	if ($mode !~ m/^[0-7]+$/) {
+		return $mode;
+	} else {
+		$mode = oct $mode;
+	}
 
 	if (S_ISDIR($mode & S_IFMT)) {
 		return "directory";
@@ -618,6 +624,26 @@ sub git_get_hash_by_path {
 	return $3;
 }
 
+# converts symbolic name to hash
+sub git_to_hash {
+	my @params = @_;
+	return undef unless @params;
+
+	open my $fd, "-|", $GIT, "rev-parse", @params
+		or return undef;
+	my @hashes = map { chomp; $_ } <$fd>;
+	close $fd;
+
+	if (wantarray) {
+		return @hashes;
+	} elsif (scalar(@hashes) == 1) {
+		# single hash
+		return $hashes[0];
+	} else {
+		return \@hashes;
+	}
+}
+
 ## ......................................................................
 ## git utility functions, directly accessing git repository
 
@@ -1672,7 +1698,8 @@ sub git_patchset_body {
 				      "</div>\n"; # class="diff_info"
 
 			} elsif ($diffinfo->{'status'} eq "R" || # renamed
-			         $diffinfo->{'status'} eq "C") { # copied
+			         $diffinfo->{'status'} eq "C" || # copied
+			         $diffinfo->{'status'} eq "2") { # with two filenames (from git_blobdiff)
 				print "<div class=\"diff_info\">" .
 				      file_type($diffinfo->{'from_mode'}) . ":" .
 				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
@@ -2788,14 +2815,102 @@ sub git_commit {
 }
 
 sub git_blobdiff {
-	mkdir($git_temp, 0700);
-	git_header_html();
+	my $fd;
+	my @difftree;
+	my %diffinfo;
+
+	# preparing $fd and %diffinfo for git_patchset_body
+	# new style URI
+	if (defined $hash_base && defined $hash_parent_base) {
+		if (defined $file_name) {
+			# read raw output
+			open $fd, "-|", $GIT, "diff-tree", '-r', '-M', '-C', $hash_parent_base, $hash_base,
+				"--", $file_name
+				or die_error(undef, "Open git-diff-tree failed");
+			@difftree = map { chomp; $_ } <$fd>;
+			close $fd
+				or die_error(undef, "Reading git-diff-tree failed");
+			@difftree
+				or die_error('404 Not Found', "Blob diff not found");
+
+		} elsif (defined $hash) { # try to find filename from $hash
+			if ($hash !~ /[0-9a-fA-F]{40}/) {
+				$hash = git_to_hash($hash);
+			}
+
+			# read filtered raw output
+			open $fd, "-|", $GIT, "diff-tree", '-r', '-M', '-C', $hash_parent_base, $hash_base
+				or die_error(undef, "Open git-diff-tree failed");
+			@difftree =
+				# ':100644 100644 03b21826... 3b93d5e7... M	ls-files.c'
+				# $hash == to_id
+				grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
+				map { chomp; $_ } <$fd>;
+			close $fd
+				or die_error(undef, "Reading git-diff-tree failed");
+			@difftree
+				or die_error('404 Not Found', "Blob diff not found");
+
+		} else {
+			die_error('404 Not Found', "Missing one of the blob diff parameters");
+		}
+
+		if (@difftree > 1) {
+			die_error('404 Not Found', "Ambiguous blob diff specification");
+		}
+
+		%diffinfo = parse_difftree_raw_line($difftree[0]);
+		$file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'};
+		$file_name   ||= $diffinfo{'to_file'}   || $diffinfo{'file'};
+
+		$hash_parent ||= $diffinfo{'from_id'};
+		$hash        ||= $diffinfo{'to_id'};
+
+		# open patch output
+		open $fd, "-|", $GIT, "diff-tree", '-r', '-p', '-M', '-C', $hash_parent_base, $hash_base,
+			"--", $file_name
+			or die_error(undef, "Open git-diff-tree failed");
+	}
+
+	# old/legacy style URI
+	if (!%diffinfo && # if new style URI failed
+	    defined $hash && defined $hash_parent) {
+		# fake git-diff-tree raw output
+		$diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
+		$diffinfo{'from_id'} = $hash_parent;
+		$diffinfo{'to_id'}   = $hash;
+		if (defined $file_name) {
+			if (defined $file_parent) {
+				$diffinfo{'status'} = '2';
+				$diffinfo{'from_file'} = $file_parent;
+				$diffinfo{'to_file'}   = $file_name;
+			} else { # assume not renamed
+				$diffinfo{'status'} = '1';
+				$diffinfo{'from_file'} = $file_name;
+				$diffinfo{'to_file'}   = $file_name;
+			}
+		} else { # no filename given
+			$diffinfo{'status'} = '2';
+			$diffinfo{'from_file'} = $hash_parent;
+			$diffinfo{'to_file'}   = $hash;
+		}
+
+		#open $fd, "-|", $GIT, "diff", '-p', $hash_parent, $hash
+		open $fd, "-|", $GIT, "diff", '-p', $hash, $hash_parent
+			or die_error(undef, "Open git-diff failed");
+	} else  {
+		die_error('404 Not Found', "Missing one of the blob diff parameters")
+			unless %diffinfo;
+	}
+
+	# header
 	my $formats_nav =
 		$cgi->a({-href => href(action=>"blobdiff_plain",
 		                       hash=>$hash, hash_parent=>$hash_parent,
 		                       hash_base=>$hash_base, hash_parent_base=>$hash_parent_base,
 		                       file_name=>$file_name, file_parent=>$file_parent)},
 		        "plain");
+	git_header_html();
 	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
 		git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 		git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
@@ -2803,19 +2918,19 @@ sub git_blobdiff {
 		print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
 		print "<div class=\"title\">$hash vs $hash_parent</div>\n";
 	}
-	git_print_page_path($file_name, "blob", $hash_base);
-	print "<div class=\"page_body\">\n" .
-	      "<div class=\"diff_info\">blob:" .
-	      $cgi->a({-href => href(action=>"blob", hash=>$hash_parent,
-	                             hash_base=>$hash_parent_base, file_name=>($file_parent || $file_name))},
-	              $hash_parent) .
-	      " -> blob:" .
-	      $cgi->a({-href => href(action=>"blob", hash=>$hash,
-	                             hash_base=>$hash_base, file_name=>$file_name)},
-	              $hash) .
-	      "</div>\n";
-	git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
-	print "</div>"; # page_body
+	if (defined $file_name) {
+		git_print_page_path($file_name, "blob", $hash_base);
+	} else {
+		print "<div class=\"page_path\"></div>\n";
+	}
+
+	# patch
+	print "<div class=\"page_body\">\n";
+
+	git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
+	close $fd;
+
+	print "</div>\n"; # class="page_body"
 	git_footer_html();
 }
 
-- 
1.4.1.1