[PATCH] gitweb: Incremental blame

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This is a proof of concept patch to add support for incrementally
displaying author data in the blame view.

It has been lightly tested in a couple of browsers (Firefox, Mozilla,
Konqueror, Galeon, Opera and IE6).


There are a couple of issues with this patch:

* If it is going to be merged, we probably need to fallback to the old
  non-incremental blame view as the current code does not work in all
  browsers. Furthermore, not all browsers support javascript. I am not
  sure how this should be done.

* For some unknown reason it does not work in Epiphany.

* In IE6, all author data is eventually shown, but it is not done
  incrementally.

Any comments or suggestions for how to fix any of the issues above is
greatly appreciated.

Signed-off-by: Fredrik Kuivinen <frekui@xxxxxxxxx>

---

 Makefile           |    6 +-
 git-instaweb.sh    |    7 ++
 gitweb/blame.js    |  193 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 gitweb/gitweb.perl |  112 ++++++++++++++++++++++++++++++
 4 files changed, 316 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile
index 10cdaee..e498c5e 100644
--- a/Makefile
+++ b/Makefile
@@ -139,6 +139,7 @@ GITWEB_HOMETEXT = indextext.html
 GITWEB_CSS = gitweb.css
 GITWEB_LOGO = git-logo.png
 GITWEB_FAVICON = git-favicon.png
+GITWEB_BLAMEJS = blame.js
 GITWEB_SITE_HEADER =
 GITWEB_SITE_FOOTER =
 
@@ -691,13 +692,14 @@ gitweb/gitweb.cgi: gitweb/gitweb.perl
 	    -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \
 	    -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \
 	    -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \
+	    -e 's|++GITWEB_BLAMEJS++|$(GITWEB_BLAMEJS)|g' \
 	    -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \
 	    -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \
 	    $< >$@+
 	chmod +x $@+
 	mv $@+ $@
 
-git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css
+git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css gitweb/blame.js
 	rm -f $@ $@+
 	sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
 	    -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
@@ -706,6 +708,8 @@ git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css
 	    -e '/@@GITWEB_CGI@@/d' \
 	    -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \
 	    -e '/@@GITWEB_CSS@@/d' \
+	    -e '/@@GITWEB_BLAMEJS@@/r gitweb/blame.js' \
+	    -e '/@@GITWEB_BLAMEJS@@/d' \
 	    $@.sh > $@+
 	chmod +x $@+
 	mv $@+ $@
diff --git a/git-instaweb.sh b/git-instaweb.sh
index cbc7418..dddbb6b 100755
--- a/git-instaweb.sh
+++ b/git-instaweb.sh
@@ -233,8 +233,15 @@ gitweb_css () {
 EOFGITWEB
 }
 
+gitweb_blamejs () {
+	cat > "$1" <<\EOFGITWEB
+@@GITWEB_BLAMEJS@@
+EOFGITWEB
+}
+
 gitweb_cgi $GIT_DIR/gitweb/gitweb.cgi
 gitweb_css $GIT_DIR/gitweb/gitweb.css
+gitweb_blamejs $GIT_DIR/gitweb/blame.js
 
 case "$httpd" in
 *lighttpd*)
