{"thread":{"id":"20084","subject":"[PATCH 0/3] gitweb: 'blame' view improvements","startedAt":"2009-07-10T21:54:42Z","lastAt":"2009-07-14T19:21:29Z","messageCount":11,"participants":["Jakub Narebski","Junio C Hamano","Luben Tuikov"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"117800","messageId":"200907102354.43232.jnareb@gmail.com","threadId":"20084","inReplyTo":null,"subject":"[PATCH 0/3] gitweb: 'blame' view improvements","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-10T21:54:42Z","receivedAt":"2009-07-10T21:54:42Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This patch series was inspired by the study before sending proposal\nfor git-blame improvements; I have noticed that \"previous\"/\"parent\"\nblame header was already implemented, just not documented:\n  Subject: git-blame: Documenting \"previous\" header\n  Message-ID: <200907071423.40938.jnareb@gmail.com>\n  http://thread.gmane.org/gmane.comp.version-control.git/122837\n\nTherefore proposal got cut down to tree blame:\n  Subject: [RFC] Tree blame (git blame <directory>)\n  Message-ID: <200907071058.39390.jnareb@gmail.com>\n  http://thread.gmane.org/gmane.comp.version-control.git/122830\n\n\nUsing \"previous\" header should improve gitweb performance and (as\nI have noticed during implementing it) allow to follow 'linenr'\nlinks correctly through rename in a blamed commit.\n\nMarking \"boundary\" somewhat was required to make it possible to\ndistinguish which 'linenr' links would lead to parent of a blame\ncommit (to previous version of a file), and which would elad to blamed\ncommit.  Current styling is up to debate.\n\nAfter implementing using \"previous\" header in 'blame' view (which\nwould allow to do the same for proposed 'blame_incremental' view)\nI remembered that last version of implementing 'blame_incremental'\nadded author initials a'la \"git gui blame\".  So I did the same for\nnon-incremental 'blame' view.  Exact formatting is up to debate.\n\nTable of contents:\n==================\n [PATCH 1/3] gitweb: Mark boundary commits in 'blame' view\n [PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view\n [PATCH 3/3] gitweb: Add author initials in 'blame' view, a la \"git gui blame\"\n\nJakub Narebski (3):\n  gitweb: Mark boundary commits in 'blame' view\n  gitweb: Use \"previous\" header of git-blame -p in 'blame' view\n  gitweb: Add author initials in 'blame' view, a la \"git gui blame\"\n\n\n gitweb/gitweb.css  |    4 ++++\n gitweb/gitweb.perl |   51 ++++++++++++++++++++++++++++++++++++---------------\n 2 files changed, 40 insertions(+), 15 deletions(-)\n\n-- \nJakub Narebski\nPoland\n"},{"id":"117801","messageId":"200907102355.44364.jnareb@gmail.com","threadId":"20084","inReplyTo":"200907102354.43232.jnareb@gmail.com","subject":"[PATCH 1/3] gitweb: Mark boundary commits in 'blame' view","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-10T21:55:44Z","receivedAt":"2009-07-10T21:55:44Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Use \"boundary\" class to mark boundary commits, which currently results\nin using bold weight font for SHA-1 of a commit (to be more exact for\nall text in the cell that contains SHA-1 of a commit).\n\nDetecting boundary commits is done by watching for \"boundary\" header\nin \"git blame -p\" output.  Because this header doesn't carry\nadditional data the regular expression for blame header fields had to\nbe adjusted.\n\nWith current gitweb API only root (parentless) commits can be boundary\ncommits.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nFormatting (styling) of boundary commits is currently very minimal.\nI'd like to see what other possible solution would you like to have.\n\nBikeshedding open!\n\n gitweb/gitweb.css  |    4 ++++\n gitweb/gitweb.perl |    6 ++++--\n 2 files changed, 8 insertions(+), 2 deletions(-)\n\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex d05bc37..5e2f629 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -242,6 +242,10 @@ tr.dark:hover {\n \tbackground-color: #edece6;\n }\n \n+tr.boundary td.sha1 {\n+\tfont-weight: bold;\n+}\n+\n td {\n \tpadding: 2px 5px;\n \tfont-size: 100%;\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 6a1b5b5..fe73c2c 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4826,7 +4826,7 @@ HTML\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\tif ($data =~ /^(\\S+)(?: (.*))?$/) {\n \t\t\t\t$meta->{$1} = $2;\n \t\t\t}\n \t\t}\n@@ -4838,7 +4838,9 @@ HTML\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\tmy $tr_class = $rev_color[$current_color];\n+\t\t$tr_class .= ' boundary' if (exists $meta->{'boundary'});\n+\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$tr_class\\\">\\n\";\n \t\tif ($group_size) {\n \t\t\tprint \"<td class=\\\"sha1\\\"\";\n \t\t\tprint \" title=\\\"\". esc_html($author) . \", $date\\\"\";\n-- \n1.6.3.3\n"},{"id":"117803","messageId":"200907102357.43475.jnareb@gmail.com","threadId":"20084","inReplyTo":"200907102354.43232.jnareb@gmail.com","subject":"[PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-10T21:57:42Z","receivedAt":"2009-07-10T21:57:42Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Luben Tuikov changed 'lineno' link (line number link) from pointing to\n'blame' view at given line at blamed commit, to the one at parent of\nblamed commit in\n  244a70e (Blame \"linenr\" link jumps to previous state at\n           \"orig_lineno\", 2007-01-04).\nThis made it possible to do data mining using 'blame' view, by going\nthrough history of a line using mentioned line number link.\n\nOriginal implementation called \"git rev-parse <commit>^\" to find SHA-1\nof a parent of a given commit once per each blamed line.  In\n  39c19ce (gitweb: cache $parent_commit info in git_blame(),\n           2008-12-11)\nthis was improved so rev-parse was called once per each unique commit\nin git-blame output.  Alternate solution would be to relax validation\nfor 'hb' parameter by allowing extended SHA-1 syntax of the form\n<rev>^ (perhaps redirecting to gitweb URL with <rev>^ resolved, in\npractice moving call to rev-parse to 'the other side of link').\n\nThis solution had a bug that it didn't work for boundary commits,\nwhich did not have parents, so \"git rev-parse <commit>^\" returned\nliteral \"<commit>^\" (which didn't exists), which gitweb passed\nas 'hb' parameter in 'linenr' link... following which gave\n  400 - Invalid hash base parameter\nerror.  This bug could have been fixed by checking if commit is\nboundary commit, or check if rev-parse result is unchanged (still\nends in '^' prefix).\n\nThe solution employing rev-parse to find parent of commit had inherent\nproblem if blamed commit renamed file; then name of file would be\ndifferent in its parent.  Solving this outside git-blame would be\ndifficult and costly (at least cost of additional fork for extra git\ncommand).\n\n\nCurrently gitweb uses information in \"previous\" header, which was\nintroduced by Junio C Hamano in\n  96e1170 (blame: show \"previous\" information in\n           --porcelain/--incremental format, 2008-06-04)\nThis (currently undocumented) header has the following format:\n  \"previous <sha1 of parent commit> <filename at parent>\"\nUsing \"previous\" header solves both problem of performance and the\nproblem that blamed commit could have renaming blamed file.\n\nBecause \"previous\" header can be repeated for the same commit when\nblamed commit is merge (has more than one parent), and we are\ninterested usually in _first_ parent, currently we store only first\nvalue if blame header repeats.  Using first parent (first \"previous\"\nline) was what gitweb did before; without this change gitweb would use\nlast parent instead.\n\nWhile at it introduce helper subroutine unquote_maybe(), which\nunquotes filename if it is needed, which is marked by filename being\nsurrounded in doublequotes (which are not part of name, and which are\nstripped by unquote_maybe() - which makes this function idempotent).\nCurrently unquote_maybe() us used only in git_blame.\n\nIf there is no previous commit 'linenr' link points to blamed commit\nand blamed filename, making it work correctly for boundary commits.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\nIMHO more important is that result is MORE CORRECT, not the better\nperformance.\n\nIn the table below you can see simple benchmark comparing gitweb\nperformance before and after this patch.  Operating system used was\nLinux 2.6.14, on 1 GHz AMD Athlon processor (2002.43 BogoMIPS).\nAs there is one \"git rev-parse <rev>^\" per each individual commit\nin blame output, it would be much worse 'before' for operating\nsystems with costly fork.\n\nFile                  |  C[1] || Time0[2] | Before[3] | After[3]\n=================================================================\nrevision.c            |   121 ||   2.820s |    5.548s |   5.172s\ngitweb/gitweb.perl[4] |   428 ||  11.749s |   19.797s |  17.293s\n\nTable footnotes:\n~~~~~~~~~~~~~~~~\n[1] Individual commits in blame output:\n    $ git blame -p <file> | grep author-time | wc -l\n[2] Time for running \"git blame -p\" (user time, single run):\n    $ time git blame -p <file> >/dev/null\n[3] Time to run gitweb as Perl script from command line:\n    $ time gitweb-run.sh \"p=.git;a=blame;f=<file>\" >/dev/null 2>&1\n[4] Starting at 'origin', which is v1.6.3.3-412-gf581de1\n    > git blame -p origin -- gitweb/gitweb.perl\n    > \"p=.git;a=blame;hb=origin;f=gitweb/gitweb.perl\"\n\n\nFor comparison there is similar table for my 39c19ce (gitweb: cache\n$parent_commit info in git_blame(), 2008-12-11):\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\n  Legend:\n\n  [1] Number of lines:\n      $ wc -l <file>\n  [2] Number of unique commits in the 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\n gitweb/gitweb.perl |   37 ++++++++++++++++++++++++-------------\n 1 files changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex fe73c2c..36b1ce5 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -1187,6 +1187,16 @@ sub unquote {\n \treturn $str;\n }\n \n+# if filename is surrounded in double quotes, it need to be unquoted\n+sub unquote_maybe {\n+\tmy $str = shift;\n+\n+\tif ($str =~ /^\"(.*)\"$/) {\n+\t\treturn unquote($1);\n+\t}\n+\treturn $str;\n+}\n+\n # escape tabs (convert tabs to spaces)\n sub untabify {\n \tmy $line = shift;\n@@ -4827,7 +4837,7 @@ HTML\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\t$meta->{$1} = $2 unless exists $meta->{$1};\n \t\t\t}\n \t\t}\n \t\tmy $short_rev = substr($full_rev, 0, 8);\n@@ -4852,20 +4862,21 @@ HTML\n \t\t\t              esc_html($short_rev));\n \t\t\tprint \"</td>\\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\t# 'previous' <sha1 of parent commit> <filename at commit>\n+\t\tif (exists $meta->{'previous'} &&\n+\t\t    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n+\t\t\t$meta->{'parent'} = $1;\n+\t\t\t$meta->{'file_parent'} = unquote_maybe($2);\n+\t\t}\n+\t\tmy $linenr_commit =\n+\t\t\texists($meta->{'parent'}) ?\n+\t\t\t$meta->{'parent'} : $full_rev;\n+\t\tmy $linenr_filename =\n+\t\t\texists($meta->{'file_parent'}) ?\n+\t\t\t$meta->{'file_parent'} : unquote_maybe($meta->{'filename'});\n \t\tmy $blamed = href(action => 'blame',\n-\t\t                  file_name => $meta->{'filename'},\n-\t\t                  hash_base => $parent_commit);\n+\t\t                  file_name => $linenr_filename,\n+\t\t                  hash_base => $linenr_commit);\n \t\tprint \"<td class=\\\"linenr\\\">\";\n \t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n \t\t                -class => \"linenr\" },\n-- \n1.6.3.3\n"},{"id":"117802","messageId":"200907110001.24093.jnareb@gmail.com","threadId":"20084","inReplyTo":"200907102354.43232.jnareb@gmail.com","subject":"[PATCH 3/3] gitweb: Add author initials in 'blame' view, a la \"git gui blame\"","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-10T22:01:23Z","receivedAt":"2009-07-10T22:01:23Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"For example for \"Junio C Hamano\" initials would be \"JH\".  Of course\ninitials are added (below shortened SHA-1 of blamed commit) only if\ngroup of lines has 2 lines or more in it.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nLet the bikeshedding begin!\n\nShould it be \"JH\" or perhaps \"J.H.\" for \"Junio C Hamano\"?  Or perhaps\nusername part of author email would be better solution than initials?\nShould we use different style for those initials?\n\nThis patch was inspired by me adding the same feature in similar \n'blame_incremental' view in\n  http://thread.gmane.org/gmane.comp.version-control.git/102657/focus=102712\n\n gitweb/gitweb.perl |    8 ++++++++\n 1 files changed, 8 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 36b1ce5..5336c92 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4860,6 +4860,14 @@ HTML\n \t\t\t                             hash=>$full_rev,\n \t\t\t                             file_name=>$file_name)},\n \t\t\t              esc_html($short_rev));\n+\t\t\tif ($group_size >= 2) {\n+\t\t\t\tmy @author_initials = ($author =~ /\\b([[:upper:]])\\B/g);\n+\t\t\t\tif (@author_initials) {\n+\t\t\t\t\tprint \"<br />\" .\n+\t\t\t\t\t      esc_html(join('', @author_initials));\n+\t\t\t\t\t#           or join('.', ...)\n+\t\t\t\t}\n+\t\t\t}\n \t\t\tprint \"</td>\\n\";\n \t\t}\n \t\t# 'previous' <sha1 of parent commit> <filename at commit>\n-- \n1.6.3.3\n"},{"id":"117805","messageId":"7v4otkuqlq.fsf@alter.siamese.dyndns.org","threadId":"20084","inReplyTo":"200907102357.43475.jnareb@gmail.com","subject":"Re: [PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2009-07-10T22:21:05Z","receivedAt":"2009-07-10T22:21: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> Luben Tuikov changed 'lineno' link (line number link) from pointing to\n> 'blame' view at given line at blamed commit, to the one at parent of\n> blamed commit in\n>   244a70e (Blame \"linenr\" link jumps to previous state at\n>            \"orig_lineno\", 2007-01-04).\n> This made it possible to do data mining using 'blame' view, by going\n> through history of a line using mentioned line number link.\n\nI was playing with this feature the other day (and I think you can guess\nwhat I was writing when I was doing so as preparation).  I was mildly\nannoyed that these links on the commit object names go to the commit view.\n\nI think going to commitdiff view would make it far more useful while\ndigging.\n\nSuppose if you were somehow interested in the recent commit by Peff,\n\"Makefile: install 'git' in execdir\".  You go to:\n\n    http://repo.or.cz/w/alt-git.git\n\nand look at commitdiff of the commit from the shortlog part.\n\nYou read the diff, understand what the changed Makefile does, but you get\ncurious to see the blame.  Nicely, the commitdiff view has a list of the\nfiles changed, and each entry in the list has \"blame\" link.\n\nClicking it would give you the blame on each line from the Makefile.\n\nSo far, very smooth experience.  Then you scroll to an area of the file\nyou are interested in, and click on one of the commits.\n\nOops.\n\nIt does not show the change of the commit made by this one, even though it\ndoes list Makefile in the list of files changed, and it has a blame link,\nthe commit view without diff disrupts the thought process I had in the\nprevious blame page, and I have to go to commitdiff to reorient myself.\n"},{"id":"117819","messageId":"200907111117.22206.jnareb@gmail.com","threadId":"20084","inReplyTo":"7v4otkuqlq.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-11T09:17:20Z","receivedAt":"2009-07-11T09:17:20Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Sat, 11 July 2009, Junio C Hamano wrote:\n> Jakub Narebski <jnareb@gmail.com> writes:\n> \n> > Luben Tuikov changed 'lineno' link (line number link) from pointing to\n> > 'blame' view at given line at blamed commit, to the one at parent of\n> > blamed commit in\n> >   244a70e (Blame \"linenr\" link jumps to previous state at\n> >            \"orig_lineno\", 2007-01-04).\n> > This made it possible to do data mining using 'blame' view, by going\n> > through history of a line using mentioned line number link.\n> \n> I was playing with this feature the other day (and I think you can guess\n> what I was writing when I was doing so as preparation).  I was mildly\n> annoyed that these links on the commit object names go to the commit view.\n> \n> I think going to commitdiff view would make it far more useful while\n> digging.\n[...]\n\nAlso with a slight extension of diff part of 'commitdiff', by adding\nline numbers for preimage in chunk and line numbers for postimage in\nchunk, like e.g. in 'commit' view on GitHub[1] or on Gitorious[2] or\nin SVN::Web[3] or in Atlassian FishEye, and adding anchors for those\nline numbers we could go to given line in dif/patch output and examine\nhow it looked like before.\n\n [1] http://github.com/jnareb/softsnow-xchat2-filter/commit/7b68fcd777f94534f0b794c5dc2e109c49938395\n [2] http://gitorious.org/softsnow-xchat2-filter/mainline/commit/7b68fcd777f94534f0b794c5dc2e109c49938395\n [3] http://jc.ngo.org.uk/svnweb/jc/diff/nik/CPAN/SVN-Web/trunk/README?revs=1283&revs=981\n\n(This would probably require adding 'fmt' / 'format' parameter, where\none could choose between possible ways to view diff, or possible ways\nto view log or log-like view.)\n\n\nP.S. BTW. 'blame' view passes _original_ filename (not filename at\nblamed commit) to 'commit' view link... but it doesn't matter because\n'commit' view does not use 'f'/$file_name parameter.\n-- \nJakub Narebski\nPoland\n"},{"id":"117828","messageId":"200907111856.54621.jnareb@gmail.com","threadId":"20084","inReplyTo":"200907102354.43232.jnareb@gmail.com","subject":"Re: [PATCH 0/3] gitweb: 'blame' view improvements","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-11T16:56:52Z","receivedAt":"2009-07-11T16:56:52Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Fri, 10 July 2009, Jakub Narebski wrote:\n\n> Table of contents:\n> ==================\n>  [PATCH 1/3] gitweb: Mark boundary commits in 'blame' view\n>  [PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view\n>  [PATCH 3/3] gitweb: Add author initials in 'blame' view, a la \"git gui blame\"\n>\n\nBy the way I plan on submitting 'blame_incremental' view similar to\nthe one in \"[RFC/PATCH 4/3] gitweb: Incremental blame (proof of concept)\"\n\n  http://thread.gmane.org/gmane.comp.version-control.git/102657/focus=102712\n\nThis time having the same (or nearly the same) features as ordinary\n'blame' view.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"117868","messageId":"533104.39947.qm@web31807.mail.mud.yahoo.com","threadId":"20084","inReplyTo":"200907102357.43475.jnareb@gmail.com","subject":"Re: [PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view","fromName":"Luben Tuikov","fromEmail":"ltuikov@yahoo.com","sentAt":"2009-07-12T17:21:45Z","receivedAt":"2009-07-12T17:21:45Z","isPatch":true,"sender":{"key":"ltuikov@yahoo.com","avatar":null},"body":"Acked-by: Luben Tuikov <ltuikov@yahoo.com>\n\nJunio, can you also verify that this patch doesn't alter\nbehaviour in code data mining, before accepting it.\n\nThanks,\n     Luben\n\n________________________________\nFrom: Jakub Narebski <jnareb@gmail.com>\nTo: git@vger.kernel.org\nCc: Luben Tuikov <ltuikov@yahoo.com>; Junio C Hamano <gitster@pobox.com>\nSent: Friday, July 10, 2009 2:57:42 PM\nSubject: [PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view\n\nLuben Tuikov changed 'lineno' link (line number link) from pointing to\n'blame' view at given line at blamed commit, to the one at parent of\nblamed commit in\n  244a70e (Blame \"linenr\" link jumps to previous state at\n           \"orig_lineno\", 2007-01-04).\nThis made it possible to do data mining using 'blame' view, by going\nthrough history of a line using mentioned line number link.\n\nOriginal implementation called \"git rev-parse <commit>^\" to find SHA-1\nof a parent of a given commit once per each blamed line.  In\n  39c19ce (gitweb: cache $parent_commit info in git_blame(),\n           2008-12-11)\nthis was improved so rev-parse was called once per each unique commit\nin git-blame output.  Alternate solution would be to relax validation\nfor 'hb' parameter by allowing extended SHA-1 syntax of the form\n<rev>^ (perhaps redirecting to gitweb URL with <rev>^ resolved, in\npractice moving call to rev-parse to 'the other side of link').\n\nThis solution had a bug that it didn't work for boundary commits,\nwhich did not have parents, so \"git rev-parse <commit>^\" returned\nliteral \"<commit>^\" (which didn't exists), which gitweb passed\nas 'hb' parameter in 'linenr' link... following which gave\n  400 - Invalid hash base parameter\nerror.  This bug could have been fixed by checking if commit is\nboundary commit, or check if rev-parse result is unchanged (still\nends in '^' prefix).\n\nThe solution employing rev-parse to find parent of commit had inherent\nproblem if blamed commit renamed file; then name of file would be\ndifferent in its parent.  Solving this outside git-blame would be\ndifficult and costly (at least cost of additional fork for extra git\ncommand).\n\n\nCurrently gitweb uses information in \"previous\" header, which was\nintroduced by Junio C Hamano in\n  96e1170 (blame: show \"previous\" information in\n           --porcelain/--incremental format, 2008-06-04)\nThis (currently undocumented) header has the following format:\n  \"previous <sha1 of parent commit> <filename at parent>\"\nUsing \"previous\" header solves both problem of performance and the\nproblem that blamed commit could have renaming blamed file.\n\nBecause \"previous\" header can be repeated for the same commit when\nblamed commit is merge (has more than one parent), and we are\ninterested usually in _first_ parent, currently we store only first\nvalue if blame header repeats.  Using first parent (first \"previous\"\nline) was what gitweb did before; without this change gitweb would use\nlast parent instead.\n\nWhile at it introduce helper subroutine unquote_maybe(), which\nunquotes filename if it is needed, which is marked by filename being\nsurrounded in doublequotes (which are not part of name, and which are\nstripped by unquote_maybe() - which makes this function idempotent).\nCurrently unquote_maybe() us used only in git_blame.\n\nIf there is no previous commit 'linenr' link points to blamed commit\nand blamed filename, making it work correctly for boundary commits.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\nIMHO more important is that result is MORE CORRECT, not the better\nperformance.\n\nIn the table below you can see simple benchmark comparing gitweb\nperformance before and after this patch.  Operating system used was\nLinux 2.6.14, on 1 GHz AMD Athlon processor (2002.43 BogoMIPS).\nAs there is one \"git rev-parse <rev>^\" per each individual commit\nin blame output, it would be much worse 'before' for operating\nsystems with costly fork.\n\nFile                  |  C[1] || Time0[2] | Before[3] | After[3]\n=================================================================\nrevision.c            |   121 ||   2.820s |    5.548s |   5.172s\ngitweb/gitweb.perl[4] |   428 ||  11.749s |   19.797s |  17.293s\n\nTable footnotes:\n~~~~~~~~~~~~~~~~\n[1] Individual commits in blame output:\n    $ git blame -p <file> | grep author-time | wc -l\n[2] Time for running \"git blame -p\" (user time, single run):\n    $ time git blame -p <file> >/dev/null\n[3] Time to run gitweb as Perl script from command line:\n    $ time gitweb-run.sh \"p=.git;a=blame;f=<file>\" >/dev/null 2>&1\n[4] Starting at 'origin', which is v1.6.3.3-412-gf581de1\n    > git blame -p origin -- gitweb/gitweb.perl\n    > \"p=.git;a=blame;hb=origin;f=gitweb/gitweb.perl\"\n\n\nFor comparison there is similar table for my 39c19ce (gitweb: cache\n$parent_commit info in git_blame(), 2008-12-11):\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\n  Legend:\n\n  [1] Number of lines:\n      $ wc -l <file>\n  [2] Number of unique commits in the 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\ngitweb/gitweb.perl |   37 ++++++++++++++++++++++++-------------\n1 files changed, 24 insertions(+), 13 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex fe73c2c..36b1ce5 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -1187,6 +1187,16 @@ sub unquote {\n    return $str;\n}\n\n+# if filename is surrounded in double quotes, it need to be unquoted\n+sub unquote_maybe {\n+    my $str = shift;\n+\n+    if ($str =~ /^\"(.*)\"$/) {\n+        return unquote($1);\n+    }\n+    return $str;\n+}\n+\n# escape tabs (convert tabs to spaces)\nsub untabify {\n    my $line = shift;\n@@ -4827,7 +4837,7 @@ HTML\n            chomp $data;\n            last if ($data =~ s/^\\t//); # contents of line\n            if ($data =~ /^(\\S+)(?: (.*))?$/) {\n-                $meta->{$1} = $2;\n+                $meta->{$1} = $2 unless exists $meta->{$1};\n            }\n        }\n        my $short_rev = substr($full_rev, 0, 8);\n@@ -4852,20 +4862,21 @@ HTML\n                          esc_html($short_rev));\n            print \"</td>\\n\";\n        }\n-        my $parent_commit;\n-        if (!exists $meta->{'parent'}) {\n-            open (my $dd, \"-|\", git_cmd(), \"rev-parse\", \"$full_rev^\")\n-                or die_error(500, \"Open git-rev-parse failed\");\n-            $parent_commit = <$dd>;\n-            close $dd;\n-            chomp($parent_commit);\n-            $meta->{'parent'} = $parent_commit;\n-        } else {\n-            $parent_commit = $meta->{'parent'};\n-        }\n+        # 'previous' <sha1 of parent commit> <filename at commit>\n+        if (exists $meta->{'previous'} &&\n+            $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n+            $meta->{'parent'} = $1;\n+            $meta->{'file_parent'} = unquote_maybe($2);\n+        }\n+        my $linenr_commit =\n+            exists($meta->{'parent'}) ?\n+            $meta->{'parent'} : $full_rev;\n+        my $linenr_filename =\n+            exists($meta->{'file_parent'}) ?\n+            $meta->{'file_parent'} : unquote_maybe($meta->{'filename'});\n        my $blamed = href(action => 'blame',\n-                          file_name => $meta->{'filename'},\n-                          hash_base => $parent_commit);\n+                          file_name => $linenr_filename,\n+                          hash_base => $linenr_commit);\n        print \"<td class=\\\"linenr\\\">\";\n        print $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n                        -class => \"linenr\" },\n-- \n1.6.3.3\n"},{"id":"117879","messageId":"200907130008.46168.jnareb@gmail.com","threadId":"20084","inReplyTo":"200907102354.43232.jnareb@gmail.com","subject":"[PATCH 4/3] gitweb: Use light/dark class also in 'blame' view","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-12T22:08:45Z","receivedAt":"2009-07-12T22:08:45Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Instead of using \"light2\" and \"dark2\" for class names in 'blame' view\n(in place of \"light\" and \"dark\" classes in other places) to avoid\nchanging style on hover in 'blame' view while doing it for other views\n(like 'shortlog'), use more advanced CSS, relying on the fact that\nmore specific selector wins.\n\nWhile at it add a few comments to gitweb CSS file, and consolidate\nsome repeated info.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis is an RFC because \n\n1. I am not sure if I did it correctly.  I had to fiddle a bit with CSS\n(using \"table.blame .light:hover\" in place of \"table.blame tr.light:hover\")\nto get the same behaviour (well, the same as far as I have checked it).\n\n2. Commit message could use improvements (single sentence, blergh).\n\n gitweb/gitweb.css  |   16 +++++++++-------\n gitweb/gitweb.perl |    2 +-\n 2 files changed, 10 insertions(+), 8 deletions(-)\n\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex 5e2f629..4e4f8aa 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -226,22 +226,24 @@ th {\n \ttext-align: left;\n }\n \n-tr.light:hover {\n-\tbackground-color: #edece6;\n-}\n-\n-tr.dark {\n-\tbackground-color: #f6f6f0;\n+/* do not change row style on hover for 'blame' view */\n+tr.light,\n+table.blame .light:hover {\n+\tbackground-color: #ffffff;\n }\n \n-tr.dark2 {\n+tr.dark,\n+table.blame .dark:hover {\n \tbackground-color: #f6f6f0;\n }\n \n+/* currently both use the same, but it can change */\n+tr.light:hover,\n tr.dark:hover {\n \tbackground-color: #edece6;\n }\n \n+/* boundary commits in 'blame' view */\n tr.boundary td.sha1 {\n \tfont-weight: bold;\n }\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 5336c92..bb7a5a9 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4811,7 +4811,7 @@ sub git_blame {\n \tgit_print_page_path($file_name, $ftype, $hash_base);\n \n \t# page body\n-\tmy @rev_color = qw(light2 dark2);\n+\tmy @rev_color = qw(light dark);\n \tmy $num_colors = scalar(@rev_color);\n \tmy $current_color = 0;\n \tmy %metainfo = ();\n-- \n1.6.3.3\n"},{"id":"117915","messageId":"200907132108.05078.jnareb@gmail.com","threadId":"20084","inReplyTo":"200907111856.54621.jnareb@gmail.com","subject":"[RFC PATCH 5/3] gitweb: Incremental blame (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-13T19:08:02Z","receivedAt":"2009-07-13T19:08:02Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This is tweaked up further version of Petr Baudis <pasky@suse.cz> patch, \nwhich was tweaked up version of Fredrik Kuivinen <frekui@gmail.com>'s\nproof of concept patch.  It adds 'blame_incremental' view, which\nincrementally displays line data in blame view using JavaScript (AJAX).\n\nThis patch does not (contrary to the one by Petr Baudis) enable this\nview in gitweb: there are no links leading to 'blame_incremental'\naction.  You would have to generate URL 'by hand' (e.g. changing 'blame'\nor 'blob' in gitweb URL to 'blame_incremental').  Having links in gitweb\nlead to this new action (e.g. by rewriting them like in previous patch),\nif JavaScript is enabled in browser, is left for later.\n\nLike earlier patch by Petr Baudis it avoids code duplication, but it goes\none step further and use git_blame_common for ordinary blame view, for\nincremental blame, and (which is change from previous patch) for\nincremental blame data.\n\nHow the 'blame_incremental' view works:\n* gitweb generates initial info by putting file contents (from\n  git-cat-file) together with line numbers in blame table\n* then gitweb makes web browser JavaScript engine call startBlame()\n  function from blame.js\n* startBlame() opens connection to 'blame_data' view, which in turn\n  calls \"git blame --incremental\" for a file, and streams output of\n  git-blame to JavaScript (blame.js)\n* blame.js updates line info in blame view, coloring it, and updating\n  progress info; note that it has to use 3 colors to ensure that\n  different neighbour groups have different styles\n* when 'blame_data' ends, and blame.js finishes updating line info,\n  it fixes colors to match (as far as possible) ordinary 'blame' view,\n  and updates generating time info.\n\nIt deals with streamed 'blame_data' server error by notifying about them\nin the progress info area (just in case).\n\nDifferences between 'blame_incremental' and original 'blame' view:\n* 'blame_incremental' always used (partial) query form for links\n  generated by JavaScript.  The difference is visible if we use path_info\n  link (pass some or all arguments in path_info), e.g. in 'blame' view\n  called using:\n    http://git.example.com/w/git.git/blame/HEAD:/README\n  we have 'linenr' links using the same form:\n    http://git.example.com/w/git.git/blame/e83c5163316f89bfbde7d9ab23ca2e25604af290:/README#l4\n  while in 'blame_incremental' view called with:\n    http://git.example.com/w/git.git/blame_incremental/HEAD:/README\n  we have \"partial query\" form\n    http://git.example.com/w/git.git?;a=blame_incremental;hb=e83c5163316f89bfbde7d9ab23ca2e25604af290;f=README#l4\n  Changing this would require implementing something akin to href()\n  subroutine in JavaScript\n* 'blame_incremental' always uses \"rowspan\" attribute, even if\n  rowspan=\"1\".  This simplifies code, and is not visible to user.\n+ sometimes 'blame_incremental' and corresponding 'blame' view show\n  different widths for \"Line\" column.  I don't know what is the case\n  of this; it might be even bug in web browser I use.\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\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\n\nSome benchmarks:\n^^^^^^^^^^^^^^^^\nWeb server (Apache + mod_cgi) and web browser (Mozilla 1.17.2) both\nare running on the same single core single CPU computer (AMD Athlon)\n\nFile               | 'blame'[1] | 'blame_incremental'[2]\n================================================================\nblob.h             |     3.838s | 0.525s +  (3.227s /   4.019s)\nGIT-VERSION-GEN    |     4.869s | 0.469s +  (4.359s /   5.134s)\nREADME             |     6.564s | 0.581s +  (4.445s /   5.253s)\nrevision.c         |    27.817s | 4.562s + (13.619s /  38.307s)\ngitweb/gitweb.perl |    81.495s | 9.922s + (50.761s / 242.591s)\n\nFootnotes:\n~~~~~~~~~~\n[1] Total wall-clock time as returned by gitweb in the page footer.\n[2] XXs + (XXs server blame_data / XXs client JavaScript).\n\nSigned-off-by: Fredrik Kuivinen <frekui@gmail.com>\nSigned-off-by: Petr Baudis <pasky@suse.cz>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nRefrences:\n1. Original patch by Frederik Kuivinen\n   http://article.gmane.org/gmane.comp.version-control.git/41361\n2. Tweaked up version by Petr Baudis\n   http://article.gmane.org/gmane.comp.version-control.git/56657\n3. First and second version by me\n   http://thread.gmane.org/gmane.comp.version-control.git/102657/focus=102712\n\nAdding 'generated in XXX seconds' is based on snipped by Petr Baudis:\n  http://article.gmane.org/gmane.comp.version-control.git/83306\n\nREQUEST: Please, if possible, test if 'blame_incremental' works\ncorrectly (without errors in JavaScript console or equivalent)\nin other browsers than my old Mozilla 1.17.2 (Gecko / Rhino).\nI'd like to have test for:\n * Firefox 2.0, 3.0 and 3.5\n * Internet Explorer 7 and 8 (perhaps also IE6)\n * Safari (and other WebKit based browsers)\n * Konqueror\n * Opera\n * Google Chrome\n\nChanges compared to the last version:\n* Perl code (gitweb.perl) updated to latest version, which uses \n  information from \"previous\" header in \"git blame -p\" output,\n  and which can deal with boundary commits.  JavaScript code\n  was updated to do the same.  This change made 'linenr' links\n  lead to equivalent views, contrary to previous version.\n* JavaScript code (blame.js) uses now the same date format as 'blame'\n  view in Perl code (gitweb.perl), i.e. \"2005-08-07 21:49:46 +0200\"\n  where time is in localtime of given timezone.\n* JavaScript code (blame.js) got cleaned up and reordered; more\n  comments were added, some variables and functions have slightly\n  different names, some code got factored out into separate \n  functions, we use switch/case instead of ling if-else-if chain.\n\nTODO list:\n* While blame.js should now deal with filenames containing characters\n  special to URI like '?', '%' or ';' thanks to use of\n  encodeURIComponent(), it doesn't yet do unquoting of space quoted\n  filenames, so it won't work correctly for filenames e.g. containing\n  space, tab or quote character.\n* handleResponse is used both as onreadystatechange and pollTimer;\n  if onreadystatechange works for partial responses we can turn off\n  the timer.  It is protected from concurrent running by global\n  inProgress variable; we could instead pass XMLHttpResponse object\n  as a parameter (see comment in startBlame).\n* Probably 'blame_data' should use multipart/x-mixed-replace as\n  content type, instead of (in addition to?) text/plain.  I am not\n  sure about that; I am not knowledgeable in AJAX and Comet.\n* Remove timing code, or move it to separate commit, and make it\n  optional / configurable (we probably don't want to show such info\n  on site open to wide public).\n* Move td.error -> .error to separate commit.  td.error is remain \n  from old git-annotate based git_blame; 'blame_interactive' uses\n  span.warning to notify about errors (like e.g. JavaScript turned\n  off for 'blame_incremental' view).\n* Remove or move to separate commit changes which help CPerl\n  mode for GNU Emacs to deal with syntax highlighting (#', #\").\n\nA few questions about this patch:\n* Should progress info be removed after 'blame_data' finishes?\n* Should 3-coloring of blame in progress be removed, or perhaps\n  just put in a separate patch for better reviewability (and better\n  bisectability)?\n* Should debug statements (conditional on DEBUG global variable)\n  be removed from source?  There is separate mechanism to inform\n  user about errors.\n* Should we use <element>.className = <value> instead of DOM Core\n  <element>.setAttribute('class', <value>)?  What about getter?\n* Should we convert some regexps to string manipulation (e.g. split)?\n  What is better for JavaScript performance?\n\nQuestions for the future:\n* Should we split JavaScript scripts into separate files, e.g. one\n  with common utilities (gitweb.js), and one dealing specifically with\n  'blame_incremental' view (gitweb-blame.js)?  How to configure it\n  (some scripts might be loaded in page header, some at the end of\n  page to not block progressive loading)?\n\n* Should we use some light-weight JavaScript framework (perhaps loaded\n  from Google[1]), instead of / in addition to handcrafted JavaScript (as\n  fallback)[2]?  What framework to use: jQuery, Prototype, YUI? CGI::Ajax?\n\n  [1] http://www.google.com/jsapi and google.load, or something\n  [2] http://stackoverflow.com/questions/1014203/best-way-to-use-googles-hosted-jquery-but-fall-back-to-my-hosted-library-on-goo\n\n* Perhaps 'blame_data' should pre-process data from git-blame to reduce\n  amount of work JavaScript has to do, and e.g. pre-parse data and\n  transform to JSON / JSONP?\n\n* To reduce size of JavaScript file user has to download to use\n  'blame_incremental' view, while keeping clean and easy to modify\n  original code, perhaps we could run some JavaScript minimizing\n  tool when building gitweb, something like Douglas Crockford's \n  JSMin[3] (if the tool is available during install / build)?\n\n  [3] http://www.crockford.com/javascript/jsmin.html\n\n* Should we use JSDoc comments?  But no other parts of code use\n  similar structured comments (Doxygen, ROBODoc, Natural Docs)...\n\n Makefile           |    6 +-\n git-instaweb.sh    |    7 +\n gitweb/blame.js    |  561 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n gitweb/gitweb.css  |   21 ++-\n gitweb/gitweb.perl |  286 ++++++++++++++++++---------\n 5 files changed, 788 insertions(+), 93 deletions(-)\n create mode 100644 gitweb/blame.js\n\ndiff --git a/Makefile b/Makefile\nindex bde27ed..95b577c 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -265,6 +265,7 @@ GITWEB_HOMETEXT = indextext.html\n GITWEB_CSS = gitweb.css\n GITWEB_LOGO = git-logo.png\n GITWEB_FAVICON = git-favicon.png\n+GITWEB_BLAMEJS = blame.js\n GITWEB_SITE_HEADER =\n GITWEB_SITE_FOOTER =\n \n@@ -1406,13 +1407,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@@ -1421,6 +1423,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 5f4419b..fd6341a 100755\n--- a/git-instaweb.sh\n+++ b/git-instaweb.sh\n@@ -331,8 +331,15 @@ gitweb_css () {\n EOFGITWEB\n }\n \n+gitweb_blamejs () {\n+\tcat > \"$1\" <<\\EOFGITWEB\n+@@GITWEB_BLAMEJS@@\n+EOFGITWEB\n+}\n+\n gitweb_cgi \"$GIT_DIR/gitweb/gitweb.cgi\"\n gitweb_css \"$GIT_DIR/gitweb/gitweb.css\"\n+gitweb_blamejs \"$GIT_DIR/gitweb/blame.js\"\n \n case \"$httpd\" in\n *lighttpd*)\ndiff --git a/gitweb/blame.js b/gitweb/blame.js\nnew file mode 100644\nindex 0000000..6b89143\n--- /dev/null\n+++ b/gitweb/blame.js\n@@ -0,0 +1,561 @@\n+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>\n+\n+/* ============================================================ */\n+/* generic utility functions */\n+\n+var DEBUG = 0;\n+function debug(str) {\n+\tif (DEBUG) {\n+\t\talert(str);\n+\t}\n+}\n+\n+// 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 prefix = '';\n+\n+\twhile (width > 1) {\n+\t\tscale *= 10;\n+\t\tif (n < scale) {\n+\t\t\tprefix += '&nbsp;';\n+\t\t}\n+\t\twidth--;\n+\t}\n+\treturn prefix + n;\n+}\n+\n+// create XMLHttpRequest object in cross-browser way\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+/* ============================================================ */\n+/* utility/helper functions (and variables) */\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+/* ............................................................ */\n+/* progress info, timing */\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+\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+/* coloring rows during blame_data (git blame --incremental) run */\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 (curently least used one)\n+// example: chooseColorNoFrom(2, 3) returns 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+// 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+\t// THEN we can use any of 3 possible colors\n+\tif (!color_prev && !color_next) {\n+\t\treturn chooseColorNoFrom(1,2,3);\n+\t}\n+\n+\t// either both neighbours have the same color,\n+\t// or only one of neighbours have color set\n+\t// THEN we can use any color except given\n+\tvar color;\n+\tif (color_prev == color_next) {\n+\t\tcolor = color_prev; // = color_next;\n+\t} else if (!color_prev) {\n+\t\tcolor = color_next;\n+\t} else if (!color_next) {\n+\t\tcolor = color_prev;\n+\t}\n+\tif (color) {\n+\t\treturn chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1);\n+\t}\n+\n+\t// neighbours have different colors\n+\t// THEN there is only one color left\n+\treturn (3 - ((color_prev + color_next) % 3));\n+}\n+\n+/* ............................................................ */\n+/* coloring rows like 'blame' after 'blame_data' finishes */\n+\n+// returns true if given row element (tr) is first in commit group\n+function isStartOfGroup(tr) {\n+\treturn tr.firstChild.getAttribute('class') == 'sha1';\n+}\n+\n+// change colors to use zebra coloring (2 colors) instead of 3 colors\n+// concatenate neighbour commit groups belonging to the same commit\n+function fixColorsAndGroups() {\n+\tvar colorClasses = ['light', 'dark'];\n+\tvar linenum = 1;\n+\tvar tr, prev_group;\n+\tvar colorClass = 0;\n+\n+\twhile ((tr = document.getElementById('l'+linenum))) {\n+\t\tif (isStartOfGroup(tr, linenum, document)) {\n+\t\t\tif (prev_group &&\n+\t\t\t    prev_group.firstChild.firstChild.href ==\n+\t\t\t            tr.firstChild.firstChild.href) {\n+\t\t\t\t// we have to concatenate groups\n+\t\t\t\tvar 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\tvar tr_class = colorClasses[colorClass];\n+\t\tif (tr.className.indexOf('boundary') != -1)\n+\t\t\ttr_class += ' boundary';\n+\t\ttr.setAttribute('class', tr_class);\n+\t\ttr.className = tr_class;\n+\t\t// Internet Explorer needs this\n+\t\ttr.setAttribute('className', tr_class);\n+\t\tlinenum++;\n+\t}\n+}\n+\n+/* ............................................................ */\n+/* time and data */\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+// return date in local time formatted in iso-8601 like format\n+// 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200'\n+function formatDateIsoTz(epoch, timezoneInfo) {\n+\tvar match = tzRe.exec(timezoneInfo);\n+\t// date corrected by timezone\n+\tvar localDate = new Date(1000 * (epoch +\n+\t\t(parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));\n+\tvar localDateStr = // e.g. '2005-08-07'\n+\t\tlocalDate.getUTCFullYear()         + '-' +\n+\t\tzeroPad(localDate.getUTCMonth()+1) + '-' +\n+\t\tzeroPad(localDate.getUTCDate());\n+\tvar localTimeStr = // e.g. '21:49:46'\n+\t\tzeroPad(localDate.getUTCHours())   + ':' +\n+\t\tzeroPad(localDate.getUTCMinutes()) + ':' +\n+\t\tzeroPad(localDate.getUTCSeconds());\n+\n+\treturn localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;\n+}\n+\n+/* ............................................................ */\n+/* unquoting/unescaping filenames */\n+\n+/* TODO */\n+\n+/* ============================================================ */\n+/* main part: parsing response */\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+\n+\t   <tr id=\"l123\" class=\"\">\n+\t     <td class=\"sha1\" title=\"\"><a href=\"\"></a></td>\n+\t     <td class=\"linenr\"><a class=\"linenr\" href=\"\">123</a></td>\n+\t     <td class=\"pre\"># times (my ext3 doesn&#39;t).</td>\n+\t   </tr>\n+\t*/\n+\n+\tvar resline = commit.resline;\n+\n+\t// format date and time string only once per commit\n+\tif (!commit.info) {\n+\t\t/* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */\n+\t\tcommit.info = commit.author + ', ' +\n+\t\t\tformatDateIsoTz(commit.authorTime, commit.authorTimezone);\n+\t}\n+\n+\t// color depends on group of lines, not only on blamed commit\n+\tvar colorNo = findColorNo(\n+\t\tdocument.getElementById('l'+(resline-1)),\n+\t\tdocument.getElementById('l'+(resline+commit.numlines))\n+\t);\n+\n+\t// loop over lines in commit group\n+\tfor (var i = 0; i < commit.numlines; i++) {\n+\t\tvar tr = document.getElementById('l'+resline);\n+\t\tif (!tr) {\n+\t\t\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\tvar tr_class = '';\n+\t\tif (colorNo !== null)\n+\t\t\ttr_class = 'color'+colorNo;\n+\t\tif (commit.boundary)\n+\t\t\ttr_class += ' boundary';\n+\t\ttr.setAttribute('class', tr_class);\n+\t\t// Internet Explorer needs this\n+\t\ttr.setAttribute('className', tr_class);\n+\n+\t\t/* <td class=\"sha1\" title=\"?\" rowspan=\"?\"><a href=\"?\">?</a></td> */\n+\t\tif (i === 0) {\n+\t\t\ttd_sha1.title = commit.info;\n+\t\t\ttd_sha1.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+\t\t\tif (commit.numlines >= 2) {\n+\t\t\t\tvar br   = document.createElement(\"br\");\n+\t\t\t\tvar text = document.createTextNode(\n+\t\t\t\t\tcommit.author.match(/\\b([A-Z])\\B/g).join(''));\n+\t\t\t\tif (br && text) {\n+\t\t\t\t\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\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\tlinenr_commit =\n+\t\t\tcommit.previous ? commit.previous : commit.sha1;\n+\t\tlinenr_filename =\n+\t\t\tcommit.file_parent ? commit.file_parent : commit.filename;\n+\t\ta_linenr.href = projectUrl + ';a=blame_incremental' +\n+\t\t\t';hb=' + linenr_commit +\n+\t\t\t';f='  + encodeURIComponent(linenr_filename) +\n+\t\t\t'#l' + (commit.srcline + i);\n+\n+\t\tresline++;\n+\t\tblamedLines++;\n+\n+\t\t//updateProgressInfo();\n+\t}\n+}\n+\n+// ----------------------------------------------------------------------\n+\n+var prevDataLength = -1;\n+var nextLine = 0;\n+var inProgress = false;\n+\n+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+\n+\t\t\t} else if ((match = infoRe.exec(lines[i]))) {\n+\t\t\t\tvar info = match[1];\n+\t\t\t\tvar data = match[2];\n+\t\t\t\tswitch (info) {\n+\t\t\t\tcase 'filename':\n+\t\t\t\t\tcurCommit.filename = data; // unquote_maybe\n+\t\t\t\t\t// 'filename' information terminates the entry\n+\t\t\t\t\thandleLine(curCommit);\n+\t\t\t\t\tupdateProgressInfo();\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'author':\n+\t\t\t\t\tcurCommit.author = data;\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'author-time':\n+\t\t\t\t\tcurCommit.authorTime = parseInt(data, 10);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'author-tz':\n+\t\t\t\t\tcurCommit.authorTimezone = data;\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'previous':\n+\t\t\t\t\tvar parts = data.split(' ', 2);\n+\t\t\t\t\tcurCommit.previous    = parts[0];\n+\t\t\t\t\tcurCommit.file_parent = parts[1]; // unquote_maybe\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 'boundary':\n+\t\t\t\t\tdebug('Boundary commit: '+curCommit.sha1);\n+\t\t\t\t\tcurCommit.boundary = true;\n+\t\t\t\t\tbreak;\n+\t\t\t\t} // end switch\n+\n+\t\t\t} else if ((match = endRe.exec(lines[i]))) {\n+\t\t\t\tt_interval_server = match[1];\n+\t\t\t\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 = '<b>ERROR:</b> 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.setRequestHeader('Accept', 'text/plain'); // in case of future changes\n+\thttp.onreadystatechange = handleResponse;\n+\t//http.onreadystatechange = function() { handleResponse(http); };\n+\thttp.send(null);\n+\n+\t// not all browsers call onreadystatechange event on each server flush\n+\tif (!DEBUG)\n+\t\tpollTimer = setInterval(handleResponse, 1000);\n+}\n+\n+// end of blame.js\ndiff --git a/gitweb/gitweb.css b/gitweb/gitweb.css\nindex 4e4f8aa..ac9033f 100644\n--- a/gitweb/gitweb.css\n+++ b/gitweb/gitweb.css\n@@ -248,6 +248,14 @@ tr.boundary td.sha1 {\n \tfont-weight: bold;\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@@ -268,7 +276,7 @@ td.sha1 {\n \tfont-family: monospace;\n }\n \n-td.error {\n+.error {\n \tcolor: red;\n \tbackground-color: yellow;\n }\n@@ -339,6 +347,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 bb7a5a9..dea521e 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@@ -90,6 +93,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@@ -550,6 +555,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@@ -992,7 +999,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@@ -1716,7 +1724,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@@ -3042,13 +3050,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@@ -3215,6 +3223,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@@ -4004,7 +4020,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@@ -4768,7 +4784,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@@ -4790,10 +4808,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@@ -4804,104 +4848,164 @@ 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(light dark);\n \tmy $num_colors = scalar(@rev_color);\n \tmy $current_color = 0;\n-\tmy %metainfo = ();\n \n-\tprint <<HTML;\n-<div class=\"page_body\">\n-<table class=\"blame\">\n-<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n-HTML\n- LINE:\n-\twhile (my $line = <$fd>) {\n-\t\tchomp $line;\n-\t\t# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]\n-\t\t# no <lines in group> for subsequent lines in group of lines\n-\t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n-\t\t   ($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n-\t\tif (!exists $metainfo{$full_rev}) {\n-\t\t\t$metainfo{$full_rev} = {};\n-\t\t}\n-\t\tmy $meta = $metainfo{$full_rev};\n-\t\tmy $data;\n-\t\twhile ($data = <$fd>) {\n-\t\t\tchomp $data;\n-\t\t\tlast if ($data =~ s/^\\t//); # contents of line\n-\t\t\tif ($data =~ /^(\\S+)(?: (.*))?$/) {\n-\t\t\t\t$meta->{$1} = $2 unless exists $meta->{$1};\n-\t\t\t}\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 $short_rev = substr($full_rev, 0, 8);\n-\t\tmy $author = $meta->{'author'};\n-\t\tmy %date =\n-\t\t\tparse_date($meta->{'author-time'}, $meta->{'author-tz'});\n-\t\tmy $date = $date{'iso-tz'};\n-\t\tif ($group_size) {\n-\t\t\t$current_color = ($current_color + 1) % $num_colors;\n-\t\t}\n-\t\tmy $tr_class = $rev_color[$current_color];\n-\t\t$tr_class .= ' boundary' if (exists $meta->{'boundary'});\n-\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$tr_class\\\">\\n\";\n-\t\tif ($group_size) {\n-\t\t\tprint \"<td class=\\\"sha1\\\"\";\n-\t\t\tprint \" title=\\\"\". esc_html($author) . \", $date\\\"\";\n-\t\t\tprint \" rowspan=\\\"$group_size\\\"\" if ($group_size > 1);\n-\t\t\tprint \">\";\n-\t\t\tprint $cgi->a({-href => href(action=>\"commit\",\n-\t\t\t                             hash=>$full_rev,\n-\t\t\t                             file_name=>$file_name)},\n-\t\t\t              esc_html($short_rev));\n-\t\t\tif ($group_size >= 2) {\n-\t\t\t\tmy @author_initials = ($author =~ /\\b([[:upper:]])\\B/g);\n-\t\t\t\tif (@author_initials) {\n-\t\t\t\t\tprint \"<br />\" .\n-\t\t\t\t\t      esc_html(join('', @author_initials));\n-\t\t\t\t\t#           or join('.', ...)\n+\n+\t} else { # porcelain, i.e. ordinary blame\n+\t\tmy %metainfo = (); # saves information about commits\n+\n+\t\t# blame data\n+\tLINE:\n+\t\twhile (my $line = <$fd>) {\n+\t\t\tchomp $line;\n+\t\t\t# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]\n+\t\t\t# no <lines in group> for subsequent lines in group of lines\n+\t\t\tmy ($full_rev, $orig_lineno, $lineno, $group_size) =\n+\t\t\t   ($line =~ /^([0-9a-f]{40}) (\\d+) (\\d+)(?: (\\d+))?$/);\n+\t\t\tif (!exists $metainfo{$full_rev}) {\n+\t\t\t\t$metainfo{$full_rev} = {};\n+\t\t\t}\n+\t\t\tmy $meta = $metainfo{$full_rev};\n+\t\t\tmy $data;\n+\t\t\twhile ($data = <$fd>) {\n+\t\t\t\tchomp $data;\n+\t\t\t\tlast if ($data =~ s/^\\t//); # contents of line\n+\t\t\t\tif ($data =~ /^(\\S+)(?: (.*))?$/) {\n+\t\t\t\t\t$meta->{$1} = $2 unless exists $meta->{$1};\n \t\t\t\t}\n \t\t\t}\n-\t\t\tprint \"</td>\\n\";\n-\t\t}\n-\t\t# 'previous' <sha1 of parent commit> <filename at commit>\n-\t\tif (exists $meta->{'previous'} &&\n-\t\t    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n-\t\t\t$meta->{'parent'} = $1;\n-\t\t\t$meta->{'file_parent'} = unquote_maybe($2);\n-\t\t}\n-\t\tmy $linenr_commit =\n-\t\t\texists($meta->{'parent'}) ?\n-\t\t\t$meta->{'parent'} : $full_rev;\n-\t\tmy $linenr_filename =\n-\t\t\texists($meta->{'file_parent'}) ?\n-\t\t\t$meta->{'file_parent'} : unquote_maybe($meta->{'filename'});\n-\t\tmy $blamed = href(action => 'blame',\n-\t\t                  file_name => $linenr_filename,\n-\t\t                  hash_base => $linenr_commit);\n-\t\tprint \"<td class=\\\"linenr\\\">\";\n-\t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n-\t\t                -class => \"linenr\" },\n-\t\t              esc_html($lineno));\n-\t\tprint \"</td>\";\n-\t\tprint \"<td class=\\\"pre\\\">\" . esc_html($data) . \"</td>\\n\";\n-\t\tprint \"</tr>\\n\";\n+\t\t\tmy $short_rev = substr($full_rev, 0, 8);\n+\t\t\tmy $author = $meta->{'author'};\n+\t\t\tmy %date =\n+\t\t\t\tparse_date($meta->{'author-time'}, $meta->{'author-tz'});\n+\t\t\tmy $date = $date{'iso-tz'};\n+\t\t\tif ($group_size) {\n+\t\t\t\t$current_color = ($current_color + 1) % $num_colors;\n+\t\t\t}\n+\t\t\tmy $tr_class = $rev_color[$current_color];\n+\t\t\t$tr_class .= ' boundary' if (exists $meta->{'boundary'});\n+\t\t\tprint \"<tr id=\\\"l$lineno\\\" class=\\\"$tr_class\\\">\\n\";\n+\t\t\tif ($group_size) {\n+\t\t\t\tprint \"<td class=\\\"sha1\\\"\";\n+\t\t\t\tprint \" title=\\\"\". esc_html($author) . \", $date\\\"\";\n+\t\t\t\tprint \" rowspan=\\\"$group_size\\\"\" if ($group_size > 1);\n+\t\t\t\tprint \">\";\n+\t\t\t\tprint $cgi->a({-href => href(action=>\"commit\",\n+\t\t\t\t                             hash=>$full_rev,\n+\t\t\t\t                             file_name=>$file_name)},\n+\t\t\t\t              esc_html($short_rev));\n+\t\t\t\tif ($group_size >= 2) {\n+\t\t\t\t\tmy @author_initials = ($author =~ /\\b([[:upper:]])\\B/g);\n+\t\t\t\t\tif (@author_initials) {\n+\t\t\t\t\t\tprint \"<br />\" .\n+\t\t\t\t\t\t      esc_html(join('', @author_initials));\n+\t\t\t\t\t\t#           or join('.', ...)\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\tprint \"</td>\\n\";\n+\t\t\t}\n+\t\t\t# 'previous' <sha1 of parent commit> <filename at commit>\n+\t\t\tif (exists $meta->{'previous'} &&\n+\t\t\t    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {\n+\t\t\t\t$meta->{'parent'} = $1;\n+\t\t\t\t$meta->{'file_parent'} = unquote_maybe($2);\n+\t\t\t}\n+\t\t\tmy $linenr_commit =\n+\t\t\t\texists($meta->{'parent'}) ?\n+\t\t\t\t$meta->{'parent'} : $full_rev;\n+\t\t\tmy $linenr_filename =\n+\t\t\t\texists($meta->{'file_parent'}) ?\n+\t\t\t\t$meta->{'file_parent'} : unquote_maybe($meta->{'filename'});\n+\t\t\tmy $blamed = href(action => 'blame',\n+\t\t\t                  file_name => $linenr_filename,\n+\t\t\t                  hash_base => $linenr_commit);\n+\t\t\tprint \"<td class=\\\"linenr\\\">\";\n+\t\t\tprint $cgi->a({ -href => \"$blamed#l$orig_lineno\",\n+\t\t\t                -class => \"linenr\" },\n+\t\t\t              esc_html($lineno));\n+\t\t\tprint \"</td>\";\n+\t\t\tprint \"<td class=\\\"pre\\\">\" . esc_html($data) . \"</td>\\n\";\n+\t\t\tprint \"</tr>\\n\";\n+\t\t} # end while\n+\n \t}\n-\tprint \"</table>\\n\";\n-\tprint \"</div>\";\n+\n+\t# footer\n+\tprint \"</tbody>\\n\".\n+\t      \"</table>\\n\"; # class=\"blame\"\n+\tprint \"</div>\\n\";   # class=\"blame_body\"\n \tclose $fd\n \t\tor print \"Reading blob failed\\n\";\n \n-\t# page footer\n+\tif ($format eq 'incremental') {\n+\t\tprint qq!<script type=\"text/javascript\" src=\"$blamejs\"></script>\\n!.\n+\t\t      qq!<script type=\"text/javascript\">\\n!.\n+\t\t      qq!startBlame(\"!. href(action=>\"blame_data\", -replay=>1) .qq!\",\\n!.\n+\t\t      qq!           \"!. href(-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.3.3\n"},{"id":"117981","messageId":"200907142121.31012.jnareb@gmail.com","threadId":"20084","inReplyTo":"200907102357.43475.jnareb@gmail.com","subject":"Re: [PATCH 2/3] gitweb: Use \"previous\" header of git-blame -p in 'blame' view","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2009-07-14T19:21:29Z","receivedAt":"2009-07-14T19:21:29Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Fri, 10 July 2009, Jakub Narebski wrote:\n\n> +# if filename is surrounded in double quotes, it need to be unquoted\n> +sub unquote_maybe {\n> +       my $str = shift;\n> +\n> +       if ($str =~ /^\"(.*)\"$/) {\n> +               return unquote($1);\n> +       }\n> +       return $str;\n> +}\n\nI'm sorry about that, but this is totally unnecessary, as \nunquote == unquote_maybe (unquotes only when necessary).\n\n-- \nJakub Narebski\nPoland\n"}]}