All of lore.kernel.org
 help / color / mirror / Atom feed
From: Fredrik Kuivinen <frekui@gmail.com>
To: git@vger.kernel.org
Subject: [PATCH] gitweb: Incremental blame
Date: Sun, 04 Mar 2007 17:40:28 +0100	[thread overview]
Message-ID: <20070304164028.9686.81040.stgit@c165> (raw)


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@gmail.com>

---

 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@gmail.com>
+
+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;

             reply	other threads:[~2007-03-04 16:40 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2007-03-04 16:40 Fredrik Kuivinen [this message]
  -- strict thread matches above, loose matches on Subject: below --
2007-05-18 15:15 [PATCH] gitweb: Incremental blame Petr Baudis
2007-05-18 15:26 ` Petr Baudis

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20070304164028.9686.81040.stgit@c165 \
    --to=frekui@gmail.com \
    --cc=git@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.