{"thread":{"id":"9642","subject":"[PATCH 1/3] gitweb: Incremental blame","startedAt":"2007-08-25T22:24:04Z","lastAt":"2007-08-25T22:24:15Z","messageCount":3,"participants":["Petr Baudis"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"51528","messageId":"20070825222404.16967.9402.stgit@rover","threadId":"9642","inReplyTo":null,"subject":"[PATCH 1/3] gitweb: Incremental blame","fromName":"Petr Baudis","fromEmail":"pasky@suse.cz","sentAt":"2007-08-25T22:24:04Z","receivedAt":"2007-08-25T22:24:04Z","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). Also, this version of the patch avoids duplicated\ncode with git_blame2(). blame.js indentation has been fixed.\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 |  123 ++++++++++++++++++++++++++++-----\n 4 files changed, 310 insertions(+), 19 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex e0f99a0..e143cbe 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -169,6 +169,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@@ -819,13 +820,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) $@ $@+ && \\\n \tsed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \\\n \t    -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \\\n@@ -834,6 +836,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 b79c6b6..5425615 100755\n--- a/git-instaweb.sh\n+++ b/git-instaweb.sh\n@@ -227,8 +227,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..bd51275\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+\tif (DEBUG)\n+\t\talert(str);\n+}\n+\n+function createRequestObject() {\n+\tvar ro;\n+\tif (window.XMLHttpRequest) {\n+\t\tro = new XMLHttpRequest();\n+\t} else {\n+\t\tro = new ActiveXObject(\"Microsoft.XMLHTTP\");\n+\t}\n+\treturn 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+\tthis.sha1 = sha1;\n+}\n+\n+function zeroPad(n)\n+{\n+\tif (n < 10)\n+\t\treturn '0' + n;\n+\telse\n+\t\treturn n.toString();\n+}\n+\n+function handleLine(commit)\n+{\n+\t/* This is the structure of the HTML fragment we are working\n+\t   with:\n+\t   \n+\t   <tr id=\"l123\" class=\"light2\">\n+\t   <td class=\"sha1\" title=\"\">\n+\t   <a href=\"\"></a>\n+\t   </td>\n+\t   <td class=\"linenr\">\n+\t   <a class=\"linenr\" href=\"\">123</a>\n+\t   </td>\n+\t   <td class=\"pre\"># times (my ext3 doesn&#39;t).</td>\n+\t   </tr>\n+\t */\n+\n+\tvar resline = commit.resline;\n+\tfor (var i = 0; i < commit.numlines; i++) {\n+\t\tvar tr = document.getElementById('l'+resline);\n+\t\tif (!tr) {\n+\t\t\tdebug('tr is null! resline: ' + resline);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tvar date = new Date();\n+\t\tdate.setTime(commit.authorTime * 1000);\n+\t\tvar dateStr =\n+\t\t\tdate.getUTCFullYear() + '-'\n+\t\t\t+ zeroPad(date.getUTCMonth()+1) + '-'\n+\t\t\t+ zeroPad(date.getUTCDate());\n+\t\tvar timeStr =\n+\t\t\tzeroPad(date.getUTCHours()) + ':'\n+\t\t\t+ zeroPad(date.getUTCMinutes()) + ':'\n+\t\t\t+ zeroPad(date.getUTCSeconds());\n+\t\ttr.firstChild.title = commit.author + ', ' + dateStr + ' ' + timeStr;\n+\t\tvar shaAnchor = tr.firstChild.firstChild;\n+\t\tif (i == 0) {\n+\t\t\tshaAnchor.href = baseUrl + ';a=commit;h=' + commit.sha1;\n+\t\t\tshaAnchor.innerHTML = commit.sha1.substr(0, 8);\n+\t\t} else {\n+\t\t\tshaAnchor.innerHTML = '';\n+\t\t}\n+\n+\t\tvar lineAnchor = tr.firstChild.nextSibling.firstChild;\n+\t\tlineAnchor.href = baseUrl + ';a=blame;hb=' + commit.sha1\n+\t\t\t+ ';f=' + commit.filename + '#l' + commit.srcline;\n+\t\tresline++;\n+\t}\n+}\n+\n+function fixColors()\n+{\n+\tvar colorClasses = ['light2', 'dark2'];\n+\tvar linenum = 1;\n+\tvar tr;\n+\tvar colorClass = 0;\n+\n+\twhile ((tr = document.getElementById('l'+linenum))) {\n+\t\tif (tr.firstChild.firstChild.innerHTML != '') {\n+\t\t\tcolorClass = (colorClass + 1) % 2;\n+\t\t}\n+\t\ttr.setAttribute('class', colorClasses[colorClass]);\n+\t\t// Internet Explorer needs this\n+\t\ttr.setAttribute('className', colorClasses[colorClass]);\n+\t\tlinenum++;\n+\t}\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+\tdebug('handleResp ready: ' + http.readyState\n+\t      + ' respText null?: ' + (http.responseText === null)\n+\t      + ' progress: ' + inProgress);\n+\n+\tif (http.readyState != 4 && http.readyState != 3)\n+\t\treturn;\n+\n+\t// In konqueror http.responseText is sometimes null here...\n+\tif (http.responseText === null)\n+\t\treturn;\n+\n+\tif (inProgress)\n+\t\treturn;\n+\telse\n+\t\tinProgress = true;\n+\n+\twhile (prevDataLength != http.responseText.length) {\n+\t\tif (http.readyState == 4\n+\t\t    && prevDataLength == http.responseText.length) {\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tprevDataLength = http.responseText.length;\n+\t\tvar response = http.responseText.substring(nextLine);\n+\t\tvar lines = response.split('\\n');\n+\t\tnextLine = nextLine + response.lastIndexOf('\\n') + 1;\n+\t\tif (response[response.length-1] != '\\n') {\n+\t\t\tlines.pop();\n+\t\t}\n+\n+\t\tfor (var i = 0; i < lines.length; i++) {\n+\t\t\tvar match = sha1Re.exec(lines[i]);\n+\t\t\tif (match) {\n+\t\t\t\tvar sha1 = match[1];\n+\t\t\t\tvar srcline = parseInt(match[2]);\n+\t\t\t\tvar resline = parseInt(match[3]);\n+\t\t\t\tvar numlines = parseInt(match[4]);\n+\t\t\t\tvar c = commits[sha1];\n+\t\t\t\tif (!c) {\n+\t\t\t\t\tc = new Commit(sha1);\n+\t\t\t\t\tcommits[sha1] = c;\n+\t\t\t\t}\n+\n+\t\t\t\tc.srcline = srcline;\n+\t\t\t\tc.resline = resline;\n+\t\t\t\tc.numlines = numlines;\n+\t\t\t\tcurCommit = c;\n+\t\t\t} else if ((match = infoRe.exec(lines[i]))) {\n+\t\t\t\tvar info = match[1];\n+\t\t\t\tvar data = match[2];\n+\t\t\t\tif (info == 'filename') {\n+\t\t\t\t\tcurCommit.filename = data;\n+\t\t\t\t\thandleLine(curCommit);\n+\t\t\t\t} else if (info == 'author') {\n+\t\t\t\t\tcurCommit.author = data;\n+\t\t\t\t} else if (info == 'author-time') {\n+\t\t\t\t\tcurCommit.authorTime = parseInt(data);\n+\t\t\t\t}\n+\t\t\t} else if (lines[i] != '') {\n+\t\t\t\tdebug('malformed line: ' + lines[i]);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tif (http.readyState == 4 && prevDataLength == http.responseText.length)\n+\t\tfixColors();\n+\n+\tinProgress = false;\n+}\n+\n+function startBlame(blamedataUrl, bUrl)\n+{\n+\tbaseUrl = bUrl;\n+\thttp = createRequestObject();\n+\thttp.open('get', blamedataUrl);\n+\thttp.onreadystatechange = handleResponse;\n+\thttp.send(null);\n+}\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex f7b5b4c..7c3073c 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@@ -536,6 +538,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@@ -643,7 +647,7 @@ sub href(%) {\n \t\t\t}\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@@ -2332,6 +2336,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@@ -2371,7 +2385,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@@ -2770,7 +2784,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@@ -3059,7 +3073,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@@ -3109,7 +3123,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@@ -3154,7 +3168,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@@ -3980,7 +3994,47 @@ sub git_tag {\n \tgit_footer_html();\n }\n \n-sub git_blame2 {\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_common {\n+\tmy ($type) = @_;\n+\n \tmy $fd;\n \tmy $ftype;\n \n@@ -3999,11 +4053,16 @@ sub git_blame2 {\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\tdie_error(\"400 Bad Request\", \"Object is not a blob\");\n+\t}\n+\tif ($type eq 'incremental') {\n+\t\topen ($fd, \"-|\", git_cmd(), 'cat-file', 'blob', $hash)\n+\t\t\tor die_error(undef, \"Open git-cat-file failed\");\n+\t} else {\n+\t\topen ($fd, \"-|\", git_cmd(), 'blame', '-p', '--',\n+\t\t      $file_name, $hash_base)\n+\t\t\tor die_error(undef, \"Open git-blame failed\");\n \t}\n-\topen ($fd, \"-|\", git_cmd(), \"blame\", '-p', '--',\n-\t      $file_name, $hash_base)\n-\t\tor die_error(undef, \"Open git-blame 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@@ -4012,7 +4071,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@@ -4021,15 +4080,26 @@ sub git_blame2 {\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\" if $type eq 'incremental';\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-\twhile (1) {\n-\t\t$_ = <$fd>;\n-\t\tlast unless defined $_;\n+\tmy $linenr = 0;\n+\twhile (<$fd>) {\n+\t\tchomp;\n+\t\tif ($type eq 'incremental') {\n+\t\t\t# Empty stage with just the file contents\n+\t\t\t$linenr += 1;\n+\t\t\tprint \"<tr id=\\\"l$linenr\\\" class=\\\"light2\\\">\";\n+\t\t\tprint '<td class=\"sha1\"><a href=\"\"></a></td>';\n+\t\t\tprint \"<td class=\\\"linenr\\\"><a class=\\\"linenr\\\" href=\\\"\\\">$linenr</a></td><td class=\\\"pre\\\">\" . esc_html($_) . \"</td>\\n\";\n+\t\t\tprint \"</tr>\\n\";\n+\t\t\tnext;\n+\t\t}\n+\n \t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n \t\t    /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/;\n \t\tif (!exists $metainfo{$full_rev}) {\n@@ -4081,13 +4151,30 @@ HTML\n \t\tprint \"<td class=\\\"pre\\\">\" . esc_html($data) . \"</td>\\n\";\n \t\tprint \"</tr>\\n\";\n \t}\n+\n \tprint \"</table>\\n\";\n \tprint \"</div>\";\n \tclose $fd\n \t\tor print \"Reading blob failed\\n\";\n+\n+\tif ($type eq 'incremental') {\n+\t\tprint \"<script type=\\\"text/javascript\\\">\\n\";\n+\t\tprint \"startBlame(\\\"\" . href(action=>\"blame_data\", hash_base=>$hash_base, file_name=>$file_name) . \"\\\", \\\"\" .\n+\t\t  href(-partial_query=>1) . \"\\\");\\n\";\n+\t\tprint \"</script>\\n\";\n+\t}\n+\n \tgit_footer_html();\n }\n \n+sub git_blame_incremental {\n+\tgit_blame_common('incremental');\n+}\n+\n+sub git_blame2 {\n+\tgit_blame_common('oneshot');\n+}\n+\n sub git_blame {\n \tmy $fd;\n \n@@ -4114,7 +4201,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@@ -4287,7 +4374,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@@ -5635,7 +5722,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":"51529","messageId":"20070825222409.16967.16960.stgit@rover","threadId":"9642","inReplyTo":"20070825222404.16967.9402.stgit@rover","subject":"[PATCH 2/3] gitweb: Extra columns in blame","fromName":"Petr Baudis","fromEmail":"pasky@suse.cz","sentAt":"2007-08-25T22:24:10Z","receivedAt":"2007-08-25T22:24:10Z","isPatch":true,"sender":{"key":"pasky@ucw.cz","avatar":"https://avatars.githubusercontent.com/u/18439?v=4"},"body":"This patch adds extra columns to blame output, containing\nline author and creation date. These columns are by default hidden by\ndisplay: none but by clicking on the expansion \"button\" you can display\nthem (and hide again). I think seeing this information without tooltips\nfishing can give much better overview of the content evolution.\n\nThis patch depends on the interactive blame patch (but can be factored out;\nthe common required parts are just the blame.js infrastructure).\n\nSigned-off-by: Petr Baudis <pasky@suse.cz>\n\n---\n\nThis version is updated for the new version of incremental blame and\nfeatures updated javascript code based on a friend's suggestions that\nshould improve MSIE compatibility and performance.\n---\n\n gitweb/blame.js    |   48 +++++++++++++++++++++++++++++++++++++++++++++++-\n gitweb/gitweb.css  |    5 +++++\n gitweb/gitweb.perl |   17 +++++++++++++----\n 3 files changed, 65 insertions(+), 5 deletions(-)\n\ndiff --git a/gitweb/blame.js b/gitweb/blame.js\nindex bd51275..f6d661a 100644\n--- a/gitweb/blame.js\n+++ b/gitweb/blame.js\n@@ -1,4 +1,44 @@\n // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n+// Copyright (C) 2007, Petr Baudis <pasky@suse.cz>\n+\n+\n+// blame extra columns\n+\n+// I would like to note here that JavaScript is utterly stupid.\n+function findStyleRule(styleName) {\n+\tfor (i = 0; i < document.styleSheets.length; i++) { \n+\t\t// MSIE has .rules, Mozilla has .cssRules\n+\t\tvar cssRules = document.styleSheets[i].cssRules ? document.styleSheets[i].cssRules : document.styleSheets[i].rules;\n+\t\tfor (j = 0; j < cssRules.length; j++) {\n+\t\t\tvar rule = cssRules[j];\n+\t\t\tif (rule.selectorText.toLowerCase() == styleName) {\n+\t\t\t\treturn rule;\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n+var isIE = (navigator.appName.toLowerCase().indexOf(\"microsoft\") != -1);\n+var extra_columns = 0;\n+var extra_column_rule = null;\n+function extra_blame_columns() {\n+\tif (!extra_column_rule)\n+\t\textra_column_rule = findStyleRule(\".extra_column\");\n+\n+\tif (!extra_columns) {\n+\t\tdocument.getElementById(\"columns_expander\").innerHTML = \"[-]\";\n+\t\textra_column_rule.style.display = isIE ? \"inline\" : \"table-cell\";\n+\t\textra_columns = 1;\n+\t} else {\n+\t\tdocument.getElementById(\"columns_expander\").innerHTML = \"[+]\";\n+\t\textra_column_rule.style.display = \"none\";\n+\t\textra_columns = 0;\n+\t}\n+}\n+\n+\n+// blame_interactive\n+\n \n var DEBUG = 0;\n function debug(str)\n@@ -72,14 +112,20 @@ function handleLine(commit)\n \t\t\t+ zeroPad(date.getUTCSeconds());\n \t\ttr.firstChild.title = commit.author + ', ' + dateStr + ' ' + timeStr;\n \t\tvar shaAnchor = tr.firstChild.firstChild;\n+\t\tvar authorField = tr.firstChild.nextSibling;\n+\t\tvar dateField = tr.firstChild.nextSibling.nextSibling;\n \t\tif (i == 0) {\n \t\t\tshaAnchor.href = baseUrl + ';a=commit;h=' + commit.sha1;\n \t\t\tshaAnchor.innerHTML = commit.sha1.substr(0, 8);\n+\t\t\tauthorField.innerHTML = commit.author;\n+\t\t\tdateField.innerHTML = dateStr + ' ' + timeStr;\n \t\t} else {\n \t\t\tshaAnchor.innerHTML = '';\n+\t\t\tauthorField.innerHTML = '';\n+\t\t\tdateField.innerHTML = '';\n \t\t}\n \n-\t\tvar lineAnchor = tr.firstChild.nextSibling.firstChild;\n+\t\tvar lineAnchor = tr.firstChild.nextSibling.nextSibling.nextSibling.firstChild;\n \t\tlineAnchor.href = baseUrl + ';a=blame;hb=' + commit.sha1\n \t\t\t+ ';f=' + commit.filename + '#l' + commit.srcline;\n \t\tresline++;\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex 096313b..4e93a1a 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -499,3 +499,8 @@ span.match {\n div.binary {\n \tfont-style: italic;\n }\n+\n+/* This selector is hardcoded in gitweb.perl */\n+.extra_column {\n+\tdisplay: none;\n+}\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 7c3073c..8536c50 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4080,11 +4080,16 @@ sub git_blame_common {\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\" if $type eq 'incremental';\n+\tprint \"<script type=\\\"text/javascript\\\" src=\\\"$blamejs\\\"></script>\\n\";\n \tprint <<HTML;\n+\n <div class=\"page_body\">\n <table class=\"blame\">\n-<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n+<tr><th>Commit&nbsp;<a href=\"javascript:extra_blame_columns()\" id=\"columns_expander\">[+]</a></th>\n+<th class=\"extra_column\">Author</th>\n+<th class=\"extra_column\">Date</th>\n+<th>Line</th>\n+<th>Data</th></tr>\n HTML\n \tmy %metainfo = ();\n \tmy $linenr = 0;\n@@ -4095,6 +4100,8 @@ HTML\n \t\t\t$linenr += 1;\n \t\t\tprint \"<tr id=\\\"l$linenr\\\" class=\\\"light2\\\">\";\n \t\t\tprint '<td class=\"sha1\"><a href=\"\"></a></td>';\n+\t\t\tprint \"<td class=\\\"extra_column\\\"></td>\";\n+\t\t\tprint \"<td class=\\\"extra_column\\\"></td>\";\n \t\t\tprint \"<td class=\\\"linenr\\\"><a class=\\\"linenr\\\" href=\\\"\\\">$linenr</a></td><td class=\\\"pre\\\">\" . esc_html($_) . \"</td>\\n\";\n \t\t\tprint \"</tr>\\n\";\n \t\t\tnext;\n@@ -4124,15 +4131,17 @@ HTML\n \t\t}\n \t\tprint \"<tr class=\\\"$rev_color[$current_color]\\\">\\n\";\n \t\tif ($group_size) {\n+\t\t\tmy $rowspan = $group_size > 1 ? \" rowspan=\\\"$group_size\\\"\" : \"\";\n \t\t\tprint \"<td class=\\\"sha1\\\"\";\n \t\t\tprint \" title=\\\"\". esc_html($author) . \", $date\\\"\";\n-\t\t\tprint \" rowspan=\\\"$group_size\\\"\" if ($group_size > 1);\n-\t\t\tprint \">\";\n+\t\t\tprint \"$rowspan>\";\n \t\t\tprint $cgi->a({-href => href(action=>\"commit\",\n \t\t\t                             hash=>$full_rev,\n \t\t\t                             file_name=>$file_name)},\n \t\t\t              esc_html($rev));\n \t\t\tprint \"</td>\\n\";\n+\t\t\tprint \"<td class=\\\"extra_column\\\" $rowspan>\". esc_html($author) . \"</td>\";\n+\t\t\tprint \"<td class=\\\"extra_column\\\" $rowspan>\". $date . \"</td>\";\n \t\t}\n \t\topen (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n \t\t\tor die_error(undef, \"Open git-rev-parse failed\");\n"},{"id":"51530","messageId":"20070825222415.16967.28292.stgit@rover","threadId":"9642","inReplyTo":"20070825222404.16967.9402.stgit@rover","subject":"[PATCH 3/3] gitweb: Remove git_blame (superseded by git_blame2)","fromName":"Petr Baudis","fromEmail":"pasky@suse.cz","sentAt":"2007-08-25T22:24:15Z","receivedAt":"2007-08-25T22:24:15Z","isPatch":true,"sender":{"key":"pasky@ucw.cz","avatar":"https://avatars.githubusercontent.com/u/18439?v=4"},"body":"This patch definitely removes git_blame() from the source and renames\ngit_blame2() to git_blame(); it was already the default handler for the\nblame action for a long time and it has been actually broken for some time\nnow (I'm not sure how long), so noone probably cares about it much (I have\nan alternative trivial patch to fix it too). All the information listing is\nalready included in git_blame2() output now.\n\nSigned-off-by: Petr Baudis <pasky@suse.cz>\n---\n\n gitweb/gitweb.perl |  101 +---------------------------------------------------\n 1 files changed, 2 insertions(+), 99 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 8536c50..23eaff3 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -537,7 +537,7 @@ $git_dir = \"$projectroot/$project\" if $project;\n \n # dispatch\n my %actions = (\n-\t\"blame\" => \\&git_blame2,\n+\t\"blame\" => \\&git_blame,\n \t\"blame_incremental\" => \\&git_blame_incremental,\n \t\"blame_data\" => \\&git_blame_data,\n \t\"blobdiff\" => \\&git_blobdiff,\n@@ -4180,105 +4180,8 @@ sub git_blame_incremental {\n \tgit_blame_common('incremental');\n }\n \n-sub git_blame2 {\n-\tgit_blame_common('oneshot');\n-}\n-\n sub git_blame {\n-\tmy $fd;\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 lookup file\");\n-\t}\n-\topen ($fd, \"-|\", git_cmd(), \"annotate\", '-l', '-t', '-r', $file_name, $hash_base)\n-\t\tor die_error(undef, \"Open git-annotate 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, 'blob', $hash_base);\n-\tprint \"<div class=\\\"page_body\\\">\\n\";\n-\tprint <<HTML;\n-<table class=\"blame\">\n-  <tr>\n-    <th>Commit</th>\n-    <th>Age</th>\n-    <th>Author</th>\n-    <th>Line</th>\n-    <th>Data</th>\n-  </tr>\n-HTML\n-\tmy @line_class = (qw(light dark));\n-\tmy $line_class_len = scalar (@line_class);\n-\tmy $line_class_num = $#line_class;\n-\twhile (my $line = <$fd>) {\n-\t\tmy $long_rev;\n-\t\tmy $short_rev;\n-\t\tmy $author;\n-\t\tmy $time;\n-\t\tmy $lineno;\n-\t\tmy $data;\n-\t\tmy $age;\n-\t\tmy $age_str;\n-\t\tmy $age_class;\n-\n-\t\tchomp $line;\n-\t\t$line_class_num = ($line_class_num + 1) % $line_class_len;\n-\n-\t\tif ($line =~ m/^([0-9a-fA-F]{40})\\t\\(\\s*([^\\t]+)\\t(\\d+) [+-]\\d\\d\\d\\d\\t(\\d+)\\)(.*)$/) {\n-\t\t\t$long_rev = $1;\n-\t\t\t$author   = $2;\n-\t\t\t$time     = $3;\n-\t\t\t$lineno   = $4;\n-\t\t\t$data     = $5;\n-\t\t} else {\n-\t\t\tprint qq(  <tr><td colspan=\"5\" class=\"error\">Unable to parse: $line</td></tr>\\n);\n-\t\t\tnext;\n-\t\t}\n-\t\t$short_rev  = substr ($long_rev, 0, 8);\n-\t\t$age        = time () - $time;\n-\t\t$age_str    = age_string ($age);\n-\t\t$age_str    =~ s/ /&nbsp;/g;\n-\t\t$age_class  = age_class($age);\n-\t\t$author     = esc_html ($author);\n-\t\t$author     =~ s/ /&nbsp;/g;\n-\n-\t\t$data = untabify($data);\n-\t\t$data = esc_html ($data);\n-\n-\t\tprint <<HTML;\n-  <tr class=\"$line_class[$line_class_num]\">\n-    <td class=\"sha1\"><a href=\"${\\href (action=>\"commit\", hash=>$long_rev)}\" class=\"text\">$short_rev..</a></td>\n-    <td class=\"$age_class\">$age_str</td>\n-    <td>$author</td>\n-    <td class=\"linenr\"><a id=\"$lineno\" href=\"#$lineno\" class=\"linenr\">$lineno</a></td>\n-    <td class=\"pre\">$data</td>\n-  </tr>\n-HTML\n-\t} # while (my $line = <$fd>)\n-\tprint \"</table>\\n\\n\";\n-\tclose $fd\n-\t\tor print \"Reading blob failed.\\n\";\n-\tprint \"</div>\";\n-\tgit_footer_html();\n+\tgit_blame_common('oneshot');\n }\n \n sub git_tags {\n"}]}