diff --git a/gitweb/blame.js b/gitweb/blame.js
new file mode 100644
index 0000000..88b6499
--- /dev/null
+++ b/gitweb/blame.js
@@ -0,0 +1,193 @@
+// Copyright (C) 2007, Fredrik Kuivinen <frekui@xxxxxxxxx>
+
+var DEBUG = 0;
+function debug(str)
+{
+    if (DEBUG)
+        alert(str);
+}
+
+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 resline = commit.resline;
+    for (var i = 0; i < commit.numlines; i++) {
+        var tr = document.getElementById('l'+resline);
+        if (!tr) {
+            debug('tr is null! resline: ' + resline);
+            break;
+        }
+
+        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());
+        tr.firstChild.title = commit.author + ', ' + dateStr + ' ' + timeStr;
+        var shaAnchor = tr.firstChild.firstChild;
+        if (i == 0) {
+            shaAnchor.href = baseUrl + ';a=commit;h=' + commit.sha1;
+            shaAnchor.innerHTML = commit.sha1.substr(0, 8);
+        } else {
+            shaAnchor.innerHTML = '';
+        }
+
+        var lineAnchor = tr.firstChild.nextSibling.firstChild;
+        lineAnchor.href = baseUrl + ';a=blame;hb=' + commit.sha1 +
+            ';f=' + commit.filename + '#l' + commit.srcline;
+        resline++;
+    }
+}
+
+function fixColors()
+{
+    var colorClasses = ['light2', 'dark2'];
+    var linenum = 1;
+    var tr;
+    var colorClass = 0;
+
+    while((tr = document.getElementById('l'+linenum))) {
+        if(tr.firstChild.firstChild.innerHTML != '') {
+            colorClass = (colorClass + 1) % 2;
+        }
+        tr.setAttribute('class', colorClasses[colorClass]);
+        // Internet Explorer needs this
+        tr.setAttribute('className', colorClasses[colorClass]);
+        linenum++;
+    }
+}
+
+var prevDataLength = -1;
+var nextLine = 0;
+var inProgress = false;
+
+var sha1Re = new RegExp('([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)');
+var infoRe = new RegExp('([a-z-]+) ?(.*)');
+var curCommit = new Commit();
+
+function handleResponse() {
+    debug('handleResp ready: ' + http.readyState +
+          ' respText null?: ' + (http.responseText === null) +
+          ' progress: ' + inProgress);
+
+    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 match = sha1Re.exec(lines[i]);
+            if (match) {
+                var sha1 = match[1];
+                var srcline = parseInt(match[2]);
+                var resline = parseInt(match[3]);
+                var numlines = parseInt(match[4]);
+                var c = commits[sha1];
+                if (!c) {
+                    c = new Commit(sha1);
+                    commits[sha1] = c;
+                }
+
+                c.srcline = srcline;
+                c.resline = resline;
+                c.numlines = numlines;
+                curCommit = c;
+            } else if ((match = infoRe.exec(lines[i]))) {
+                var info = match[1];
+                var data = match[2];
+                if (info == 'filename') {
+                    curCommit.filename = data;
+                    handleLine(curCommit);
+                } else if (info == 'author') {
+                    curCommit.author = data;
+                } else if (info == 'author-time') {
+                    curCommit.authorTime = parseInt(data);
+                }
+            } else if (lines[i] != '') {
+                debug('malformed line: ' + lines[i]);
+            }
+        }
+    }
+
+    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 653ca3c..9c07f09 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -61,6 +61,8 @@ our $stylesheet = undef;
 our $logo = "++GITWEB_LOGO++";
 # URI of GIT favicon, assumed to be image/png type
 our $favicon = "++GITWEB_FAVICON++";
+# URI of blame.js
+our $blamejs = "++GITWEB_BLAMEJS++";
 
 # URI and label (title) of GIT logo link
 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";;
@@ -425,7 +427,9 @@ $git_dir = "$projectroot/$project" if $project;
 
 # dispatch
 my %actions = (
-	"blame" => \&git_blame2,
+#	"blame" => \&git_blame2,
+	"blame" => \&git_blame_incremental,
+	"blamedata" => \&git_blame_data,
 	"blobdiff" => \&git_blobdiff,
 	"blobdiff_plain" => \&git_blobdiff_plain,
 	"blob" => \&git_blob,
@@ -3135,6 +3139,112 @@ sub git_tag {
 	git_footer_html();
 }
 
+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_incremental {
+	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(), 'cat-file', 'blob', $hash)
+		or die_error(undef, "Open git-cat-file failed");
+	git_header_html();
+	my $formats_nav =
+		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
+		        "blob") .
+		" | " .
+		$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
+			"history") .
+		" | " .
+		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
+		        "HEAD");
+	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
+	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+	git_print_page_path($file_name, $ftype, $hash_base);
+	my @rev_color = (qw(light2 dark2));
+	my $num_colors = scalar(@rev_color);
+	my $current_color = 0;
+	my $last_rev;
+	print "<script type=\"text/javascript\" src=\"$blamejs\"></script>\n";
+	print <<HTML;
+<div class="page_body">
+<table class="blame">
+<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
+HTML
+	my %metainfo = ();
+	my $linenr = 0;
+	while (<$fd>) {
+		chomp;
+		$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"
+	}
+
+	print "</table>\n";
+	print "</div>";
+	close $fd or print "Reading blob failed\n";
+	print "<script type=\"text/javascript\">\n";
+	print "startBlame(\"" . href(action=>"blamedata", hash_base=>$hash_base, file_name=>$file_name) . "\", \"" .
+	  href() . "\");\n";
+	print "</script>\n";
+	git_footer_html();
+}
+
+
 sub git_blame2 {
 	my $fd;
 	my $ftype;
-
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html

[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]