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'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;
next 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).