{"thread":{"id":"16659","subject":"[PATCH 0/3] gitweb: Improve git_blame in preparation for incremental blame","startedAt":"2008-12-09T22:43:23Z","lastAt":"2008-12-17T08:34:55Z","messageCount":31,"participants":["Jakub Narebski","Nanako Shiraishi","Luben Tuikov","Junio C Hamano","Petr Baudis"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"97457","messageId":"20081209223703.28106.29198.stgit@localhost.localdomain","threadId":"16659","inReplyTo":null,"subject":"[PATCH 0/3] gitweb: Improve git_blame in preparation for incremental blame","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-09T22:43:23Z","receivedAt":"2008-12-09T22:43:23Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"The following series implements a few improvements to git_blame code\nand 'blame' view output to prepare for WIP/RFC patch adding incremental\nblame output to gitweb using AJAX (JavaScript and XMLHttpRequest); the\ncode in question is based on code by Petr Baudis from 26 Aug 2007\n  http://permalink.gmane.org/gmane.comp.version-control.git/56657\nwhich in turn was based on Fredrik Kuivinen proof of concept patch.\n\n\nThe first patch in series (moving id to tr element) is needed in\nblame_incremental, and it makes it easier to use DOM to manipulate\ngitweb blame output.\n\nSecond patch is about what I have noticed when examining git_blame\ncode.\n\nThe last patch is not necessarily required; but please tell me if it\nis to be accepted or to be dropped, to know whether to base\nincremental blame on it.\n\n---\nJakub Narebski (3):\n      gitweb: A bit of code cleanup in git_blame()\n      gitweb: Cache $parent_commit info in git_blame()\n      gitweb: Move 'lineno' id from link to row element in git_blame\n\n gitweb/gitweb.perl |   84 ++++++++++++++++++++++++++++++----------------------\n 1 files changed, 49 insertions(+), 35 deletions(-)\n\n-- \nJakub Narebski\nShadeHawk on #git\nPoland\n"},{"id":"97458","messageId":"20081209224330.28106.18301.stgit@localhost.localdomain","threadId":"16659","inReplyTo":"20081209223703.28106.29198.stgit@localhost.localdomain","subject":"[PATCH 1/3] gitweb: Move 'lineno' id from link to row element in git_blame","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-09T22:46:16Z","receivedAt":"2008-12-09T22:46:16Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Move l<line number> ID from <a> link element inside table row (inside\ncell element for column with line numbers), to encompassing <tr> table\nrow element.  It was done to make it easier to manipulate result HTML\nwith DOM, and to be able write 'blame_incremental' view with the same,\nor nearly the same result.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nFor blame_incremental I need easy way to manipulate rows of blame\ntable, to add information about blamed commits as it arrives.\n\nSo there it is.\n\n gitweb/gitweb.perl |    3 +--\n 1 files changed, 1 insertions(+), 2 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 6eb370d..1b800f4 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4645,7 +4645,7 @@ HTML\n \t\tif ($group_size) {\n \t\t\t$current_color = ++$current_color % $num_colors;\n \t\t}\n-\t\tprint \"<tr class=\\\"$rev_color[$current_color]\\\">\\n\";\n+\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$rev_color[$current_color]\\\">\\n\";\n \t\tif ($group_size) {\n \t\t\tprint \"<td class=\\\"sha1\\\"\";\n \t\t\tprint \" title=\\\"\". esc_html($author) . \", $date\\\"\";\n@@ -4667,7 +4667,6 @@ HTML\n \t\t                  hash_base => $parent_commit);\n \t\tprint \"<td class=\\\"linenr\\\">\";\n \t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n-\t\t                -id => \"l$lineno\",\n \t\t                -class => \"linenr\" },\n \t\t              esc_html($lineno));\n \t\tprint \"</td>\";\n"},{"id":"97459","messageId":"20081209224622.28106.89325.stgit@localhost.localdomain","threadId":"16659","inReplyTo":"20081209223703.28106.29198.stgit@localhost.localdomain","subject":"[PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-09T22:48:08Z","receivedAt":"2008-12-09T22:48:08Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Luben Tuikov changed 'lineno' link from leading to commit which lead\nto current version of given block of lines, to leading to parent of\nthis commit in 244a70e (Blame \"linenr\" link jumps to previous state at\n\"orig_lineno\").  This supposedly made data mining possible (or just\nbetter).\n\nUnfortunately the implementation in 244a70e used one call for\ngit-rev-parse to find parent revision per line in file, instead of\nusing long lived \"git cat-file --batch-check\" (which might not existed\nthen), or changing validate_refname to validate_revision and made it\naccept <rev>^, <rev>^^, <rev>^^^ etc. syntax.\n\nThis patch attempts to migitate issue a bit by caching $parent_commit\ninfo in %metainfo, which makes gitweb to call git-rev-parse only once\nper unique commit in blame output.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThat is what I have noticed during browsing git_blame() code.\n\nWe can change it to even more effective implementation (like the ones\nproposed above in the commit message) later.\n\nIndenting is cause for artifically large diff\n\n gitweb/gitweb.perl |   16 +++++++++++-----\n 1 files changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 1b800f4..916396a 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4657,11 +4657,17 @@ HTML\n \t\t\t              esc_html($rev));\n \t\t\tprint \"</td>\\n\";\n \t\t}\n-\t\topen (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n-\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n-\t\tmy $parent_commit = <$dd>;\n-\t\tclose $dd;\n-\t\tchomp($parent_commit);\n+\t\tmy $parent_commit;\n+\t\tif (!exists $meta->{'parent'}) {\n+\t\t\topen (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n+\t\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n+\t\t\t$parent_commit = <$dd>;\n+\t\t\tclose $dd;\n+\t\t\tchomp($parent_commit);\n+\t\t\t$meta->{'parent'} = $parent_commit;\n+\t\t} else {\n+\t\t\t$parent_commit = $meta->{'parent'};\n+\t\t}\n \t\tmy $blamed = href(action => 'blame',\n \t\t                  file_name => $meta->{'filename'},\n \t\t                  hash_base => $parent_commit);\n"},{"id":"97460","messageId":"20081209224814.28106.83387.stgit@localhost.localdomain","threadId":"16659","inReplyTo":"20081209223703.28106.29198.stgit@localhost.localdomain","subject":"[PATCH 3/3] gitweb: A bit of code cleanup in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-09T22:48:51Z","receivedAt":"2008-12-09T22:48:51Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Among others: \n * move variable declaration closer to the place it is set and used,\n   if possible,\n * uniquify and simplify coding style a bit, which includes removing\n   unnecessary '()'.\n * check type only if $hash was defined, as otherwise from the way\n   git_get_hash_by_path() is called (and works), we know that it is\n   a blob,\n * use modern calling convention for git-blame,\n * remove unused variable,\n * don't use implicit variables ($_),\n * add some comments\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nNot stricly necessary... but the code looked not very nice\n\n gitweb/gitweb.perl |   65 ++++++++++++++++++++++++++++++----------------------\n 1 files changed, 37 insertions(+), 28 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 916396a..68aa3f8 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4575,28 +4575,32 @@ sub git_tag {\n }\n \n sub git_blame {\n-\tmy $fd;\n-\tmy $ftype;\n-\n+\t# permissions\n \tgitweb_check_feature('blame')\n-\t    or die_error(403, \"Blame view not allowed\");\n+\t\tor die_error(403, \"Blame view not allowed\");\n \n+\t# error checking\n \tdie_error(400, \"No file name given\") unless $file_name;\n \t$hash_base ||= git_get_head_hash($project);\n-\tdie_error(404, \"Couldn't find base commit\") unless ($hash_base);\n+\tdie_error(404, \"Couldn't find base commit\") unless $hash_base;\n \tmy %co = parse_commit($hash_base)\n \t\tor die_error(404, \"Commit not found\");\n \tif (!defined $hash) {\n \t\t$hash = git_get_hash_by_path($hash_base, $file_name, \"blob\")\n \t\t\tor die_error(404, \"Error looking up file\");\n+\t} else {\n+\t\tmy $ftype = git_get_type($hash);\n+\t\tif ($ftype !~ \"blob\") {\n+\t\t\tdie_error(400, \"Object is not a blob\");\n+\t\t}\n \t}\n-\t$ftype = git_get_type($hash);\n-\tif ($ftype !~ \"blob\") {\n-\t\tdie_error(400, \"Object is not a blob\");\n-\t}\n-\topen ($fd, \"-|\", git_cmd(), \"blame\", '-p', '--',\n-\t      $file_name, $hash_base)\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+\n+\t# page header\n \tgit_header_html();\n \tmy $formats_nav =\n \t\t$cgi->a({-href => href(action=>\"blob\", -replay=>1)},\n@@ -4610,40 +4614,43 @@ sub git_blame {\n \tgit_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);\n \tgit_print_header_div('commit', esc_html($co{'title'}), $hash_base);\n \tgit_print_page_path($file_name, $ftype, $hash_base);\n-\tmy @rev_color = (qw(light2 dark2));\n+\n+\t# page body\n+\tmy @rev_color = qw(light2 dark2);\n \tmy $num_colors = scalar(@rev_color);\n \tmy $current_color = 0;\n-\tmy $last_rev;\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-\tmy %metainfo = ();\n-\twhile (1) {\n-\t\t$_ = <$fd>;\n-\t\tlast unless defined $_;\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    /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/;\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} = {};\n \t\t}\n \t\tmy $meta = $metainfo{$full_rev};\n-\t\twhile (<$fd>) {\n-\t\t\tlast if (s/^\\t//);\n-\t\t\tif (/^(\\S+) (.*)$/) {\n+\t\twhile (my $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;\n \t\t\t}\n \t\t}\n-\t\tmy $data = $_;\n-\t\tchomp $data;\n-\t\tmy $rev = substr($full_rev, 0, 8);\n+\t\tmy $short_rev = substr($full_rev, 0, 8);\n \t\tmy $author = $meta->{'author'};\n-\t\tmy %date = parse_date($meta->{'author-time'},\n-\t\t                      $meta->{'author-tz'});\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 % $num_colors;\n+\t\t\t$current_color = ($current_color + 1) % $num_colors;\n \t\t}\n \t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$rev_color[$current_color]\\\">\\n\";\n \t\tif ($group_size) {\n@@ -4654,7 +4661,7 @@ HTML\n \t\t\tprint $cgi->a({-href => href(action=>\"commit\",\n \t\t\t                             hash=>$full_rev,\n \t\t\t                             file_name=>$file_name)},\n-\t\t\t              esc_html($rev));\n+\t\t\t              esc_html($short_rev));\n \t\t\tprint \"</td>\\n\";\n \t\t}\n \t\tmy $parent_commit;\n@@ -4683,6 +4690,8 @@ HTML\n \tprint \"</div>\";\n \tclose $fd\n \t\tor print \"Reading blob failed\\n\";\n+\n+\t# page footer\n \tgit_footer_html();\n }\n \n"},{"id":"97464","messageId":"ghn8jv$hg9$1@ger.gmane.org","threadId":"16659","inReplyTo":"20081209224814.28106.83387.stgit@localhost.localdomain","subject":"Re: [PATCH 3/3] gitweb: A bit of code cleanup in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-10T02:13:24Z","receivedAt":"2008-12-10T02:13:24Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Jakub Narebski wrote:\n\nI'm sorry, there should be\n\n  +       my $ftype = \"blob\";\n>         if (!defined $hash) {\n>                 $hash = git_get_hash_by_path($hash_base, $file_name, \"blob\")\n>                         or die_error(404, \"Error looking up file\");\n> +       } else {\n> +               $ftype = git_get_type($hash);\n> +               if ($ftype !~ \"blob\") {\n> +                       die_error(400, \"Object is not a blob\");\n> +               }\n\n-- \nJakub Narebski\nWarsaw, Poland\nShadeHawk on #git\n"},{"id":"97465","messageId":"20081210124901.6117@nanako3.lavabit.com","threadId":"16659","inReplyTo":"20081209224622.28106.89325.stgit@localhost.localdomain","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Nanako Shiraishi","fromEmail":"nanako3@lavabit.com","sentAt":"2008-12-10T03:49:01Z","receivedAt":"2008-12-10T03:49:01Z","isPatch":true,"sender":{"key":"nanako3@lavabit.com","avatar":"https://gravatar.com/avatar/3777b9e201c5883a62b1a6fdf7c53f2d712d1d80989146063ea861e33aad72a8?d=mp&s=160"},"body":"Quoting Jakub Narebski <jnareb@gmail.com>:\n\n> Unfortunately the implementation in 244a70e used one call for\n> git-rev-parse to find parent revision per line in file, instead of\n> using long lived \"git cat-file --batch-check\" (which might not existed\n> then), or changing validate_refname to validate_revision and made it\n> accept <rev>^, <rev>^^, <rev>^^^ etc. syntax.\n\nCould you substantiate why this is \"Unfortunate\"?  Is the new implementation faster?  By how much?\n\nWhen \"previous\" commit information is available in the output from \"git blame\", can you make use of it?\n\n-- \nNanako Shiraishi\nhttp://ivory.ap.teacup.com/nanako3/\n"},{"id":"97466","messageId":"661413.30278.qm@web31807.mail.mud.yahoo.com","threadId":"16659","inReplyTo":"20081209224330.28106.18301.stgit@localhost.localdomain","subject":"Re: [PATCH 1/3] gitweb: Move 'lineno' id from link to row element in git_blame","fromName":"Luben Tuikov","fromEmail":"ltuikov@yahoo.com","sentAt":"2008-12-10T05:55:18Z","receivedAt":"2008-12-10T05:55:18Z","isPatch":true,"sender":{"key":"ltuikov@yahoo.com","avatar":null},"body":"\n--- On Tue, 12/9/08, Jakub Narebski <jnareb@gmail.com> wrote:\n> From: Jakub Narebski <jnareb@gmail.com>\n> Subject: [PATCH 1/3] gitweb: Move 'lineno' id from link to row element in git_blame\n> To: git@vger.kernel.org\n> Cc: \"Luben Tuikov\" <ltuikov@yahoo.com>, \"Jakub Narebski\" <jnareb@gmail.com>\n> Date: Tuesday, December 9, 2008, 2:46 PM\n> Move l<line number> ID from <a> link element\n> inside table row (inside\n> cell element for column with line numbers), to encompassing\n> <tr> table\n> row element.  It was done to make it easier to manipulate\n> result HTML\n> with DOM, and to be able write 'blame_incremental'\n> view with the same,\n> or nearly the same result.\n> \n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\nAcked-by: Luben Tuikov <ltuikov@yahoo.com>\n\n   Luben\n\n> ---\n> For blame_incremental I need easy way to manipulate rows of\n> blame\n> table, to add information about blamed commits as it\n> arrives.\n> \n> So there it is.\n> \n>  gitweb/gitweb.perl |    3 +--\n>  1 files changed, 1 insertions(+), 2 deletions(-)\n> \n> diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\n> index 6eb370d..1b800f4 100755\n> --- a/gitweb/gitweb.perl\n> +++ b/gitweb/gitweb.perl\n> @@ -4645,7 +4645,7 @@ HTML\n>  \t\tif ($group_size) {\n>  \t\t\t$current_color = ++$current_color % $num_colors;\n>  \t\t}\n> -\t\tprint \"<tr\n> class=\\\"$rev_color[$current_color]\\\">\\n\";\n> +\t\tprint \"<tr id=\\\"l$lineno\\\"\n> class=\\\"$rev_color[$current_color]\\\">\\n\";\n>  \t\tif ($group_size) {\n>  \t\t\tprint \"<td\n> class=\\\"sha1\\\"\";\n>  \t\t\tprint \" title=\\\"\". esc_html($author)\n> . \", $date\\\"\";\n> @@ -4667,7 +4667,6 @@ HTML\n>  \t\t                  hash_base => $parent_commit);\n>  \t\tprint \"<td\n> class=\\\"linenr\\\">\";\n>  \t\tprint $cgi->a({ -href =>\n> \"$blamed#l$orig_lineno\",\n> -\t\t                -id => \"l$lineno\",\n>  \t\t                -class => \"linenr\" },\n>  \t\t              esc_html($lineno));\n>  \t\tprint \"</td>\";\n"},{"id":"97467","messageId":"182871.96175.qm@web31804.mail.mud.yahoo.com","threadId":"16659","inReplyTo":"20081209224622.28106.89325.stgit@localhost.localdomain","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Luben Tuikov","fromEmail":"ltuikov@yahoo.com","sentAt":"2008-12-10T06:20:33Z","receivedAt":"2008-12-10T06:20:33Z","isPatch":true,"sender":{"key":"ltuikov@yahoo.com","avatar":null},"body":"\n--- On Tue, 12/9/08, Jakub Narebski <jnareb@gmail.com> wrote:\n> From: Jakub Narebski <jnareb@gmail.com>\n> Subject: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()\n> To: git@vger.kernel.org\n> Cc: \"Luben Tuikov\" <ltuikov@yahoo.com>, \"Jakub Narebski\" <jnareb@gmail.com>\n> Date: Tuesday, December 9, 2008, 2:48 PM\n> Luben Tuikov changed 'lineno' link from leading to\n> commit which lead\n> to current version of given block of lines, to leading to\n> parent of\n> this commit in 244a70e (Blame \"linenr\" link jumps\n> to previous state at\n> \"orig_lineno\").  This supposedly made data mining\n> possible (or just\n> better).\n\nBefore 244a70e, clicking on linenr links would display\nthe same commit id as displayed to the left, which is no\ndifferent than the block of lines displayed, thus data\nmining was impossible, i.e. I had to manually (commands)\ngo back in history to see how this line or block of lines\ndeveloped and/or changed.\n\n244a70e didn't make data mining perfect, just possible.\n\n> This patch attempts to migitate issue a bit by caching\n> $parent_commit\n> info in %metainfo, which makes gitweb to call git-rev-parse\n> only once\n> per unique commit in blame output.\n\nHave you tested this patch that it gives the same commit chain\nas before it?\n\n   Luben\n\n\n> \n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\n> ---\n> That is what I have noticed during browsing git_blame()\n> code.\n\nWhat?\n\n> We can change it to even more effective implementation\n> (like the ones\n> proposed above in the commit message) later.\n\nWhere?\n\n> \n> Indenting is cause for artifically large diff\n> \n>  gitweb/gitweb.perl |   16 +++++++++++-----\n>  1 files changed, 11 insertions(+), 5 deletions(-)\n> \n> diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\n> index 1b800f4..916396a 100755\n> --- a/gitweb/gitweb.perl\n> +++ b/gitweb/gitweb.perl\n> @@ -4657,11 +4657,17 @@ HTML\n>  \t\t\t              esc_html($rev));\n>  \t\t\tprint \"</td>\\n\";\n>  \t\t}\n> -\t\topen (my $dd, \"-|\", git_cmd(),\n> \"rev-parse\", \"$full_rev^\")\n> -\t\t\tor die_error(500, \"Open git-rev-parse\n> failed\");\n> -\t\tmy $parent_commit = <$dd>;\n> -\t\tclose $dd;\n> -\t\tchomp($parent_commit);\n> +\t\tmy $parent_commit;\n> +\t\tif (!exists $meta->{'parent'}) {\n> +\t\t\topen (my $dd, \"-|\", git_cmd(),\n> \"rev-parse\", \"$full_rev^\")\n> +\t\t\t\tor die_error(500, \"Open git-rev-parse\n> failed\");\n> +\t\t\t$parent_commit = <$dd>;\n> +\t\t\tclose $dd;\n> +\t\t\tchomp($parent_commit);\n> +\t\t\t$meta->{'parent'} = $parent_commit;\n> +\t\t} else {\n> +\t\t\t$parent_commit = $meta->{'parent'};\n> +\t\t}\n>  \t\tmy $blamed = href(action => 'blame',\n>  \t\t                  file_name =>\n> $meta->{'filename'},\n>  \t\t                  hash_base => $parent_commit);\n"},{"id":"97468","messageId":"910359.52983.qm@web31807.mail.mud.yahoo.com","threadId":"16659","inReplyTo":"20081209224814.28106.83387.stgit@localhost.localdomain","subject":"Re: [PATCH 3/3] gitweb: A bit of code cleanup in git_blame()","fromName":"Luben Tuikov","fromEmail":"ltuikov@yahoo.com","sentAt":"2008-12-10T06:24:49Z","receivedAt":"2008-12-10T06:24:49Z","isPatch":true,"sender":{"key":"ltuikov@yahoo.com","avatar":null},"body":"\n--- On Tue, 12/9/08, Jakub Narebski <jnareb@gmail.com> wrote:\n> From: Jakub Narebski <jnareb@gmail.com>\n> Subject: [PATCH 3/3] gitweb: A bit of code cleanup in git_blame()\n> To: git@vger.kernel.org\n> Cc: \"Luben Tuikov\" <ltuikov@yahoo.com>, \"Jakub Narebski\" <jnareb@gmail.com>\n> Date: Tuesday, December 9, 2008, 2:48 PM\n> Among others: \n>  * move variable declaration closer to the place it is set\n> and used,\n>    if possible,\n>  * uniquify and simplify coding style a bit, which includes\n> removing\n>    unnecessary '()'.\n>  * check type only if $hash was defined, as otherwise from\n> the way\n>    git_get_hash_by_path() is called (and works), we know\n> that it is\n>    a blob,\n>  * use modern calling convention for git-blame,\n>  * remove unused variable,\n>  * don't use implicit variables ($_),\n>  * add some comments\n> \n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\nAcked-by: Luben Tuikov <ltuikov@yahoo.com>\n\nLooks good.\n\n> ---\n> Not stricly necessary... but the code looked not very nice\n> \n>  gitweb/gitweb.perl |   65\n> ++++++++++++++++++++++++++++++----------------------\n>  1 files changed, 37 insertions(+), 28 deletions(-)\n> \n> diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\n> index 916396a..68aa3f8 100755\n> --- a/gitweb/gitweb.perl\n> +++ b/gitweb/gitweb.perl\n> @@ -4575,28 +4575,32 @@ sub git_tag {\n>  }\n>  \n>  sub git_blame {\n> -\tmy $fd;\n> -\tmy $ftype;\n> -\n> +\t# permissions\n>  \tgitweb_check_feature('blame')\n> -\t    or die_error(403, \"Blame view not\n> allowed\");\n> +\t\tor die_error(403, \"Blame view not allowed\");\n>  \n> +\t# error checking\n>  \tdie_error(400, \"No file name given\") unless\n> $file_name;\n>  \t$hash_base ||= git_get_head_hash($project);\n> -\tdie_error(404, \"Couldn't find base commit\")\n> unless ($hash_base);\n> +\tdie_error(404, \"Couldn't find base commit\")\n> unless $hash_base;\n>  \tmy %co = parse_commit($hash_base)\n>  \t\tor die_error(404, \"Commit not found\");\n>  \tif (!defined $hash) {\n>  \t\t$hash = git_get_hash_by_path($hash_base, $file_name,\n> \"blob\")\n>  \t\t\tor die_error(404, \"Error looking up file\");\n> +\t} else {\n> +\t\tmy $ftype = git_get_type($hash);\n> +\t\tif ($ftype !~ \"blob\") {\n> +\t\t\tdie_error(400, \"Object is not a blob\");\n> +\t\t}\n>  \t}\n> -\t$ftype = git_get_type($hash);\n> -\tif ($ftype !~ \"blob\") {\n> -\t\tdie_error(400, \"Object is not a blob\");\n> -\t}\n> -\topen ($fd, \"-|\", git_cmd(), \"blame\",\n> '-p', '--',\n> -\t      $file_name, $hash_base)\n> +\n> +\t# run git-blame --porcelain\n> +\topen my $fd, \"-|\", git_cmd(),\n> \"blame\", '-p',\n> +\t\t$hash_base, '--', $file_name\n>  \t\tor die_error(500, \"Open git-blame failed\");\n> +\n> +\t# page header\n>  \tgit_header_html();\n>  \tmy $formats_nav =\n>  \t\t$cgi->a({-href =>\n> href(action=>\"blob\", -replay=>1)},\n> @@ -4610,40 +4614,43 @@ sub git_blame {\n>  \tgit_print_page_nav('','',\n> $hash_base,$co{'tree'},$hash_base, $formats_nav);\n>  \tgit_print_header_div('commit',\n> esc_html($co{'title'}), $hash_base);\n>  \tgit_print_page_path($file_name, $ftype, $hash_base);\n> -\tmy @rev_color = (qw(light2 dark2));\n> +\n> +\t# page body\n> +\tmy @rev_color = qw(light2 dark2);\n>  \tmy $num_colors = scalar(@rev_color);\n>  \tmy $current_color = 0;\n> -\tmy $last_rev;\n> +\tmy %metainfo = ();\n> +\n>  \tprint <<HTML;\n>  <div class=\"page_body\">\n>  <table class=\"blame\">\n> \n> <tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n>  HTML\n> -\tmy %metainfo = ();\n> -\twhile (1) {\n> -\t\t$_ = <$fd>;\n> -\t\tlast unless defined $_;\n> + LINE:\n> +\twhile (my $line = <$fd>) {\n> +\t\tchomp $line;\n> +\t\t# the header: <SHA-1> <src lineno> <dst\n> lineno> [<lines in group>]\n> +\t\t# no <lines in group> for subsequent lines in\n> group of lines\n>  \t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n> -\t\t    /^([0-9a-f]{40}) (\\d+) (\\d+)(?:\n> (\\d+))?$/;\n> +\t\t   ($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?:\n> (\\d+))?$/);\n>  \t\tif (!exists $metainfo{$full_rev}) {\n>  \t\t\t$metainfo{$full_rev} = {};\n>  \t\t}\n>  \t\tmy $meta = $metainfo{$full_rev};\n> -\t\twhile (<$fd>) {\n> -\t\t\tlast if (s/^\\t//);\n> -\t\t\tif (/^(\\S+) (.*)$/) {\n> +\t\twhile (my $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;\n>  \t\t\t}\n>  \t\t}\n> -\t\tmy $data = $_;\n> -\t\tchomp $data;\n> -\t\tmy $rev = substr($full_rev, 0, 8);\n> +\t\tmy $short_rev = substr($full_rev, 0, 8);\n>  \t\tmy $author = $meta->{'author'};\n> -\t\tmy %date = parse_date($meta->{'author-time'},\n> -\t\t                      $meta->{'author-tz'});\n> +\t\tmy %date =\n> +\t\t\tparse_date($meta->{'author-time'},\n> $meta->{'author-tz'});\n>  \t\tmy $date = $date{'iso-tz'};\n>  \t\tif ($group_size) {\n> -\t\t\t$current_color = ++$current_color % $num_colors;\n> +\t\t\t$current_color = ($current_color + 1) % $num_colors;\n>  \t\t}\n>  \t\tprint \"<tr id=\\\"l$lineno\\\"\n> class=\\\"$rev_color[$current_color]\\\">\\n\";\n>  \t\tif ($group_size) {\n> @@ -4654,7 +4661,7 @@ HTML\n>  \t\t\tprint $cgi->a({-href =>\n> href(action=>\"commit\",\n>  \t\t\t                             hash=>$full_rev,\n>  \t\t\t                            \n> file_name=>$file_name)},\n> -\t\t\t              esc_html($rev));\n> +\t\t\t              esc_html($short_rev));\n>  \t\t\tprint \"</td>\\n\";\n>  \t\t}\n>  \t\tmy $parent_commit;\n> @@ -4683,6 +4690,8 @@ HTML\n>  \tprint \"</div>\";\n>  \tclose $fd\n>  \t\tor print \"Reading blob failed\\n\";\n> +\n> +\t# page footer\n>  \tgit_footer_html();\n>  }\n"},{"id":"97474","messageId":"7voczk1l7q.fsf@gitster.siamese.dyndns.org","threadId":"16659","inReplyTo":"ghn8jv$hg9$1@ger.gmane.org","subject":"Re: [PATCH 3/3] gitweb: A bit of code cleanup in git_blame()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2008-12-10T08:35:37Z","receivedAt":"2008-12-10T08:35:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> writes:\n\n> Jakub Narebski wrote:\n>\n> I'm sorry, there should be\n>\n>   +       my $ftype = \"blob\";\n>>         if (!defined $hash) {\n>>                 $hash = git_get_hash_by_path($hash_base, $file_name, \"blob\")\n>>                         or die_error(404, \"Error looking up file\");\n>> +       } else {\n>> +               $ftype = git_get_type($hash);\n>> +               if ($ftype !~ \"blob\") {\n>> +                       die_error(400, \"Object is not a blob\");\n>> +               }\n\nI will squash in the following and queue [1/3] and [3/3] to 'pu', as there\nseem to be a few comments on [2/3] that look worth addressing.\n\n gitweb/gitweb.perl |    6 ++++--\n 1 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git c/gitweb/gitweb.perl w/gitweb/gitweb.perl\nindex d491a1d..ccbf5d4 100755\n--- c/gitweb/gitweb.perl\n+++ w/gitweb/gitweb.perl\n@@ -4585,11 +4585,12 @@ sub git_blame {\n \tdie_error(404, \"Couldn't find base commit\") unless $hash_base;\n \tmy %co = parse_commit($hash_base)\n \t\tor die_error(404, \"Commit not found\");\n+\tmy $ftype = \"blob\";\n \tif (!defined $hash) {\n \t\t$hash = git_get_hash_by_path($hash_base, $file_name, \"blob\")\n \t\t\tor die_error(404, \"Error looking up file\");\n \t} else {\n-\t\tmy $ftype = git_get_type($hash);\n+\t\t$ftype = git_get_type($hash);\n \t\tif ($ftype !~ \"blob\") {\n \t\t\tdie_error(400, \"Object is not a blob\");\n \t\t}\n@@ -4637,7 +4638,8 @@ HTML\n \t\t\t$metainfo{$full_rev} = {};\n \t\t}\n \t\tmy $meta = $metainfo{$full_rev};\n-\t\twhile (my $data = <$fd>) {\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"},{"id":"97484","messageId":"200812101439.18526.jnareb@gmail.com","threadId":"16659","inReplyTo":"20081210124901.6117@nanako3.lavabit.com","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-10T13:39:17Z","receivedAt":"2008-12-10T13:39:17Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Wed, 10 Dec 2008, Nanako Shiraishi wrote:\n> Quoting Jakub Narebski <jnareb@gmail.com>:\n> \n> > Unfortunately the implementation in 244a70e used one call for\n> > git-rev-parse to find parent revision per line in file, instead of\n> > using long lived \"git cat-file --batch-check\" (which might not existed\n> > then), or changing validate_refname to validate_revision and made it\n> > accept <rev>^, <rev>^^, <rev>^^^ etc. syntax.\n> \n> Could you substantiate why this is \"Unfortunate\"?\n\nBecause it calls git-rev-parse once for _each line_, even if for lines\nin the group of neighbour lines blamed by same commit $parent_commit\nis the same, and even if you need to calculate $parent_commit only once\nper unique individual commit present in blame output.\n \n> Is the new implementation faster?  By how much?\n\nFile               | L[1] | C[2] || Time0[3] | Before[4] | After[4]\n====================================================================\nblob.h             |   18 |    4 || 0m1.727s |  0m2.545s |  0m2.474s\nGIT-VERSION-GEN    |   42 |   13 || 0m2.165s |  0m2.448s |  0m2.071s\nREADME             |   46 |    6 || 0m1.593s |  0m2.727s |  0m2.242s\nrevision.c         | 1923 |  121 || 0m2.357s | 0m30.365s |  0m7.028s\ngitweb/gitweb.perl | 6291 |  428 || 0m8.080s | 1m37.244s | 0m20.627s\n\nFile               | L/C  | Before/After\n=========================================\nblob.h             |  4.5 |         1.03\nGIT-VERSION-GEN    |  3.2 |         1.18\nREADME             |  7.7 |         1.22\nrevision.c         | 15.9 |         4.32\ngitweb/gitweb.perl | 14.7 |         4.71\n\nAs you can see the greater ratio of lines in file to unique commits\nin blame output, the greater gain from the new implementation.\n\nFootnotes:\n~~~~~~~~~~\n[1] Lines: \n    $ wc -l <file>\n[2] Individual commits in blame output:\n    $ git blame -p <file> | grep author-time | wc -l\n[3] Time for running \"git blame -p\" (user time, single run):\n    $ time git blame -p <file> >/dev/null\n[4] Time to run gitweb as Perl script from command line:\n    $ gitweb-run.sh \"p=git.git;a=blame;f=<file>\" > /dev/null 2>&1\n\nAppendix A:\n~~~~~~~~~~~\n#!/bin/bash\n\nexport GATEWAY_INTERFACE=\"CGI/1.1\"\nexport HTTP_ACCEPT=\"*/*\"\nexport REQUEST_METHOD=\"GET\"\nexport QUERY_STRING=\"\"$1\"\"\nexport PATH_INFO=\"\"$2\"\"\n\nexport GITWEB_CONFIG=\"/home/jnareb/git/gitweb/gitweb_config.perl\"\n\nperl -- /home/jnareb/git/gitweb/gitweb.perl\n\n# end of gitweb-run.sh\n\n\n> When \"previous\" commit information is available in the output from\n> \"git blame\", can you make use of it? \n\nYes, I could; but I don't think it got implemented.\n-- \nJakub Narebski\nPoland\n"},{"id":"97492","messageId":"200812101615.55340.jnareb@gmail.com","threadId":"16659","inReplyTo":"182871.96175.qm@web31804.mail.mud.yahoo.com","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-10T15:15:54Z","receivedAt":"2008-12-10T15:15:54Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Wed, 10 Dec 2008, Luben Tuikov wrote:\n> --- On Tue, 12/9/08, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> > This patch attempts to migitate issue [of performance] a bit by\n> > caching $parent_commit info in %metainfo, which makes gitweb to\n> > call git-rev-parse only once per unique commit in blame output.\n> \n> Have you tested this patch that it gives the same commit chain\n> as before it?\n\nThe only difference between precious version and this patch is that\nnow, if you calculate sha-1 of $long_rev^, it is stored in \n$metainfo{$long_rev}{'parent'} and not calculated second time.\n\nBut I have checked that (at least for single example file) the blame\noutput is identical for before and after this patch.\n\n\n> > That is what I have noticed during browsing git_blame()\n> > code.\n> \n> What?\n\nWhat I have noticed? I have noticed this inefficiency.\nWhy I was browsing git_blame? To write git_blame_incremental...\n\nSee also my reply to Nanako Shiraishi with simple benchmark.\n\n> > We can change it to even more effective implementation\n> > (like the ones proposed above in the commit message) later.\n> \n> Where?\n\njn> Unfortunately the implementation in 244a70e used one call for\njn> git-rev-parse to find parent revision per line in file, instead of\njn> using long lived \"git cat-file --batch-check\" (which might not existed\njn> then), or changing validate_refname to validate_revision and made it\njn> accept <rev>^, <rev>^^, <rev>^^^ etc. syntax.\n\nOne solution mentioned there is to open bidi pipe (like in Git::Repo\nin gitweb caching by Lea Wiemann) to \"git cat-file --batch-check\"\n(the '--batch-check' option was added to git-cat-file by Adam Roben\non Apr 23, 2008 in v1.5.6-rc0~8^2~9), feed it $long_rev^, and parse\nits output of the form:\n\n  926b07e694599d86cec668475071b32147c95034 commit 637\n\nsee manpage for git-cat-file(1). Unfortunately it seems like \ncommand_bidi_pipe doesn't work as _I_ expected...\n\n\nAnother solution mentioned there is to change validate_refname to\nvalidate_revision when checking script parameters (CGI query or\npath_info), with validate_revision being something like:\n\n  sub validate_revision {\n  \tmy $rev = shift;\n\treturn validate_refname(strip_rev_suffixes($rev));\n  }\n\nor something like that, so we don't need to calculate $long_rev^,\nbut can pass \"$long_rev^\" as 'hb' parameter ($long_rev can in turn\nalso end in '^').\n\n-- \nJakub Narebski\nPoland\n"},{"id":"97509","messageId":"710873.22344.qm@web31806.mail.mud.yahoo.com","threadId":"16659","inReplyTo":"200812101615.55340.jnareb@gmail.com","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Luben Tuikov","fromEmail":"ltuikov@yahoo.com","sentAt":"2008-12-10T20:05:30Z","receivedAt":"2008-12-10T20:05:30Z","isPatch":true,"sender":{"key":"ltuikov@yahoo.com","avatar":null},"body":"\n--- On Wed, 12/10/08, Jakub Narebski <jnareb@gmail.com> wrote:\n> > Have you tested this patch that it gives the same\n> commit chain\n> > as before it?\n> \n> The only difference between precious version and this patch\n> is that\n> now, if you calculate sha-1 of $long_rev^, it is stored in \n> $metainfo{$long_rev}{'parent'} and not calculated\n> second time.\n\nYes, I agree a patch to this effect would improve performance\nproportionally to the history of the lines of a file.  So it's a\ngood thing, as most commits change a contiguous block of size more\nthan one line of a file.\n\n\"$parent_commit\" depends on \"$full_rev^\" which depends on \"$full_rev\".\nSo as soon as \"$full_rev\" != \"$old_full_rev\", you'd know that you need\nto update \"$parent_commit\".  \"$old_full_rev\" needs to exist outside the\nscope of \"while (1)\".  I didn't see this in the code or in the patch.\n\n> But I have checked that (at least for single example file)\n> the blame output is identical for before and after this patch.\n\nI've always tested it on something like \"gitweb.perl\", etc.\n\n    Luben\n"},{"id":"97512","messageId":"20081210200908.16899.36727.stgit@localhost.localdomain","threadId":"16659","inReplyTo":"20081209223703.28106.29198.stgit@localhost.localdomain","subject":"[RFC/PATCH 4/3] gitweb: Incremental blame (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-10T20:11:18Z","receivedAt":"2008-12-10T20:11:18Z","isPatch":true,"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\nThe original patch by Fredrik Kuivinen has been lightly tested in a\ncouple of browsers (Firefox, Mozilla, Konqueror, Galeon, Opera and IE6).\nThe next patch by Petr Baudis has been tested in Firefox and Epiphany.\nThis patch has been lightly tested in Mozilla 1.17.2 and Konqueror\n3.5.3.\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 for incremental 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\nThis code uses http://ajaxpatterns.org/HTTP_Streaming pattern.\n\nIt deals with streamed 'blame_data' server error by notifying about them\nin the progress info area (just in case).\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\nWhile at it, this patch uniquifies td.dark and td.dark2 style: they\ndiffer only in that td.dark2 doesn't have style for :hover.\n\n\nThis patch also adds showing time (in seconds) it took to generate\na page in page footer (based on example code by Pasky), even though\nit is independent change, to be able to evaluate incremental blame in\ngitweb better.  In proper patch series it would be independent commit;\nand it probably would provide fallback if Time::HiRes is not available\n(by e.g. not showing generating time info), even though this is\nunlikely.\n\nCc: Fredrik Kuivinen <frekui@gmail.com>\nSigned-off-by: Petr Baudis <pasky@suse.cz>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nI'm sorry if you have received duplicate copy of this message...\n\nNOTE: This patch is RFC proof of concept patch!: it should be split\nonto many smaller patches for easy review (and bug finding) in version\nmeant to be applied.\n\n[Please tell me if some of info below should be put in the commit\n message instead of here]\n\nPatch by Petr Baudis this one is based on:\n  http://permalink.gmane.org/gmane.comp.version-control.git/56657\n\nOriginal patch by Fredrik Kuivinen:\n  http://article.gmane.org/gmane.comp.version-control.git/41361\n\nSnippet adding 'generated in' to gitweb, by Petr Baudis:\n  http://article.gmane.org/gmane.comp.version-control.git/83306\n\nShould I post interdiff to Petr Baudis patch, and comments about\ndifference between them? This post is quite long as it is now...\n\n\nDifferences between 'blame' and 'blame_incremental' output:\n* Line number links in 'blame' lead to parent of blamed commit, i.e. to\n  the view where given line _changed_; this behavior was introduced in\n  244a70e (Blame \"linenr\" link jumps to previous state at \"orig_lineno\")\n  by Luben Tuikov on Jan 2008 to make data mining possible.\n\n  Currently 'blame_incremental' lead to version at blamed commit; this\n  would be hard to change without opening another stream, or without\n  having gitweb accept <sha1>^ for 'hb' (hash_base) parameter.\n\n* The title attribute (shown in popup on mouseover) for \"sha1\" cell\n  for 'blame_incremental' view looks like the following:\n    'Kay Sievers, 2005-08-07 19:49:46 +0000 (21:49:46 +0200)'\n  more like the date format used in 'commit' view, rather than shorter\n    'Kay Sievers, 2005-08-07 21:49:46 +0200'\n\n  This is a bit of accident, as in originla patch(es) there was error\n  where the time and date shown were for UTC (GMT), and not for shown\n  together timezone, i.e.\n    'Kay Sievers, 2005-08-07 19:49:46 +0200'\n  and I have added local time instead of replacing it. But perhaps it\n  is 'blame' view that should change format? \n\n* In 'blame_incremental' the cell (<td>) with sha1 has rowspan\n  attribute set even if it is at its default value rowspan=\"1\".\n  This should be very easy to fix.\n\n* The 'blame_incremental' view has new feature inspired by output of\n  \"git gui blame <file>\", that if group of lines blamed to the same\n  commit has more than two lines, then below shortened sha-1 of a\n  commit it is shown shortened author (initials, in uppercase).\n\n  If you think it is worth it, this feature should be fairly easy to\n  port to ordinary non-incremental 'blame' view.\n\n* Sometimes \"git blame --porcelain\" and \"git blame --incremental\" can\n  give different output.  Compare for example 'blame' and\n  'blame_incremental' view for GIT-VERSION-GEN in git.git repository,\n  and note that in 'blame_incremental' the last two groups are for the\n  same commit (compare bottom parts of pages).  'blame_incremental'\n  currently does not consolidate groups; if it did that, it should do\n  it before fixColors()\n\n* During filling blame info 'blame_incremental' view uses three colors\n  (three styles: color1, color2, color3) instead of two color zebra\n  coloring (two styles: light2, dark2) to ensure that the color of\n  current group is different from both of its neighbours.\n\n  This is non-issue: this is fixed at the end (although intermediate\n  versions of blame.js script didn't fo fixColors() to make it easier\n  to check if the 3-coloring is correct).\n\n* The 'blame_incremental' view contains unique progress bar and\n  progress report; perhaps they should vanish after succesfull loading\n  of all data?\n\n\nBugs and suspected bugs in Mozilla 1.17.2 (my main browser); perhaps\nthose got corrected, as 1.17.2 is ancient (Gecko/20050923) version:\n* HTML 4.01 Transitional specification at W3C states only ID and NAME\n  tokens must begin with a letter ([A-Za-z]); it looks like class\n  named \"1\" or \"2\" or \"3\" has style specified by CSS ignored.\n\n* With XHTML 1.0 DTD and application/xhtml+xml content type, if there\n  were <col /> elements in blame table (currently commented out in the\n  source), then row with column headings (with <td> elements) was not\n  visible.\n\n* With XHTML 1.0 DTD and application/xhtml+xml content type, if there\n  was an error in JavaScript, instead of having it visible as error\n  message in JavaScript Console, Mozilla behaved as if the script\n  wasn't there at all.\n\n* With XHTML 1.0 DTD and application/xhtml+xml content type, setting\n  innerHTML doesn't work: it raises cryptic JavaScript exception:\n\n    Component returned failure code: 0x80004005 (NS_ERROR_FAILURE)\n    [nsIDOMNSHTMLElement.innerHTML]\t\n\n  Correct solution would be to use only DOM, or rather depending on\n  what web browser supports, use either .innerHTML (which is\n  proprietary Microsoft extension) or DOM (which is standard but not\n  all browser use it).  Current *workaround* is to simply always use\n  'text/html' content type, and HTML 4.01 DTD.\n\n  I wonder whether innerHTML or DOM is faster; and how many of web\n  browser implements other similar properties like innerText,\n  outerHTML and insertAdjacentHTML.\n\n* XHTML 1.0 doesn't allow for trick with using HTML comments to hide\n  contents of <script> element from old browsers, as XML compliant\n  browser can remove XML comments before seeing JavaScript, so we\n  don't use it.  Besides I think this issue is irrelevant now.\n\n\nP.S. I have removed a few spurious one line change chunks from\ngitweb/gitweb.perl, which were done to not confuse Perl syntax\nhighlighting (lazy-lock-mode) from cperl-mode in GNU Emacs 21.4.1,\nand to not confuse imenu parser in GNU Emacs (which allow to go to\nspecified subroutine, and to display which-function-info in\nmodeline), so diffstat is slightly out of sync.\n\nThose were #\" or #' after regexp with single unescaped \" or ',\nand ($) in definition of \"sub S_ISGITLINK($)\".\n\nP.P.S. What is the stance for copyrigth assesments in the files\nfor git code, like the ones in gitweb/gitweb.perl and gitweb/blame.js?\n\nP.P.P.S. Should I use Signed-off-by from Pasky and Fredrik if I based\nmy code on theirs, and if they all signed their patches?\n\n Makefile           |    4 +\n git-instaweb.sh    |    6 +\n gitweb/blame.js    |  398 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.css  |   27 +++-\n gitweb/gitweb.perl |  252 ++++++++++++++++++++++-----------\n 5 files changed, 597 insertions(+), 90 deletions(-)\n create mode 100644 gitweb/blame.js\n\ndiff --git a/Makefile b/Makefile\nindex 5158197..05ac246 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -218,6 +218,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@@ -1209,6 +1210,7 @@ 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@@ -1224,6 +1226,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 0843372..f41354c 100755\n--- a/git-instaweb.sh\n+++ b/git-instaweb.sh\n@@ -268,6 +268,12 @@ 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 \ndiff --git a/gitweb/blame.js b/gitweb/blame.js\nnew file mode 100644\nindex 0000000..197d615\n--- /dev/null\n+++ b/gitweb/blame.js\n@@ -0,0 +1,398 @@\n+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n+\n+var DEBUG = 0;\n+function debug(str) {\n+\tif (DEBUG)\n+\t\talert(str);\n+}\n+\n+function createRequestObject() {\n+\tvar ro;\n+\tif (window.XMLHttpRequest) {\n+\t\tro = new XMLHttpRequest();\n+\t} else {\n+\t\tro = new ActiveXObject(\"Microsoft.XMLHTTP\");\n+\t}\n+\tif (!ro)\n+\t\tdebug(\"Couldn't start XMLHttpRequest object\");\n+\treturn ro;\n+}\n+\n+var http;\n+var baseUrl;\n+\n+// 'commits' is an associative map. It maps SHA1s to Commit objects.\n+var commits = new Object();\n+\n+function Commit(sha1) {\n+\tthis.sha1 = sha1;\n+}\n+\n+// convert month or day of the month to string, padding it with\n+// '0' (zero) to two characters width if necessary\n+function zeroPad(n) {\n+\tif (n < 10)\n+\t\treturn '0' + n;\n+\telse\n+\t\treturn n.toString();\n+}\n+\n+function spacePad(n,width) {\n+\tvar scale = 1;\n+\tvar str = '';\n+\n+\twhile (width > 1) {\n+\t\tscale *= 10;\n+\t\tif (n < scale)\n+\t\t\tstr += '&nbsp;';\n+\t\twidth--;\n+\t}\n+\treturn str + n.toString();\n+}\n+\n+\n+var blamedLines = 0;\n+var totalLines  = '???';\n+var div_progress_bar;\n+var div_progress_info;\n+\n+function countLines() {\n+\tvar table = document.getElementById('blame_table');\n+\tif (!table)\n+\t\ttable = document.getElementsByTagName('table').item(0);\n+\n+\tif (table)\n+\t\treturn table.getElementsByTagName('tr').length - 1; // for header\n+\telse\n+\t\treturn '...';\n+}\n+\n+function updateProgressInfo() {\n+\tif (!div_progress_info)\n+\t\tdiv_progress_info = document.getElementById('progress_info');\n+\tif (!div_progress_bar)\n+\t\tdiv_progress_bar = document.getElementById('progress_bar');\n+\tif (!div_progress_info && !div_progress_bar)\n+\t\treturn;\n+\n+\tvar percentage = Math.floor(100.0*blamedLines/totalLines);\n+\n+\tif (div_progress_info) {\n+\t\tdiv_progress_info.innerHTML  = blamedLines + ' / ' + totalLines\n+\t\t\t+ ' ('+spacePad(percentage,3)+'%)';\n+\t}\n+\n+\tif (div_progress_bar) {\n+\t\tdiv_progress_bar.setAttribute('style', 'width: '+percentage+'%;');\n+\t}\n+}\n+\n+var colorRe = new RegExp('color([0-9]*)');\n+/* return N if <tr class=\"colorN\">, otherwise return null */\n+function getColorNo(tr) {\n+\tvar className = tr.getAttribute('class');\n+\tif (className) {\n+\t\tmatch = colorRe.exec(className);\n+\t\tif (match)\n+\t\t\treturn parseInt(match[1]);\n+\t}\n+\treturn null;\n+}\n+\n+function findColorNo(tr_prev, tr_next) {\n+\tvar color_prev;\n+\tvar color_next;\n+\n+\tif (tr_prev)\n+\t\tcolor_prev = getColorNo(tr_prev);\n+\tif (tr_next)\n+\t\tcolor_next = getColorNo(tr_next);\n+\n+\tif (!color_prev && !color_next)\n+\t\treturn 1;\n+\tif (color_prev == color_next)\n+\t\treturn ((color_prev % 3) + 1);\n+\tif (!color_prev)\n+\t\treturn ((color_next % 3) + 1);\n+\tif (!color_next)\n+\t\treturn ((color_prev % 3) + 1);\n+\treturn (3 - ((color_prev + color_next) % 3));\n+}\n+\n+var tzRe = new RegExp('^([+-][0-9][0-9])([0-9][0-9])$');\n+// called for each blame entry, as soon as it finishes\n+function handleLine(commit) {\n+\t/* This is the structure of the HTML fragment we are working\n+\t   with:\n+\t   \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+\tif (!commit.info) {\n+\t\tvar date = new Date();\n+\t\tdate.setTime(commit.authorTime * 1000); // milliseconds\n+\t\tvar dateStr =\n+\t\t\tdate.getUTCFullYear() + '-'\n+\t\t\t+ zeroPad(date.getUTCMonth()+1) + '-'\n+\t\t\t+ zeroPad(date.getUTCDate());\n+\t\tvar timeStr =\n+\t\t\tzeroPad(date.getUTCHours()) + ':'\n+\t\t\t+ zeroPad(date.getUTCMinutes()) + ':'\n+\t\t\t+ zeroPad(date.getUTCSeconds());\n+\n+\t\tvar localDate = new Date();\n+\t\tvar match = tzRe.exec(commit.authorTimezone);\n+\t\tlocalDate.setTime(1000 * (commit.authorTime\n+\t\t\t+ (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));\n+\t\tvar localTimeStr =\n+\t\t\tzeroPad(localDate.getUTCHours()) + ':'\n+\t\t\t+ zeroPad(localDate.getUTCMinutes()) + ':'\n+\t\t\t+ zeroPad(localDate.getUTCSeconds());\n+\n+\t\t/* e.g. 'Kay Sievers, 2005-08-07 19:49:46 +0000 (21:49:46 +0200)' */\n+\t\tcommit.info = commit.author + ', ' + dateStr + ' '\n+\t\t\t+ timeStr + ' +0000'\n+\t\t\t+ ' (' + localTimeStr + ' ' + commit.authorTimezone + ')';\n+\t}\n+\n+\tvar color = findColorNo(\n+\t\tdocument.getElementById('l'+(resline-1)),\n+\t\tdocument.getElementById('l'+(resline+commit.numlines))\n+\t);\n+\n+\n+\tfor (var i = 0; i < commit.numlines; i++) {\n+\t\tvar tr = document.getElementById('l'+resline);\n+\t\tif (!tr) {\n+\t\t\tdebug('tr is null! resline: ' + resline);\n+\t\t\tbreak;\n+\t\t}\n+\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\tif (color) {\n+\t\t\ttr.setAttribute('class', 'color'+color.toString());\n+\t\t\t// Internet Explorer needs this\n+\t\t\t//tr.setAttribute('className', color.toString);\n+\t\t}\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.setAttribute('rowspan', commit.numlines);\n+\n+\t\t\ta_sha1.href = baseUrl + '?a=commit;h=' + commit.sha1;\n+\t\t\ta_sha1.innerHTML = commit.sha1.substr(0, 8);\n+\t\t\tif (commit.numlines >= 2) {\n+\t\t\t\tvar br   = document.createElement(\"br\");\n+\t\t\t\tvar text = document.createTextNode(commit.author.match(/\\b([A-Z])\\B/g).join(''));\n+\t\t\t\tif (br && text) {\n+\t\t\t\t\ttd_sha1.appendChild(br);\n+\t\t\t\t\ttd_sha1.appendChild(text);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\ttr.removeChild(td_sha1);\n+\t\t}\n+\n+\t\t/* <td class=\"linenr\"><a class=\"linenr\" href=\"?\">123</a></td> */\n+\t\ta_linenr.href = baseUrl + '?a=blame;hb=' + commit.sha1\n+\t\t\t+ ';f=' + commit.filename + '#l' + (commit.srcline + i);\n+\n+\t\tresline++;\n+\t\tblamedLines++;\n+\n+\t\t//updateProgressInfo();\n+\t}\n+}\n+\n+function startOfGroup(tr) {\n+\treturn tr.firstChild.getAttribute('class') == 'sha1';\n+}\n+\n+function fixColors() {\n+\tvar colorClasses = ['light2', 'dark2'];\n+\tvar linenum = 1;\n+\tvar tr;\n+\tvar colorClass = 0;\n+\n+\twhile ((tr = document.getElementById('l'+linenum))) {\n+\t\tif (startOfGroup(tr, linenum, document)) {\n+\t\t\tcolorClass = (colorClass + 1) % 2;\n+\t\t}\n+\t\ttr.setAttribute('class', colorClasses[colorClass]);\n+\t\t// Internet Explorer needs this\n+\t\ttr.setAttribute('className', colorClasses[colorClass]);\n+\t\tlinenum++;\n+\t}\n+}\n+\n+var t_interval_server = '';\n+var t0 = new Date();\n+\n+function writeTimeInterval() {\n+\tvar info = document.getElementById('generate_time');\n+\tif (!info)\n+\t\treturn;\n+\tvar t1 = new Date();\n+\n+\tinfo.innerHTML += ' + '\n+\t\t+ t_interval_server+'s server (blame_data) / '\n+\t\t+ (t1.getTime() - t0.getTime())/1000 + 's client (JavaScript)';\n+}\n+\n+// ----------------------------------------------------------------------\n+\n+var prevDataLength = -1;\n+var nextLine = 0;\n+var inProgress = false;\n+\n+var sha1Re = new RegExp('([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)');\n+var infoRe = new RegExp('([a-z-]+) ?(.*)');\n+var endRe = new RegExp('END ?(.*)');\n+var curCommit = new Commit();\n+\n+var pollTimer = null;\n+\n+function handleResponse() {\n+\tdebug('handleResp ready: ' + http.readyState\n+\t      + ' respText null?: ' + (http.responseText === null)\n+\t      + ' progress: ' + inProgress);\n+\n+\tif (http.readyState != 4 && http.readyState != 3)\n+\t\treturn;\n+\n+\t// the server stream is incorrect\n+\tif (http.readyState == 3 && http.status != 200)\n+\t\treturn;\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+\n+\t\tif (div_progress_info) {\n+\t\t\tdiv_progress_info.setAttribute('class', 'error');\n+\t\t\tdiv_progress_info.innerHTML =\n+\t\t\t\thttp.status+' - Error contacting server\\n';\n+\t\t} else {\n+\t\t\tdocument.write(\"<b>ERROR:</b> HTTP status is \"+http.status+\"<br />\\n\");\n+\t\t}\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+\n+\t/*\n+\tvar resp = document.getElementById('state');\n+\tif (resp) {\n+\t\tresp.innerHTML = http.readyState + ' : ' + http.status\n+\t\t\t+ '<br />len = ' + http.responseText.length\n+\t\t\t+ '; inProgress='+inProgress;\n+\t\t//inProgress = true;\n+\t}\n+\t*/\n+\n+\tif (inProgress)\n+\t\treturn;\n+\telse\n+\t\tinProgress = true;\n+\n+\twhile (prevDataLength != http.responseText.length) {\n+\t\tif (http.readyState == 4\n+\t\t    && prevDataLength == http.responseText.length) {\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tprevDataLength = http.responseText.length;\n+\t\tvar response = http.responseText.substring(nextLine);\n+\t\tvar lines = response.split('\\n');\n+\t\tnextLine = nextLine + response.lastIndexOf('\\n') + 1;\n+\t\tif (response[response.length-1] != '\\n') {\n+\t\t\tlines.pop();\n+\t\t}\n+\n+\t\tfor (var i = 0; i < lines.length; i++) {\n+\t\t\tvar match = sha1Re.exec(lines[i]);\n+\t\t\tif (match) {\n+\t\t\t\tvar sha1 = match[1];\n+\t\t\t\tvar srcline = parseInt(match[2]);\n+\t\t\t\tvar resline = parseInt(match[3]);\n+\t\t\t\tvar numlines = parseInt(match[4]);\n+\t\t\t\tvar c = commits[sha1];\n+\t\t\t\tif (!c) {\n+\t\t\t\t\tc = new Commit(sha1);\n+\t\t\t\t\tcommits[sha1] = c;\n+\t\t\t\t}\n+\n+\t\t\t\tc.srcline = srcline;\n+\t\t\t\tc.resline = resline;\n+\t\t\t\tc.numlines = numlines;\n+\t\t\t\tcurCommit = c;\n+\t\t\t} else if ((match = infoRe.exec(lines[i]))) {\n+\t\t\t\tvar info = match[1];\n+\t\t\t\tvar data = match[2];\n+\t\t\t\tif (info == 'filename') {\n+\t\t\t\t\tcurCommit.filename = data;\n+\t\t\t\t\thandleLine(curCommit);\n+\t\t\t\t\tupdateProgressInfo();\n+\t\t\t\t} else if (info == 'author') {\n+\t\t\t\t\tcurCommit.author = data;\n+\t\t\t\t} else if (info == 'author-time') {\n+\t\t\t\t\tcurCommit.authorTime = parseInt(data);\n+\t\t\t\t} else if (info == 'author-tz') {\n+\t\t\t\t\tcurCommit.authorTimezone = data;\n+\t\t\t\t}\n+\t\t\t} else if ((match = endRe.exec(lines[i]))) {\n+\t\t\t\tt_interval_server = match[1];\n+\t\t\t\tdebug('END: '+lines[i]);\n+\t\t\t} else if (lines[i] != '') {\n+\t\t\t\tdebug('malformed line: ' + lines[i]);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tif (http.readyState == 4 &&\n+\t    prevDataLength == http.responseText.length) {\n+\t\tclearInterval(pollTimer);\n+\n+\t\tfixColors();\n+\t\twriteTimeInterval();\n+\t}\n+\n+\tinProgress = false;\n+}\n+\n+function startBlame(blamedataUrl, bUrl) {\n+\tdebug('startBlame('+blamedataUrl+', '+bUrl+')');\n+\n+\tt0 = new Date();\n+\tbaseUrl = bUrl;\n+\ttotalLines = countLines();\n+\tupdateProgressInfo();\n+\n+\thttp = createRequestObject();\n+\thttp.open('get', blamedataUrl);\n+\thttp.onreadystatechange = handleResponse;\n+\thttp.send(null);\n+\n+\tpollTimer = setInterval(handleResponse, 1000);\n+}\n+\n+// end of blame.js\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex a01eac8..e359618 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -223,11 +223,7 @@ tr.light:hover {\n \tbackground-color: #edece6;\n }\n \n-tr.dark {\n-\tbackground-color: #f6f6f0;\n-}\n-\n-tr.dark2 {\n+tr.dark, tr.dark2 {\n \tbackground-color: #f6f6f0;\n }\n \n@@ -235,6 +231,14 @@ tr.dark:hover {\n \tbackground-color: #edece6;\n }\n \n+tr.color1:hover { background-color: #e6ede6; }\n+tr.color2:hover { background-color: #e6e6ed; }\n+tr.color3:hover { background-color: #ede6e6; }\n+\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@@ -255,7 +259,7 @@ td.sha1 {\n \tfont-family: monospace;\n }\n \n-td.error {\n+.error {\n \tcolor: red;\n \tbackground-color: yellow;\n }\n@@ -326,6 +330,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 4987fdc..774bcc6 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -18,6 +18,9 @@ use File::Find qw();\n use File::Basename qw(basename);\n binmode STDOUT, ':utf8';\n \n+use Time::HiRes qw(gettimeofday tv_interval);\n+our $t0 = [gettimeofday];\n+\n BEGIN {\n \tCGI->compile() if $ENV{'MOD_PERL'};\n }\n@@ -74,6 +77,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@@ -493,6 +498,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@@ -2874,13 +2881,13 @@ sub git_header_html {\n \t# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.\n \t# we have to do this because MSIE sometimes globs '*/*', pretending to\n \t# support xhtml+xml but choking when it gets what it asked for.\n-\tif (defined $cgi->http('HTTP_ACCEPT') &&\n-\t    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\\s|^)application\\/xhtml\\+xml(,|;|\\s|$)/ &&\n-\t    $cgi->Accept('application/xhtml+xml') != 0) {\n-\t\t$content_type = 'application/xhtml+xml';\n-\t} else {\n+\t#if (defined $cgi->http('HTTP_ACCEPT') &&\n+\t#    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\\s|^)application\\/xhtml\\+xml(,|;|\\s|$)/ &&\n+\t#    $cgi->Accept('application/xhtml+xml') != 0) {\n+\t#\t$content_type = 'application/xhtml+xml';\n+\t#} else {\n \t\t$content_type = 'text/html';\n-\t}\n+\t#}\n \tprint $cgi->header(-type=>$content_type, -charset => 'utf-8',\n \t                   -status=> $status, -expires => $expires);\n \tmy $mod_perl_version = $ENV{'MOD_PERL'} ? \" $ENV{'MOD_PERL'}\" : '';\n@@ -3042,6 +3049,14 @@ sub git_footer_html {\n \t}\n \tprint \"</div>\\n\"; # class=\"page_footer\"\n \n+\tprint \"<div class=\\\"page_footer\\\">\\n\";\n+\tprint 'This page took '.\n+\t      '<span id=\"generate_time\" class=\"time_span\">'.\n+\t      tv_interval($t0, [gettimeofday]).'s'.\n+\t      '</span>'.\n+\t      \" to generate.\\n\";\n+\tprint \"</div>\\n\"; # class=\"page_footer\"\n+\n \tif (-f $site_footer) {\n \t\tinsert_file($site_footer);\n \t}\n@@ -4574,7 +4589,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@@ -4596,10 +4613,36 @@ 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+\t\tprint \"END \".tv_interval($t0, [gettimeofday]).\"\\n\";\n+\n+\t\treturn;\n+\t}\n \n \t# page header\n \tgit_header_html();\n@@ -4610,93 +4653,134 @@ 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+\tprint qq!<div id=\"progress_bar\" style=\"width: 100%; background-color: yellow\"></div>\\n!\n+\t\tif ($format eq 'incremental');\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!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\\n!;\n+\n \tmy @rev_color = qw(light2 dark2);\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} = {};\n-\t\t}\n-\t\tmy $meta = $metainfo{$full_rev};\n-\t\tmy $data; # last line is used later\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;\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\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$rev_color[$current_color]\\\">\\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\tprint \"</td>\\n\";\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-\t\tmy $parent_commit;\n-\t\tif (!exists $meta->{'parent'}) {\n-\t\t\topen (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n-\t\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n-\t\t\t$parent_commit = <$dd>;\n-\t\t\tclose $dd;\n-\t\t\tchomp($parent_commit);\n-\t\t\t$meta->{'parent'} = $parent_commit;\n-\t\t} else {\n-\t\t\t$parent_commit = $meta->{'parent'};\n-\t\t}\n-\t\tmy $blamed = href(action => 'blame',\n-\t\t                  file_name => $meta->{'filename'},\n-\t\t                  hash_base => $parent_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}\n-\tprint \"</table>\\n\";\n-\tprint \"</div>\";\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\t($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n+\t\t\t$metainfo{$full_rev} ||= {};\n+\t\t\tmy $meta = $metainfo{$full_rev};\n+\t\t\tmy $data; # last line is used later\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;\n+\t\t\t\t}\n+\t\t\t}\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\tprint qq!<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\\n!;\n+\t\t\tif ($group_size) {\n+\t\t\t\tprint qq!<td class=\"sha1\"!.\n+\t\t\t\t      qq! title=\"!. esc_html($author) . qq!, $date\"!;\n+\t\t\t\tprint qq! rowspan=\"$group_size\"! if ($group_size > 1);\n+\t\t\t\tprint qq!>!;\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\tprint qq!</td>\\n!;\n+\t\t\t}\n+\t\t\tif (!exists $meta->{'parent'}) {\n+\t\t\t\topen my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\"\n+\t\t\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n+\t\t\t\t$meta->{'parent'} = <$dd>;\n+\t\t\t\tclose $dd;\n+\t\t\t\tchomp $meta->{'parent'};\n+\t\t\t}\n+\t\t\tmy $blamed = href(action => 'blame',\n+\t\t\t                  file_name => $meta->{'filename'},\n+\t\t\t                  hash_base => $meta->{'parent'});\n+\t\t\tprint qq!<td class=\"linenr\">!.\n+\t\t\t       $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n+\t\t\t                -class => \"linenr\" },\n+\t\t\t               esc_html($lineno)).\n+\t\t\t      qq!</td>!;\n+\t\t\tprint qq!<td class=\"pre\">! . esc_html($data) . \"</td>\\n\";\n+\t\t\tprint qq!</tr>\\n!;\n+\t\t}\n+\t}\n+\n+\t# footer\n+\tprint \"</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!           \"$my_url\");\\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\n-- \nStacked GIT 0.14.3\ngit version 1.6.0.4\n"},{"id":"97517","messageId":"7v7i67zsgj.fsf@gitster.siamese.dyndns.org","threadId":"16659","inReplyTo":"200812101439.18526.jnareb@gmail.com","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2008-12-10T20:27:24Z","receivedAt":"2008-12-10T20:27:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> writes:\n\n> On Wed, 10 Dec 2008, Nanako Shiraishi wrote:\n>> Quoting Jakub Narebski <jnareb@gmail.com>:\n>> \n>> > Unfortunately the implementation in 244a70e used one call for\n>> > git-rev-parse to find parent revision per line in file, instead of\n>> > using long lived \"git cat-file --batch-check\" (which might not existed\n>> > then), or changing validate_refname to validate_revision and made it\n>> > accept <rev>^, <rev>^^, <rev>^^^ etc. syntax.\n>> \n>> Could you substantiate why this is \"Unfortunate\"?\n>\n> Because it calls git-rev-parse once for _each line_, even if for lines\n> in the group of neighbour lines blamed by same commit $parent_commit\n> is the same, and even if you need to calculate $parent_commit only once\n> per unique individual commit present in blame output.\n\nIt probably was obvious that this was meant as a patch for better\nperformance without changing the functionality.  I tend to think the\npresentation wasn't so great, though.\n\n    Luben Tuikov changed 'lineno' link from leading to commit which lead\n    to current version of given block of lines, to leading to parent of\n    this commit in 244a70e (Blame \"linenr\" link jumps to previous state at\n    \"orig_lineno\").  This supposedly made data mining possible (or just\n    better).\n\nOther than \"supposedly\" which I do not think should be there, I think this\nis a great opening paragraph to establish the context.\n\n    Unfortunately the implementation in 244a70e used one call for\n    git-rev-parse to find parent revision per line in file, instead of\n    using long lived \"git cat-file --batch-check\" (which might not existed\n    then), or changing validate_refname to validate_revision and made it\n    accept <rev>^, <rev>^^, <rev>^^^ etc. syntax.\n\nBut I do not think this is such a great second paragraph that states what\nproblem it tries to solve.  I'd say something like this instead:\n\n        The current implementation calls rev-parse once per line to find\n        parent revision of blamed commit, even when the same commit\n        appears more than once, which is inefficient.\n\nAnd then the outline of the solution:\n\n    This patch attempts to migitate issue a bit by caching $parent_commit\n    info in %metainfo, which makes gitweb to call git-rev-parse only once\n    per unique commit in blame output.\n\nwhich is very good, except that I do not think you need to say \"a bit\".\nAnd have your benchmark (two tables and footnotes) after this outline of\nthe solution.\n\nI think the first part of \"Unfortunately\" paragraph can be dropped\n(because that is already in the first half of problem description) and the\nrest can come as the last paragraph as \"Possible future enhancements\".\n\n> Appendix A:\n> ~~~~~~~~~~~\n> #!/bin/bash\n>\n> export GATEWAY_INTERFACE=\"CGI/1.1\"\n> export HTTP_ACCEPT=\"*/*\"\n> export REQUEST_METHOD=\"GET\"\n> export QUERY_STRING=\"\"$1\"\"\n> export PATH_INFO=\"\"$2\"\"\n>\n> export GITWEB_CONFIG=\"/home/jnareb/git/gitweb/gitweb_config.perl\"\n>\n> perl -- /home/jnareb/git/gitweb/gitweb.perl\n>\n> # end of gitweb-run.sh\n\nI'd suggest making a separate patch to add \"gitweb-run.sh\" in contrib/ so\nthat other people can use it when checking their changes to gitweb.  The\nscript might need some more polishing, though.  For example, it is not\nvery obvious if you have *_config.perl only to customize for your\nenvironment (e.g. where the test repositories are) or you need to have\nsome overrides in there when you are running gitweb as a standalone script.\n\nTo recap, I think the commit log for this patch would have been much\neasier to read if it were presented in this order:\n\n\ta paragraph to establish the context;\n\n\ta paragraph to state what problem it tries to solve;\n\n        a paragraph (or more) to explain the solution; and finally\n\n\ta paragraph to discuss possible future enhancements.\n"},{"id":"97520","messageId":"200812102203.30486.jnareb@gmail.com","threadId":"16659","inReplyTo":"710873.22344.qm@web31806.mail.mud.yahoo.com","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-10T21:03:28Z","receivedAt":"2008-12-10T21:03:28Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Wed, 10 Dec 2008, Luben Tuikov wrote:\n> --- On Wed, 12/10/08, Jakub Narebski <jnareb@gmail.com> wrote:\n\nSide note: is it Yahoo web mail interface that removes attributions?\n\n> > > Have you tested this patch that it gives the same commit chain\n> > > as before it?\n> > \n> > The only difference between precious version and this patch\n> > is that now, if you calculate sha-1 of $long_rev^, it is stored in \n> > $metainfo{$long_rev}{'parent'} and not calculated second time.\n> \n> Yes, I agree a patch to this effect would improve performance\n> proportionally to the history of the lines of a file.\n\nOr rather proportionally to the ratio of number of lines of a file\nto number of unique commits (not groups of lines) which are blamed\nfor given contents of a file.\n\n> So it's a good thing, as most commits change a contiguous block\n> of size more than one line of a file.\n> \n> \"$parent_commit\" depends on \"$full_rev^\" which depends on \"$full_rev\".\n> So as soon as \"$full_rev\" != \"$old_full_rev\", you'd know that you need\n> to update \"$parent_commit\".  \"$old_full_rev\" needs to exist outside\n> the  scope of \"while (1)\".  I didn't see this in the code or in the\n> patch. \n\nYou don't need $old_full_rev. We have to save data about commit in\n%metainfo hash because information about commit appears in \n\"git blame --porcelain\" output _once_ per commit. So we use the same\ncache to store $full_rev^ in $meta{'parent'}, which means storing\nit in $metainfo{$full_rev}{'parent'}.\n\nNow if the commit we saved this info about appears again in git-blame\noutput, be it in group of lines for which the same commit is blamed,\nor later in unrelated chunk, we use saved info.\n\nLet me try to explain it using the following diagram:\n\n  Commit N Line Original code      This patch\n  ------------------------------------------------------\n  3a4046 1 xxx  rev-parse 3a4046^  rev-parse 3a4046^\n         2 xxx  rev-parse 3a4046^  $mi{3a4046}{'parent'}\n         3 xxx  rev-parse 3a4046^  $mi{3a4046}{'parent'}\n  f47c19 5 xxx  rev-parse f47c19^  rev-parse f47c19^\n         6 xxx  rev-parse f47c19^  $mi{f47c19}{'parent'}\n  3a4046 7 xxx  rev-parse 3a4046^  $mi{3a4046}{'parent'}  <--\n         8 xxx  rev-parse 3a4046^  $mi{3a4046}{'parent'}\n\nwhere \"rev-parse 3a4046^\" means call to git-rev-parse, and $mi{<rev>}\naccessing $metainfo{$full_rev} (via $meta).\n \nIn place marked by arrow '<--' you don't need to calculate 3a4046^\nagain...\n\n> > But I have checked that (at least for single example file)\n> > the blame output is identical for before and after this patch.\n> \n> I've always tested it on something like \"gitweb.perl\", etc.\n\nI've checked it on blob.h.  Other good example is README (with boundary\ncommits) and GIT-VERSION-GEN (with different output between git-blame\n--porcelain and --incremental), both of which take much less time than\ngitweb/gitweb.perl (see benchmarks in other post).\n\n-- \nJakub Narebski\nPoland\n"},{"id":"97522","messageId":"755680.28599.qm@web31804.mail.mud.yahoo.com","threadId":"16659","inReplyTo":"200812102203.30486.jnareb@gmail.com","subject":"Re: [PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()","fromName":"Luben Tuikov","fromEmail":"ltuikov@yahoo.com","sentAt":"2008-12-10T21:15:02Z","receivedAt":"2008-12-10T21:15:02Z","isPatch":true,"sender":{"key":"ltuikov@yahoo.com","avatar":null},"body":"\n--- On Wed, 12/10/08, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> You don't need $old_full_rev. We have to save data\n> about commit in\n> %metainfo hash because information about commit appears in \n\nOh, yeah, the hash... Then, it looks correct.\n\nAcked-by: Luben Tuikov <ltuikov@yahoo.com>\n"},{"id":"97554","messageId":"200812110133.33124.jnareb@gmail.com","threadId":"16659","inReplyTo":"7v7i67zsgj.fsf@gitster.siamese.dyndns.org","subject":"[PATCH 2/3 (edit v2)] gitweb: Cache $parent_commit info in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-11T00:33:29Z","receivedAt":"2008-12-11T00:33:29Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Luben Tuikov changed 'lineno' link from leading to commit which gave\ncurrent version of given block of lines, to leading to parent of this\ncommit in 244a70e (Blame \"linenr\" link jumps to previous state at\n\"orig_lineno\").  This made possible data mining using 'blame' view.\n\nThe current implementation calls rev-parse once per each blamed line\nto find parent revision of blamed commit, even when the same commit\nappears more than once, which is inefficient.\n\nThis patch attempts to mitigate this issue by storing (caching)\n$parent_commit info in %metainfo, which makes gitweb call\ngit-rev-parse only once per each unique commit in blame output.\n\n\nIn the tables below you can see simple benchmark comparing gitweb\nperformance before and after this patch\n\nFile               | L[1] | C[2] || Time0[3] | Before[4] | After[4]\n====================================================================\nblob.h             |   18 |    4 || 0m1.727s |  0m2.545s |  0m2.474s\nGIT-VERSION-GEN    |   42 |   13 || 0m2.165s |  0m2.448s |  0m2.071s\nREADME             |   46 |    6 || 0m1.593s |  0m2.727s |  0m2.242s\nrevision.c         | 1923 |  121 || 0m2.357s | 0m30.365s |  0m7.028s\ngitweb/gitweb.perl | 6291 |  428 || 0m8.080s | 1m37.244s | 0m20.627s\n\nFile               | L/C  | Before/After\n=========================================\nblob.h             |  4.5 |         1.03\nGIT-VERSION-GEN    |  3.2 |         1.18\nREADME             |  7.7 |         1.22\nrevision.c         | 15.9 |         4.32\ngitweb/gitweb.perl | 14.7 |         4.71\n\nAs you can see the greater ratio of lines in file to unique commits\nin blame output, the greater gain from the new implementation.\n\nFootnotes:\n~~~~~~~~~~\n[1] Lines:\n    $ wc -l <file>\n[2] Individual commits in blame output:\n    $ git blame -p <file> | grep author-time | wc -l\n[3] Time for running \"git blame -p\" (user time, single run):\n    $ time git blame -p <file> >/dev/null\n[4] Time to run gitweb as Perl script from command line:\n    $ gitweb-run.sh \"p=.git;a=blame;f=<file>\" > /dev/null 2>&1\n\nThe gitweb-run.sh script includes slightly modified (with adjusted\npathnames) code from gitweb_run() function from the test script\nt/t9500-gitweb-standalone-no-errors.sh; gitweb config file\ngitweb_config.perl contents (again up to adjusting pathnames; in\nparticular $projectroot variable should point to top directory of git\nrepository) can be found in the same place.\n\n\nAlternate solutions:\n~~~~~~~~~~~~~~~~~~~~\nAlternate solution would be to open bidi pipe to \"git cat-file\n--batch-check\", (like in Git::Repo in gitweb caching by Lea Wiemann),\nfeed $long_rev^ to it, and parse its output which has the following\nform:\n\n  926b07e694599d86cec668475071b32147c95034 commit 637\n\nThis would mean one call to git-cat-file for the whole 'blame' view,\ninstead of one call to git-rev-parse per each unique commit in blame\noutput.\n\n\nYet another solution would be to change use of validate_refname() to\nvalidate_revision() when checking script parameters (CGI query or\npath_info), with validate_revision being something like the following:\n\n  sub validate_revision {\n        my $rev = shift;\n        return validate_refname(strip_rev_suffixes($rev));\n  }\n\nso we don't need to calculate $long_rev^, but can pass \"$long_rev^\" as\n'hb' parameter.\n\nThis solution has the advantage that it can be easily adapted to\nfuture incremental blame output.\n\nAcked-by: Luben Tuikov <ltuikov@yahoo.com>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nOn Wed, 10 Dec 2008, Junio C Hamano wrote:\n\n> To recap, I think the commit log for this patch would have been much\n> easier to read if it were presented in this order:\n> \n>         a paragraph to establish the context;\n> \n>         a paragraph to state what problem it tries to solve;\n> \n>         a paragraph (or more) to explain the solution; and finally\n> \n>         a paragraph to discuss possible future enhancements.\n\nLike this?\n\nOnly commit message has changed.\n\n gitweb/gitweb.perl |   16 +++++++++++-----\n 1 files changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 1b800f4..916396a 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4657,11 +4657,17 @@ HTML\n \t\t\t              esc_html($rev));\n \t\t\tprint \"</td>\\n\";\n \t\t}\n-\t\topen (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n-\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n-\t\tmy $parent_commit = <$dd>;\n-\t\tclose $dd;\n-\t\tchomp($parent_commit);\n+\t\tmy $parent_commit;\n+\t\tif (!exists $meta->{'parent'}) {\n+\t\t\topen (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n+\t\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n+\t\t\t$parent_commit = <$dd>;\n+\t\t\tclose $dd;\n+\t\t\tchomp($parent_commit);\n+\t\t\t$meta->{'parent'} = $parent_commit;\n+\t\t} else {\n+\t\t\t$parent_commit = $meta->{'parent'};\n+\t\t}\n \t\tmy $blamed = href(action => 'blame',\n \t\t                  file_name => $meta->{'filename'},\n \t\t                  hash_base => $parent_commit);\n-- \n1.6.0.4\n"},{"id":"97558","messageId":"7v3agvy1v3.fsf@gitster.siamese.dyndns.org","threadId":"16659","inReplyTo":"20081210200908.16899.36727.stgit@localhost.localdomain","subject":"Re: [RFC/PATCH 4/3] gitweb: Incremental blame (proof of concept)","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2008-12-11T00:47:12Z","receivedAt":"2008-12-11T00:47:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> writes:\n\n> NOTE: This patch is RFC proof of concept patch!: it should be split\n> onto many smaller patches for easy review (and bug finding) in version\n> meant to be applied.\n\nHmm, the comments an RFC requests for would certainly be based on reviews\nof the patch in question, so if the patch is known to be unsuitable for\nreviewing, what would that tell us, I wonder ;-)?\n\nAmong the 700 lines added/deleted, 400 lines are from a single new file,\nso what may benefit from splitting would be the changes to gitweb.perl but\nit does not look so bad (I haven't really read the patch, though).\n\n> Differences between 'blame' and 'blame_incremental' output:\n\nHmm, are these by design in the sense that \"when people are getting\nincremental blame output, the normal blame output format is unsuitable for\nsuch and such reasons and that is why there have to be these differences\",\nor \"the code happens to produce slightly different results because it is\nimplemented differently; the differences are listed here as due\ndiligence\"?\n\n> P.P.S. What is the stance for copyrigth assesments in the files\n> for git code, like the ones in gitweb/gitweb.perl and gitweb/blame.js?\n\nThere is no copyright assignment.  Everybody retains the own copyright on\ntheir own work.\n\n> P.P.P.S. Should I use Signed-off-by from Pasky and Fredrik if I based\n> my code on theirs, and if they all signed their patches?\n\nI think that is in line with what Certificate of Origin asks you to do.\n"},{"id":"97564","messageId":"200812110222.04663.jnareb@gmail.com","threadId":"16659","inReplyTo":"7v3agvy1v3.fsf@gitster.siamese.dyndns.org","subject":"Re: [RFC/PATCH 4/3] gitweb: Incremental blame (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-11T01:22:03Z","receivedAt":"2008-12-11T01:22:03Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 11 Dec 2008, Junio C Hamano wrote:\n> Jakub Narebski <jnareb@gmail.com> writes:\n> \n> > NOTE: This patch is RFC proof of concept patch!: it should be split\n> > onto many smaller patches for easy review (and bug finding) in version\n> > meant to be applied.\n> \n> Hmm, the comments an RFC requests for would certainly be based on reviews\n> of the patch in question, so if the patch is known to be unsuitable for\n> reviewing, what would that tell us, I wonder ;-)?\n\nWell, you can apply patch and test how it works, for example if\nJavaScript code works in other browsers that have JavaScript and DOM\nsupport (Firefox, IE, Opera, Safari, Google Chrome)... Or what\nfeatures or what interface one would like to have...\n\n> Among the 700 lines added/deleted, 400 lines are from a single new file,\n> so what may benefit from splitting would be the changes to gitweb.perl but\n> it does not look so bad (I haven't really read the patch, though).\n\nThere are a few features which could be split in separate commits:\n * there are a few improvements to gitweb.css, independent of \n   incremental blame view, like td.warning -> .warning\n * adding to gitweb writing how long it took to generate page should\n   be made into separate commit, probably made optional, use better\n   HTML style, and have some fallback if there is no Time::HiRes\n\n * progress report could be made into separate commit; I needed it to\n   debug code, to check if it progress nicely, but it is not strictly\n   required (but it is nice to have visual indicator of progress)\n * 3-coloring of blamed lines during adding blame info was added for\n   the fun of it, and should probably be in separate commit\n * adding author initials a'la \"git gui blame\" while nice could also\n   be put in separate commit, probably adding this feature also to\n   ordinary 'blame' output\n\n[...] \n> > Differences between 'blame' and 'blame_incremental' output:\n> \n> Hmm, are these by design in the sense that \"when people are getting\n> incremental blame output, the normal blame output format is unsuitable for\n> such and such reasons and that is why there have to be these differences\",\n> or \"the code happens to produce slightly different results because it is\n> implemented differently; the differences are listed here as due\n> diligence\"?\n\nActually it is both. Some of differences are _currently_ not possible\nto resolve (parent commit 'lineno' links, split group of lines blamed\nby the same commit), some are coded differently (title attribute for\nsha1, rowspan=\"1\", author initials feature), and some are impossible\nin incremental blame at least during generation (zebra table) or does\nnot make sense in 'blame' view (progress indicators).\n\n> > P.P.S. What is the stance for copyrigth assesments in the files\n> > for git code, like the ones in gitweb/gitweb.perl and gitweb/blame.js?\n> \n> There is no copyright assignment.  Everybody retains the own copyright on\n> their own work.\n\nErrr... I'm sorry, I haven't made myself clear. I wanted to ask what\nis the best practices about copyright statement lines like\n\n  // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n\nand other results of \"git grep Copyright\": should it be added for\ninitial author, for main authors... I guess not for all authors.\n\n> > P.P.P.S. Should I use Signed-off-by from Pasky and Fredrik if I based\n> > my code on theirs, and if they all signed their patches?\n> \n> I think that is in line with what Certificate of Origin asks you to do.\n \nI was bit confused because Petr Baudis in his patch used Cc: and not\nSigned-off-by: to Fredrik Kuivinen...\n-- \nJakub Narebski\nPoland\n"},{"id":"97572","messageId":"506479.53667.qm@web31812.mail.mud.yahoo.com","threadId":"16659","inReplyTo":"200812110133.33124.jnareb@gmail.com","subject":"Re: [PATCH 2/3 (edit v2)] gitweb: Cache $parent_commit info in git_blame()","fromName":"Luben Tuikov","fromEmail":"ltuikov@yahoo.com","sentAt":"2008-12-11T04:08:25Z","receivedAt":"2008-12-11T04:08:25Z","isPatch":true,"sender":{"key":"ltuikov@yahoo.com","avatar":null},"body":"\n--- On Wed, 12/10/08, Jakub Narebski <jnareb@gmail.com> wrote:\n> Acked-by: Luben Tuikov <ltuikov@yahoo.com>\n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\n\nI've always seen \"Acked-by:\" follows \"Signed-off-by:\".  Junio, has this\nchanged?\n\n   Luben\n"},{"id":"97573","messageId":"7v8wqnwdiz.fsf@gitster.siamese.dyndns.org","threadId":"16659","inReplyTo":"506479.53667.qm@web31812.mail.mud.yahoo.com","subject":"Re: [PATCH 2/3 (edit v2)] gitweb: Cache $parent_commit info in git_blame()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2008-12-11T04:18:12Z","receivedAt":"2008-12-11T04:18:12Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Luben Tuikov <ltuikov@yahoo.com> writes:\n\n> --- On Wed, 12/10/08, Jakub Narebski <jnareb@gmail.com> wrote:\n>> Acked-by: Luben Tuikov <ltuikov@yahoo.com>\n>> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\n>\n> I've always seen \"Acked-by:\" follows \"Signed-off-by:\".  Junio, has this\n> changed?\n\nI think the order is supposed to show the order of things happened.  Jakub\nsigns off the patch, you Ack, and I see the patch and append my sign-off.\n\nYou saw the exact same patch text, said that looked Ok to you, and Jakub\nupdated the log message to present the change better and signed off the\nwhole thing again.  You could say that there should be another, original,\nsign off by Jakub before your Ack, but I do not think it adds anything of\nvalue.\n\nIn any case, the change will be queued to 'pu'.  It is great that it is a\ntrivial change that gives us great performance boost, and I wish all our\npatches are like that ;-).\n"},{"id":"97606","messageId":"m3r64ehba7.fsf@localhost.localdomain","threadId":"16659","inReplyTo":"20081210200908.16899.36727.stgit@localhost.localdomain","subject":"Re: [RFC/PATCH 4/3] gitweb: Incremental blame (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-11T17:28:09Z","receivedAt":"2008-12-11T17:28:09Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> writes:\n\n> This is tweaked up version of Petr Baudis <pasky@suse.cz> patch, which\n> in turn was tweaked up version of Fredrik Kuivinen <frekui@gmail.com>'s\n> proof of concept patch.  It adds 'blame_incremental' view, which\n> incrementally displays line data in blame view using JavaScript (AJAX).\n\n[...]\n> Patch by Petr Baudis this one is based on:\n>   http://permalink.gmane.org/gmane.comp.version-control.git/56657\n> \n> Original patch by Fredrik Kuivinen:\n>   http://article.gmane.org/gmane.comp.version-control.git/41361\n> \n> Snippet adding 'generated in' to gitweb, by Petr Baudis:\n>   http://article.gmane.org/gmane.comp.version-control.git/83306\n> \n> Should I post interdiff to Petr Baudis patch, and comments about\n> difference between them? [...]\n\nHere is the list of differences between Petr Baudis patch and the one\nI have just send.  No interdiff, as it is artificially large because\nprevious patch was based on much older version, so ranges does not\nmatch.\n\nBugs I have made:\n * I forgot to make some changes for git-instaweb.sh to have support\n   for incremental blame, namely dependency of 'git-instaweb' target\n   in Makefile on gitweb/blame.js, and lack of the following line in\n   git-instaweb.sh:\n        gitweb_blamejs $GIT_DIR/gitweb/blame.js\n\n * Pasky's patch added support for href(...,-partial_query=>1) extra\n   parameter, which ensured that gitweb link had '?' in it, and used\n   it to generate 'baseUrl' parameter for startBlame.  I have\n   misunderstood what baseUrl is about, and used $my_url there, while\n   it is partial URL for blame links: it is projectUrl.\n\n   Therefore links in blame table current 'blame_incremental' would\n   not work. I'm sorry about that, I thought I have checked it...\n\nIntentionally omitted features:\n * In patch this one is based on there was fixBlameLinks() JavaScript\n   function (put directly in the HTML head inside <script> element),\n   which was used in body.onLoad event to change 'a=blame' to\n   'a=blame_incremental' in links marked with class=\"blamelink\".\n\n   First, this IMHO should be put as separate patch; you can test\n   'blame_incremental' view by hand-crafting gitweb URL.  Second, it\n   would be not enough in current gitweb, as action can be put in\n   path_info. So either fixBlameLinks() should be made work in both\n   cases, or it should be done in different way, for example adding\n   'js=1' for all links, or doing JavaScript redirect from 'blame'\n   view (although this way we won't be able to view ordinary 'blame'\n   view without turning off JavaScript).\n\nDifferences in coding of the same features:\n * In Pasky's patch git_blame (then named git_blame2) and\n   git_blame_incremental were just wrappers around git_blame_common;\n   in this patch git_blame_data is also wrapper (to avoid duplicating\n   permissions and parameter checking code).\n\n * Instead of calculating title string for \"Commit\" column cell, and\n   necessary data for each row, we now calculate it once per commit\n   and save (cache) in 'commits' associative array (hash).\n\n   This is a bit of performance improvement, similar to the one in \n     \"[PATCH 2/3] gitweb: Cache $parent_commit info in git_blame()\"\n   for 'blame' view in gitweb. It is just using Date() and string\n   concatenation, and not extra fork.\n\n * Variables holding manipulated elements are named a bit differently,\n   and calculated upfront:\n      td_sha1   instead of  tr.firstChild\n      a_sha1    instead of  ahsAnchor\n      a_linenr  instead of  lineAnchor\n\n * There were a few of style changes in gitweb/blame.js; for example\n   it is used the following style of function definition\n\n      function functionName(arg1, arg2) {\n\n   thorough the code.\n\nFixes for bugs in Pasky's patch, and changes related to changes in\nordinary 'blame' output: \n * handleResponse function was called only from XMLHttpRequest\n   onreadystatechange event. Not all browsers call onreadystatechange\n   each time the server flushes output (Firefox does that), so we use\n   a timer to check (poll) for changes.\n\n   See http://ajaxpatterns.org/HTTP_Streaming#Refactoring_Illustration\n\n * The 'linenr' link was to line number commit.srcline, while it\n   should be to (commit.srcline + i), as commit.srcline is _start_\n   line in group, and not the line equivalent to current line in\n   source.\n\n   This might be thought a (mis)feature, and not a bug, though.\n\n * Currently 'blame' view uses single cell (with rowspan=\"N\") spanning\n   the whole group of lines blaming the same commit, instead of using\n   empty cells for subsequent lines in group.  Therefore instead of\n   setting \"shaAnchor.innerHTML = '';\" to make subsequent cells in\n   \"Commit\" ('sha1') column empty (or rather to make link element <a>\n   in cell empty), we use \"tr.removeChild(td_sha1);\"\n\n   This change was made necessary by changes in the 'blame' view.\n   This also meant that the code checking if lines are in the same\n   group has to be changes; it was refactored into startOfGroup(tr)\n   function.\n\n * The title for cells in \"Commit\" column used UTC (GMT) date and time\n      'Kay Sievers, 2005-08-07 19:49:46'\n   instead of the localtime format used currently by 'blame' view:\n      'Kay Sievers, 2005-08-07 21:49:46 +0200'\n   Current code uses neither, but 'commit'-like format:\n      'Kay Sievers, 2005-08-07 19:49:46 +0000 (21:49:46 +0200)'\n\nNew features (in short):\n * 3-coloring of lines with blame data during incremental blaming\n * Adding author initials below shortened SHA-1 of a commit\n   (if there is place for it, i.e. if group has more than 1 row)\n * progress indicator: progress info and progress bar\n * information about how long it took to run 'blame_data',\n   and how long it took to run JavaScript script\n\n-- \nJakub Narebski\nPoland\nShadeHawk on #git\n"},{"id":"97630","messageId":"m3fxkugx5m.fsf@localhost.localdomain","threadId":"16659","inReplyTo":"m3r64ehba7.fsf@localhost.localdomain","subject":"Re: [RFC/PATCH 4/3] gitweb: Incremental blame (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-11T22:34:06Z","receivedAt":"2008-12-11T22:34:06Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> writes:\n\n> New features (in short):\n>  * 3-coloring of lines with blame data during incremental blaming\n>  * Adding author initials below shortened SHA-1 of a commit\n>    (if there is place for it, i.e. if group has more than 1 row)\n>  * progress indicator: progress info and progress bar\n>  * information about how long it took to run 'blame_data',\n>    and how long it took to run JavaScript script\n\n * handling server ('blame_data') errors, i.e. status != 200.\n   (I needed that to debug blame.js when I entered URL incorrectly).\n\n-- \nJakub Narebski\nPoland\nShadeHawk on #git\n"},{"id":"97678","messageId":"7vr64e9jq6.fsf@gitster.siamese.dyndns.org","threadId":"16659","inReplyTo":"200812110133.33124.jnareb@gmail.com","subject":"Re: [PATCH 2/3 (edit v2)] gitweb: Cache $parent_commit info in git_blame()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2008-12-12T03:05:05Z","receivedAt":"2008-12-12T03:05:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> writes:\n\n> ...\n> Alternate solutions:\n> ~~~~~~~~~~~~~~~~~~~~\n> ...\n> Acked-by: Luben Tuikov <ltuikov@yahoo.com>\n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\n> ---\n> On Wed, 10 Dec 2008, Junio C Hamano wrote:\n>\n>> To recap, I think the commit log for this patch would have been much\n>> easier to read if it were presented in this order:\n>> \n>>         a paragraph to establish the context;\n>> \n>>         a paragraph to state what problem it tries to solve;\n>> \n>>         a paragraph (or more) to explain the solution; and finally\n>> \n>>         a paragraph to discuss possible future enhancements.\n>\n> Like this?\n\nYes, basically.\n\nThe \"future possibilities\" section might be a bit too heavy, and also\ncalling it \"Alternate solutions\" makes it slightly unclear if it is\ntalking about what is implemented, or only talking about idle speculation\nwithout an actual code (in this case, it is the latter), though.\n\n> Only commit message has changed.\n\nWhich is a bit unnice, because it will conflict with the original [3/3]\nthat I queued already (with a pair of fixes, including but not limited to\nthe one you sent \"Oops, it should have been like this\" for).\n\nI can hand wiggle the patch to make it apply, but I'd prefer if I did not\nhave to do this every time I receive a patch.\n\nI think the conflict was trivial (just a single s/rev/short_rev/) and I\ndid not make a silly mistake when I fixed it up, but please check the\nresult on 'pu' after I push the results out.\n\nThanks.\n"},{"id":"97713","messageId":"200812121820.34695.jnareb@gmail.com","threadId":"16659","inReplyTo":"7vr64e9jq6.fsf@gitster.siamese.dyndns.org","subject":"Re: [PATCH 2/3 (edit v2)] gitweb: Cache $parent_commit info in git_blame()","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-12T17:20:33Z","receivedAt":"2008-12-12T17:20:33Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Fri, 12 Dec 2008, Junio C Hamano wrote:\n> Jakub Narebski <jnareb@gmail.com> writes:\n\n> > Only commit message has changed.\n> \n> Which is a bit unnice, because it will conflict with the original [3/3]\n> that I queued already (with a pair of fixes, including but not limited to\n> the one you sent \"Oops, it should have been like this\" for).\n> \n> I can hand wiggle the patch to make it apply, but I'd prefer if I did not\n> have to do this every time I receive a patch.\n\nI'm sorry about that; I have forgot to change order of patches to have\noriginal 1/3, 3/3, 2/3 (I should have used 'stg float' for that).\n\n> I think the conflict was trivial (just a single s/rev/short_rev/) and I\n> did not make a silly mistake when I fixed it up, but please check the\n> result on 'pu' after I push the results out.\n\nI did the reordering, and gitweb on compared top of reordered stack\nof patches with gitweb from top of 'pu' branch, and the only\ndifference in the area touched by git_blame improvements series is\none comment I have added in v2 of 3/3.\n\nThank you for your work.\n-- \nJakub Narebski\nPoland\n"},{"id":"97830","messageId":"1229213850-12787-1-git-send-email-jnareb@gmail.com","threadId":"16659","inReplyTo":"20081210200908.16899.36727.stgit@localhost.localdomain","subject":"[RFC/PATCH v2] gitweb: Incremental blame (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-14T00:17:30Z","receivedAt":"2008-12-14T00:17:30Z","isPatch":true,"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\nThe original patch by Fredrik Kuivinen has been lightly tested in a\ncouple of browsers (Firefox, Mozilla, Konqueror, Galeon, Opera and IE6).\nThe next patch by Petr Baudis has been tested in Firefox and Epiphany.\nThis patch has been lightly tested in Mozilla 1.17.2 and Konqueror\n3.5.3.\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\nThis code uses http://ajaxpatterns.org/HTTP_Streaming pattern.\n\nIt deals with streamed 'blame_data' server error by notifying about them\nin the progress info area (just in case).\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\nWhile at it, this patch uniquifies td.dark and td.dark2 style: they\ndiffer only in that td.dark2 doesn't have style for :hover.\n\n\nThis patch also adds showing time (in seconds) it took to generate\na page in page footer (based on example code by Pasky), even though\nit is independent change, to be able to evaluate incremental blame in\ngitweb better.  In proper patch series it would be independent commit;\nand it probably would provide fallback if Time::HiRes is not available\n(by e.g. not showing generating time info), even though this is\nunlikely.\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---\nDifferences from previous version of patch:\n * Fixed copying git-instaweb related changes from Petr Baudis patch;\n   git-instaweb should not work with 'blame_incremental' view\n * Fixed links in blame table in 'blame_incremental' view, and add\n   support for href(..., -partial_query=>1)\n * The title attribute for \"Commit\" column (\"sha1\" cells) agrees with\n   the one used for 'blame' view, meaning using localtime e.g. \n     'Kay Sievers, 2005-08-07 21:49:46 +0200'\n   There was a bug in Pasky and Frederik patch here...\n * Incremental blame data generation can lead to neighbour groups\n   blaming the same commit; such groups are now concatenated when\n   fixing colors to zebra pattern. This mean that 'blame_incremental'\n   output should match 'blame' view.\n * New feature adding author initials below shortened sha1 of commit\n   was dropped; it was not present in 'blame' view, and it would make\n   fixing of line grouping mentioned in previous point much more\n   difficult.\n\n * Use more robust createRequestObject(), using try ... catch,\n   taken from AJAX article at WikiPedia.\n * Better error handling: show big warning with link to 'blame'\n   view if scripts are disabled, show error if XMLHttpRequest object\n   cannot be started, show statusText on server returning status != 200\n * use deleteCell (DOM HTML) rather than removeChild (DOM Core)\n   to delete cell(s) below cell spanning multiple rows\n * Set 'commits' to empty associative array to mark memory to be freed\n * A few improvements to findColorNo and its helper functions\n * Remember about Internet Explorer quirk when setting class attr\n * Added many comments, changed names of few variables to be more\n   readable, rename few functions, split off functions, etc.\n * A bit of style cleanup: always use block with if, in continued\n   (broken) lines put operator at the end of line, use literal object\n   notation \"{}\" to initialize empty associative array rather than\n   cryptic \"new Object()\", use === and !=== instead of == and !=,\n   always use radix parameter to parseInt (i.e. parseInt(str, 10))  \n   All those changes were recommended by JSLint.\n\n Makefile           |    6 +-\n git-instaweb.sh    |    7 +\n gitweb/blame.js    |  470 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.css  |   27 +++-\n gitweb/gitweb.perl |  263 ++++++++++++++++++++---------\n 5 files changed, 683 insertions(+), 90 deletions(-)\n create mode 100644 gitweb/blame.js\n\ndiff --git a/Makefile b/Makefile\nindex 5158197..d2d3fff 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -218,6 +218,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@@ -1209,13 +1210,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@@ -1224,6 +1226,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 0843372..510789f 100755\n--- a/git-instaweb.sh\n+++ b/git-instaweb.sh\n@@ -268,8 +268,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..6288bd1\n--- /dev/null\n+++ b/gitweb/blame.js\n@@ -0,0 +1,470 @@\n+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n+\n+var DEBUG = 0;\n+function debug(str) {\n+\tif (DEBUG) {\n+\t\talert(str);\n+\t}\n+}\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+\tdebug(\"XMLHttpRequest not supported\");\n+\treturn null;\n+}\n+\n+var http; // XMLHttpRequest object\n+var projectUrl; // partial query\n+\n+// 'commits' is an associative map. It maps SHA1s to Commit objects.\n+var commits = {};\n+\n+function Commit(sha1) {\n+\tthis.sha1 = sha1;\n+}\n+\n+// convert month or day of the month to string, padding it with\n+// '0' (zero) to two characters width if necessary, e.g. 2 -> '02'\n+function zeroPad(n) {\n+\tif (n < 10) {\n+\t\treturn '0' + n;\n+\t} else {\n+\t\treturn n.toString();\n+\t}\n+}\n+\n+// pad number N with nonbreakable spaces on the right, to WIDTH characters\n+// example: spacePad(12, 3) == '&nbsp;12' ('&nbsp;' is nonbreakable space)\n+function spacePad(n,width) {\n+\tvar scale = 1;\n+\tvar str = '';\n+\n+\twhile (width > 1) {\n+\t\tscale *= 10;\n+\t\tif (n < scale) {\n+\t\t\tstr += '&nbsp;';\n+\t\t}\n+\t\twidth--;\n+\t}\n+\treturn str + n;\n+}\n+\n+var blamedLines = 0;\n+var totalLines  = '???';\n+var div_progress_bar;\n+var div_progress_info;\n+\n+// how many lines does a file have, used in progress info\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+// update progress info and length (width) of progress bar\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.innerHTML  = blamedLines + ' / ' + totalLines +\n+\t\t\t' ('+spacePad(percentage,3)+'%)';\n+\t}\n+\n+\tif (div_progress_bar) {\n+\t\tdiv_progress_bar.setAttribute('style', 'width: '+percentage+'%;');\n+\t}\n+}\n+\n+// used to extract N from colorN, where N is a number,\n+var colorRe = new RegExp('color([0-9]*)');\n+\n+// return N if <tr class=\"colorN\">, otherwise return null\n+// (some browsers require CSS class names to begin with letter)\n+function getColorNo(tr) {\n+\tif (!tr) {\n+\t\treturn null;\n+\t}\n+\tvar className = tr.getAttribute('class');\n+\tif (className) {\n+\t\tmatch = 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+// return one of given possible colors\n+// example: chooseColorNoFrom(2, 3) might be 2 or 3\n+function chooseColorNoFrom() {\n+\t// simplest version\n+\treturn arguments[0];\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+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+\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+\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+\treturn (3 - ((color_prev + color_next) % 3));\n+}\n+\n+// used to extract hours and minutes from timezone info, e.g '-0900'\n+var tzRe = new RegExp('^([+-][0-9][0-9])([0-9][0-9])$');\n+\n+// called for each blame entry, as soon as it finishes\n+function handleLine(commit) {\n+\t/* \n+\t   This is the structure of the HTML fragment we are working\n+\t   with:\n+\t   \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\tvar localDate = new Date(); // date corrected by timezone\n+\t\tvar match = tzRe.exec(commit.authorTimezone);\n+\t\tlocalDate.setTime(1000 * (commit.authorTime +\n+\t\t\t(parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));\n+\t\tvar localDateStr = // e.g. '2005-08-07'\n+\t\t\tlocalDate.getUTCFullYear()         + '-' +\n+\t\t\tzeroPad(localDate.getUTCMonth()+1) + '-' +\n+\t\t\tzeroPad(localDate.getUTCDate());\n+\t\tvar localTimeStr = // e.g. '21:49:46'\n+\t\t\tzeroPad(localDate.getUTCHours())   + ':' +\n+\t\t\tzeroPad(localDate.getUTCMinutes()) + ':' +\n+\t\t\tzeroPad(localDate.getUTCSeconds());\n+\n+\t\t/* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */\n+\t\tcommit.info = commit.author + ', ' + localDateStr + ' ' +\n+\t\t\tlocalTimeStr + ' ' + 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+\n+\tfor (var i = 0; i < commit.numlines; i++) {\n+\t\tvar tr = document.getElementById('l'+resline);\n+\t\tif (!tr) {\n+\t\t\tdebug('tr is null! resline: ' + resline);\n+\t\t\tbreak;\n+\t\t}\n+\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\tif (colorNo !== null) {\n+\t\t\ttr.setAttribute('class', 'color'+colorNo);\n+\t\t\t// Internet Explorer needs this\n+\t\t\ttr.setAttribute('className', 'color'+colorNo);\n+\t\t}\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.setAttribute('rowspan', commit.numlines);\n+\n+\t\t\ta_sha1.href = projectUrl + ';a=commit;h=' + commit.sha1;\n+\t\t\ta_sha1.innerHTML = commit.sha1.substr(0, 8);\n+\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\ta_linenr.href = projectUrl + ';a=blame;hb=' + commit.sha1 +\n+\t\t\t';f=' + commit.filename + '#l' + (commit.srcline + i);\n+\n+\t\tresline++;\n+\t\tblamedLines++;\n+\n+\t\t//updateProgressInfo();\n+\t}\n+}\n+\n+function startOfGroup(tr) {\n+\treturn tr.firstChild.getAttribute('class') == 'sha1';\n+}\n+\n+function fixColorsAndGroups() {\n+\tvar colorClasses = ['light2', 'dark2'];\n+\tvar linenum = 1;\n+\tvar tr, prev_group;\n+\tvar colorClass = 0;\n+\n+\twhile ((tr = document.getElementById('l'+linenum))) {\n+\t\tif (startOfGroup(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 rows = prev_group.firstChild.getAttribute('rowspan');\n+\t\t\t\t// assume that we have rowspan even for rowspan=\"1\"\n+\t\t\t\tprev_group.firstChild.setAttribute('rowspan',\n+\t\t\t\t\t(rows + tr.firstChild.getAttribute('rowspan')));\n+\t\t\t\ttr.removeChild(tr.firstChild);\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\ttr.setAttribute('class', colorClasses[colorClass]);\n+\t\t// Internet Explorer needs this\n+\t\ttr.setAttribute('className', colorClasses[colorClass]);\n+\t\tlinenum++;\n+\t}\n+}\n+\n+var t_interval_server = '';\n+var t0 = new Date();\n+\n+// write how much it took to generate data, and to run script\n+function writeTimeInterval() {\n+\tvar info = document.getElementById('generate_time');\n+\tif (!info) {\n+\t\treturn;\n+\t}\n+\tvar t1 = new Date();\n+\n+\tinfo.innerHTML += ' + ' +\n+\t\tt_interval_server+'s server (blame_data) / ' +\n+\t\t(t1.getTime() - t0.getTime())/1000 + 's client (JavaScript)';\n+}\n+\n+// ----------------------------------------------------------------------\n+\n+var prevDataLength = -1;\n+var nextLine = 0;\n+var inProgress = false;\n+\n+var sha1Re = new RegExp('([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)');\n+var infoRe = new RegExp('([a-z-]+) ?(.*)');\n+var endRe = new RegExp('END ?(.*)');\n+var curCommit = new Commit();\n+\n+var pollTimer = null;\n+\n+function handleResponse() {\n+\tdebug('handleResp ready: ' + http.readyState +\n+\t      ' respText null?: ' + (http.responseText === null) +\n+\t      ' progress: ' + inProgress);\n+\n+\tif (http.readyState != 4 && http.readyState != 3) {\n+\t\treturn;\n+\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\tif (div_progress_info) {\n+\t\t\tdiv_progress_info.setAttribute('class', 'error');\n+\t\t\t// Internet Explorer needs this\n+\t\t\tdiv_progress_info.setAttribute('className', 'error');\n+\t\t\tdiv_progress_info.innerHTML = 'Server error: ' +\n+\t\t\t\thttp.status+' - '+(http.statusText || 'Error contacting server');\n+\t\t}\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+\t\t\t} else if ((match = infoRe.exec(lines[i]))) {\n+\t\t\t\tvar info = match[1];\n+\t\t\t\tvar data = match[2];\n+\t\t\t\tif (info == 'filename') {\n+\t\t\t\t\tcurCommit.filename = data;\n+\t\t\t\t\thandleLine(curCommit);\n+\t\t\t\t\tupdateProgressInfo();\n+\t\t\t\t} else if (info == 'author') {\n+\t\t\t\t\tcurCommit.author = data;\n+\t\t\t\t} else if (info == 'author-time') {\n+\t\t\t\t\tcurCommit.authorTime = parseInt(data,10);\n+\t\t\t\t} else if (info == 'author-tz') {\n+\t\t\t\t\tcurCommit.authorTimezone = data;\n+\t\t\t\t}\n+\t\t\t} else if ((match = endRe.exec(lines[i]))) {\n+\t\t\t\tt_interval_server = match[1];\n+\t\t\t\tdebug('END: '+lines[i]);\n+\t\t\t} else if (lines[i] !== '') {\n+\t\t\t\tdebug('malformed line: ' + lines[i]);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\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+\tFunction: startBlame\n+\n+\tIncrementally update line data in blame_incremental view in gitweb.\n+\n+\tParameters:\n+\n+\t\tblamedataUrl - URL to server script generating blame data.\n+\t\tbUrl -partial URL to project, used to generate links in blame.\n+\n+\tComments:\n+\n+\tCalled from 'blame_incremental' view after loading table with\n+\tfile contents, a base for blame view.\n+*/\n+function startBlame(blamedataUrl, bUrl) {\n+\tdebug('startBlame('+blamedataUrl+', '+bUrl+')');\n+\n+\thttp = createRequestObject();\n+\tif (!http) {\n+\t\tdiv_progress_info = document.getElementById('progress_info');\n+\n+\t\tif (div_progress_info) {\n+\t\t\tdiv_progress_info.setAttribute('class', 'error');\n+\t\t\t// Internet Explorer needs this\n+\t\t\tdiv_progress_info.setAttribute('className', 'error');\n+\t\t\tdiv_progress_info.innerHTML = 'XMLHttpRequest not supported';\n+\t\t}\n+\n+\t\treturn;\n+\t}\n+\n+\tt0 = new Date();\n+\tprojectUrl = bUrl;\n+\ttotalLines = countLines();\n+\tupdateProgressInfo();\n+\n+\thttp.open('get', blamedataUrl);\n+\thttp.onreadystatechange = handleResponse;\n+\thttp.send(null);\n+\n+\t// not all browsers call onreadystatechange event on each server flush\n+\tpollTimer = setInterval(handleResponse, 2000);\n+}\n+\n+// end of blame.js\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex a01eac8..e359618 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -223,11 +223,7 @@ tr.light:hover {\n \tbackground-color: #edece6;\n }\n \n-tr.dark {\n-\tbackground-color: #f6f6f0;\n-}\n-\n-tr.dark2 {\n+tr.dark, tr.dark2 {\n \tbackground-color: #f6f6f0;\n }\n \n@@ -235,6 +231,14 @@ tr.dark:hover {\n \tbackground-color: #edece6;\n }\n \n+tr.color1:hover { background-color: #e6ede6; }\n+tr.color2:hover { background-color: #e6e6ed; }\n+tr.color3:hover { background-color: #ede6e6; }\n+\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@@ -255,7 +259,7 @@ td.sha1 {\n \tfont-family: monospace;\n }\n \n-td.error {\n+.error {\n \tcolor: red;\n \tbackground-color: yellow;\n }\n@@ -326,6 +330,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 4987fdc..93a4e82 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -18,6 +18,9 @@ use File::Find qw();\n use File::Basename qw(basename);\n binmode STDOUT, ':utf8';\n \n+use Time::HiRes qw(gettimeofday tv_interval);\n+our $t0 = [gettimeofday];\n+\n BEGIN {\n \tCGI->compile() if $ENV{'MOD_PERL'};\n }\n@@ -74,6 +77,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@@ -493,6 +498,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@@ -919,7 +926,8 @@ sub href (%) {\n \t\t\t}\n \t\t}\n \t}\n-\t$href .= \"?\" . join(';', @result) if scalar @result;\n+\t$href .= \"?\" . join(';', @result)\n+\t\tif ($params{-partial_query} or scalar @result);\n \n \treturn $href;\n }\n@@ -1272,7 +1280,7 @@ use constant {\n };\n \n # submodule/subproject, a commit object reference\n-sub S_ISGITLINK($) {\n+sub S_ISGITLINK {\n \tmy $mode = shift;\n \n \treturn (($mode & S_IFMT) == S_IFGITLINK)\n@@ -1558,7 +1566,7 @@ sub format_diff_from_to_header {\n \t# no extra formatting for \"^--- /dev/null\"\n \tif (! $diffinfo->{'nparents'}) {\n \t\t# ordinary (single parent) diff\n-\t\tif ($line =~ m!^--- \"?a/!) {\n+\t\tif ($line =~ m!^--- \"?a/!) {#\"\n \t\t\tif ($from->{'href'}) {\n \t\t\t\t$line = '--- a/' .\n \t\t\t\t        $cgi->a({-href=>$from->{'href'}, -class=>\"path\"},\n@@ -1816,7 +1824,7 @@ sub git_cmd {\n # Try to avoid using this function wherever possible.\n sub quote_command {\n \treturn join(' ',\n-\t\t    map( { my $a = $_; $a =~ s/(['!])/'\\\\$1'/g; \"'$a'\" } @_ ));\n+\t\t    map( { my $a = $_; $a =~ s/(['!])/'\\\\$1'/g; \"'$a'\" } @_ ));#'\n }\n \n # get HEAD ref of given project as hash\n@@ -2874,13 +2882,13 @@ sub git_header_html {\n \t# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.\n \t# we have to do this because MSIE sometimes globs '*/*', pretending to\n \t# support xhtml+xml but choking when it gets what it asked for.\n-\tif (defined $cgi->http('HTTP_ACCEPT') &&\n-\t    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\\s|^)application\\/xhtml\\+xml(,|;|\\s|$)/ &&\n-\t    $cgi->Accept('application/xhtml+xml') != 0) {\n-\t\t$content_type = 'application/xhtml+xml';\n-\t} else {\n+\t#if (defined $cgi->http('HTTP_ACCEPT') &&\n+\t#    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\\s|^)application\\/xhtml\\+xml(,|;|\\s|$)/ &&\n+\t#    $cgi->Accept('application/xhtml+xml') != 0) {\n+\t#\t$content_type = 'application/xhtml+xml';\n+\t#} else {\n \t\t$content_type = 'text/html';\n-\t}\n+\t#}\n \tprint $cgi->header(-type=>$content_type, -charset => 'utf-8',\n \t                   -status=> $status, -expires => $expires);\n \tmy $mod_perl_version = $ENV{'MOD_PERL'} ? \" $ENV{'MOD_PERL'}\" : '';\n@@ -3042,6 +3050,14 @@ sub git_footer_html {\n \t}\n \tprint \"</div>\\n\"; # class=\"page_footer\"\n \n+\tprint \"<div class=\\\"page_footer\\\">\\n\";\n+\tprint 'This page took '.\n+\t      '<span id=\"generate_time\" class=\"time_span\">'.\n+\t      tv_interval($t0, [gettimeofday]).'s'.\n+\t      '</span>'.\n+\t      \" to generate.\\n\";\n+\tprint \"</div>\\n\"; # class=\"page_footer\"\n+\n \tif (-f $site_footer) {\n \t\tinsert_file($site_footer);\n \t}\n@@ -3803,7 +3819,7 @@ sub git_patchset_body {\n \twhile ($patch_line) {\n \n \t\t# parse \"git diff\" header line\n-\t\tif ($patch_line =~ m/^diff --git (\\\"(?:[^\\\\\\\"]*(?:\\\\.[^\\\\\\\"]*)*)\\\"|[^ \"]*) (.*)$/) {\n+\t\tif ($patch_line =~ m/^diff --git (\\\"(?:[^\\\\\\\"]*(?:\\\\.[^\\\\\\\"]*)*)\\\"|[^ \"]*) (.*)$/) {#\"\n \t\t\t# $1 is from_name, which we do not use\n \t\t\t$to_name = unquote($2);\n \t\t\t$to_name =~ s!^b/!!;\n@@ -4574,7 +4590,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@@ -4596,10 +4614,36 @@ 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+\t\tprint \"END \".tv_interval($t0, [gettimeofday]).\"\\n\";\n+\n+\t\treturn;\n+\t}\n \n \t# page header\n \tgit_header_html();\n@@ -4610,93 +4654,146 @@ 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\\nUse \".\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(light2 dark2);\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} = {};\n-\t\t}\n-\t\tmy $meta = $metainfo{$full_rev};\n-\t\tmy $data; # last line is used later\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;\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\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$rev_color[$current_color]\\\">\\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\tprint \"</td>\\n\";\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-\t\tmy $parent_commit;\n-\t\tif (!exists $meta->{'parent'}) {\n-\t\t\topen (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n-\t\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n-\t\t\t$parent_commit = <$dd>;\n-\t\t\tclose $dd;\n-\t\t\tchomp($parent_commit);\n-\t\t\t$meta->{'parent'} = $parent_commit;\n-\t\t} else {\n-\t\t\t$parent_commit = $meta->{'parent'};\n-\t\t}\n-\t\tmy $blamed = href(action => 'blame',\n-\t\t                  file_name => $meta->{'filename'},\n-\t\t                  hash_base => $parent_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}\n-\tprint \"</table>\\n\";\n-\tprint \"</div>\";\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\t($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n+\t\t\t$metainfo{$full_rev} ||= {};\n+\t\t\tmy $meta = $metainfo{$full_rev};\n+\t\t\tmy $data; # last line is used later\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;\n+\t\t\t\t}\n+\t\t\t}\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\tprint qq!<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\\n!;\n+\t\t\tif ($group_size) {\n+\t\t\t\tprint qq!<td class=\"sha1\"!.\n+\t\t\t\t      qq! title=\"!. esc_html($author) . qq!, $date\"!;\n+\t\t\t\tprint qq! rowspan=\"$group_size\"! if ($group_size > 1);\n+\t\t\t\tprint qq!>!;\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\tprint qq!</td>\\n!;\n+\t\t\t}\n+\t\t\tif (!exists $meta->{'parent'}) {\n+\t\t\t\topen my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\"\n+\t\t\t\t\tor die_error(500, \"Open git-rev-parse failed\");\n+\t\t\t\t$meta->{'parent'} = <$dd>;\n+\t\t\t\tclose $dd;\n+\t\t\t\tchomp $meta->{'parent'};\n+\t\t\t}\n+\t\t\tmy $blamed = href(action => 'blame',\n+\t\t\t                  file_name => $meta->{'filename'},\n+\t\t\t                  hash_base => $meta->{'parent'});\n+\t\t\tprint qq!<td class=\"linenr\">!.\n+\t\t\t       $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n+\t\t\t                -class => \"linenr\" },\n+\t\t\t               esc_html($lineno)).\n+\t\t\t      qq!</td>!;\n+\t\t\tprint qq!<td class=\"pre\">! . esc_html($data) . \"</td>\\n\";\n+\t\t\tprint qq!</tr>\\n!;\n+\t\t}\n+\t}\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(-partial_query=>1) .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.0.4\n"},{"id":"97883","messageId":"m34p16eny3.fsf_-_@localhost.localdomain","threadId":"16659","inReplyTo":"1229213850-12787-1-git-send-email-jnareb@gmail.com","subject":"[RFC] gitweb: Incremental blame - suggestions for improvements","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2008-12-14T16:11:54Z","receivedAt":"2008-12-14T16:11:54Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"A few suggestions for further improvement of blame_incremental:\n * Better support for data mining in 'blame_incremental' view,\n   (for \"lineno\" links to (approximately) lead to previous version of\n   line) would probably require for gitweb to accept \"<rev>^\" for 'hb'\n   parameter (and perhaps resolve/parse it before saving to\n   $hash_base). This would also help performance of 'blame' view.\n\n * Move some of processing to server, to 'blame_data' action, and for\n   example send whole saved and processed info for a group of lines as\n   JSON or as variation of \"git blame --incremental\" output.\n\n * Better error checking: not only check if scripting is turned on,\n   but also if required methods like document.getElementById are\n   available. Probably would require falback to *ugh* document.write.\n\n * Perhaps fallback on (slower) DOM methods if innerHTML is not\n   available or doesn't work, which is the case for some versions of\n   some browsers in strict XHTML (application/xml+html + XHTML DTD)\n   mode.\n\n * Fallback on DOM Core methods of deleting cell if DOM HTML\n   deleteCell method is not available; check that DOM Core way does\n   not lead to memory leaks (by deleting element, but not its\n   children).\n\n * Use 'one second spotlight' or other similar user interface patter\n   to signal in more visible way than reaching 100% in progress info,\n   and changing colors from 3-coloring to 2-color zebra in blame table\n   that all blame data was received and entered.\n\n * Check in handleResponse if browser calls it on [each] server flush,\n   by checking if it is called more than once with http.readyState == 3\n   and with different http.responseText, and disable poll timer then.\n\n * Mark boundary commits (this applies both to 'blame' and\n   'blame_incremental' views), using UNDOCUMENTED \"boundary\" header.\n\n * A toy. Make progress bar indicator more smooth by interpolating\n   changes between updates, so it moves smoothly. This would make it\n   provide impression for user, not an accurate measure of blamed\n   percentage.\n\n * A toy. Make sure that 3-coloring during updating blame table use\n   all three colors with similar frequency, for example by using the\n   following implementation of chooseColorNoFrom function:\n\n    // return one of given possible colors\n    // example: chooseColorNoFrom(2, 3) might be 2 or 3\n    var colorsFreq = [0, 0, 0];\n    // assumes that  1 <= arguments[i] <= colorsFreq.length\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\nDo you have your own suggestions?  Comments?\nWould incremental blame help git-instaweb use?\n-- \nJakub Narebski\nPoland\nShadeHawk on #git\n"},{"id":"98105","messageId":"20081217081300.GB3640@machine.or.cz","threadId":"16659","inReplyTo":"20081209224330.28106.18301.stgit@localhost.localdomain","subject":"Re: [PATCH 1/3] gitweb: Move 'lineno' id from link to row element in git_blame","fromName":"Petr Baudis","fromEmail":"pasky@suse.cz","sentAt":"2008-12-17T08:13:00Z","receivedAt":"2008-12-17T08:13:00Z","isPatch":true,"sender":{"key":"pasky@ucw.cz","avatar":"https://avatars.githubusercontent.com/u/18439?v=4"},"body":"On Tue, Dec 09, 2008 at 11:46:16PM +0100, Jakub Narebski wrote:\n> Move l<line number> ID from <a> link element inside table row (inside\n> cell element for column with line numbers), to encompassing <tr> table\n> row element.  It was done to make it easier to manipulate result HTML\n> with DOM, and to be able write 'blame_incremental' view with the same,\n> or nearly the same result.\n> \n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\n\nAcked-by: Petr Baudis <pasky@suse.cz>\n"},{"id":"98106","messageId":"20081217081935.GC3640@machine.or.cz","threadId":"16659","inReplyTo":"200812110133.33124.jnareb@gmail.com","subject":"Re: [PATCH 2/3 (edit v2)] gitweb: Cache $parent_commit info in git_blame()","fromName":"Petr Baudis","fromEmail":"pasky@suse.cz","sentAt":"2008-12-17T08:19:35Z","receivedAt":"2008-12-17T08:19:35Z","isPatch":true,"sender":{"key":"pasky@ucw.cz","avatar":"https://avatars.githubusercontent.com/u/18439?v=4"},"body":"On Thu, Dec 11, 2008 at 01:33:29AM +0100, Jakub Narebski wrote:\n> Luben Tuikov changed 'lineno' link from leading to commit which gave\n> current version of given block of lines, to leading to parent of this\n> commit in 244a70e (Blame \"linenr\" link jumps to previous state at\n> \"orig_lineno\").  This made possible data mining using 'blame' view.\n> \n> The current implementation calls rev-parse once per each blamed line\n> to find parent revision of blamed commit, even when the same commit\n> appears more than once, which is inefficient.\n> \n> This patch attempts to mitigate this issue by storing (caching)\n> $parent_commit info in %metainfo, which makes gitweb call\n> git-rev-parse only once per each unique commit in blame output.\n> \n> \n> In the tables below you can see simple benchmark comparing gitweb\n> performance before and after this patch\n> \n> File               | L[1] | C[2] || Time0[3] | Before[4] | After[4]\n> ====================================================================\n> blob.h             |   18 |    4 || 0m1.727s |  0m2.545s |  0m2.474s\n> GIT-VERSION-GEN    |   42 |   13 || 0m2.165s |  0m2.448s |  0m2.071s\n> README             |   46 |    6 || 0m1.593s |  0m2.727s |  0m2.242s\n> revision.c         | 1923 |  121 || 0m2.357s | 0m30.365s |  0m7.028s\n> gitweb/gitweb.perl | 6291 |  428 || 0m8.080s | 1m37.244s | 0m20.627s\n> \n> File               | L/C  | Before/After\n> =========================================\n> blob.h             |  4.5 |         1.03\n> GIT-VERSION-GEN    |  3.2 |         1.18\n> README             |  7.7 |         1.22\n> revision.c         | 15.9 |         4.32\n> gitweb/gitweb.perl | 14.7 |         4.71\n> \n> As you can see the greater ratio of lines in file to unique commits\n> in blame output, the greater gain from the new implementation.\n> \n> Footnotes:\n> ~~~~~~~~~~\n> [1] Lines:\n>     $ wc -l <file>\n> [2] Individual commits in blame output:\n>     $ git blame -p <file> | grep author-time | wc -l\n> [3] Time for running \"git blame -p\" (user time, single run):\n>     $ time git blame -p <file> >/dev/null\n> [4] Time to run gitweb as Perl script from command line:\n>     $ gitweb-run.sh \"p=.git;a=blame;f=<file>\" > /dev/null 2>&1\n> \n> The gitweb-run.sh script includes slightly modified (with adjusted\n> pathnames) code from gitweb_run() function from the test script\n> t/t9500-gitweb-standalone-no-errors.sh; gitweb config file\n> gitweb_config.perl contents (again up to adjusting pathnames; in\n> particular $projectroot variable should point to top directory of git\n> repository) can be found in the same place.\n> \n> \n> Alternate solutions:\n> ~~~~~~~~~~~~~~~~~~~~\n> Alternate solution would be to open bidi pipe to \"git cat-file\n> --batch-check\", (like in Git::Repo in gitweb caching by Lea Wiemann),\n> feed $long_rev^ to it, and parse its output which has the following\n> form:\n> \n>   926b07e694599d86cec668475071b32147c95034 commit 637\n> \n> This would mean one call to git-cat-file for the whole 'blame' view,\n> instead of one call to git-rev-parse per each unique commit in blame\n> output.\n> \n> \n> Yet another solution would be to change use of validate_refname() to\n> validate_revision() when checking script parameters (CGI query or\n> path_info), with validate_revision being something like the following:\n> \n>   sub validate_revision {\n>         my $rev = shift;\n>         return validate_refname(strip_rev_suffixes($rev));\n>   }\n> \n> so we don't need to calculate $long_rev^, but can pass \"$long_rev^\" as\n> 'hb' parameter.\n> \n> This solution has the advantage that it can be easily adapted to\n> future incremental blame output.\n> \n> Acked-by: Luben Tuikov <ltuikov@yahoo.com>\n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\n\nAcked-by: Petr Baudis <pasky@suse.cz>\n\n(though I think the commit message is total overkill for such an obvious\nchange ;-)\n"},{"id":"98111","messageId":"7v63ljp5cg.fsf@gitster.siamese.dyndns.org","threadId":"16659","inReplyTo":"20081217081935.GC3640@machine.or.cz","subject":"Re: [PATCH 2/3 (edit v2)] gitweb: Cache $parent_commit info in git_blame()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2008-12-17T08:34:55Z","receivedAt":"2008-12-17T08:34:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Petr Baudis <pasky@suse.cz> writes:\n\n> (though I think the commit message is total overkill for such an obvious\n> change ;-)\n\nI think so too; the performance figures are nice but there is no need to\ntalk about what is not implemented.\n"}]}