git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 5/5] gitweb: Incremental blame

From
MKMartin Koegler <mkoegler@auto.tuwien.ac.at>
Date
May 20, 2007, 20:23 UTC
Message-ID
<11796926121907-git-send-email-mkoegler@auto.tuwien.ac.at>
In-Reply-To
<11796926122089-git-send-email-mkoegler@auto.tuwien.ac.at>

Rewrite ontop of diff patches + some performance tuning. --- The first chunk is the new version of the link rewriting for git-blame. It does not need the blamelink class.

As gitweb.js is included in each page (and I didn't wanted to add a second JavaScript file), I added the content of blame.js at the end of gitweb.js.

I also tried some optimizations. In IceWeasel 2.0.0.3 (= FireFox), the blame of sha1_file.c needs now 33 instead of 46 seconds on my computer.

The "optimizations" are not tested in other browsers. According to my experience, I expect that the incremental blame in IE will be slower then in Mozilla.

 gitweb/gitweb.js   |  181 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 gitweb/gitweb.perl |   92 ++++++++++++++++++++++++---
 2 files changed, 264 insertions(+), 9 deletions(-)
diff --git a/gitweb/gitweb.js b/gitweb/gitweb.js
index cacab5a..001fac6 100644
--- a/gitweb/gitweb.js
+++ b/gitweb/gitweb.js
@@ -259,6 +259,11 @@ function GitAddLinks ()
     {
       var link = links[i];
       var url = splitGitURL (link.href);
+      if (url.a == "blame")
+	{
+	  url.a = "blame_incremental";
+	  link.href = url.ToURL();
+	}
       if (link.innerHTML == 'commit' || link.innerHTML == 'tag')
 	{
 	  if (!url.h)
@@ -296,3 +301,179 @@ function GitAddLinks ()
 }
 
 window.onload = GitAddLinks;
+
+// Blame Javascript functions
+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>
+// modifed by Martin Koegler <mkoegler@auto.tuwien.ac.at>
+
+function createRequestObject() {
+	var ro;
+	if (window.XMLHttpRequest) {
+		ro = new XMLHttpRequest();
+	} else {
+		ro = new ActiveXObject("Microsoft.XMLHTTP");
+	}
+	return ro;
+}
+
+var http;
+var baseUrl;
+
+// 'commits' is an associative map. It maps SHA1s to Commit objects.
+var commits = new Object();
+
+function Commit(sha1)
+{
+	this.sha1 = sha1;
+}
+
+function zeroPad(n)
+{
+	if (n < 10)
+		return '0' + n;
+	else
+		return n.toString();
+}
+
+function handleLine(commit)
+{
+	/* This is the structure of the HTML fragment we are working
+	   with:
+	   
+	   <tr id="l123" class="light2">
+	   <td class="sha1" title="">
+	   <a href=""></a>
+	   </td>
+	   <td class="linenr">
+	   <a class="linenr" href="">123</a>
+	   </td>
+	   <td class="pre"># times (my ext3 doesn&#39;t).</td>
+	   </tr>
+	 */
+	var tr = document.getElementById('l'+commit.resline);
+	var linkurl = baseUrl + ';a=blame_incremental;hb=' + commit.sha1
+	  + ';f=' + commit.filename + '#l' + commit.srcline;
+
+	var date = new Date();
+	date.setTime(commit.authorTime * 1000);
+	var dateStr = date.getUTCFullYear() + '-'
+	  + zeroPad(date.getUTCMonth()+1) + '-'
+	  + zeroPad(date.getUTCDate());
+	var timeStr = zeroPad(date.getUTCHours()) + ':'
+	  + zeroPad(date.getUTCMinutes()) + ':'
+	  + zeroPad(date.getUTCSeconds());
+	var title = commit.author + ', ' + dateStr + ' ' + timeStr;
+
+	for (var i = 0; i < commit.numlines; i++) {
+		tr.firstChild.title = title;
+		var shaAnchor = tr.firstChild.firstChild;
+		if (i == 0) {
+			shaAnchor.href = baseUrl + ';a=commit;h=' + commit.sha1;
+			shaAnchor.innerHTML = commit.sha1.substr(0, 8);
+		} else {
+		  if (shaAnchor.innerHTML != '')
+		    shaAnchor.innerHTML = '';
+		}
+
+		tr.firstChild.nextSibling.firstChild.href = linkurl;
+
+		tr = tr.nextSibling;
+		while (tr && tr.nodeType == 3)
+		  tr = tr.nextSibling;
+	}
+}
+
+function fixColors()
+{
+	var colorClasses = ['light2', 'dark2'];
+	var tr = document.getElementById('l1');
+	var colorClass = 0;
+
+	while (tr) {
+		if (tr.firstChild.firstChild.innerHTML != '')
+			colorClass = (colorClass+1)%2;
+
+		if (tr.className != colorClasses[colorClass])
+		  tr.className = colorClasses[colorClass];
+		tr = tr.nextSibling;
+		while (tr && tr.nodeType == 3)
+		  tr = tr.nextSibling;
+	}
+}
+
+var prevDataLength = -1;
+var nextLine = 0;
+var inProgress = false;
+
+var curCommit = new Commit();
+
+function handleResponse() {
+	if (http.readyState != 4 && http.readyState != 3)
+		return;
+
+	// In konqueror http.responseText is sometimes null here...
+	if (http.responseText === null)
+		return;
+
+	if (inProgress)
+		return;
+	else
+		inProgress = true;
+
+	while (prevDataLength != http.responseText.length) {
+		if (http.readyState == 4
+		    && prevDataLength == http.responseText.length) {
+			break;
+		}
+
+		prevDataLength = http.responseText.length;
+		var response = http.responseText.substring(nextLine);
+		var lines = response.split('\n');
+		nextLine = nextLine + response.lastIndexOf('\n') + 1;
+		if (response[response.length-1] != '\n') {
+			lines.pop();
+		}
+
+		for (var i = 0; i < lines.length; i++) {
+			var split = lines[i].split(' ');
+			if (split.length == 4 && split[0].length == 40) {
+				var sha1 = split[0];
+				var c = commits[sha1];
+				if (!c) {
+					c = new Commit(sha1);
+					commits[sha1] = c;
+				}
+
+				c.srcline = split[1];
+				c.resline = split[2];
+				c.numlines = split[3];
+				curCommit = c;
+			} else {
+				var info = split[0];
+				var data = lines[i].substr (info.length + 1);
+				if (info == 'filename') {
+					curCommit.filename = data;
+					handleLine(curCommit);
+				} else if (info == 'author') {
+					curCommit.author = data;
+				} else if (info == 'author-time') {
+					curCommit.authorTime = parseInt(data);
+				}
+			}
+		}
+	}
+
+	if (http.readyState == 4 && prevDataLength == http.responseText.length)
+		fixColors();
+
+	inProgress = false;
+}
+
+function startBlame(blamedataUrl, bUrl)
+{
+	baseUrl = bUrl;
+	http = createRequestObject();
+	http.open('get', blamedataUrl);
+	http.onreadystatechange = handleResponse;
+	http.send(null);
+}
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index f59a4b5..45787a6 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -462,6 +462,8 @@ $git_dir = "$projectroot/$project" if $project;
 # dispatch
 my %actions = (
 	"blame" => \&git_blame2,
+	"blame_incremental" => \&git_blame_incremental,
+	"blame_data" => \&git_blame_data,
 	"blobdiff" => \&git_blobdiff,
 	"blobdiff_plain" => \&git_blobdiff_plain,
 	"blob" => \&git_blob,
@@ -562,7 +564,7 @@ sub href(%) {
 			push @result, $symbol . "=" . esc_param($params{$name});
 		}
 	}
-	$href .= "?" . join(';', @result) if scalar @result;
+	$href .= "?" . join(';', @result) if $params{-partial_query} or scalar @result;
 
 	return $href;
 }
@@ -3517,7 +3519,47 @@ sub git_tag {
 	git_footer_html();
 }
 
-sub git_blame2 {
+sub git_blame_data {
+	my $fd;
+	my $ftype;
+
+	my ($have_blame) = gitweb_check_feature('blame');
+	if (!$have_blame) {
+		die_error('403 Permission denied', "Permission denied");
+	}
+	die_error('404 Not Found', "File name not defined") if (!$file_name);
+	$hash_base ||= git_get_head_hash($project);
+	die_error(undef, "Couldn't find base commit") unless ($hash_base);
+	my %co = parse_commit($hash_base)
+		or die_error(undef, "Reading commit failed");
+	if (!defined $hash) {
+		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
+			or die_error(undef, "Error looking up file");
+	}
+	$ftype = git_get_type($hash);
+	if ($ftype !~ "blob") {
+		die_error("400 Bad Request", "Object is not a blob");
+	}
+	open ($fd, "-|", git_cmd(), "blame", '--incremental', $hash_base, '--',
+	      $file_name)
+		or die_error(undef, "Open git-blame --incremental failed");
+
+	print $cgi->header(-type=>"text/plain", -charset => 'utf-8',
+	                   -status=> "200 OK");
+
+	while(<$fd>) {
+ 	  if (/^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/ or
+	     /^author-time |^author |^filename /) {
+ 	    print;
+	  }
+	}
+
+	close $fd or print "Reading blame data failed\n";
+}
+
+sub git_blame_common {
+	my ($type) = @_;
+
 	my $fd;
 	my $ftype;
 
@@ -3536,11 +3578,16 @@ sub git_blame2 {
 	}
 	$ftype = git_get_type($hash);
 	if ($ftype !~ "blob") {
-		die_error('400 Bad Request', "Object is not a blob");
+		die_error("400 Bad Request", "Object is not a blob");
+	}
+	if ($type eq 'incremental') {
+		open ($fd, "-|", git_cmd(), 'cat-file', 'blob', $hash)
+			or die_error(undef, "Open git-cat-file failed");
+	} else {
+		open ($fd, "-|", git_cmd(), 'blame', '-p', '--',
+		      $file_name, $hash_base)
+			or die_error(undef, "Open git-blame failed");
 	}
-	open ($fd, "-|", git_cmd(), "blame", '-p', '--',
-	      $file_name, $hash_base)
-		or die_error(undef, "Open git-blame failed");
 	git_header_html();
 	my $formats_nav =
 		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
@@ -3564,9 +3611,19 @@ sub git_blame2 {
 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
 HTML
 	my %metainfo = ();
-	while (1) {
-		$_ = <$fd>;
-		last unless defined $_;
+	my $linenr = 0;
+	while (<$fd>) {
+		chomp;
+		if ($type eq 'incremental') {
+			# Empty stage with just the file contents
+			$linenr += 1;
+			print "<tr id=\"l$linenr\" class=\"light2\">";
+			print '<td class="sha1"><a href=""></a></td>';
+			print "<td class=\"linenr\"><a class=\"linenr\" href=\"\">$linenr</a></td><td class=\"pre\">" . esc_html($_) . "</td>\n";
+			print "</tr>\n";
+			next;
+		}
+
 		my ($full_rev, $orig_lineno, $lineno, $group_size) =
 		    /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
 		if (!exists $metainfo{$full_rev}) {
@@ -3618,13 +3675,30 @@ HTML
 		print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
 		print "</tr>\n";
 	}
+
 	print "</table>\n";
 	print "</div>";
 	close $fd
 		or print "Reading blob failed\n";
+
+	if ($type eq 'incremental') {
+		print "<script type=\"text/javascript\">\n";
+		print "startBlame(\"" . href(action=>"blame_data", hash_base=>$hash_base, file_name=>$file_name) . "\", \"" .
+		  href(-partial_query=>1) . "\");\n";
+		print "</script>\n";
+	}
+
 	git_footer_html();
 }
 
+sub git_blame_incremental {
+	git_blame_common('incremental');
+}
+
+sub git_blame2 {
+	git_blame_common('oneshot');
+}
+
 sub git_blame {
 	my $fd;
 
-- 
1.5.2.rc3.802.g4b4b7
Previous: Martin KoeglerNext: Petr Baudis
Message 5 of 8 in “gitweb: Support comparing blobs with different names”
  1. 1/5 gitweb: Support comparing blobs with different namesMartin Koegler, May 20, 2007
  2. 2/5 gitweb: support filename prefix in git_patchset_body/git_difftree_bodyMartin Koegler, May 20, 2007
  3. 3/5 gitweb: Add treediff viewMartin Koegler, May 20, 2007
  4. 4/5 gitweb: Selecting diffs in JavaScriptMartin Koegler, May 20, 2007
  5. 5/5 gitweb: Incremental blameMartin Koegler, May 20, 2007
  6. Petr BaudisAug 26, 2007
  7. Martin KoeglerAug 26, 2007
  8. Petr BaudisAug 26, 2007

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.