{"thread":{"id":"20443","subject":"[PATCH/RFC 1/3] gitweb: Add optional \"time to generate page\" info in footer","startedAt":"2009-08-06T17:11:49Z","lastAt":"2009-08-12T17:08:08Z","messageCount":5,"participants":["Jakub Narebski"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"119804","messageId":"1249578712-3862-1-git-send-email-jnareb@gmail.com","threadId":"20443","inReplyTo":null,"subject":"[PATCH 0/3] gitweb: Incremental blame series","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-08-06T17:11:49Z","receivedAt":"2009-08-06T17:11:49Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This series is meant to replace the top 4 commits of 'jn/gitweb-blame'\ntopic branch, those dealing with AJAX-y blame.\n\n* jn/gitweb-blame:\n      gitweb: Create links leading to 'blame_incremental' using JavaScript\n      gitweb: Incremental blame (proof of concept)\n      gitweb: Add optional \"time to generate page\" info in footer\n      gitweb: Add -partial_query option to href() subroutine\n\n      gitweb: Use light/dark for class names also in 'blame' view\n      gitweb: Add author initials in 'blame' view, a la \"git gui blame\"\n      gitweb: Mark commits with no \"previous\" in 'blame' view\n      gitweb: Use \"previous\" header of git-blame -p in 'blame' view\n      gitweb: Mark boundary commits in 'blame' view\n      gitweb: Make .error style generic\n\n\nThe commit adding -partial_query option to href() is no longer\nnecessary, and was removed from this improved series.\n\nThis version at last doesn't turn off application/xhtml+xml as content\ntype in gitweb, because blame.js no longer uses innerHTML (and I can\ndebug it in my web browser).  Major changes are to second patch in the\nseries.\n\nTable of contents:\n==================\n [PATCH/RFC 1/3]   gitweb: Add optional \"time to generate page\" info\n                   in footer\n [PATCHv3/RFC 2/3] gitweb: Incremental blame (proof of concept)\n [PATCHv2/RFC 3/3] gitweb: Create links leading to 'blame_incremental'\n                   using JavaScript\n\nDiffstat:\n=========\n\n Makefile           |    6 +-\n git-instaweb.sh    |    7 +\n gitweb/blame.js    |  731 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.css  |   16 ++\n gitweb/gitweb.perl |  321 +++++++++++++++++------\n 5 files changed, 994 insertions(+), 87 deletions(-)\n create mode 100644 gitweb/blame.js\n"},{"id":"119801","messageId":"1249578712-3862-2-git-send-email-jnareb@gmail.com","threadId":"20443","inReplyTo":"1249578712-3862-1-git-send-email-jnareb@gmail.com","subject":"[PATCH/RFC 1/3] gitweb: Add optional \"time to generate page\" info in footer","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-08-06T17:11:50Z","receivedAt":"2009-08-06T17:11:50Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add \"This page took XXXs and Y git commands to generate\" to page\nfooter, if global feature 'timed' is enabled (disabled by default).\nRequires Time::HiRes installed for high precision 'wallclock' time.\n\nNote that setting $t0 variable should be fairly early to have running\ntime of the whole script.  The same for $number_of_git_cmds.\n\nThis code is based on example code by Petr 'Pasky' Baudis.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch is unchanged from previous version.\n\nCurrent formatting is very basic, just like before.  It simply uses \n'page_footer' style.  At least \"time to generate page\" info should\nbe clearly separate from the gitweb footer proper.\n\n gitweb/gitweb.perl |   29 +++++++++++++++++++++++++++++\n 1 files changed, 29 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 2cb60be..c6894e2 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -18,6 +18,12 @@ use File::Find qw();\n use File::Basename qw(basename);\n binmode STDOUT, ':utf8';\n \n+our $t0;\n+if (eval { require Time::HiRes; 1; }) {\n+\t$t0 = [Time::HiRes::gettimeofday()];\n+}\n+our $number_of_git_cmds = 0;\n+\n BEGIN {\n \tCGI->compile() if $ENV{'MOD_PERL'};\n }\n@@ -394,6 +400,13 @@ our %feature = (\n \t\t'sub' => \\&feature_avatar,\n \t\t'override' => 0,\n \t\t'default' => ['']},\n+\n+\t# Enable displaying how much time and how many git commands\n+\t# it took to generate and display page.  Disabled by default.\n+\t# Project specific override is not supported.\n+\t'timed' => {\n+\t\t'override' => 0,\n+\t\t'default' => [0]},\n );\n \n sub gitweb_get_feature {\n@@ -507,6 +520,7 @@ if (-e $GITWEB_CONFIG) {\n \n # version of the core git binary\n our $git_version = qx(\"$GIT\" --version) =~ m/git version (.*)$/ ? $1 : \"unknown\";\n+$number_of_git_cmds++;\n \n $projects_list ||= $projectroot;\n \n@@ -1955,6 +1969,7 @@ sub get_feed_info {\n \n # returns path to the core git executable and the --git-dir parameter as list\n sub git_cmd {\n+\t$number_of_git_cmds++;\n \treturn $GIT, '--git-dir='.$git_dir;\n }\n \n@@ -3205,6 +3220,20 @@ sub git_footer_html {\n \t}\n \tprint \"</div>\\n\"; # class=\"page_footer\"\n \n+\tif (defined $t0 && gitweb_check_feature('timed')) {\n+\t\tprint \"<div id=\\\"generate_info\\\" class=\\\"page_footer\\\">\\n\";\n+\t\tprint 'This page took '.\n+\t\t      '<span id=\"generate_time\" class=\"time_span\">'.\n+\t\t      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).'s'.\n+\t\t      '</span>'.\n+\t\t      ' and '.\n+\t\t      '<span id=\"generate_cmd\">'.\n+\t\t      $number_of_git_cmds.\n+\t\t      '</span> git commands '.\n+\t\t      \" to generate.\\n\";\n+\t\tprint \"</div>\\n\"; # class=\"page_footer\"\n+\t}\n+\n \tif (-f $site_footer) {\n \t\tinsert_file($site_footer);\n \t}\n-- \n1.6.3.3\n"},{"id":"119803","messageId":"1249578712-3862-3-git-send-email-jnareb@gmail.com","threadId":"20443","inReplyTo":"1249578712-3862-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv3/RFC 2/3] gitweb: Incremental blame (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-08-06T17:11:51Z","receivedAt":"2009-08-06T17:11:51Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This is tweaked up version of Petr Baudis <pasky@suse.cz> patch, which\nin turn was tweaked up version of Fredrik Kuivinen <frekui@gmail.com>'s\nproof of concept patch.  It adds 'blame_incremental' view, which\nincrementally displays line data in blame view using JavaScript (AJAX).\n\nThis patch does not (contrary to the one by Petr Baudis) enable this\nview in gitweb: there are no links leading to 'blame_incremental'\naction.  You would have to generate URL 'by hand' (e.g. changing 'blame'\nor 'blob' in gitweb URL to 'blame_incremental').  Having links in gitweb\nlead to this new action (e.g. by rewriting them like in previous patch,\nif JavaScript is enabled in browser) is left for later.\n\nLike earlier patch by Per Baudis it avoids code duplication, but it goes\none step further and use git_blame_common for ordinary blame view, for\nincremental blame, and (which is change from previous patch) for\nincremental blame data.\n\nHow the 'blame_incremental' view works:\n* gitweb generates initial info by putting file contents (from\n  git-cat-file) together with line numbers in blame table\n* then gitweb makes web browser JavaScript engine call startBlame()\n  function from blame.js\n* startBlame() opens connection to 'blame_data' view, which in turn\n  calls \"git blame --incremental\" for a file, and streams output of\n  git-blame to JavaScript (blame.js)\n* blame.js updates line info in blame view, coloring it, and updating\n  progress info; note that it has to use 3 colors to ensure that\n  different neighbour groups have different styles\n* when 'blame_data' ends, and blame.js finishes updating line info,\n  it fixes colors to match (as far as possible) ordinary 'blame' view,\n  and updates generating time info.\n\nIt deals with streamed 'blame_data' server error by notifying about them\nin the progress info area (just in case).\n\nDifferences between 'blame_incremental' and original 'blame' view:\n* 'blame_incremental' always used (partial) query form for links\n  generated by JavaScript.  The difference is visible if we use path_info\n  link (pass some or all arguments in path_info), e.g. in 'blame' view\n  called using:\n    http://git.example.com/w/git.git/blame/HEAD:/README\n  we have 'linenr' links using the same form:\n    http://git.example.com/w/git.git/blame/e83c5163316f89bfbde7d9ab23ca2e25604af290:/README#l4\n  while in 'blame_incremental' view called with:\n    http://git.example.com/w/git.git/blame_incremental/HEAD:/README\n  we have \"partial query\" form\n    http://git.example.com/w/git.git?;a=blame_incremental;hb=e83c5163316f89bfbde7d9ab23ca2e25604af290;f=README#l4\n  Changing this would require implementing something akin to href()\n  subroutine from gitweb.perl in JavaScript\n* 'blame_incremental' currently always uses \"rowspan\" attribute,\n  even when it leads to torowspan=\"1\".  This is not visible to user.\n\nThis patch adds GITWEB_BLAMEJS compile configuration option, and\nmodifies git-instaweb.sh to take blame.js into account, but it does not\nupdate gitweb/README file (as it is only proof of concept patch).  The\ncode for git-instaweb.sh was taken from Pasky's patch.\n\nSigned-off-by: Fredrik Kuivinen <frekui@gmail.com>\nSigned-off-by: Petr Baudis <pasky@suse.cz>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nRefrences:\n1. Original patch by Frederik Kuivinen\n   http://article.gmane.org/gmane.comp.version-control.git/41361\n2. Tweaked up version by Petr Baudis\n   http://article.gmane.org/gmane.comp.version-control.git/47614\n   http://article.gmane.org/gmane.comp.version-control.git/56657\n3. New link rewriting and some optimization in Matrin Koegler\n   series introducing some JavaScript support in Git\n   http://thread.gmane.org/gmane.comp.version-control.git/47902/focus=47905   \n4. My earlier patches\n   http://thread.gmane.org/gmane.comp.version-control.git/102657/focus=102712\n   http://article.gmane.org/gmane.comp.version-control.git/123202\n   http://thread.gmane.org/gmane.comp.version-control.git/123957/focus=123968\n\nChanges compared to last version (v2):\n * projectUrl now contains '?' or ';' as terminator; the bUrl\n   parameter to startBlame function doesn't need to be \n   href(..., -partial_query), but just href(...).\n * Do not use (nonstandard but widely implemented) elem.innerHTML,\n   but elem.firstChild.data (DOM 2 Core: CharacterData).  It is less\n   powerfull (only text) and more fragile (text node has to exist,\n   and has to be first child) currently, but a bit faster.  Code was\n   modified to ensure that (' ' as tag content as a start value).\n   See comment at beginning of blame.js file. \n * Remove commenting out content type negotiation, which prevented\n   sending 'application/xhtml+xml' content type if it is preferred by\n   browser (gitweb always used 'text/html' in v2).   This content type\n   causes in most browsers turning on strict XML mode; and in Firefox\n   up to 1.5 it turned on strict _JavaScript_ mode, which meant no\n   innerHTML.  Now that blame.js doesn't use innerHTML it is no longer\n   necessary to have for me to be able to debug 'blame_incremental'\n   view.\n\n * remove '#\"' used to help with confused syntax highlighting\n * remove tr.colorN:hover style (doesn't work well with commit\n   groups, i.e. blamed commit being responsible/blamed for several\n   consecutive lines)\n * use document fragment to (try to) speed up DOM manipulation, \n   if possible (if browser supports createDocumentFragment())\n * use generic \"padLeft(num, 2, '0')\" in place of specilized\n   \"zeroPad(num)\"; remove zeroPad() function\n * added JSDoc documentation and other comments\n * comment out debug(...) calls\n * extended copyright statement \n * future-proof so 'var = Commmit(sha1);' works as 'var = new Commit(sha1);'\n   (nice trick, but probably not necessary)\n\nTODO list:\n* Use W3C Progress Events in addition/instead of readystatechange\n    http://www.w3.org/TR/progress-events/\n    http://www.nczonline.net/blog/2009/07/09/firefox-35firebug-xmlhttprequest-and-readystatechange-bug/\n  event, if XMLHttpResponse supports it\n* handleResponse is used both as onreadystatechange and pollTimer;\n  if onreadystatechange works for partial responses we can turn off\n  the timer.\n* handleResponse is protected from concurrent running by global\n  inProgress variable; we could instead pass XMLHttpResponse object\n  as a parameter (see comment in startBlame).\n* Instead of running startBlame, put it in window.onload handler\n* Rename blame.js to gitweb.js to have all JavaScript in a single\n  file.  Note that this would require changes to next commit in\n  series (enabling 'blame_incremental' view if browser has javaScript\n  turned on).\n\nTODO for future commits:\n* Remove (fade out) progress bar and progress info after blame\n  incremental finished run (left for a separate commit).\n* Profile blame.js using YUI Profiler or other JavaScript profile tool\n    http://developer.yahoo.com/yui/profiler/\n  to check whether performance improvements described in articles\n  on NCZOnline blog by Nicholas C. Zakas are required, and would help\n    http://www.nczonline.net/blog/\n* Check whether it is worth to do some server processing of\n  incremental blame output, and send e.g. JSON and not text/plain\n  of raw 'git blame --incremental' output.\n* Check whether it would make sense to use some lightweight JavaScript\n  library (framework) like jQuery, Prototype, ExtJS, MooTools, etc.\n* Minify JavaScript during building gitweb/gitweb.cgi, if minification\n  tool is found.\n\n Makefile           |    6 +-\n git-instaweb.sh    |    7 +\n gitweb/blame.js    |  731 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.css  |   16 ++\n gitweb/gitweb.perl |  271 +++++++++++++------\n 5 files changed, 944 insertions(+), 87 deletions(-)\n create mode 100644 gitweb/blame.js\n\ndiff --git a/Makefile b/Makefile\nindex daf4296..cb93aa3 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -265,6 +265,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@@ -1407,13 +1408,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@@ -1422,6 +1424,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    -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \\\n \t    $@.sh > $@+ && \\\n \tchmod +x $@+ && \\\ndiff --git a/git-instaweb.sh b/git-instaweb.sh\nindex 32f6496..15dfb96 100755\n--- a/git-instaweb.sh\n+++ b/git-instaweb.sh\n@@ -331,8 +331,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..5cdd20b\n--- /dev/null\n+++ b/gitweb/blame.js\n@@ -0,0 +1,731 @@\n+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n+//               2007, Petr Baudis <pasky@suse.cz>\n+//          2008-2009, Jakub Narebski <jnareb@gmail.com>\n+\n+/**\n+ * @fileOverview JavaScript part of 'blame_incremental' action in gitweb.\n+ * @license GPLv2 or later\n+ */\n+\n+/*\n+ * This code uses DOM methods instead of (nonstandard) innerHTML\n+ * to modify page.\n+ *\n+ * innerHTML is non-standard IE extension, though supported by most\n+ * browsers; however Firefox up to version 1.5 didn't implement it in\n+ * a strict mode (application/xml+xhtml mimetype).  Also my simple\n+ * benchmarks show that using elem.firstChild.data = 'content' is\n+ * slightly faster than elem.innerHTML = 'content', if more fragile\n+ * (text element fragment must exists), and less feature-rich (we\n+ * cannot add HTML).\n+ */\n+\n+/* ============================================================ */\n+/* generic utility functions */\n+\n+var DEBUG = 0;\n+function debug(str) {\n+\tif (DEBUG) {\n+\t\talert(str);\n+\t}\n+}\n+\n+/**\n+ * pad number N with nonbreakable spaces on the left, to WIDTH characters\n+ * example: spacePad(12, 3) == '&nbsp;12' ('&nbsp;' is nonbreakable space)\n+ *\n+ * @param {Number} n: number to pad\n+ * @param {Number} width: visible width of output\n+ * @returns {String} N prefixed with (WIDTH - N.length) x '&nbsp;'\n+ */\n+function spacePad(n, width) {\n+\tvar prefix = '';\n+\n+\twidth -= n.toString().length;\n+\twhile (width > 1) {\n+\t\tprefix += '&nbsp;';\n+\t\twidth--;\n+\t}\n+\treturn prefix + n;\n+}\n+\n+/**\n+ * Pad INPUT on the left to SIZE width, using given padding character CH,\n+ * for example padLeft('a', 3, '_') is '__a'.\n+ *\n+ * @param {String} input: input value converted to string.\n+ * @param {Number} size: desired length of output.\n+ * @param {String} ch: single character to prefix to string.\n+ *\n+ * @returns {String} Modified string, at least SIZE length.\n+ */\n+function padLeft(input, size, ch) {\n+\tvar s = input + \"\";\n+\twhile (s.length < size) {\n+\t\ts = ch + s;\n+\t}\n+\treturn s;\n+}\n+\n+/**\n+ * Create XMLHttpRequest object in cross-browser way\n+ * @returns XMLHttpRequest object, or null\n+ */\n+function createRequestObject() {\n+\ttry {\n+\t\treturn new XMLHttpRequest();\n+\t} catch (e) {}\n+\ttry {\n+\t\treturn new ActiveXObject(\"Msxml2.XMLHTTP\");\n+\t} catch (e) {}\n+\ttry {\n+\t\treturn new ActiveXObject(\"Microsoft.XMLHTTP\");\n+\t} catch (e) {}\n+\n+\t//debug(\"XMLHttpRequest not supported\");\n+\treturn null;\n+}\n+\n+/* ============================================================ */\n+/* utility/helper functions (and variables) */\n+\n+var http;       // XMLHttpRequest object\n+var projectUrl; // partial query + separator ('?' or ';')\n+\n+// 'commits' is an associative map. It maps SHA1s to Commit objects.\n+var commits = {};\n+\n+/**\n+ * constructor for Commit objects, used in 'blame'\n+ * @class Represents a blamed commit\n+ * @param {String} sha1: SHA-1 identifier of a commit\n+ */\n+function Commit(sha1) {\n+\tif (this instanceof Commit) {\n+\t\tthis.sha1 = sha1;\n+\t\tthis.nprevious = 0; /* blame-specific */\n+\t} else {\n+\t\treturn new Commit(sha1);\n+\t}\n+}\n+\n+/* ............................................................ */\n+/* progress info, timing, error reporting */\n+\n+var blamedLines = 0;\n+var totalLines  = '???';\n+var div_progress_bar;\n+var div_progress_info;\n+\n+/**\n+ * Detects how many lines does a blamed file have,\n+ * This information is used in progress info\n+ *\n+ * @returns {Number|String} Number of lines in file, or string '...'\n+ */\n+function countLines() {\n+\tvar table =\n+\t\tdocument.getElementById('blame_table') ||\n+\t\tdocument.getElementsByTagName('table')[0];\n+\n+\tif (table) {\n+\t\treturn table.getElementsByTagName('tr').length - 1; // for header\n+\t} else {\n+\t\treturn '...';\n+\t}\n+}\n+\n+/**\n+ * update progress info and length (width) of progress bar\n+ */\n+function updateProgressInfo() {\n+\tif (!div_progress_info) {\n+\t\tdiv_progress_info = document.getElementById('progress_info');\n+\t}\n+\tif (!div_progress_bar) {\n+\t\tdiv_progress_bar = document.getElementById('progress_bar');\n+\t}\n+\tif (!div_progress_info && !div_progress_bar) {\n+\t\treturn;\n+\t}\n+\n+\tvar percentage = Math.floor(100.0*blamedLines/totalLines);\n+\n+\tif (div_progress_info) {\n+\t\tdiv_progress_info.firstChild.data  = blamedLines + ' / ' + totalLines +\n+\t\t\t' (' + spacePad(percentage, 3) + '%)';\n+\t}\n+\n+\tif (div_progress_bar) {\n+\t\t//div_progress_bar.setAttribute('style', 'width: '+percentage+'%;');\n+\t\tdiv_progress_bar.style.width = percentage + '%';\n+\t}\n+}\n+\n+\n+var t_interval_server = '';\n+var cmds_server = '';\n+var t0 = new Date();\n+\n+/**\n+ * write how much it took to generate data, and to run script\n+ */\n+function writeTimeInterval() {\n+\tvar info_time = document.getElementById('generate_time');\n+\tif (!info_time || !t_interval_server) {\n+\t\treturn;\n+\t}\n+\tvar t1 = new Date();\n+\tinfo_time.firstChild.data += ' + (' +\n+\t\tt_interval_server + 's server blame_data / ' +\n+\t\t(t1.getTime() - t0.getTime())/1000 + 's client JavaScript)';\n+\n+\tvar info_cmds = document.getElementById('generate_cmd');\n+\tif (!info_time || !cmds_server) {\n+\t\treturn;\n+\t}\n+\tinfo_cmds.firstChild.data += ' + ' + cmds_server;\n+}\n+\n+/**\n+ * show an error message alert to user within page (in prohress info area)\n+ * @param {String} str: plain text error message (no HTML)\n+ */\n+function errorInfo(str) {\n+\tif (!div_progress_info) {\n+\t\tdiv_progress_info = document.getElementById('progress_info');\n+\t}\n+\tif (div_progress_info) {\n+\t\tdiv_progress_info.className = 'error';\n+\t\tdiv_progress_info.firstChild.data = str;\n+\t}\n+}\n+\n+/* ............................................................ */\n+/* coloring rows during blame_data (git blame --incremental) run */\n+\n+/**\n+ * used to extract N from 'colorN', where N is a number,\n+ * @constant\n+ */\n+var colorRe = /\\bcolor([0-9]*)\\b/;\n+\n+/**\n+ * return N if <tr class=\"colorN\">, otherwise return null\n+ * (some browsers require CSS class names to begin with letter)\n+ *\n+ * @param {HTMLElement} tr: table row element to check\n+ * @param {String} tr.className: 'class' attribute of tr element\n+ * @returns {Number|null} N if tr.className == 'colorN', otherwise null\n+ */\n+function getColorNo(tr) {\n+\tif (!tr) {\n+\t\treturn null;\n+\t}\n+\tvar className = tr.className;\n+\tif (className) {\n+\t\tvar match = colorRe.exec(className);\n+\t\tif (match) {\n+\t\t\treturn parseInt(match[1], 10);\n+\t\t}\n+\t}\n+\treturn null;\n+}\n+\n+var colorsFreq = [0, 0, 0];\n+/**\n+ * return one of given possible colors (curently least used one)\n+ * example: chooseColorNoFrom(2, 3) returns 2 or 3\n+ *\n+ * @param {Number[]} arguments: one or more numbers\n+ *        assumes that  1 <= arguments[i] <= colorsFreq.length\n+ * @returns {Number} Least used color number from arguments\n+ */\n+function chooseColorNoFrom() {\n+\t// choose the color which is least used\n+\tvar colorNo = arguments[0];\n+\tfor (var i = 1; i < arguments.length; i++) {\n+\t\tif (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) {\n+\t\t\tcolorNo = arguments[i];\n+\t\t}\n+\t}\n+\tcolorsFreq[colorNo-1]++;\n+\treturn colorNo;\n+}\n+\n+/**\n+ * given two neigbour <tr> elements, find color which would be different\n+ * from color of both of neighbours; used to 3-color blame table\n+ *\n+ * @param {HTMLElement} tr_prev\n+ * @param {HTMLElement} tr_next\n+ * @returns {Number} color number N such that\n+ * colorN != tr_prev.className && colorN != tr_next.className\n+ */\n+function findColorNo(tr_prev, tr_next) {\n+\tvar color_prev = getColorNo(tr_prev);\n+\tvar color_next = getColorNo(tr_next);\n+\n+\n+\t// neither of neighbours has color set\n+\t// THEN we can use any of 3 possible colors\n+\tif (!color_prev && !color_next) {\n+\t\treturn chooseColorNoFrom(1,2,3);\n+\t}\n+\n+\t// either both neighbours have the same color,\n+\t// or only one of neighbours have color set\n+\t// THEN we can use any color except given\n+\tvar color;\n+\tif (color_prev === color_next) {\n+\t\tcolor = color_prev; // = color_next;\n+\t} else if (!color_prev) {\n+\t\tcolor = color_next;\n+\t} else if (!color_next) {\n+\t\tcolor = color_prev;\n+\t}\n+\tif (color) {\n+\t\treturn chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1);\n+\t}\n+\n+\t// neighbours have different colors\n+\t// THEN there is only one color left\n+\treturn (3 - ((color_prev + color_next) % 3));\n+}\n+\n+/* ............................................................ */\n+/* coloring rows like 'blame' after 'blame_data' finishes */\n+\n+/**\n+ * returns true if given row element (tr) is first in commit group\n+ * to be used only after 'blame_data' finishes (after processing)\n+ *\n+ * @param {HTMLElement} tr: table row\n+ * @returns {Boolean} true if TR is first in commit group\n+ */\n+function isStartOfGroup(tr) {\n+\treturn tr.firstChild.className === 'sha1';\n+}\n+\n+/**\n+ * change colors to use zebra coloring (2 colors) instead of 3 colors\n+ * concatenate neighbour commit groups belonging to the same commit\n+ */\n+function fixColorsAndGroups() {\n+\tvar colorClasses = ['light', 'dark'];\n+\tvar linenum = 1;\n+\tvar tr, prev_group;\n+\tvar colorClass = 0;\n+\tvar table =\n+\t\tdocument.getElementById('blame_table') ||\n+\t\tdocument.getElementsByTagName('table')[0];\n+\n+\twhile ((tr = document.getElementById('l'+linenum))) {\n+\t// index origin is 0, which is table header; start from 1\n+\t//while ((tr = table.rows[linenum])) { // <- it is slower\n+\t\tif (isStartOfGroup(tr, linenum, document)) {\n+\t\t\tif (prev_group &&\n+\t\t\t    prev_group.firstChild.firstChild.href ===\n+\t\t\t            tr.firstChild.firstChild.href) {\n+\t\t\t\t// we have to concatenate groups\n+\t\t\t\tvar prev_rows = prev_group.firstChild.rowSpan || 1;\n+\t\t\t\tvar curr_rows =         tr.firstChild.rowSpan || 1;\n+\t\t\t\tprev_group.firstChild.rowSpan = prev_rows + curr_rows;\n+\t\t\t\t//tr.removeChild(tr.firstChild);\n+\t\t\t\ttr.deleteCell(0); // DOM2 HTML way\n+\t\t\t} else {\n+\t\t\t\tcolorClass = (colorClass + 1) % 2;\n+\t\t\t\tprev_group = tr;\n+\t\t\t}\n+\t\t}\n+\t\tvar tr_class = tr.className;\n+\t\ttr.className = tr_class.replace(colorRe, colorClasses[colorClass]);\n+\t\tlinenum++;\n+\t}\n+}\n+\n+/* ............................................................ */\n+/* time and data */\n+\n+/**\n+ * used to extract hours and minutes from timezone info, e.g '-0900'\n+ * @constant\n+ */\n+var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/;\n+\n+/**\n+ * return date in local time formatted in iso-8601 like format\n+ * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200'\n+ *\n+ * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'\n+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'\n+ * @returns {String} date in local time in iso-8601 like format\n+ */\n+function formatDateISOLocal(epoch, timezoneInfo) {\n+\tvar match = tzRe.exec(timezoneInfo);\n+\t// date corrected by timezone\n+\tvar localDate = new Date(1000 * (epoch +\n+\t\t(parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));\n+\tvar localDateStr = // e.g. '2005-08-07'\n+\t\tlocalDate.getUTCFullYear()                 + '-' +\n+\t\tpadLeft(localDate.getUTCMonth()+1, 2, '0') + '-' +\n+\t\tpadLeft(localDate.getUTCDate(),    2, '0');\n+\tvar localTimeStr = // e.g. '21:49:46'\n+\t\tpadLeft(localDate.getUTCHours(),   2, '0') + ':' +\n+\t\tpadLeft(localDate.getUTCMinutes(), 2, '0') + ':' +\n+\t\tpadLeft(localDate.getUTCSeconds(), 2, '0');\n+\n+\treturn localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;\n+}\n+\n+/* ............................................................ */\n+/* unquoting/unescaping filenames */\n+\n+/**\n+ * Used to detect escape sequences (octal e.g. \\008 and symbolic e.g. \\n)\n+ * @constant\n+ */\n+var escCodeRe = /\\\\([^0-7]|[0-7]{1,3})/g;\n+\n+/**\n+ * unquote maybe git-quoted filename\n+ * e.g. 'aa' -> 'aa', '\"a\\ta\"' -> 'a\ta'\n+ *\n+ * @param {String} str: git-quoted string\n+ * @returns {String} Unquoted and unescaped string\n+ */\n+function unquote(str) {\n+\tfunction unq(seq) {\n+\t\tvar es = { // character escape codes, aka escape sequences\n+\t\t\tt: \"\\t\",   // tab            (HT, TAB)\n+\t\t\tn: \"\\n\",   // newline        (NL)\n+\t\t\tr: \"\\r\",   // return         (CR)\n+\t\t\tf: \"\\f\",   // form feed      (FF)\n+\t\t\tb: \"\\b\",   // backspace      (BS)\n+\t\t\ta: \"\\x07\", // alarm (bell)   (BEL)\n+\t\t\te: \"\\x1B\", // escape         (ESC)\n+\t\t\tv: \"\\v\"    // vertical tab   (VT)\n+\t\t};\n+\n+\t\tif (seq.search(/^[0-7]{1,3}$/) !== -1) {\n+\t\t\t// octal char sequence\n+\t\t\treturn String.fromCharCode(parseInt(seq, 8));\n+\t\t} else if (seq in es) {\n+\t\t\t// C escape sequence, aka character escape code\n+\t\t\treturn es[seq];\n+\t\t}\n+\t\t// quoted ordinary character\n+\t\treturn seq;\n+\t}\n+\n+\tvar match = str.match(/^\\\"(.*)\\\"$/);\n+\tif (match) {\n+\t\tstr = match[1];\n+\t\t// perhaps str = eval('\"'+str+'\"'); would be enough?\n+\t\tstr = str.replace(escCodeRe,\n+\t\t\tfunction (substr, p1, offset, s) { return unq(p1); });\n+\t}\n+\treturn str;\n+}\n+\n+/* ============================================================ */\n+/* main part: parsing response */\n+\n+/**\n+ * Function called for each blame entry, as soon as it finishes\n+ *\n+ * @param {Commit} commit: blamed commit (blame entry)\n+ */\n+function handleLine(commit) {\n+\t/* \n+\t   This is the structure of the HTML fragment we are working\n+\t   with:\n+\n+\t   <tr id=\"l123\" class=\"\">\n+\t     <td class=\"sha1\" title=\"\"><a href=\"\"> </a></td>\n+\t     <td class=\"linenr\"><a class=\"linenr\" href=\"\">123</a></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+\n+\t// format date and time string only once per commit\n+\tif (!commit.info) {\n+\t\t/* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */\n+\t\tcommit.info = commit.author + ', ' +\n+\t\t\tformatDateISOLocal(commit.authorTime, commit.authorTimezone);\n+\t}\n+\n+\t// color depends on group of lines, not only on blamed commit\n+\tvar colorNo = findColorNo(\n+\t\tdocument.getElementById('l'+(resline-1)),\n+\t\tdocument.getElementById('l'+(resline+commit.numlines))\n+\t);\n+\n+\t// loop over lines in commit group\n+\tfor (var i = 0; i < commit.numlines; i++) {\n+\t\tvar tr = document.getElementById('l'+resline);\n+\t\tif (!tr) {\n+\t\t\t//debug('tr is null! resline: ' + resline);\n+\t\t\tbreak;\n+\t\t}\n+\t\t/*\n+\t\t\t<tr id=\"l123\" class=\"\">\n+\t\t\t  <td class=\"sha1\" title=\"\"><a href=\"\"> </a></td>\n+\t\t\t  <td class=\"linenr\"><a class=\"linenr\" href=\"\">123</a></td>\n+\t\t\t  <td class=\"pre\"># times (my ext3 doesn&#39;t).</td>\n+\t\t\t</tr>\n+\t\t*/\n+\t\tvar td_sha1  = tr.firstChild;\n+\t\tvar a_sha1   = td_sha1.firstChild;\n+\t\tvar a_linenr = td_sha1.nextSibling.firstChild;\n+\n+\t\t/* <tr id=\"l123\" class=\"\"> */\n+\t\tvar tr_class = '';\n+\t\tif (colorNo !== null) {\n+\t\t\ttr_class = 'color'+colorNo;\n+\t\t}\n+\t\tif (commit.boundary) {\n+\t\t\ttr_class += ' boundary';\n+\t\t}\n+\t\tif (commit.nprevious === 0) {\n+\t\t\ttr_class += ' no-previous';\n+\t\t} else if (commit.nprevious > 1) {\n+\t\t\ttr_class += ' multiple-previous';\n+\t\t}\n+\t\ttr.className = tr_class;\n+\n+\t\t/* <td class=\"sha1\" title=\"?\" rowspan=\"?\"><a href=\"?\">?</a></td> */\n+\t\tif (i === 0) {\n+\t\t\ttd_sha1.title = commit.info;\n+\t\t\ttd_sha1.rowSpan = commit.numlines;\n+\n+\t\t\ta_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1;\n+\t\t\ta_sha1.firstChild.data = commit.sha1.substr(0, 8);\n+\t\t\tif (commit.numlines >= 2) {\n+\t\t\t\tvar fragment = document.createDocumentFragment();\n+\t\t\t\tvar br   = document.createElement(\"br\");\n+\t\t\t\tvar text = document.createTextNode(\n+\t\t\t\t\tcommit.author.match(/\\b([A-Z])\\B/g).join(''));\n+\t\t\t\tif (br && text) {\n+\t\t\t\t\tvar elem = fragment || td_sha1;\n+\t\t\t\t\telem.appendChild(br);\n+\t\t\t\t\telem.appendChild(text);\n+\t\t\t\t\tif (fragment) {\n+\t\t\t\t\t\ttd_sha1.appendChild(fragment);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\t//tr.removeChild(td_sha1); // DOM2 Core way\n+\t\t\ttr.deleteCell(0); // DOM2 HTML way\n+\t\t}\n+\n+\t\t/* <td class=\"linenr\"><a class=\"linenr\" href=\"?\">123</a></td> */\n+\t\tvar linenr_commit =\n+\t\t\t('previous' in commit ? commit.previous : commit.sha1);\n+\t\tvar linenr_filename =\n+\t\t\t('file_parent' in commit ? commit.file_parent : commit.filename);\n+\t\ta_linenr.href = projectUrl + 'a=blame_incremental' +\n+\t\t\t';hb=' + linenr_commit +\n+\t\t\t';f='  + encodeURIComponent(linenr_filename) +\n+\t\t\t'#l' + (commit.srcline + i);\n+\n+\t\tresline++;\n+\t\tblamedLines++;\n+\n+\t\t//updateProgressInfo();\n+\t}\n+}\n+\n+// ----------------------------------------------------------------------\n+\n+var prevDataLength = -1;\n+var nextLine = 0;\n+var inProgress = false;\n+\n+/**#@+\n+ * @constant\n+ */\n+var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/;\n+var infoRe = /^([a-z-]+) ?(.*)/;\n+var endRe  = /^END ?([^ ]*) ?(.*)/;\n+/**@-*/\n+\n+var curCommit = new Commit();\n+var pollTimer = null;\n+\n+/**\n+ * handler for XMLHttpRequest onreadystatechange event\n+ * @see startBlame\n+ */\n+function handleResponse() {\n+\t//debug('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+\t}\n+\n+\t// the server returned error\n+\tif (http.readyState === 3 && http.status !== 200) {\n+\t\treturn;\n+\t}\n+\tif (http.readyState === 4 && http.status !== 200) {\n+\t\tif (!div_progress_info) {\n+\t\t\tdiv_progress_info = document.getElementById('progress_info');\n+\t\t}\n+\n+\t\terrorInfo('Server error: ' +\n+\t\t\thttp.status + ' - ' + (http.statusText || 'Error contacting server'));\n+\n+\t\tclearInterval(pollTimer);\n+\t\tinProgress = false;\n+\t}\n+\n+\t// In konqueror http.responseText is sometimes null here...\n+\tif (http.responseText === null) {\n+\t\treturn;\n+\t}\n+\n+\t// in case we were called before finished processing\n+\tif (inProgress) {\n+\t\treturn;\n+\t} else {\n+\t\tinProgress = true;\n+\t}\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], 10);\n+\t\t\t\tvar resline  = parseInt(match[3], 10);\n+\t\t\t\tvar numlines = parseInt(match[4], 10);\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+\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\tswitch (info) {\n+\t\t\t\tcase 'filename':\n+\t\t\t\t\tcurCommit.filename = unquote(data);\n+\t\t\t\t\t// 'filename' information terminates the entry\n+\t\t\t\t\thandleLine(curCommit);\n+\t\t\t\t\tupdateProgressInfo();\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'author':\n+\t\t\t\t\tcurCommit.author = data;\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'author-time':\n+\t\t\t\t\tcurCommit.authorTime = parseInt(data, 10);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'author-tz':\n+\t\t\t\t\tcurCommit.authorTimezone = data;\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'previous':\n+\t\t\t\t\tcurCommit.nprevious++;\n+\t\t\t\t\tif (!'previous' in curCommit) {\n+\t\t\t\t\t\tvar parts = data.split(' ', 2);\n+\t\t\t\t\t\tcurCommit.previous    = parts[0];\n+\t\t\t\t\t\tcurCommit.file_parent = unquote(parts[1]);\n+\t\t\t\t\t}\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'boundary':\n+\t\t\t\t\t//debug('Boundary commit: '+curCommit.sha1);\n+\t\t\t\t\tcurCommit.boundary = true;\n+\t\t\t\t\tbreak;\n+\t\t\t\t} // end switch\n+\n+\t\t\t} else if ((match = endRe.exec(lines[i]))) {\n+\t\t\t\tt_interval_server = match[1];\n+\t\t\t\tcmds_server = match[2];\n+\t\t\t\t//debug('END: '+lines[i]);\n+\t\t\t} else if (lines[i] !== '') {\n+\t\t\t\t//debug('malformed line: ' + lines[i]);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t// did we finish work?\n+\tif (http.readyState === 4 &&\n+\t    prevDataLength === http.responseText.length) {\n+\t\tclearInterval(pollTimer);\n+\n+\t\tfixColorsAndGroups();\n+\t\twriteTimeInterval();\n+\t\tcommits = {}; // free memory\n+\t}\n+\n+\tinProgress = false;\n+}\n+\n+// ============================================================\n+// ------------------------------------------------------------\n+\n+/**\n+ * Incrementally update line data in blame_incremental view in gitweb.\n+ *\n+ * @param {String} blamedataUrl: URL to server script generating blame data.\n+ * @param {String} bUrl: partial URL to project, used to generate links.\n+ *\n+ * Called from 'blame_incremental' view after loading table with\n+ * file contents, a base for blame view.\n+*/\n+function startBlame(blamedataUrl, bUrl) {\n+\t//debug('startBlame('+blamedataUrl+', '+bUrl+')');\n+\n+\thttp = createRequestObject();\n+\tif (!http) {\n+\t\terrorInfo('ERROR: XMLHttpRequest not supported');\n+\t\treturn;\n+\t}\n+\n+\tt0 = new Date();\n+\tprojectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';');\n+\tif ((div_progress_bar = document.getElementById('progress_bar'))) {\n+\t\tdiv_progress_bar.setAttribute('style', 'width: 100%;');\n+\t\t//div_progress_bar.style.value = 'width: 100%;';\n+\t}\n+\ttotalLines = countLines();\n+\tupdateProgressInfo();\n+\n+\thttp.open('get', blamedataUrl);\n+\thttp.setRequestHeader('Accept', 'text/plain'); // in case of future changes\n+\t// perhaps also 'multipart/x-mixed-replace'\n+\thttp.onreadystatechange = handleResponse;\n+\t//http.onreadystatechange = function () { handleResponse(http); };\n+\thttp.send(null);\n+\n+\t// not all browsers call onreadystatechange event on each server flush\n+\tif (!DEBUG) {\n+\t\tpollTimer = setInterval(handleResponse, 1000);\n+\t}\n+}\n+\n+// end of blame.js\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex 8f68fe3..c9a7501 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -250,6 +250,11 @@ tr.no-previous td.linenr {\n \tfont-weight: bold;\n }\n \n+/* for 'blame_incremental', during processing */\n+tr.color1 { background-color: #f6fff6; }\n+tr.color2 { background-color: #f6f6ff; }\n+tr.color3 { background-color: #fff6f6; }\n+\n td {\n \tpadding: 2px 5px;\n \tfont-size: 100%;\n@@ -341,6 +346,17 @@ td.mode {\n \tfont-family: monospace;\n }\n \n+/* progress of blame_interactive */\n+div#progress_bar {\n+\theight: 2px;\n+\tmargin-bottom: -2px;\n+\tbackground-color: #d8d9d0;\n+}\n+div#progress_info {\n+\tfloat: right;\n+\ttext-align: right;\n+}\n+\n /* styling of diffs (patchsets): commitdiff and blobdiff views */\n div.diff.header,\n div.diff.extended_header {\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex c6894e2..b94ce10 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -96,6 +96,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@@ -564,6 +566,8 @@ our %cgi_param_mapping = @cgi_param_mapping;\n # we will also need to know the possible actions, for validation\n our %actions = (\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 \t\"blobdiff_plain\" => \\&git_blobdiff_plain,\n \t\"blob\" => \\&git_blob,\n@@ -4787,7 +4791,9 @@ sub git_tag {\n \tgit_footer_html();\n }\n \n-sub git_blame {\n+sub git_blame_common {\n+\tmy $format = shift || 'porcelain';\n+\n \t# permissions\n \tgitweb_check_feature('blame')\n \t\tor die_error(403, \"Blame view not allowed\");\n@@ -4809,10 +4815,43 @@ sub git_blame {\n \t\t}\n \t}\n \n-\t# run git-blame --porcelain\n-\topen my $fd, \"-|\", git_cmd(), \"blame\", '-p',\n-\t\t$hash_base, '--', $file_name\n-\t\tor die_error(500, \"Open git-blame failed\");\n+\tmy $fd;\n+\tif ($format eq 'incremental') {\n+\t\t# get file contents (as base)\n+\t\topen $fd, \"-|\", git_cmd(), 'cat-file', 'blob', $hash\n+\t\t\tor die_error(500, \"Open git-cat-file failed\");\n+\t} elsif ($format eq 'data') {\n+\t\t# run git-blame --incremental\n+\t\topen $fd, \"-|\", git_cmd(), \"blame\", \"--incremental\",\n+\t\t\t$hash_base, \"--\", $file_name\n+\t\t\tor die_error(500, \"Open git-blame --incremental failed\");\n+\t} else {\n+\t\t# run git-blame --porcelain\n+\t\topen $fd, \"-|\", git_cmd(), \"blame\", '-p',\n+\t\t\t$hash_base, '--', $file_name\n+\t\t\tor die_error(500, \"Open git-blame --porcelain failed\");\n+\t}\n+\n+\t# incremental blame data returns early\n+\tif ($format eq 'data') {\n+\t\tprint $cgi->header(\n+\t\t\t-type=>\"text/plain\", -charset => \"utf-8\",\n+\t\t\t-status=> \"200 OK\");\n+\t\tlocal $| = 1; # output autoflush\n+\t\tprint while <$fd>;\n+\t\tclose $fd\n+\t\t\tor print \"ERROR $!\\n\";\n+\n+\t\tprint 'END';\n+\t\tif (defined $t0 && gitweb_check_feature('timed')) {\n+\t\t\tprint ' '.\n+\t\t\t      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).\n+\t\t\t      ' '.$number_of_git_cmds;\n+\t\t}\n+\t\tprint \"\\n\";\n+\n+\t\treturn;\n+\t}\n \n \t# page header\n \tgit_header_html();\n@@ -4823,109 +4862,169 @@ sub git_blame {\n \t\t$cgi->a({-href => href(action=>\"history\", -replay=>1)},\n \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=>$action, 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 \n \t# page body\n+\tif ($format eq 'incremental') {\n+\t\tprint \"<noscript>\\n<div class=\\\"error\\\"><center><b>\\n\".\n+\t\t      \"This page requires JavaScript to run.\\n Use \".\n+\t\t      $cgi->a({-href => href(action=>'blame',-replay=>1)}, 'this page').\n+\t\t      \" instead.\\n\".\n+\t\t      \"</b></center></div>\\n</noscript>\\n\";\n+\n+\t\tprint qq!<div id=\"progress_bar\" style=\"width: 100%; background-color: yellow\"></div>\\n!;\n+\t}\n+\n+\tprint qq!<div class=\"page_body\">\\n!;\n+\tprint qq!<div id=\"progress_info\">... / ...</div>\\n!\n+\t\tif ($format eq 'incremental');\n+\tprint qq!<table id=\"blame_table\" class=\"blame\" width=\"100%\">\\n!.\n+\t      #qq!<col width=\"5.5em\" /><col width=\"2.5em\" /><col width=\"*\" />\\n!.\n+\t      qq!<thead>\\n!.\n+\t      qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\\n!.\n+\t      qq!</thead>\\n!.\n+\t      qq!<tbody>\\n!;\n+\n \tmy @rev_color = qw(light dark);\n \tmy $num_colors = scalar(@rev_color);\n \tmy $current_color = 0;\n-\tmy %metainfo = ();\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- LINE:\n-\twhile (my $line = <$fd>) {\n-\t\tchomp $line;\n-\t\t# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]\n-\t\t# no <lines in group> for subsequent lines in group of lines\n-\t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n-\t\t   ($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n-\t\tif (!exists $metainfo{$full_rev}) {\n-\t\t\t$metainfo{$full_rev} = { 'nprevious' => 0 };\n-\t\t}\n-\t\tmy $meta = $metainfo{$full_rev};\n-\t\tmy $data;\n-\t\twhile ($data = <$fd>) {\n-\t\t\tchomp $data;\n-\t\t\tlast if ($data =~ s/^\\t//); # contents of line\n-\t\t\tif ($data =~ /^(\\S+)(?: (.*))?$/) {\n-\t\t\t\t$meta->{$1} = $2 unless exists $meta->{$1};\n+\tif ($format eq 'incremental') {\n+\t\tmy $color_class = $rev_color[$current_color];\n+\n+\t\t#contents of a file\n+\t\tmy $linenr = 0;\n+\tLINE:\n+\t\twhile (my $line = <$fd>) {\n+\t\t\tchomp $line;\n+\t\t\t$linenr++;\n+\n+\t\t\tprint qq!<tr id=\"l$linenr\" class=\"$color_class\">!.\n+\t\t\t      qq!<td class=\"sha1\"><a href=\"\"> </a></td>!.\n+\t\t\t      qq!<td class=\"linenr\">!.\n+\t\t\t      qq!<a class=\"linenr\" href=\"\">$linenr</a></td>!;\n+\t\t\tprint qq!<td class=\"pre\">! . esc_html($line) . \"</td>\\n\";\n+\t\t\tprint qq!</tr>\\n!;\n+\t\t}\n+\n+\t} else { # porcelain, i.e. ordinary blame\n+\t\tmy %metainfo = (); # saves information about commits\n+\n+\t\t# blame data\n+\tLINE:\n+\t\twhile (my $line = <$fd>) {\n+\t\t\tchomp $line;\n+\t\t\t# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]\n+\t\t\t# no <lines in group> for subsequent lines in group of lines\n+\t\t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n+\t\t\t   ($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n+\t\t\tif (!exists $metainfo{$full_rev}) {\n+\t\t\t\t$metainfo{$full_rev} = { 'nprevious' => 0 };\n \t\t\t}\n-\t\t\tif ($data =~ /^previous /) {\n-\t\t\t\t$meta->{'nprevious'}++;\n+\t\t\tmy $meta = $metainfo{$full_rev};\n+\t\t\tmy $data;\n+\t\t\twhile ($data = <$fd>) {\n+\t\t\t\tchomp $data;\n+\t\t\t\tlast if ($data =~ s/^\\t//); # contents of line\n+\t\t\t\tif ($data =~ /^(\\S+)(?: (.*))?$/) {\n+\t\t\t\t\t$meta->{$1} = $2 unless exists $meta->{$1};\n+\t\t\t\t}\n+\t\t\t\tif ($data =~ /^previous /) {\n+\t\t\t\t\t$meta->{'nprevious'}++;\n+\t\t\t\t}\n \t\t\t}\n-\t\t}\n-\t\tmy $short_rev = substr($full_rev, 0, 8);\n-\t\tmy $author = $meta->{'author'};\n-\t\tmy %date =\n-\t\t\tparse_date($meta->{'author-time'}, $meta->{'author-tz'});\n-\t\tmy $date = $date{'iso-tz'};\n-\t\tif ($group_size) {\n-\t\t\t$current_color = ($current_color + 1) % $num_colors;\n-\t\t}\n-\t\tmy $tr_class = $rev_color[$current_color];\n-\t\t$tr_class .= ' boundary' if (exists $meta->{'boundary'});\n-\t\t$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);\n-\t\t$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);\n-\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$tr_class\\\">\\n\";\n-\t\tif ($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 $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($short_rev));\n-\t\t\tif ($group_size >= 2) {\n-\t\t\t\tmy @author_initials = ($author =~ /\\b([[:upper:]])\\B/g);\n-\t\t\t\tif (@author_initials) {\n-\t\t\t\t\tprint \"<br />\" .\n-\t\t\t\t\t      esc_html(join('', @author_initials));\n-\t\t\t\t\t#           or join('.', ...)\n+\t\t\tmy $short_rev = substr($full_rev, 0, 8);\n+\t\t\tmy $author = $meta->{'author'};\n+\t\t\tmy %date =\n+\t\t\t\tparse_date($meta->{'author-time'}, $meta->{'author-tz'});\n+\t\t\tmy $date = $date{'iso-tz'};\n+\t\t\tif ($group_size) {\n+\t\t\t\t$current_color = ($current_color + 1) % $num_colors;\n+\t\t\t}\n+\t\t\tmy $tr_class = $rev_color[$current_color];\n+\t\t\t$tr_class .= ' boundary' if (exists $meta->{'boundary'});\n+\t\t\t$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);\n+\t\t\t$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);\n+\t\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$tr_class\\\">\\n\";\n+\t\t\tif ($group_size) {\n+\t\t\t\tprint \"<td class=\\\"sha1\\\"\";\n+\t\t\t\tprint \" title=\\\"\". esc_html($author) . \", $date\\\"\";\n+\t\t\t\tprint \" rowspan=\\\"$group_size\\\"\" if ($group_size > 1);\n+\t\t\t\tprint \">\";\n+\t\t\t\tprint $cgi->a({-href => href(action=>\"commit\",\n+\t\t\t\t                             hash=>$full_rev,\n+\t\t\t\t                             file_name=>$file_name)},\n+\t\t\t\t              esc_html($short_rev));\n+\t\t\t\tif ($group_size >= 2) {\n+\t\t\t\t\tmy @author_initials = ($author =~ /\\b([[:upper:]])\\B/g);\n+\t\t\t\t\tif (@author_initials) {\n+\t\t\t\t\t\tprint \"<br />\" .\n+\t\t\t\t\t\t      esc_html(join('', @author_initials));\n+\t\t\t\t\t\t#           or join('.', ...)\n+\t\t\t\t\t}\n \t\t\t\t}\n+\t\t\t\tprint \"</td>\\n\";\n \t\t\t}\n-\t\t\tprint \"</td>\\n\";\n-\t\t}\n-\t\t# 'previous' <sha1 of parent commit> <filename at commit>\n-\t\tif (exists $meta->{'previous'} &&\n-\t\t    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n-\t\t\t$meta->{'parent'} = $1;\n-\t\t\t$meta->{'file_parent'} = unquote($2);\n-\t\t}\n-\t\tmy $linenr_commit =\n-\t\t\texists($meta->{'parent'}) ?\n-\t\t\t$meta->{'parent'} : $full_rev;\n-\t\tmy $linenr_filename =\n-\t\t\texists($meta->{'file_parent'}) ?\n-\t\t\t$meta->{'file_parent'} : unquote($meta->{'filename'});\n-\t\tmy $blamed = href(action => 'blame',\n-\t\t                  file_name => $linenr_filename,\n-\t\t                  hash_base => $linenr_commit);\n-\t\tprint \"<td class=\\\"linenr\\\">\";\n-\t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n-\t\t                -class => \"linenr\" },\n-\t\t              esc_html($lineno));\n-\t\tprint \"</td>\";\n-\t\tprint \"<td class=\\\"pre\\\">\" . esc_html($data) . \"</td>\\n\";\n-\t\tprint \"</tr>\\n\";\n+\t\t\t# 'previous' <sha1 of parent commit> <filename at commit>\n+\t\t\tif (exists $meta->{'previous'} &&\n+\t\t\t    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n+\t\t\t\t$meta->{'parent'} = $1;\n+\t\t\t\t$meta->{'file_parent'} = unquote($2);\n+\t\t\t}\n+\t\t\tmy $linenr_commit =\n+\t\t\t\texists($meta->{'parent'}) ?\n+\t\t\t\t$meta->{'parent'} : $full_rev;\n+\t\t\tmy $linenr_filename =\n+\t\t\t\texists($meta->{'file_parent'}) ?\n+\t\t\t\t$meta->{'file_parent'} : unquote($meta->{'filename'});\n+\t\t\tmy $blamed = href(action => 'blame',\n+\t\t\t                  file_name => $linenr_filename,\n+\t\t\t                  hash_base => $linenr_commit);\n+\t\t\tprint \"<td class=\\\"linenr\\\">\";\n+\t\t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n+\t\t\t                -class => \"linenr\" },\n+\t\t\t              esc_html($lineno));\n+\t\t\tprint \"</td>\";\n+\t\t\tprint \"<td class=\\\"pre\\\">\" . esc_html($data) . \"</td>\\n\";\n+\t\t\tprint \"</tr>\\n\";\n+\t\t} # end while\n+\n \t}\n-\tprint \"</table>\\n\";\n-\tprint \"</div>\";\n+\n+\t# footer\n+\tprint \"</tbody>\\n\".\n+\t      \"</table>\\n\"; # class=\"blame\"\n+\tprint \"</div>\\n\";   # class=\"blame_body\"\n \tclose $fd\n \t\tor print \"Reading blob failed\\n\";\n \n-\t# page footer\n+\tif ($format eq 'incremental') {\n+\t\tprint qq!<script type=\"text/javascript\" src=\"$blamejs\"></script>\\n!.\n+\t\t      qq!<script type=\"text/javascript\">\\n!.\n+\t\t      qq!startBlame(\"!. href(action=>\"blame_data\", -replay=>1) .qq!\",\\n!.\n+\t\t      qq!           \"!. href() .qq!\");\\n!.\n+\t\t      qq!</script>\\n!;\n+\t}\n+\n \tgit_footer_html();\n }\n \n+sub git_blame {\n+\tgit_blame_common();\n+}\n+\n+sub git_blame_incremental {\n+\tgit_blame_common('incremental');\n+}\n+\n+sub git_blame_data {\n+\tgit_blame_common('data');\n+}\n+\n sub git_tags {\n \tmy $head = git_get_head_hash($project);\n \tgit_header_html();\n-- \n1.6.3.3\n"},{"id":"119802","messageId":"1249578712-3862-4-git-send-email-jnareb@gmail.com","threadId":"20443","inReplyTo":"1249578712-3862-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv2/RFC 3/3] gitweb: Create links leading to 'blame_incremental' using JavaScript","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-08-06T17:11:52Z","receivedAt":"2009-08-06T17:11:52Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"The new 'blame_incremental' view requires JavaScript to run.  Not all\nweb browsers implement JavaScript (e.g. text browsers such as Lynx),\nand not all users have JavaScript enabled.  Therefore instead of\nunconditionally link to 'blame_incremental' view, we use JavaScript to\nconvert those links to lead to view utilizing JavaScript, by adding\n'js=1' to link.\n\nThe only JavaScript-aware/using view is currently 'blame_incremental'.\nAs first, it might want to have links to non-JavaScript version, and\nsecond, it should also use window.onload, we do not add nor run\nfixLinks() for such views (currently hardcoded 'blame_incremental')\n\nPossible enhancement would be to do JavaScript redirect by setting\nwindow.location instead of modifying $format and $action in\ngit_blame_common() subroutine.\n\n\nThis idea was originally implemented by Petr Baudis in\n  http://article.gmane.org/gmane.comp.version-control.git/47614\nbut it added <script> element with fixBlameLinks() function in page\nheader, to be added as onload event using 'onload' attribute of HTML\n'body' element: <body onload=\"fixBlameLinks();\">.  This version adds\nscript at then end of page (in the page footer), and uses JavaScript\n'window.onload=fixLinks();'.  Also in Petr version only links marked\nwith 'blamelink' class were modified, and they were modified by\nreplacing \"a=blame\" by \"a=blame_incremental\"... which doesn't work for\npath_info links, and might replace wrong part if there is \"a=blame\" in\nproject name, ref name or file name.\n\nSlightly different solution was implemented by Martin Koegler in\n  http://thread.gmane.org/gmane.comp.version-control.git/47902/focus=47905\nHere GitAddLinks() function was in gitweb.js file, not as contents of\n<script> element.  This might be a better solution (although I think\nit would be better to split JavaScript file and load only parts that\nare required).  It was also included in page header (in <head>\nelement) though, which means waiting for a script to load (and run).\nIt was smarter in that to \"fix\" (modify) link, it split URL, modified\nvalue of 'a' parameter, and then recreated modified link.  It avoids\ntrouble with \"a=blame\" as substring in project name or file name, but\nit doesn't work with path_info URL/link in the way it was written.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis is nearly the same as previous (first) version, it only doesn't\nhave unrelated changes to blame.js that were included in earlier\nversion by mistake.\n\n\nTODO list:\n* Put fixLinks() function in gitweb.js, together with all code\n  required for 'blame_incremental' view.\n* Better solution to \"don't invoke for JavaScript-aware actions\"\n  problem.  Currently hardcoded 'blame_incremental'.\n\n  The problem to be solved is that we might want in views utilizing\n  JavaScript to have fallback links to version not using JavaScript.\n\n\nTODO for future commits:\n* Use 'click' event to change links to jave 'js=1' parameter appended;\n  this way we would check if JavaScript is enabled at the moment of\n  following (clicking) link, not at the moment of loading the page.\n\n  Unfortunately adding event listeners (much better solution than\n  providing/adding 'onclick' attribute) is different in different\n  browsers.\n\n gitweb/gitweb.perl |   21 +++++++++++++++++++++\n 2 files changed, 23 insertions(+), 2 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex b94ce10..32cbb20 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -3242,6 +3242,23 @@ sub git_footer_html {\n \t\tinsert_file($site_footer);\n \t}\n \n+\tif ($action ne 'blame_incremental') {\n+\t\tprint <<'HTML';\n+<script type=\"text/javascript\">/* <![CDATA[ */\n+function fixLinks() {\n+\t//var allLinks = document.getElementsByTagName(\"a\");\n+\tvar allLinks = document.links;\n+\tfor (var i = 0; i < allLinks.length; i++) {\n+\t\tvar link = allLinks[i];\n+\t\tlink.href +=\n+\t\t\t(link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1';\n+\t}\n+}\n+window.onload = fixLinks;\n+/* ]]> */</script>\n+HTML\n+\t}\n+\n \tprint \"</body>\\n\" .\n \t      \"</html>\";\n }\n@@ -4793,6 +4810,10 @@ sub git_tag {\n \n sub git_blame_common {\n \tmy $format = shift || 'porcelain';\n+\tif ($format eq 'porcelain' && $cgi->param('js')) {\n+\t\t$format = 'incremental';\n+\t\t$action = 'blame_incremental'; # for page title etc\n+\t}\n \n \t# permissions\n \tgitweb_check_feature('blame')\n-- \n1.6.3.3\n"},{"id":"120424","messageId":"1250096888-16177-1-git-send-email-jnareb@gmail.com","threadId":"20443","inReplyTo":"1249578712-3862-3-git-send-email-jnareb@gmail.com","subject":"[PATCHv4/RFC 2/3] gitweb: Incremental blame (WIP)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-08-12T17:08:08Z","receivedAt":"2009-08-12T17:08:08Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This is tweaked up version of Petr Baudis <pasky@suse.cz> patch, which\nin turn was tweaked up version of Fredrik Kuivinen <frekui@gmail.com>'s\nproof of concept patch.  It adds 'blame_incremental' view, which\nincrementally displays line data in blame view using JavaScript (AJAX).\n\nThis patch does not (contrary to the one by Petr Baudis) enable this\nview in gitweb: there are no links leading to 'blame_incremental'\naction.  You would have to generate URL 'by hand' (e.g. changing 'blame'\nor 'blob' in gitweb URL to 'blame_incremental').  Having links in gitweb\nlead to this new action (e.g. by rewriting them like in previous patch,\nif JavaScript is enabled in browser) is left for later.\n\nLike earlier patch by Per Baudis it avoids code duplication, but it goes\none step further and use git_blame_common for ordinary blame view, for\nincremental blame, and (which is change from previous patch) for\nincremental blame data.\n\nHow the 'blame_incremental' view works:\n* gitweb generates initial info by putting file contents (from\n  git-cat-file) together with line numbers in blame table\n* then gitweb makes web browser JavaScript engine call startBlame()\n  function from blame.js\n* startBlame() opens connection to 'blame_data' view, which in turn\n  calls \"git blame --incremental\" for a file, and streams output of\n  git-blame to JavaScript (blame.js)\n* blame.js updates line info in blame view, coloring it, and updating\n  progress info; note that it has to use 3 colors to ensure that\n  different neighboring groups have different styles\n* when 'blame_data' ends, and blame.js finishes updating line info,\n  it fixes colors to match (as far as possible) ordinary 'blame' view,\n  and updates generating time info.\n\nIt deals with streamed 'blame_data' server error by notifying about them\nin the progress info area (just in case).\n\nDifferences between 'blame_incremental' and original 'blame' view:\n* 'blame_incremental' always used (partial) query form for links\n  generated by JavaScript.  The difference is visible if we use path_info\n  link (pass some or all arguments in path_info), e.g. in 'blame' view\n  called using:\n    http://git.example.com/w/git.git/blame/HEAD:/README\n  we have 'linenr' links using the same form:\n    http://git.example.com/w/git.git/blame/e83c5163316f89bfbde7d9ab23ca2e25604af290:/README#l4\n  while in 'blame_incremental' view called with:\n    http://git.example.com/w/git.git/blame_incremental/HEAD:/README\n  we have \"partial query\" form\n    http://git.example.com/w/git.git?;a=blame_incremental;hb=e83c5163316f89bfbde7d9ab23ca2e25604af290;f=README#l4\n  Changing this would require implementing something akin to href()\n  subroutine from gitweb.perl in JavaScript\n* 'blame_incremental' always uses \"rowspan\" attribute, even if\n  rowspan=\"1\".  This is not visible to user.  (The code actually\n  could work without it).\n\nThis patch adds GITWEB_BLAMEJS compile configuration option, and\nmodifies git-instaweb.sh to take blame.js into account, but it does not\nupdate gitweb/README file (as it is only proof of concept patch).  The\ncode for git-instaweb.sh was taken from Pasky's patch.\n\nSigned-off-by: Fredrik Kuivinen <frekui@gmail.com>\nSigned-off-by: Petr Baudis <pasky@suse.cz>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nRefrences:\n1. Original patch by Frederik Kuivinen\n   http://article.gmane.org/gmane.comp.version-control.git/41361\n2. Tweaked up version by Petr Baudis\n   http://article.gmane.org/gmane.comp.version-control.git/47614\n   http://article.gmane.org/gmane.comp.version-control.git/56657\n3. New link rewriting and some optimization in Matrin Koegler\n   series introducing some JavaScript support in Git\n   http://thread.gmane.org/gmane.comp.version-control.git/47902/focus=47905   \n4. My earlier patches\n   http://thread.gmane.org/gmane.comp.version-control.git/102657/focus=102712\n   http://article.gmane.org/gmane.comp.version-control.git/123202\n   http://thread.gmane.org/gmane.comp.version-control.git/123957/focus=123968\n\nChanges compared to last version (v3):\n* Try yet another method (window.createrequest()) in\n  createRequestObject() function.\n* Rename variable holding XMLHttpRequest object from 'http' to 'xhr'\n* Separate data about group of lines which are changed by the smae\n  commit (which blame the same commit): srcline, resline, numlines,\n  and put it in curGroup variable instead of using Commit object\n  (curCommit) for that.  handleLine() function acquires 'group' as\n  second parameter.\n* delete no longer needed Commit properties (to be removed)\n* Simplify logic and code of selecting unprocessed part of\n  XMLHttpRequest responseText for processing.  nextLine global\n  variable is now named nextReadPos, code now check first if there\n  are any complete unprocessed lines before doing split('\\n'),\n  and there is now no need to remove incomplete line from 'lines'.\n  This code which was inside handleResponse() can now be found in\n  processData subroutine.\n* Separate parts of handleResponse() as a cleanup and in preparation\n  for having (simpler) event handlers for 'progress', 'load' and\n  'error' W3C Progress Events, and having 'readystatechange' handler\n  to be just dispatcher for those event handlers.  Introduce functions:\n  processBlameLines, processData, handleError and responseLoaded\n* Use DOM 2 HTML \"elem.style.cssText = value;\" instead of \n  DOM 2 Core \"elem.setAttribute('style', value);\"\n* Add a few 'work in progress' comments.\n* Remove comment about 'multipart/x-mixed-replace': it is\n  non-standard, and it requires server to know when chunks ands to\n  send replacements, something that cannot easily be done: incremental\n  git-blame sends data in chunks, but gitweb doesn't know when they\n  end.\n\nTODO list:\n* Use W3C Progress Events instead of 'readystatechange' event\n    http://www.w3.org/TR/progress-events/\n    http://www.w3.org/TR/XMLHttpRequest2/\n    http://www.nczonline.net/blog/2009/07/09/firefox-35firebug-xmlhttprequest-and-readystatechange-bug/\n  if XMLHttpResponse supports it, 'readystatechange' event + poll\n  timer otherwise.\n* handleResponse is used both as onreadystatechange and pollTimer;\n  if onreadystatechange works for partial responses we can turn off\n  the timer.\n* Clean up handling concurrency, i.e. dealing with situation when\n  handleResponse event handler is called before previous call finished\n  working.  Currently it is done using inProgress flag, and busy-read\n  while loop.\n* Get rid of global variables, if possible.\n* Instead of running startBlame, put it in window.onload handler\n* Rename blame.js to gitweb.js to have all JavaScript in a single\n  file.  Note that this would require changes to next commit in\n  series (enabling 'blame_incremental' view if browser has javaScript\n  turned on).\n\nTODO for future commits:\n* Remove (fade out) progress bar and progress info after blame\n  incremental finished run (left for a separate commit).\n* Profile blame.js using YUI Profiler or other JavaScript profile tool\n    http://developer.yahoo.com/yui/profiler/\n  to check whether performance improvements described in articles\n  on NCZOnline blog by Nicholas C. Zakas are required, and would help\n    http://www.nczonline.net/blog/\n* Check whether it is worth to do some server processing of\n  incremental blame output, and send e.g. JSON and not text/plain\n  of raw 'git blame --incremental' output.\n* Check how supported is 'multipart/x-mixed-replace' mimetype for data\n  pushed from server (and how to correctly implement it); perhaps that\n  would be a solution to concurrency problem?\n* Check whether it would make sense to use some lightweight JavaScript\n  library (framework) like jQuery, Prototype, ExtJS, MooTools, etc.\n* Minify JavaScript during building gitweb/gitweb.cgi, if minification\n  tool is found.\n\n\n Makefile           |    6 +-\n git-instaweb.sh    |    7 +\n gitweb/blame.js    |  819 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.css  |   16 +\n gitweb/gitweb.perl |  271 ++++++++++++------\n 5 files changed, 1032 insertions(+), 87 deletions(-)\n create mode 100644 gitweb/blame.js\n\ndiff --git a/Makefile b/Makefile\nindex daf4296..cb93aa3 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -265,6 +265,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@@ -1407,13 +1408,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@@ -1422,6 +1424,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    -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \\\n \t    $@.sh > $@+ && \\\n \tchmod +x $@+ && \\\ndiff --git a/git-instaweb.sh b/git-instaweb.sh\nindex 5f5cac7..0288450 100755\n--- a/git-instaweb.sh\n+++ b/git-instaweb.sh\n@@ -331,8 +331,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..9230cb7\n--- /dev/null\n+++ b/gitweb/blame.js\n@@ -0,0 +1,819 @@\n+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n+//               2007, Petr Baudis <pasky@suse.cz>\n+//          2008-2009, Jakub Narebski <jnareb@gmail.com>\n+\n+/**\n+ * @fileOverview JavaScript part of 'blame_incremental' action in gitweb.\n+ * @license GPLv2 or later\n+ */\n+\n+/*\n+ * This code uses DOM methods instead of (nonstandard) innerHTML\n+ * to modify page.\n+ *\n+ * innerHTML is non-standard IE extension, though supported by most\n+ * browsers; however Firefox up to version 1.5 didn't implement it in\n+ * a strict mode (application/xml+xhtml mimetype).  Also my simple\n+ * benchmarks show that using elem.firstChild.data = 'content' is\n+ * slightly faster than elem.innerHTML = 'content', if more fragile\n+ * (text element fragment must exists), and less feature-rich (we\n+ * cannot add HTML).\n+ */\n+\n+/* ============================================================ */\n+/* generic utility functions */\n+\n+var DEBUG = 0;\n+function debug(str) {\n+\tif (DEBUG) {\n+\t\talert(str);\n+\t}\n+}\n+\n+/**\n+ * pad number N with nonbreakable spaces on the left, to WIDTH characters\n+ * example: spacePad(12, 3) == '&nbsp;12' ('&nbsp;' is nonbreakable space)\n+ *\n+ * @param {Number} n: number to pad\n+ * @param {Number} width: visible width of output\n+ * @returns {String} N prefixed with (WIDTH - N.length) x '&nbsp;'\n+ */\n+function spacePad(n, width) {\n+\tvar prefix = '';\n+\n+\twidth -= n.toString().length;\n+\twhile (width > 1) {\n+\t\tprefix += '&nbsp;';\n+\t\twidth--;\n+\t}\n+\treturn prefix + n;\n+}\n+\n+/**\n+ * Pad INPUT on the left to SIZE width, using given padding character CH,\n+ * for example padLeft('a', 3, '_') is '__a'.\n+ *\n+ * @param {String} input: input value converted to string.\n+ * @param {Number} size: desired length of output.\n+ * @param {String} ch: single character to prefix to string.\n+ *\n+ * @returns {String} Modified string, at least SIZE length.\n+ */\n+function padLeft(input, size, ch) {\n+\tvar s = input + \"\";\n+\twhile (s.length < size) {\n+\t\ts = ch + s;\n+\t}\n+\treturn s;\n+}\n+\n+/**\n+ * Create XMLHttpRequest object in cross-browser way\n+ * @returns XMLHttpRequest object, or null\n+ */\n+function createRequestObject() {\n+\ttry {\n+\t\treturn new XMLHttpRequest();\n+\t} catch (e) {}\n+\ttry {\n+\t\treturn window.createRequest();\n+\t} catch (e) {}\n+\ttry {\n+\t\treturn new ActiveXObject(\"Msxml2.XMLHTTP\");\n+\t} catch (e) {}\n+\ttry {\n+\t\treturn new ActiveXObject(\"Microsoft.XMLHTTP\");\n+\t} catch (e) {}\n+\n+\t//debug(\"XMLHttpRequest not supported\");\n+\treturn null;\n+}\n+\n+/* ============================================================ */\n+/* utility/helper functions (and variables) */\n+\n+var xhr;        // XMLHttpRequest object\n+var projectUrl; // partial query + separator ('?' or ';')\n+\n+// 'commits' is an associative map. It maps SHA1s to Commit objects.\n+var commits = {};\n+\n+/**\n+ * constructor for Commit objects, used in 'blame'\n+ * @class Represents a blamed commit\n+ * @param {String} sha1: SHA-1 identifier of a commit\n+ */\n+function Commit(sha1) {\n+\tif (this instanceof Commit) {\n+\t\tthis.sha1 = sha1;\n+\t\tthis.nprevious = 0; /* number of 'previous', effective parents */\n+\t} else {\n+\t\treturn new Commit(sha1);\n+\t}\n+}\n+\n+/* ............................................................ */\n+/* progress info, timing, error reporting */\n+\n+var blamedLines = 0;\n+var totalLines  = '???';\n+var div_progress_bar;\n+var div_progress_info;\n+\n+/**\n+ * Detects how many lines does a blamed file have,\n+ * This information is used in progress info\n+ *\n+ * @returns {Number|String} Number of lines in file, or string '...'\n+ */\n+function countLines() {\n+\tvar table =\n+\t\tdocument.getElementById('blame_table') ||\n+\t\tdocument.getElementsByTagName('table')[0];\n+\n+\tif (table) {\n+\t\treturn table.getElementsByTagName('tr').length - 1; // for header\n+\t} else {\n+\t\treturn '...';\n+\t}\n+}\n+\n+/**\n+ * update progress info and length (width) of progress bar\n+ */\n+function updateProgressInfo() {\n+\tif (!div_progress_info) {\n+\t\tdiv_progress_info = document.getElementById('progress_info');\n+\t}\n+\tif (!div_progress_bar) {\n+\t\tdiv_progress_bar = document.getElementById('progress_bar');\n+\t}\n+\tif (!div_progress_info && !div_progress_bar) {\n+\t\treturn;\n+\t}\n+\n+\tvar percentage = Math.floor(100.0*blamedLines/totalLines);\n+\n+\tif (div_progress_info) {\n+\t\tdiv_progress_info.firstChild.data  = blamedLines + ' / ' + totalLines +\n+\t\t\t' (' + spacePad(percentage, 3) + '%)';\n+\t}\n+\n+\tif (div_progress_bar) {\n+\t\t//div_progress_bar.setAttribute('style', 'width: '+percentage+'%;');\n+\t\tdiv_progress_bar.style.width = percentage + '%';\n+\t}\n+}\n+\n+\n+var t_interval_server = '';\n+var cmds_server = '';\n+var t0 = new Date();\n+\n+/**\n+ * write how much it took to generate data, and to run script\n+ */\n+function writeTimeInterval() {\n+\tvar info_time = document.getElementById('generate_time');\n+\tif (!info_time || !t_interval_server) {\n+\t\treturn;\n+\t}\n+\tvar t1 = new Date();\n+\tinfo_time.firstChild.data += ' + (' +\n+\t\tt_interval_server + 's server blame_data / ' +\n+\t\t(t1.getTime() - t0.getTime())/1000 + 's client JavaScript)';\n+\n+\tvar info_cmds = document.getElementById('generate_cmd');\n+\tif (!info_time || !cmds_server) {\n+\t\treturn;\n+\t}\n+\tinfo_cmds.firstChild.data += ' + ' + cmds_server;\n+}\n+\n+/**\n+ * show an error message alert to user within page (in prohress info area)\n+ * @param {String} str: plain text error message (no HTML)\n+ */\n+function errorInfo(str) {\n+\tif (!div_progress_info) {\n+\t\tdiv_progress_info = document.getElementById('progress_info');\n+\t}\n+\tif (div_progress_info) {\n+\t\tdiv_progress_info.className = 'error';\n+\t\tdiv_progress_info.firstChild.data = str;\n+\t}\n+}\n+\n+/* ............................................................ */\n+/* coloring rows during blame_data (git blame --incremental) run */\n+\n+/**\n+ * used to extract N from 'colorN', where N is a number,\n+ * @constant\n+ */\n+var colorRe = /\\bcolor([0-9]*)\\b/;\n+\n+/**\n+ * return N if <tr class=\"colorN\">, otherwise return null\n+ * (some browsers require CSS class names to begin with letter)\n+ *\n+ * @param {HTMLElement} tr: table row element to check\n+ * @param {String} tr.className: 'class' attribute of tr element\n+ * @returns {Number|null} N if tr.className == 'colorN', otherwise null\n+ */\n+function getColorNo(tr) {\n+\tif (!tr) {\n+\t\treturn null;\n+\t}\n+\tvar className = tr.className;\n+\tif (className) {\n+\t\tvar match = colorRe.exec(className);\n+\t\tif (match) {\n+\t\t\treturn parseInt(match[1], 10);\n+\t\t}\n+\t}\n+\treturn null;\n+}\n+\n+var colorsFreq = [0, 0, 0];\n+/**\n+ * return one of given possible colors (curently least used one)\n+ * example: chooseColorNoFrom(2, 3) returns 2 or 3\n+ *\n+ * @param {Number[]} arguments: one or more numbers\n+ *        assumes that  1 <= arguments[i] <= colorsFreq.length\n+ * @returns {Number} Least used color number from arguments\n+ */\n+function chooseColorNoFrom() {\n+\t// choose the color which is least used\n+\tvar colorNo = arguments[0];\n+\tfor (var i = 1; i < arguments.length; i++) {\n+\t\tif (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) {\n+\t\t\tcolorNo = arguments[i];\n+\t\t}\n+\t}\n+\tcolorsFreq[colorNo-1]++;\n+\treturn colorNo;\n+}\n+\n+/**\n+ * given two neigbour <tr> elements, find color which would be different\n+ * from color of both of neighbours; used to 3-color blame table\n+ *\n+ * @param {HTMLElement} tr_prev\n+ * @param {HTMLElement} tr_next\n+ * @returns {Number} color number N such that\n+ * colorN != tr_prev.className && colorN != tr_next.className\n+ */\n+function findColorNo(tr_prev, tr_next) {\n+\tvar color_prev = getColorNo(tr_prev);\n+\tvar color_next = getColorNo(tr_next);\n+\n+\n+\t// neither of neighbours has color set\n+\t// THEN we can use any of 3 possible colors\n+\tif (!color_prev && !color_next) {\n+\t\treturn chooseColorNoFrom(1,2,3);\n+\t}\n+\n+\t// either both neighbours have the same color,\n+\t// or only one of neighbours have color set\n+\t// THEN we can use any color except given\n+\tvar color;\n+\tif (color_prev === color_next) {\n+\t\tcolor = color_prev; // = color_next;\n+\t} else if (!color_prev) {\n+\t\tcolor = color_next;\n+\t} else if (!color_next) {\n+\t\tcolor = color_prev;\n+\t}\n+\tif (color) {\n+\t\treturn chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1);\n+\t}\n+\n+\t// neighbours have different colors\n+\t// THEN there is only one color left\n+\treturn (3 - ((color_prev + color_next) % 3));\n+}\n+\n+/* ............................................................ */\n+/* coloring rows like 'blame' after 'blame_data' finishes */\n+\n+/**\n+ * returns true if given row element (tr) is first in commit group\n+ * to be used only after 'blame_data' finishes (after processing)\n+ *\n+ * @param {HTMLElement} tr: table row\n+ * @returns {Boolean} true if TR is first in commit group\n+ */\n+function isStartOfGroup(tr) {\n+\treturn tr.firstChild.className === 'sha1';\n+}\n+\n+/**\n+ * change colors to use zebra coloring (2 colors) instead of 3 colors\n+ * concatenate neighbour commit groups belonging to the same commit\n+ */\n+function fixColorsAndGroups() {\n+\tvar colorClasses = ['light', 'dark'];\n+\tvar linenum = 1;\n+\tvar tr, prev_group;\n+\tvar colorClass = 0;\n+\tvar table =\n+\t\tdocument.getElementById('blame_table') ||\n+\t\tdocument.getElementsByTagName('table')[0];\n+\n+\twhile ((tr = document.getElementById('l'+linenum))) {\n+\t// index origin is 0, which is table header; start from 1\n+\t//while ((tr = table.rows[linenum])) { // <- it is slower\n+\t\tif (isStartOfGroup(tr, linenum, document)) {\n+\t\t\tif (prev_group &&\n+\t\t\t    prev_group.firstChild.firstChild.href ===\n+\t\t\t            tr.firstChild.firstChild.href) {\n+\t\t\t\t// we have to concatenate groups\n+\t\t\t\tvar prev_rows = prev_group.firstChild.rowSpan || 1;\n+\t\t\t\tvar curr_rows =         tr.firstChild.rowSpan || 1;\n+\t\t\t\tprev_group.firstChild.rowSpan = prev_rows + curr_rows;\n+\t\t\t\t//tr.removeChild(tr.firstChild);\n+\t\t\t\ttr.deleteCell(0); // DOM2 HTML way\n+\t\t\t} else {\n+\t\t\t\tcolorClass = (colorClass + 1) % 2;\n+\t\t\t\tprev_group = tr;\n+\t\t\t}\n+\t\t}\n+\t\tvar tr_class = tr.className;\n+\t\ttr.className = tr_class.replace(colorRe, colorClasses[colorClass]);\n+\t\tlinenum++;\n+\t}\n+}\n+\n+/* ............................................................ */\n+/* time and data */\n+\n+/**\n+ * used to extract hours and minutes from timezone info, e.g '-0900'\n+ * @constant\n+ */\n+var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/;\n+\n+/**\n+ * return date in local time formatted in iso-8601 like format\n+ * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200'\n+ *\n+ * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'\n+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'\n+ * @returns {String} date in local time in iso-8601 like format\n+ */\n+function formatDateISOLocal(epoch, timezoneInfo) {\n+\tvar match = tzRe.exec(timezoneInfo);\n+\t// date corrected by timezone\n+\tvar localDate = new Date(1000 * (epoch +\n+\t\t(parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));\n+\tvar localDateStr = // e.g. '2005-08-07'\n+\t\tlocalDate.getUTCFullYear()                 + '-' +\n+\t\tpadLeft(localDate.getUTCMonth()+1, 2, '0') + '-' +\n+\t\tpadLeft(localDate.getUTCDate(),    2, '0');\n+\tvar localTimeStr = // e.g. '21:49:46'\n+\t\tpadLeft(localDate.getUTCHours(),   2, '0') + ':' +\n+\t\tpadLeft(localDate.getUTCMinutes(), 2, '0') + ':' +\n+\t\tpadLeft(localDate.getUTCSeconds(), 2, '0');\n+\n+\treturn localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;\n+}\n+\n+/* ............................................................ */\n+/* unquoting/unescaping filenames */\n+\n+/**\n+ * Used to detect escape sequences (octal e.g. \\008 and symbolic e.g. \\n)\n+ * @constant\n+ */\n+var escCodeRe = /\\\\([^0-7]|[0-7]{1,3})/g;\n+\n+/**\n+ * unquote maybe git-quoted filename\n+ * e.g. 'aa' -> 'aa', '\"a\\ta\"' -> 'a\ta'\n+ *\n+ * @param {String} str: git-quoted string\n+ * @returns {String} Unquoted and unescaped string\n+ */\n+function unquote(str) {\n+\tfunction unq(seq) {\n+\t\tvar es = { // character escape codes, aka escape sequences\n+\t\t\tt: \"\\t\",   // tab            (HT, TAB)\n+\t\t\tn: \"\\n\",   // newline        (NL)\n+\t\t\tr: \"\\r\",   // return         (CR)\n+\t\t\tf: \"\\f\",   // form feed      (FF)\n+\t\t\tb: \"\\b\",   // backspace      (BS)\n+\t\t\ta: \"\\x07\", // alarm (bell)   (BEL)\n+\t\t\te: \"\\x1B\", // escape         (ESC)\n+\t\t\tv: \"\\v\"    // vertical tab   (VT)\n+\t\t};\n+\n+\t\tif (seq.search(/^[0-7]{1,3}$/) !== -1) {\n+\t\t\t// octal char sequence\n+\t\t\treturn String.fromCharCode(parseInt(seq, 8));\n+\t\t} else if (seq in es) {\n+\t\t\t// C escape sequence, aka character escape code\n+\t\t\treturn es[seq];\n+\t\t}\n+\t\t// quoted ordinary character\n+\t\treturn seq;\n+\t}\n+\n+\tvar match = str.match(/^\\\"(.*)\\\"$/);\n+\tif (match) {\n+\t\tstr = match[1];\n+\t\t// perhaps str = eval('\"'+str+'\"'); would be enough?\n+\t\tstr = str.replace(escCodeRe,\n+\t\t\tfunction (substr, p1, offset, s) { return unq(p1); });\n+\t}\n+\treturn str;\n+}\n+\n+/* ============================================================ */\n+/* main part: parsing response */\n+\n+/**\n+ * Function called for each blame entry, as soon as it finishes.\n+ * It updates page via DOM manipulation, adding sha1 info, etc.\n+ *\n+ * @param {Commit} commit: blamed commit\n+ * @param {Object} group: object representing group of lines,\n+ *                        which blame the same commit (blame entry)\n+ */\n+function handleLine(commit, group) {\n+\t/* \n+\t   This is the structure of the HTML fragment we are working\n+\t   with:\n+\n+\t   <tr id=\"l123\" class=\"\">\n+\t     <td class=\"sha1\" title=\"\"><a href=\"\"> </a></td>\n+\t     <td class=\"linenr\"><a class=\"linenr\" href=\"\">123</a></td>\n+\t     <td class=\"pre\"># times (my ext3 doesn&#39;t).</td>\n+\t   </tr>\n+\t*/\n+\n+\tvar resline = group.resline;\n+\n+\t// format date and time string only once per commit\n+\tif (!commit.info) {\n+\t\t/* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */\n+\t\tcommit.info = commit.author + ', ' +\n+\t\t\tformatDateISOLocal(commit.authorTime, commit.authorTimezone);\n+\t\t// delete properties no longer needed, to reduce memory footprint\n+\t\t//delete commit.author;\n+\t\tdelete commit.authorTime;\n+\t\tdelete commit.authorTimezone;\n+\t}\n+\n+\t// color depends on group of lines, not only on blamed commit\n+\tvar colorNo = findColorNo(\n+\t\tdocument.getElementById('l'+(resline-1)),\n+\t\tdocument.getElementById('l'+(resline+group.numlines))\n+\t);\n+\n+\t// loop over lines in commit group\n+\tfor (var i = 0; i < group.numlines; i++, resline++) {\n+\t\tvar tr = document.getElementById('l'+resline);\n+\t\tif (!tr) {\n+\t\t\t//debug('tr is null! resline: ' + resline);\n+\t\t\tbreak;\n+\t\t}\n+\t\t/*\n+\t\t\t<tr id=\"l123\" class=\"\">\n+\t\t\t  <td class=\"sha1\" title=\"\"><a href=\"\"> </a></td>\n+\t\t\t  <td class=\"linenr\"><a class=\"linenr\" href=\"\">123</a></td>\n+\t\t\t  <td class=\"pre\"># times (my ext3 doesn&#39;t).</td>\n+\t\t\t</tr>\n+\t\t*/\n+\t\tvar td_sha1  = tr.firstChild;\n+\t\tvar a_sha1   = td_sha1.firstChild;\n+\t\tvar a_linenr = td_sha1.nextSibling.firstChild;\n+\n+\t\t/* <tr id=\"l123\" class=\"\"> */\n+\t\tvar tr_class = '';\n+\t\tif (colorNo !== null) {\n+\t\t\ttr_class = 'color'+colorNo;\n+\t\t}\n+\t\tif (commit.boundary) {\n+\t\t\ttr_class += ' boundary';\n+\t\t}\n+\t\tif (commit.nprevious === 0) {\n+\t\t\ttr_class += ' no-previous';\n+\t\t} else if (commit.nprevious > 1) {\n+\t\t\ttr_class += ' multiple-previous';\n+\t\t}\n+\t\ttr.className = tr_class;\n+\n+\t\t/* <td class=\"sha1\" title=\"?\" rowspan=\"?\"><a href=\"?\">?</a></td> */\n+\t\tif (i === 0) {\n+\t\t\ttd_sha1.title = commit.info;\n+\t\t\ttd_sha1.rowSpan = group.numlines;\n+\n+\t\t\ta_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1;\n+\t\t\ta_sha1.firstChild.data = commit.sha1.substr(0, 8);\n+\t\t\tif (group.numlines >= 2) {\n+\t\t\t\tvar fragment = document.createDocumentFragment();\n+\t\t\t\tvar br   = document.createElement(\"br\");\n+\t\t\t\tvar text = document.createTextNode(\n+\t\t\t\t\tcommit.author.match(/\\b([A-Z])\\B/g).join(''));\n+\t\t\t\tif (br && text) {\n+\t\t\t\t\tvar elem = fragment || td_sha1;\n+\t\t\t\t\telem.appendChild(br);\n+\t\t\t\t\telem.appendChild(text);\n+\t\t\t\t\tif (fragment) {\n+\t\t\t\t\t\ttd_sha1.appendChild(fragment);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\t//tr.removeChild(td_sha1); // DOM2 Core way\n+\t\t\ttr.deleteCell(0); // DOM2 HTML way\n+\t\t}\n+\n+\t\t/* <td class=\"linenr\"><a class=\"linenr\" href=\"?\">123</a></td> */\n+\t\tvar linenr_commit =\n+\t\t\t('previous' in commit ? commit.previous : commit.sha1);\n+\t\tvar linenr_filename =\n+\t\t\t('file_parent' in commit ? commit.file_parent : commit.filename);\n+\t\ta_linenr.href = projectUrl + 'a=blame_incremental' +\n+\t\t\t';hb=' + linenr_commit +\n+\t\t\t';f='  + encodeURIComponent(linenr_filename) +\n+\t\t\t'#l' + (group.srcline + i);\n+\n+\t\tblamedLines++;\n+\n+\t\t//updateProgressInfo();\n+\t}\n+}\n+\n+// ----------------------------------------------------------------------\n+\n+var prevDataLength = -1;  // used to detect if we have new data\n+var nextReadPos = 0;      // where unread part of response starts\n+var inProgress = false;   // are we processing response\n+\n+/**#@+\n+ * @constant\n+ */\n+var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/;\n+var infoRe = /^([a-z-]+) ?(.*)/;\n+var endRe  = /^END ?([^ ]*) ?(.*)/;\n+/**@-*/\n+\n+var curCommit = new Commit();\n+var curGroup  = {};\n+\n+var pollTimer = null;\n+\n+/*\n+ * W3C Progress Events (http://www.w3.org/TR/progress-events)\n+ * events: loadstart, progress, error, abort, load\n+ * interface: lengthComputable, loaded, total (0 if unknown)\n+ */\n+\n+/*\n+ * handleResponse ->\n+ *   handleError (or onXHReqError, onError, ...)\n+ *   handleAbort (?)\n+ *   handleProgress\n+ *     parseResponse / parseProgress\n+ *       parseBlame | parseJSON\n+ *         handleLine\n+ *         updateProgressInfo\n+ *   handleLoad\n+ */\n+\n+/**\n+ * Parse output from 'git blame --incremental [...]', received via\n+ * XMLHttpRequest from server (blamedataUrl), and call handleLine\n+ * (which updates page) as soon as blame entry is completed.\n+ *\n+ * @param {String[]} lines: new complete lines from blamedata server\n+ */\n+function processBlameLines(lines) {\n+\tvar match;\n+\n+\tfor (var i = 0, len = lines.length; i < len; i++) {\n+\n+\t\tif ((match = sha1Re.exec(lines[i]))) {\n+\t\t\tvar sha1 = match[1];\n+\t\t\tvar srcline  = parseInt(match[2], 10);\n+\t\t\tvar resline  = parseInt(match[3], 10);\n+\t\t\tvar numlines = parseInt(match[4], 10);\n+\n+\t\t\tvar c = commits[sha1];\n+\t\t\tif (!c) {\n+\t\t\t\tc = new Commit(sha1);\n+\t\t\t\tcommits[sha1] = c;\n+\t\t\t}\n+\t\t\tcurCommit = c;\n+\n+\t\t\tcurGroup.srcline = srcline;\n+\t\t\tcurGroup.resline = resline;\n+\t\t\tcurGroup.numlines = numlines;\n+\n+\t\t} else if ((match = infoRe.exec(lines[i]))) {\n+\t\t\tvar info = match[1];\n+\t\t\tvar data = match[2];\n+\t\t\tswitch (info) {\n+\t\t\tcase 'filename':\n+\t\t\t\tcurCommit.filename = unquote(data);\n+\t\t\t\t// 'filename' information terminates the entry\n+\t\t\t\thandleLine(curCommit, curGroup);\n+\t\t\t\tupdateProgressInfo();\n+\t\t\t\tbreak;\n+\t\t\tcase 'author':\n+\t\t\t\tcurCommit.author = data;\n+\t\t\t\tbreak;\n+\t\t\tcase 'author-time':\n+\t\t\t\tcurCommit.authorTime = parseInt(data, 10);\n+\t\t\t\tbreak;\n+\t\t\tcase 'author-tz':\n+\t\t\t\tcurCommit.authorTimezone = data;\n+\t\t\t\tbreak;\n+\t\t\tcase 'previous':\n+\t\t\t\tcurCommit.nprevious++;\n+\t\t\t\t// store only first 'previous' header\n+\t\t\t\tif (!'previous' in curCommit) {\n+\t\t\t\t\tvar parts = data.split(' ', 2);\n+\t\t\t\t\tcurCommit.previous    = parts[0];\n+\t\t\t\t\tcurCommit.file_parent = unquote(parts[1]);\n+\t\t\t\t}\n+\t\t\t\tbreak;\n+\t\t\tcase 'boundary':\n+\t\t\t\t//debug('Boundary commit: '+curCommit.sha1);\n+\t\t\t\tcurCommit.boundary = true;\n+\t\t\t\tbreak;\n+\t\t\t} // end switch\n+\n+\t\t} else if ((match = endRe.exec(lines[i]))) {\n+\t\t\tt_interval_server = match[1];\n+\t\t\tcmds_server = match[2];\n+\t\t\t//debug('END: '+lines[i]);\n+\n+\t\t} else if (lines[i] !== '') {\n+\t\t\t//debug('malformed line: ' + lines[i]);\n+\n+\t\t} // end if (match)\n+\n+\t} // end for (lines)\n+}\n+\n+/**\n+ * Process new data and return pointer to end of processed part\n+ *\n+ * @param {String} unprocessed: new data (from nextReadPos)\n+ * @param {Number} nextReadPos: end of last processed data\n+ * @return {Number} end of processed data (new value for nextReadPos)\n+ */\n+function processData(unprocessed, nextReadPos) {\n+\tvar lastLineEnd = unprocessed.lastIndexOf('\\n');\n+\tif (lastLineEnd !== -1) {\n+\t\tvar lines = unprocessed.substring(0, lastLineEnd).split('\\n');\n+\t\tnextReadPos += lastLineEnd + 1 /* 1 == '\\n'.length */;\n+\n+\t\tprocessBlameLines(lines);\n+\t} // end if\n+\n+\treturn nextReadPos;\n+}\n+\n+/**\n+ * Handle XMLHttpRequest errors\n+ */\n+function handleError() {\n+\terrorInfo('Server error: ' +\n+\t\txhr.status + ' - ' + (xhr.statusText || 'Error contacting server'));\n+\n+\tclearInterval(pollTimer);\n+\tcommits = {}; // free memory\n+\n+\tinProgress = false;\n+}\n+\n+/**\n+ * Called after XMLHttpRequest finishes (loads)\n+ */\n+function responseLoaded() {\n+\tclearInterval(pollTimer);\n+\n+\tfixColorsAndGroups();\n+\twriteTimeInterval();\n+\tcommits = {}; // free memory\n+\n+\tinProgress = false;\n+}\n+\n+/**\n+ * handler for XMLHttpRequest onreadystatechange event\n+ * @see startBlame\n+ */\n+function handleResponse() {\n+\t//debug('handleResp ready: ' + xhr.readyState +\n+\t//      ' respText null?: ' + (xhr.responseText === null) +\n+\t//      ' progress: ' + inProgress);\n+\n+\t/*\n+\t * xhr.readyState\n+\t *\n+\t *  Value  Constant (W3C)    Description\n+\t *  -------------------------------------------------------------------\n+\t *  0      UNSENT            open() has not been called yet.\n+\t *  1      OPENED            send() has not been called yet.\n+\t *  2      HEADERS_RECEIVED  send() has been called, and headers\n+\t *                           and status are available.\n+\t *  3      LOADING           Downloading; responseText holds partial data.\n+\t *  4      DONE              The operation is complete.\n+\t */\n+\n+\tif (xhr.readyState !== 4 && xhr.readyState !== 3) {\n+\t\treturn;\n+\t}\n+\n+\t// the server returned error\n+\tif (xhr.readyState === 3 && xhr.status !== 200) {\n+\t\treturn;\n+\t}\n+\tif (xhr.readyState === 4 && xhr.status !== 200) {\n+\t\thandleError();\n+\t\treturn;\n+\t}\n+\n+\t// In konqueror xhr.responseText is sometimes null here...\n+\tif (xhr.responseText === null) {\n+\t\treturn;\n+\t}\n+\n+\t// in case we were called before finished processing\n+\tif (inProgress) {\n+\t\treturn;\n+\t} else {\n+\t\tinProgress = true;\n+\t}\n+\n+\t// extract new whole (complete) lines, and process them\n+\twhile (prevDataLength !== xhr.responseText.length) {\n+\t\tif (xhr.readyState === 4 &&\n+\t\t    prevDataLength === xhr.responseText.length) {\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tprevDataLength = xhr.responseText.length;\n+\t\tvar unprocessed = xhr.responseText.substring(nextReadPos);\n+\t\tnextReadPos = processData(unprocessed, nextReadPos);\n+\t} // end while\n+\n+\t// did we finish work?\n+\tif (xhr.readyState === 4 &&\n+\t    prevDataLength === xhr.responseText.length) {\n+\t\tresponseLoaded();\n+\t}\n+\n+\tinProgress = false;\n+}\n+\n+// ============================================================\n+// ------------------------------------------------------------\n+\n+/**\n+ * Incrementally update line data in blame_incremental view in gitweb.\n+ *\n+ * @param {String} blamedataUrl: URL to server script generating blame data.\n+ * @param {String} bUrl: partial URL to project, used to generate links.\n+ *\n+ * Called from 'blame_incremental' view after loading table with\n+ * file contents, a base for blame view.\n+*/\n+function startBlame(blamedataUrl, bUrl) {\n+\t//debug('startBlame('+blamedataUrl+', '+bUrl+')');\n+\n+\txhr = createRequestObject();\n+\tif (!xhr) {\n+\t\terrorInfo('ERROR: XMLHttpRequest not supported');\n+\t\treturn;\n+\t}\n+\n+\tt0 = new Date();\n+\tprojectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';');\n+\tif ((div_progress_bar = document.getElementById('progress_bar'))) {\n+\t\t//div_progress_bar.setAttribute('style', 'width: 100%;');\n+\t\tdiv_progress_bar.style.cssText = 'width: 100%;';\n+\t}\n+\ttotalLines = countLines();\n+\tupdateProgressInfo();\n+\n+\txhr.open('get', blamedataUrl);\n+\txhr.setRequestHeader('Accept', 'text/plain');\n+\txhr.onreadystatechange = handleResponse;\n+\t//xhr.onreadystatechange = function () { handleResponse(xhr); };\n+\txhr.send(null);\n+\n+\t// not all browsers call onreadystatechange event on each server flush\n+\tif (!DEBUG) {\n+\t\tpollTimer = setInterval(handleResponse, 1000);\n+\t}\n+}\n+\n+// end of blame.js\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex 8f68fe3..c9a7501 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -250,6 +250,11 @@ tr.no-previous td.linenr {\n \tfont-weight: bold;\n }\n \n+/* for 'blame_incremental', during processing */\n+tr.color1 { background-color: #f6fff6; }\n+tr.color2 { background-color: #f6f6ff; }\n+tr.color3 { background-color: #fff6f6; }\n+\n td {\n \tpadding: 2px 5px;\n \tfont-size: 100%;\n@@ -341,6 +346,17 @@ td.mode {\n \tfont-family: monospace;\n }\n \n+/* progress of blame_interactive */\n+div#progress_bar {\n+\theight: 2px;\n+\tmargin-bottom: -2px;\n+\tbackground-color: #d8d9d0;\n+}\n+div#progress_info {\n+\tfloat: right;\n+\ttext-align: right;\n+}\n+\n /* styling of diffs (patchsets): commitdiff and blobdiff views */\n div.diff.header,\n div.diff.extended_header {\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 6f47659..cf1b8bb 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -96,6 +96,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@@ -564,6 +566,8 @@ our %cgi_param_mapping = @cgi_param_mapping;\n # we will also need to know the possible actions, for validation\n our %actions = (\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 \t\"blobdiff_plain\" => \\&git_blobdiff_plain,\n \t\"blob\" => \\&git_blob,\n@@ -4789,7 +4793,9 @@ sub git_tag {\n \tgit_footer_html();\n }\n \n-sub git_blame {\n+sub git_blame_common {\n+\tmy $format = shift || 'porcelain';\n+\n \t# permissions\n \tgitweb_check_feature('blame')\n \t\tor die_error(403, \"Blame view not allowed\");\n@@ -4811,10 +4817,43 @@ sub git_blame {\n \t\t}\n \t}\n \n-\t# run git-blame --porcelain\n-\topen my $fd, \"-|\", git_cmd(), \"blame\", '-p',\n-\t\t$hash_base, '--', $file_name\n-\t\tor die_error(500, \"Open git-blame failed\");\n+\tmy $fd;\n+\tif ($format eq 'incremental') {\n+\t\t# get file contents (as base)\n+\t\topen $fd, \"-|\", git_cmd(), 'cat-file', 'blob', $hash\n+\t\t\tor die_error(500, \"Open git-cat-file failed\");\n+\t} elsif ($format eq 'data') {\n+\t\t# run git-blame --incremental\n+\t\topen $fd, \"-|\", git_cmd(), \"blame\", \"--incremental\",\n+\t\t\t$hash_base, \"--\", $file_name\n+\t\t\tor die_error(500, \"Open git-blame --incremental failed\");\n+\t} else {\n+\t\t# run git-blame --porcelain\n+\t\topen $fd, \"-|\", git_cmd(), \"blame\", '-p',\n+\t\t\t$hash_base, '--', $file_name\n+\t\t\tor die_error(500, \"Open git-blame --porcelain failed\");\n+\t}\n+\n+\t# incremental blame data returns early\n+\tif ($format eq 'data') {\n+\t\tprint $cgi->header(\n+\t\t\t-type=>\"text/plain\", -charset => \"utf-8\",\n+\t\t\t-status=> \"200 OK\");\n+\t\tlocal $| = 1; # output autoflush\n+\t\tprint while <$fd>;\n+\t\tclose $fd\n+\t\t\tor print \"ERROR $!\\n\";\n+\n+\t\tprint 'END';\n+\t\tif (defined $t0 && gitweb_check_feature('timed')) {\n+\t\t\tprint ' '.\n+\t\t\t      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).\n+\t\t\t      ' '.$number_of_git_cmds;\n+\t\t}\n+\t\tprint \"\\n\";\n+\n+\t\treturn;\n+\t}\n \n \t# page header\n \tgit_header_html();\n@@ -4825,109 +4864,169 @@ sub git_blame {\n \t\t$cgi->a({-href => href(action=>\"history\", -replay=>1)},\n \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=>$action, 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 \n \t# page body\n+\tif ($format eq 'incremental') {\n+\t\tprint \"<noscript>\\n<div class=\\\"error\\\"><center><b>\\n\".\n+\t\t      \"This page requires JavaScript to run.\\n Use \".\n+\t\t      $cgi->a({-href => href(action=>'blame',-replay=>1)}, 'this page').\n+\t\t      \" instead.\\n\".\n+\t\t      \"</b></center></div>\\n</noscript>\\n\";\n+\n+\t\tprint qq!<div id=\"progress_bar\" style=\"width: 100%; background-color: yellow\"></div>\\n!;\n+\t}\n+\n+\tprint qq!<div class=\"page_body\">\\n!;\n+\tprint qq!<div id=\"progress_info\">... / ...</div>\\n!\n+\t\tif ($format eq 'incremental');\n+\tprint qq!<table id=\"blame_table\" class=\"blame\" width=\"100%\">\\n!.\n+\t      #qq!<col width=\"5.5em\" /><col width=\"2.5em\" /><col width=\"*\" />\\n!.\n+\t      qq!<thead>\\n!.\n+\t      qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\\n!.\n+\t      qq!</thead>\\n!.\n+\t      qq!<tbody>\\n!;\n+\n \tmy @rev_color = qw(light dark);\n \tmy $num_colors = scalar(@rev_color);\n \tmy $current_color = 0;\n-\tmy %metainfo = ();\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- LINE:\n-\twhile (my $line = <$fd>) {\n-\t\tchomp $line;\n-\t\t# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]\n-\t\t# no <lines in group> for subsequent lines in group of lines\n-\t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n-\t\t   ($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n-\t\tif (!exists $metainfo{$full_rev}) {\n-\t\t\t$metainfo{$full_rev} = { 'nprevious' => 0 };\n-\t\t}\n-\t\tmy $meta = $metainfo{$full_rev};\n-\t\tmy $data;\n-\t\twhile ($data = <$fd>) {\n-\t\t\tchomp $data;\n-\t\t\tlast if ($data =~ s/^\\t//); # contents of line\n-\t\t\tif ($data =~ /^(\\S+)(?: (.*))?$/) {\n-\t\t\t\t$meta->{$1} = $2 unless exists $meta->{$1};\n+\tif ($format eq 'incremental') {\n+\t\tmy $color_class = $rev_color[$current_color];\n+\n+\t\t#contents of a file\n+\t\tmy $linenr = 0;\n+\tLINE:\n+\t\twhile (my $line = <$fd>) {\n+\t\t\tchomp $line;\n+\t\t\t$linenr++;\n+\n+\t\t\tprint qq!<tr id=\"l$linenr\" class=\"$color_class\">!.\n+\t\t\t      qq!<td class=\"sha1\"><a href=\"\"> </a></td>!.\n+\t\t\t      qq!<td class=\"linenr\">!.\n+\t\t\t      qq!<a class=\"linenr\" href=\"\">$linenr</a></td>!;\n+\t\t\tprint qq!<td class=\"pre\">! . esc_html($line) . \"</td>\\n\";\n+\t\t\tprint qq!</tr>\\n!;\n+\t\t}\n+\n+\t} else { # porcelain, i.e. ordinary blame\n+\t\tmy %metainfo = (); # saves information about commits\n+\n+\t\t# blame data\n+\tLINE:\n+\t\twhile (my $line = <$fd>) {\n+\t\t\tchomp $line;\n+\t\t\t# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]\n+\t\t\t# no <lines in group> for subsequent lines in group of lines\n+\t\t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n+\t\t\t   ($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n+\t\t\tif (!exists $metainfo{$full_rev}) {\n+\t\t\t\t$metainfo{$full_rev} = { 'nprevious' => 0 };\n \t\t\t}\n-\t\t\tif ($data =~ /^previous /) {\n-\t\t\t\t$meta->{'nprevious'}++;\n+\t\t\tmy $meta = $metainfo{$full_rev};\n+\t\t\tmy $data;\n+\t\t\twhile ($data = <$fd>) {\n+\t\t\t\tchomp $data;\n+\t\t\t\tlast if ($data =~ s/^\\t//); # contents of line\n+\t\t\t\tif ($data =~ /^(\\S+)(?: (.*))?$/) {\n+\t\t\t\t\t$meta->{$1} = $2 unless exists $meta->{$1};\n+\t\t\t\t}\n+\t\t\t\tif ($data =~ /^previous /) {\n+\t\t\t\t\t$meta->{'nprevious'}++;\n+\t\t\t\t}\n \t\t\t}\n-\t\t}\n-\t\tmy $short_rev = substr($full_rev, 0, 8);\n-\t\tmy $author = $meta->{'author'};\n-\t\tmy %date =\n-\t\t\tparse_date($meta->{'author-time'}, $meta->{'author-tz'});\n-\t\tmy $date = $date{'iso-tz'};\n-\t\tif ($group_size) {\n-\t\t\t$current_color = ($current_color + 1) % $num_colors;\n-\t\t}\n-\t\tmy $tr_class = $rev_color[$current_color];\n-\t\t$tr_class .= ' boundary' if (exists $meta->{'boundary'});\n-\t\t$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);\n-\t\t$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);\n-\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$tr_class\\\">\\n\";\n-\t\tif ($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 $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($short_rev));\n-\t\t\tif ($group_size >= 2) {\n-\t\t\t\tmy @author_initials = ($author =~ /\\b([[:upper:]])\\B/g);\n-\t\t\t\tif (@author_initials) {\n-\t\t\t\t\tprint \"<br />\" .\n-\t\t\t\t\t      esc_html(join('', @author_initials));\n-\t\t\t\t\t#           or join('.', ...)\n+\t\t\tmy $short_rev = substr($full_rev, 0, 8);\n+\t\t\tmy $author = $meta->{'author'};\n+\t\t\tmy %date =\n+\t\t\t\tparse_date($meta->{'author-time'}, $meta->{'author-tz'});\n+\t\t\tmy $date = $date{'iso-tz'};\n+\t\t\tif ($group_size) {\n+\t\t\t\t$current_color = ($current_color + 1) % $num_colors;\n+\t\t\t}\n+\t\t\tmy $tr_class = $rev_color[$current_color];\n+\t\t\t$tr_class .= ' boundary' if (exists $meta->{'boundary'});\n+\t\t\t$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);\n+\t\t\t$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);\n+\t\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$tr_class\\\">\\n\";\n+\t\t\tif ($group_size) {\n+\t\t\t\tprint \"<td class=\\\"sha1\\\"\";\n+\t\t\t\tprint \" title=\\\"\". esc_html($author) . \", $date\\\"\";\n+\t\t\t\tprint \" rowspan=\\\"$group_size\\\"\" if ($group_size > 1);\n+\t\t\t\tprint \">\";\n+\t\t\t\tprint $cgi->a({-href => href(action=>\"commit\",\n+\t\t\t\t                             hash=>$full_rev,\n+\t\t\t\t                             file_name=>$file_name)},\n+\t\t\t\t              esc_html($short_rev));\n+\t\t\t\tif ($group_size >= 2) {\n+\t\t\t\t\tmy @author_initials = ($author =~ /\\b([[:upper:]])\\B/g);\n+\t\t\t\t\tif (@author_initials) {\n+\t\t\t\t\t\tprint \"<br />\" .\n+\t\t\t\t\t\t      esc_html(join('', @author_initials));\n+\t\t\t\t\t\t#           or join('.', ...)\n+\t\t\t\t\t}\n \t\t\t\t}\n+\t\t\t\tprint \"</td>\\n\";\n \t\t\t}\n-\t\t\tprint \"</td>\\n\";\n-\t\t}\n-\t\t# 'previous' <sha1 of parent commit> <filename at commit>\n-\t\tif (exists $meta->{'previous'} &&\n-\t\t    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n-\t\t\t$meta->{'parent'} = $1;\n-\t\t\t$meta->{'file_parent'} = unquote($2);\n-\t\t}\n-\t\tmy $linenr_commit =\n-\t\t\texists($meta->{'parent'}) ?\n-\t\t\t$meta->{'parent'} : $full_rev;\n-\t\tmy $linenr_filename =\n-\t\t\texists($meta->{'file_parent'}) ?\n-\t\t\t$meta->{'file_parent'} : unquote($meta->{'filename'});\n-\t\tmy $blamed = href(action => 'blame',\n-\t\t                  file_name => $linenr_filename,\n-\t\t                  hash_base => $linenr_commit);\n-\t\tprint \"<td class=\\\"linenr\\\">\";\n-\t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n-\t\t                -class => \"linenr\" },\n-\t\t              esc_html($lineno));\n-\t\tprint \"</td>\";\n-\t\tprint \"<td class=\\\"pre\\\">\" . esc_html($data) . \"</td>\\n\";\n-\t\tprint \"</tr>\\n\";\n+\t\t\t# 'previous' <sha1 of parent commit> <filename at commit>\n+\t\t\tif (exists $meta->{'previous'} &&\n+\t\t\t    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n+\t\t\t\t$meta->{'parent'} = $1;\n+\t\t\t\t$meta->{'file_parent'} = unquote($2);\n+\t\t\t}\n+\t\t\tmy $linenr_commit =\n+\t\t\t\texists($meta->{'parent'}) ?\n+\t\t\t\t$meta->{'parent'} : $full_rev;\n+\t\t\tmy $linenr_filename =\n+\t\t\t\texists($meta->{'file_parent'}) ?\n+\t\t\t\t$meta->{'file_parent'} : unquote($meta->{'filename'});\n+\t\t\tmy $blamed = href(action => 'blame',\n+\t\t\t                  file_name => $linenr_filename,\n+\t\t\t                  hash_base => $linenr_commit);\n+\t\t\tprint \"<td class=\\\"linenr\\\">\";\n+\t\t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n+\t\t\t                -class => \"linenr\" },\n+\t\t\t              esc_html($lineno));\n+\t\t\tprint \"</td>\";\n+\t\t\tprint \"<td class=\\\"pre\\\">\" . esc_html($data) . \"</td>\\n\";\n+\t\t\tprint \"</tr>\\n\";\n+\t\t} # end while\n+\n \t}\n-\tprint \"</table>\\n\";\n-\tprint \"</div>\";\n+\n+\t# footer\n+\tprint \"</tbody>\\n\".\n+\t      \"</table>\\n\"; # class=\"blame\"\n+\tprint \"</div>\\n\";   # class=\"blame_body\"\n \tclose $fd\n \t\tor print \"Reading blob failed\\n\";\n \n-\t# page footer\n+\tif ($format eq 'incremental') {\n+\t\tprint qq!<script type=\"text/javascript\" src=\"$blamejs\"></script>\\n!.\n+\t\t      qq!<script type=\"text/javascript\">\\n!.\n+\t\t      qq!startBlame(\"!. href(action=>\"blame_data\", -replay=>1) .qq!\",\\n!.\n+\t\t      qq!           \"!. href() .qq!\");\\n!.\n+\t\t      qq!</script>\\n!;\n+\t}\n+\n \tgit_footer_html();\n }\n \n+sub git_blame {\n+\tgit_blame_common();\n+}\n+\n+sub git_blame_incremental {\n+\tgit_blame_common('incremental');\n+}\n+\n+sub git_blame_data {\n+\tgit_blame_common('data');\n+}\n+\n sub git_tags {\n \tmy $head = git_get_head_hash($project);\n \tgit_header_html();\n-- \n1.6.3.3\n"}]}