{"thread":{"id":"8204","subject":"[PATCH] gitweb: Incremental blame","startedAt":"2007-05-18T15:15:02Z","lastAt":"2007-05-18T15:26:34Z","messageCount":2,"participants":["Petr Baudis"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"42489","messageId":"20070518151502.27621.36135.stgit@rover","threadId":"8204","inReplyTo":null,"subject":"[PATCH] gitweb: Incremental blame","fromName":"Petr Baudis","fromEmail":"pasky@suse.cz","sentAt":"2007-05-18T15:15:02Z","receivedAt":"2007-05-18T15:15:02Z","isPatch":true,"sender":{"key":"pasky@ucw.cz","avatar":"https://avatars.githubusercontent.com/u/18439?v=4"},"body":"This is tweaked up version of Fredrik Kuivinen <frekui@gmail.com>'s proof\nof concept patch to add support for incrementally displaying line data in\nthe blame view using some javascript gadgetry.\n\nThe original patch has been lightly tested in a couple of browsers\n(Firefox, Mozilla, Konqueror, Galeon, Opera and IE6). The new patch has\nbeen tested in Firefox and Epiphany (and works fine in Epiphany 2.14.3,\ncontrary to what the original patch claimed).\n\nCompared to the original patch, this one works with pathinfo-ish URLs as\nwell, and should play well with non-javascript browsers as well (the HTML\npoints to the blame action, while javascript code rewrites the links to use\nthe blame_incremental action; it is somewhat hackish but I couldn't think\nof a better solution).\n\nAs usual, you can see it in action at repo.or.cz. blame view now feels a\nlot more friendly.\n\nCc: Fredrik Kuivinen <frekui@gmail.com>\nSigned-off-by: Petr Baudis <pasky@suse.cz>\n---\n\n Makefile           |    6 +-\n git-instaweb.sh    |    7 ++\n gitweb/blame.js    |  193 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.perl |  140 +++++++++++++++++++++++++++++++++++---\n 4 files changed, 335 insertions(+), 11 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex ed12577..46a9fcb 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -168,6 +168,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@@ -815,13 +816,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 \t$(QUIET_GEN)rm -f $@ $@+ && \\\n \tsed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \\\n \t    -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \\\n@@ -830,6 +832,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 981fe41..cc671b1 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@@ -472,6 +474,8 @@ $git_dir = \"$projectroot/$project\" if $project;\n # dispatch\n my %actions = (\n \t\"blame\" => \\&git_blame2,\n+\t\"blame_incremental\" => \\&git_blame_incremental,\n+\t\"blame_data\" => \\&git_blame_data,\n \t\"blobdiff\" => \\&git_blobdiff,\n \t\"blobdiff_plain\" => \\&git_blobdiff_plain,\n \t\"blob\" => \\&git_blob,\n@@ -570,7 +574,7 @@ sub href(%) {\n \t\t\tpush @result, $symbol . \"=\" . esc_param($params{$name});\n \t\t}\n \t}\n-\t$href .= \"?\" . join(';', @result) if scalar @result;\n+\t$href .= \"?\" . join(';', @result) if $params{-partial_query} or scalar @result;\n \n \treturn $href;\n }\n@@ -1937,6 +1941,16 @@ sub git_header_html {\n <meta name=\"generator\" content=\"gitweb/$version git/$git_version$mod_perl_version\"/>\n <meta name=\"robots\" content=\"index, nofollow\"/>\n <title>$title</title>\n+<script type=\"text/javascript\">/* <![CDATA[ */\n+function fixBlameLinks() {\n+\tvar allLinks = document.getElementsByTagName(\"a\");\n+\tfor (var i = 0; i < allLinks.length; i++) {\n+\t\tvar link = allLinks.item(i);\n+\t\tif (link.className == 'blamelink')\n+\t\t\tlink.href = link.href.replace(\"a=blame\", \"a=blame_incremental\");\n+\t}\n+}\n+/* ]]> */</script>\n EOF\n # print out each stylesheet that exist\n \tif (defined $stylesheet) {\n@@ -1968,7 +1982,7 @@ EOF\n \t}\n \n \tprint \"</head>\\n\" .\n-\t      \"<body>\\n\";\n+\t      \"<body onload=\\\"fixBlameLinks();\\\">\\n\";\n \n \tif (-f $site_header) {\n \t\topen (my $fd, $site_header);\n@@ -2358,7 +2372,7 @@ sub git_print_tree_entry {\n \t\tif ($have_blame) {\n \t\t\tprint \" | \" .\n \t\t\t      $cgi->a({-href => href(action=>\"blame\", hash=>$t->{'hash'},\n-\t\t\t                             file_name=>\"$basedir$t->{'name'}\", %base_key)},\n+\t\t\t                             file_name=>\"$basedir$t->{'name'}\", %base_key), -class => \"blamelink\"},\n \t\t\t              \"blame\");\n \t\t}\n \t\tif (defined $hash_base) {\n@@ -2608,7 +2622,7 @@ sub git_difftree_body {\n \t\t\t              \"blob\") . \" | \";\n \t\t\tif ($have_blame) {\n \t\t\t\tprint $cgi->a({-href => href(action=>\"blame\", hash_base=>$parent,\n-\t\t\t\t                             file_name=>$diff->{'file'})},\n+\t\t\t\t                             file_name=>$diff->{'file'}), -class => \"blamelink\"},\n \t\t\t\t              \"blame\") . \" | \";\n \t\t\t}\n \t\t\tprint $cgi->a({-href => href(action=>\"history\", hash_base=>$parent,\n@@ -2658,7 +2672,7 @@ sub git_difftree_body {\n \t\t\t               \"blob\") . \" | \";\n \t\t\tif ($have_blame) {\n \t\t\t\tprint $cgi->a({-href => href(action=>\"blame\", hash_base=>$hash,\n-\t\t\t\t                             file_name=>$diff->{'file'})},\n+\t\t\t\t                             file_name=>$diff->{'file'}), -class => \"blamelink\"},\n \t\t\t\t              \"blame\") . \" | \";\n \t\t\t}\n \t\t\tprint $cgi->a({-href => href(action=>\"history\", hash_base=>$hash,\n@@ -2703,7 +2717,7 @@ sub git_difftree_body {\n \t\t\t              \"blob\") . \" | \";\n \t\t\tif ($have_blame) {\n \t\t\t\tprint $cgi->a({-href => href(action=>\"blame\", hash_base=>$hash,\n-\t\t\t\t                             file_name=>$diff->{'to_file'})},\n+\t\t\t\t                             file_name=>$diff->{'to_file'}), -class => \"blamelink\"},\n \t\t\t\t              \"blame\") . \" | \";\n \t\t\t}\n \t\t\tprint $cgi->a({-href => href(action=>\"history\", hash_base=>$hash,\n@@ -3577,6 +3591,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), -class => \"blamelink\"},\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=>\"blame_data\", hash_base=>$hash_base, file_name=>$file_name) . \"\\\", \\\"\" .\n+\t  href(-partial_query=>1) . \"\\\");\\n\";\n+\tprint \"</script>\\n\";\n+\tgit_footer_html();\n+}\n+\n+\n sub git_blame2 {\n \tmy $fd;\n \tmy $ftype;\n@@ -3609,7 +3729,7 @@ sub git_blame2 {\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$cgi->a({-href => href(action=>\"blame\", file_name=>$file_name), -class => \"blamelink\"},\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@@ -3711,7 +3831,7 @@ sub git_blame {\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$cgi->a({-href => href(action=>\"blame\", file_name=>$file_name), -class => \"blamelink\"},\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@@ -3884,7 +4004,7 @@ sub git_blob {\n \t\t\tif ($have_blame) {\n \t\t\t\t$formats_nav .=\n \t\t\t\t\t$cgi->a({-href => href(action=>\"blame\", hash_base=>$hash_base,\n-\t\t\t\t\t                       hash=>$hash, file_name=>$file_name)},\n+\t\t\t\t\t                       hash=>$hash, file_name=>$file_name), -class => \"blamelink\"},\n \t\t\t\t\t        \"blame\") .\n \t\t\t\t\t\" | \";\n \t\t\t}\n@@ -5179,7 +5299,7 @@ XML\n \t\t\t              -title => \"diff\"}, 'D');\n \t\t\tif ($have_blame) {\n \t\t\t\tprint $cgi->a({-href => href(-full=>1, action=>\"blame\",\n-\t\t\t\t                             file_name=>$file, hash_base=>$commit),\n+\t\t\t\t                             file_name=>$file, hash_base=>$commit), -class => \"blamelink\",\n \t\t\t\t              -title => \"blame\"}, 'B');\n \t\t\t}\n \t\t\t# if this is not a feed of a file history\n"},{"id":"42490","messageId":"20070518152634.GY4489@pasky.or.cz","threadId":"8204","inReplyTo":"20070518151502.27621.36135.stgit@rover","subject":"Re: [PATCH] gitweb: Incremental blame","fromName":"Petr Baudis","fromEmail":"pasky@suse.cz","sentAt":"2007-05-18T15:26:34Z","receivedAt":"2007-05-18T15:26:34Z","isPatch":true,"sender":{"key":"pasky@ucw.cz","avatar":"https://avatars.githubusercontent.com/u/18439?v=4"},"body":"On Fri, May 18, 2007 at 05:15:02PM CEST, Petr Baudis wrote:\n> Compared to the original patch, this one works with pathinfo-ish URLs as\n> well, and should play well with non-javascript browsers as well (the HTML\n> points to the blame action, while javascript code rewrites the links to use\n> the blame_incremental action; it is somewhat hackish but I couldn't think\n> of a better solution).\n\nTo make it more clear, the additional changes in this patch are really\ntotally trivial (except the link rewriting javascript code, which is\nrather simple too and I spent only two happy hours of my life debugging\nit) - maybe I should've kept Fredrik signed off and maybe even the patch\nauthor and add just my signoff. I'm not really sure how the procedures\nshould work here.\n\n-- \n\t\tPetr \"Pasky the I HATE THE\n\t\t\t!@#*($!*(^%@#*$233.. JAVASCRIPT!!!!!!!!\" Baudis\nStuff: http://pasky.or.cz/\nEver try. Ever fail. No matter. // Try again. Fail again. Fail better.\n\t\t-- Samuel Beckett\n"}]}