{"thread":{"id":"7079","subject":"[PATCH] gitweb: Incremental blame","startedAt":"2007-03-04T16:40:28Z","lastAt":"2007-03-04T16:40:28Z","messageCount":1,"participants":["Fredrik Kuivinen"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"36240","messageId":"20070304164028.9686.81040.stgit@c165","threadId":"7079","inReplyTo":null,"subject":"[PATCH] gitweb: Incremental blame","fromName":"Fredrik Kuivinen","fromEmail":"frekui@gmail.com","sentAt":"2007-03-04T16:40:28Z","receivedAt":"2007-03-04T16:40:28Z","isPatch":true,"sender":{"key":"frekui@gmail.com","avatar":"https://avatars.githubusercontent.com/u/13770967?v=4"},"body":"\nThis is a proof of concept patch to add support for incrementally\ndisplaying author data in the blame view.\n\nIt has been lightly tested in a couple of browsers (Firefox, Mozilla,\nKonqueror, Galeon, Opera and IE6).\n\n\nThere are a couple of issues with this patch:\n\n* If it is going to be merged, we probably need to fallback to the old\n  non-incremental blame view as the current code does not work in all\n  browsers. Furthermore, not all browsers support javascript. I am not\n  sure how this should be done.\n\n* For some unknown reason it does not work in Epiphany.\n\n* In IE6, all author data is eventually shown, but it is not done\n  incrementally.\n\nAny comments or suggestions for how to fix any of the issues above is\ngreatly appreciated.\n\nSigned-off-by: Fredrik Kuivinen <frekui@gmail.com>\n\n---\n\n Makefile           |    6 +-\n git-instaweb.sh    |    7 ++\n gitweb/blame.js    |  193 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.perl |  112 ++++++++++++++++++++++++++++++\n 4 files changed, 316 insertions(+), 2 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 10cdaee..e498c5e 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -139,6 +139,7 @@ GITWEB_HOMETEXT = indextext.html\n GITWEB_CSS = gitweb.css\n GITWEB_LOGO = git-logo.png\n GITWEB_FAVICON = git-favicon.png\n+GITWEB_BLAMEJS = blame.js\n GITWEB_SITE_HEADER =\n GITWEB_SITE_FOOTER =\n \n@@ -691,13 +692,14 @@ gitweb/gitweb.cgi: gitweb/gitweb.perl\n \t    -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \\\n \t    -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \\\n \t    -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \\\n+\t    -e 's|++GITWEB_BLAMEJS++|$(GITWEB_BLAMEJS)|g' \\\n \t    -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \\\n \t    -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \\\n \t    $< >$@+\n \tchmod +x $@+\n \tmv $@+ $@\n \n-git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css\n+git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css gitweb/blame.js\n \trm -f $@ $@+\n \tsed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \\\n \t    -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \\\n@@ -706,6 +708,8 @@ git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css\n \t    -e '/@@GITWEB_CGI@@/d' \\\n \t    -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \\\n \t    -e '/@@GITWEB_CSS@@/d' \\\n+\t    -e '/@@GITWEB_BLAMEJS@@/r gitweb/blame.js' \\\n+\t    -e '/@@GITWEB_BLAMEJS@@/d' \\\n \t    $@.sh > $@+\n \tchmod +x $@+\n \tmv $@+ $@\ndiff --git a/git-instaweb.sh b/git-instaweb.sh\nindex cbc7418..dddbb6b 100755\n--- a/git-instaweb.sh\n+++ b/git-instaweb.sh\n@@ -233,8 +233,15 @@ gitweb_css () {\n EOFGITWEB\n }\n \n+gitweb_blamejs () {\n+\tcat > \"$1\" <<\\EOFGITWEB\n+@@GITWEB_BLAMEJS@@\n+EOFGITWEB\n+}\n+\n gitweb_cgi $GIT_DIR/gitweb/gitweb.cgi\n gitweb_css $GIT_DIR/gitweb/gitweb.css\n+gitweb_blamejs $GIT_DIR/gitweb/blame.js\n \n case \"$httpd\" in\n *lighttpd*)\ndiff --git a/gitweb/blame.js b/gitweb/blame.js\nnew file mode 100644\nindex 0000000..88b6499\n--- /dev/null\n+++ b/gitweb/blame.js\n@@ -0,0 +1,193 @@\n+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n+\n+var DEBUG = 0;\n+function debug(str)\n+{\n+    if (DEBUG)\n+        alert(str);\n+}\n+\n+function createRequestObject() {\n+    var ro;\n+    if (window.XMLHttpRequest) {\n+        ro = new XMLHttpRequest();\n+    } else {\n+        ro = new ActiveXObject(\"Microsoft.XMLHTTP\");\n+    }\n+    return ro;\n+}\n+\n+var http;\n+var baseUrl;\n+\n+// 'commits' is an associative map. It maps SHA1s to Commit objects.\n+var commits = new Object();\n+\n+function Commit(sha1)\n+{\n+    this.sha1 = sha1;\n+}\n+\n+function zeroPad(n)\n+{\n+    if (n < 10)\n+        return '0' + n;\n+    else\n+        return n.toString();\n+}\n+\n+function handleLine(commit)\n+{\n+        /* This is the structure of the HTML fragment we are working\n+         with:\n+\n+         <tr id=\"l123\" class=\"light2\">\n+           <td class=\"sha1\" title=\"\">\n+             <a href=\"\"></a>\n+           </td>\n+           <td class=\"linenr\">\n+             <a class=\"linenr\" href=\"\">123</a>\n+           </td>\n+           <td class=\"pre\"># times (my ext3 doesn&#39;t).</td>\n+         </tr>\n+        */\n+\n+    var resline = commit.resline;\n+    for (var i = 0; i < commit.numlines; i++) {\n+        var tr = document.getElementById('l'+resline);\n+        if (!tr) {\n+            debug('tr is null! resline: ' + resline);\n+            break;\n+        }\n+\n+        var date = new Date();\n+        date.setTime(commit.authorTime*1000);\n+        var dateStr =\n+            date.getUTCFullYear() + '-' +\n+            zeroPad(date.getUTCMonth()+1) + '-' +\n+            zeroPad(date.getUTCDate());\n+        var timeStr =\n+            zeroPad(date.getUTCHours()) + ':' +\n+            zeroPad(date.getUTCMinutes()) + ':' +\n+            zeroPad(date.getUTCSeconds());\n+        tr.firstChild.title = commit.author + ', ' + dateStr + ' ' + timeStr;\n+        var shaAnchor = tr.firstChild.firstChild;\n+        if (i == 0) {\n+            shaAnchor.href = baseUrl + ';a=commit;h=' + commit.sha1;\n+            shaAnchor.innerHTML = commit.sha1.substr(0, 8);\n+        } else {\n+            shaAnchor.innerHTML = '';\n+        }\n+\n+        var lineAnchor = tr.firstChild.nextSibling.firstChild;\n+        lineAnchor.href = baseUrl + ';a=blame;hb=' + commit.sha1 +\n+            ';f=' + commit.filename + '#l' + commit.srcline;\n+        resline++;\n+    }\n+}\n+\n+function fixColors()\n+{\n+    var colorClasses = ['light2', 'dark2'];\n+    var linenum = 1;\n+    var tr;\n+    var colorClass = 0;\n+\n+    while((tr = document.getElementById('l'+linenum))) {\n+        if(tr.firstChild.firstChild.innerHTML != '') {\n+            colorClass = (colorClass + 1) % 2;\n+        }\n+        tr.setAttribute('class', colorClasses[colorClass]);\n+        // Internet Explorer needs this\n+        tr.setAttribute('className', colorClasses[colorClass]);\n+        linenum++;\n+    }\n+}\n+\n+var prevDataLength = -1;\n+var nextLine = 0;\n+var inProgress = false;\n+\n+var sha1Re = new RegExp('([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)');\n+var infoRe = new RegExp('([a-z-]+) ?(.*)');\n+var curCommit = new Commit();\n+\n+function handleResponse() {\n+    debug('handleResp ready: ' + http.readyState +\n+          ' respText null?: ' + (http.responseText === null) +\n+          ' progress: ' + inProgress);\n+\n+    if (http.readyState != 4 && http.readyState != 3)\n+        return;\n+\n+    // In konqueror http.responseText is sometimes null here...\n+    if (http.responseText === null)\n+        return;\n+\n+    if (inProgress)\n+        return;\n+    else\n+        inProgress = true;\n+\n+    while (prevDataLength != http.responseText.length) {\n+        if (http.readyState == 4 &&\n+            prevDataLength == http.responseText.length) {\n+            break;\n+        }\n+\n+        prevDataLength = http.responseText.length;\n+        var response = http.responseText.substring(nextLine);\n+        var lines = response.split('\\n');\n+        nextLine = nextLine + response.lastIndexOf('\\n') + 1;\n+        if (response[response.length-1] != '\\n') {\n+            lines.pop();\n+        }\n+\n+        for (var i = 0; i < lines.length; i++) {\n+            var match = sha1Re.exec(lines[i]);\n+            if (match) {\n+                var sha1 = match[1];\n+                var srcline = parseInt(match[2]);\n+                var resline = parseInt(match[3]);\n+                var numlines = parseInt(match[4]);\n+                var c = commits[sha1];\n+                if (!c) {\n+                    c = new Commit(sha1);\n+                    commits[sha1] = c;\n+                }\n+\n+                c.srcline = srcline;\n+                c.resline = resline;\n+                c.numlines = numlines;\n+                curCommit = c;\n+            } else if ((match = infoRe.exec(lines[i]))) {\n+                var info = match[1];\n+                var data = match[2];\n+                if (info == 'filename') {\n+                    curCommit.filename = data;\n+                    handleLine(curCommit);\n+                } else if (info == 'author') {\n+                    curCommit.author = data;\n+                } else if (info == 'author-time') {\n+                    curCommit.authorTime = parseInt(data);\n+                }\n+            } else if (lines[i] != '') {\n+                debug('malformed line: ' + lines[i]);\n+            }\n+        }\n+    }\n+\n+    if (http.readyState == 4 && prevDataLength == http.responseText.length)\n+        fixColors();\n+\n+    inProgress = false;\n+}\n+\n+function startBlame(blamedataUrl, bUrl)\n+{\n+    baseUrl = bUrl;\n+    http = createRequestObject();\n+    http.open('get', blamedataUrl);\n+    http.onreadystatechange = handleResponse;\n+    http.send(null);\n+}\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 653ca3c..9c07f09 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -61,6 +61,8 @@ our $stylesheet = undef;\n our $logo = \"++GITWEB_LOGO++\";\n # URI of GIT favicon, assumed to be image/png type\n our $favicon = \"++GITWEB_FAVICON++\";\n+# URI of blame.js\n+our $blamejs = \"++GITWEB_BLAMEJS++\";\n \n # URI and label (title) of GIT logo link\n #our $logo_url = \"http://www.kernel.org/pub/software/scm/git/docs/\";\n@@ -425,7 +427,9 @@ $git_dir = \"$projectroot/$project\" if $project;\n \n # dispatch\n my %actions = (\n-\t\"blame\" => \\&git_blame2,\n+#\t\"blame\" => \\&git_blame2,\n+\t\"blame\" => \\&git_blame_incremental,\n+\t\"blamedata\" => \\&git_blame_data,\n \t\"blobdiff\" => \\&git_blobdiff,\n \t\"blobdiff_plain\" => \\&git_blobdiff_plain,\n \t\"blob\" => \\&git_blob,\n@@ -3135,6 +3139,112 @@ sub git_tag {\n \tgit_footer_html();\n }\n \n+sub git_blame_data {\n+\tmy $fd;\n+\tmy $ftype;\n+\n+\tmy ($have_blame) = gitweb_check_feature('blame');\n+\tif (!$have_blame) {\n+\t\tdie_error('403 Permission denied', \"Permission denied\");\n+\t}\n+\tdie_error('404 Not Found', \"File name not defined\") if (!$file_name);\n+\t$hash_base ||= git_get_head_hash($project);\n+\tdie_error(undef, \"Couldn't find base commit\") unless ($hash_base);\n+\tmy %co = parse_commit($hash_base)\n+\t\tor die_error(undef, \"Reading commit failed\");\n+\tif (!defined $hash) {\n+\t\t$hash = git_get_hash_by_path($hash_base, $file_name, \"blob\")\n+\t\t\tor die_error(undef, \"Error looking up file\");\n+\t}\n+\t$ftype = git_get_type($hash);\n+\tif ($ftype !~ \"blob\") {\n+\t\tdie_error(\"400 Bad Request\", \"Object is not a blob\");\n+\t}\n+\topen ($fd, \"-|\", git_cmd(), \"blame\", '--incremental', $hash_base, '--',\n+\t      $file_name)\n+\t\tor die_error(undef, \"Open git-blame --incremental failed\");\n+\n+\tprint $cgi->header(-type=>\"text/plain\", -charset => 'utf-8',\n+\t                   -status=> \"200 OK\");\n+\n+\twhile(<$fd>) {\n+ \t  if (/^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/ or\n+\t     /^author-time |^author |^filename /) {\n+ \t    print;\n+\t  }\n+\t}\n+\n+\tclose $fd or print \"Reading blame data failed\\n\";\n+}\n+\n+sub git_blame_incremental {\n+\tmy $fd;\n+\tmy $ftype;\n+\n+\tmy ($have_blame) = gitweb_check_feature('blame');\n+\tif (!$have_blame) {\n+\t\tdie_error('403 Permission denied', \"Permission denied\");\n+\t}\n+\tdie_error('404 Not Found', \"File name not defined\") if (!$file_name);\n+\t$hash_base ||= git_get_head_hash($project);\n+\tdie_error(undef, \"Couldn't find base commit\") unless ($hash_base);\n+\tmy %co = parse_commit($hash_base)\n+\t\tor die_error(undef, \"Reading commit failed\");\n+\tif (!defined $hash) {\n+\t\t$hash = git_get_hash_by_path($hash_base, $file_name, \"blob\")\n+\t\t\tor die_error(undef, \"Error looking up file\");\n+\t}\n+\t$ftype = git_get_type($hash);\n+\tif ($ftype !~ \"blob\") {\n+\t\tdie_error(\"400 Bad Request\", \"Object is not a blob\");\n+\t}\n+\topen ($fd, \"-|\", git_cmd(), 'cat-file', 'blob', $hash)\n+\t\tor die_error(undef, \"Open git-cat-file failed\");\n+\tgit_header_html();\n+\tmy $formats_nav =\n+\t\t$cgi->a({-href => href(action=>\"blob\", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},\n+\t\t        \"blob\") .\n+\t\t\" | \" .\n+\t\t$cgi->a({-href => href(action=>\"history\", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},\n+\t\t\t\"history\") .\n+\t\t\" | \" .\n+\t\t$cgi->a({-href => href(action=>\"blame\", file_name=>$file_name)},\n+\t\t        \"HEAD\");\n+\tgit_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);\n+\tgit_print_header_div('commit', esc_html($co{'title'}), $hash_base);\n+\tgit_print_page_path($file_name, $ftype, $hash_base);\n+\tmy @rev_color = (qw(light2 dark2));\n+\tmy $num_colors = scalar(@rev_color);\n+\tmy $current_color = 0;\n+\tmy $last_rev;\n+\tprint \"<script type=\\\"text/javascript\\\" src=\\\"$blamejs\\\"></script>\\n\";\n+\tprint <<HTML;\n+<div class=\"page_body\">\n+<table class=\"blame\">\n+<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n+HTML\n+\tmy %metainfo = ();\n+\tmy $linenr = 0;\n+\twhile (<$fd>) {\n+\t\tchomp;\n+\t\t$linenr += 1;\n+\t\tprint \"<tr id=\\\"l$linenr\\\" class=\\\"light2\\\">\";\n+\t\tprint '<td class=\"sha1\"><a href=\"\"></a></td>';\n+\t\tprint \"<td class=\\\"linenr\\\"><a class=\\\"linenr\\\" href=\\\"\\\">$linenr</a></td><td class=\\\"pre\\\">\" . esc_html($_) . \"</td>\\n\";\n+\t\tprint \"</tr>\\n\"\n+\t}\n+\n+\tprint \"</table>\\n\";\n+\tprint \"</div>\";\n+\tclose $fd or print \"Reading blob failed\\n\";\n+\tprint \"<script type=\\\"text/javascript\\\">\\n\";\n+\tprint \"startBlame(\\\"\" . href(action=>\"blamedata\", hash_base=>$hash_base, file_name=>$file_name) . \"\\\", \\\"\" .\n+\t  href() . \"\\\");\\n\";\n+\tprint \"</script>\\n\";\n+\tgit_footer_html();\n+}\n+\n+\n sub git_blame2 {\n \tmy $fd;\n \tmy $ftype;\n"}]}