All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/5] gitweb: Incremental blame series (1 Sep 09)
@ 2009-09-01 11:39 Jakub Narebski
  2009-09-01 11:39 ` [PATCHv2 1/5] gitweb: Add optional "time to generate page" info in footer Jakub Narebski
  0 siblings, 1 reply; 12+ messages in thread
From: Jakub Narebski @ 2009-09-01 11:39 UTC (permalink / raw)
  To: git
  Cc: Petr Baudis, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov,
	Martin Koegler, Jakub Narebski

This series is meant to replace the 'jn/gitweb-blame' topic branch
(it is merged into 'pu', b08ae28).

Jakub Narebski (5):
  gitweb: Add optional "time to generate page" info in footer
  gitweb: Incremental blame (using JavaScript)
  gitweb: Colorize 'blame_incremental' view during processing
  gitweb: Create links leading to 'blame_incremental' using JavaScript
  gitweb: Minify gitweb.js if JSMIN is defined

The "time to generate page" info got its own style, and don't looks
bolted on now.  Incremental blame patch is no longer marked as proof
of concept, or as a work in progress; I think it is mature enough.

Last two patches (creating links and JSMIN) are in active development,
and should be considered work in progress (or even proof of concept
code).

This series has 3-color colorizing of 'blame_incremental' view is put
into separate patch.  JavaScript code used to create links leading to
'blame_incremental' view is now put in gitweb.js, which is meant to
contain all JavaScript code used by Git.

The last patch (modifying Makefile) was added because of concern that
heavily commented gitweb.js could be a burden on server.

 Makefile           |   26 ++-
 git-instaweb.sh    |    7 +
 gitweb/README      |    4 +
 gitweb/gitweb.css  |   23 ++
 gitweb/gitweb.js   |  862 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 gitweb/gitweb.perl |  311 ++++++++++++++------
 6 files changed, 1146 insertions(+), 87 deletions(-)
 create mode 100644 gitweb/gitweb.js

-- 
Jakub Narebski
Poland

^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCHv2 1/5] gitweb: Add optional "time to generate page" info in footer
  2009-09-01 11:39 [PATCH 0/5] gitweb: Incremental blame series (1 Sep 09) Jakub Narebski
@ 2009-09-01 11:39 ` Jakub Narebski
  2009-09-01 11:39   ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Jakub Narebski
  0 siblings, 1 reply; 12+ messages in thread
From: Jakub Narebski @ 2009-09-01 11:39 UTC (permalink / raw)
  To: git
  Cc: Petr Baudis, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov,
	Martin Koegler, Jakub Narebski

Add "This page took XXX seconds and Y git commands to generate"
to page footer, if global feature 'timed' is enabled (disabled
by default).  Requires Time::HiRes installed for high precision
'wallclock' time.

Note that Time::HiRes is being required unconditionally of 'timed'
feature, because setting $t0 variable should be fairly early to have
running time of the whole script.  If Time::HiRes module was required
only if 'timed' feature is enabled, the earliest place where starting
time ($t0) could be calculated would be after reading gitweb config,
making "time to generate page" info inaccurate.

This code is based on example code by Petr 'Pasky' Baudis.

Signed-off-by: Jakub Narebski <jnareb@gmail.com>
---
This version adds custom formatting for "time to generate page"
information in the page footer, instead of reusing 'page_footer'
style.  It is now clearly separated from the gitweb footer proper.

While at it the ids were changed from "generate_info" etc. to
"generating_info" etc.

There is still a question whether Time::HiRes should be require'd
unconditionally, but otherwise this patch is out of RFC stage.


Sidenote: I have noticed that gitweb's CSS rules are unnecessary
strict, making rendering slower because of extra non needed checks
(the rule is that selectors should be as simple as possible).

 gitweb/gitweb.css  |    7 +++++++
 gitweb/gitweb.perl |   29 +++++++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 0 deletions(-)

diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css
index b59fcaf..00e2e4c 100644
--- a/gitweb/gitweb.css
+++ b/gitweb/gitweb.css
@@ -75,6 +75,13 @@ div.page_footer_text {
 	font-style: italic;
 }
 
+div#generating_info {
+	margin: 4px;
+	font-size: smaller;
+	text-align: center;
+	color: #505050;
+}
+
 div.page_body {
 	padding: 8px;
 	font-family: monospace;
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index ff2d42a..08d410d 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -18,6 +18,12 @@ use File::Find qw();
 use File::Basename qw(basename);
 binmode STDOUT, ':utf8';
 
+our $t0;
+if (eval { require Time::HiRes; 1; }) {
+	$t0 = [Time::HiRes::gettimeofday()];
+}
+our $number_of_git_cmds = 0;
+
 BEGIN {
 	CGI->compile() if $ENV{'MOD_PERL'};
 }
@@ -404,6 +410,13 @@ our %feature = (
 		'sub' => \&feature_avatar,
 		'override' => 0,
 		'default' => ['']},
+
+	# Enable displaying how much time and how many git commands
+	# it took to generate and display page.  Disabled by default.
+	# Project specific override is not supported.
+	'timed' => {
+		'override' => 0,
+		'default' => [0]},
 );
 
 sub gitweb_get_feature {
@@ -518,6 +531,7 @@ if (-e $GITWEB_CONFIG) {
 
 # version of the core git binary
 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
+$number_of_git_cmds++;
 
 $projects_list ||= $projectroot;
 
@@ -1969,6 +1983,7 @@ sub get_feed_info {
 
 # returns path to the core git executable and the --git-dir parameter as list
 sub git_cmd {
+	$number_of_git_cmds++;
 	return $GIT, '--git-dir='.$git_dir;
 }
 
@@ -3218,6 +3233,20 @@ sub git_footer_html {
 	}
 	print "</div>\n"; # class="page_footer"
 
+	if (defined $t0 && gitweb_check_feature('timed')) {
+		print "<div id=\"generating_info\">\n";
+		print 'This page took '.
+		      '<span id="generating_time" class="time_span">'.
+		      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
+		      ' seconds </span>'.
+		      ' and '.
+		      '<span id="generating_cmd">'.
+		      $number_of_git_cmds.
+		      '</span> git commands '.
+		      " to generate.\n";
+		print "</div>\n"; # class="page_footer"
+	}
+
 	if (-f $site_footer) {
 		insert_file($site_footer);
 	}
-- 
1.6.3.3

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript)
  2009-09-01 11:39 ` [PATCHv2 1/5] gitweb: Add optional "time to generate page" info in footer Jakub Narebski
@ 2009-09-01 11:39   ` Jakub Narebski
  2009-09-01 11:39     ` [PATCHv1 3/5] gitweb: Colorize 'blame_incremental' view during processing Jakub Narebski
  2009-11-05 20:22     ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Petr Baudis
  0 siblings, 2 replies; 12+ messages in thread
From: Jakub Narebski @ 2009-09-01 11:39 UTC (permalink / raw)
  To: git
  Cc: Petr Baudis, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov,
	Martin Koegler, Jakub Narebski

Add 'blame_incremental' view, which uses "git blame --incremental"
and JavaScript (Ajax), where 'blame' use "git blame --porcelain".

* gitweb generates initial info by putting file contents (from
  "git cat-file") together with line numbers in blame table
* then gitweb makes web browser JavaScript engine call startBlame()
  function from gitweb.js
* startBlame() opens XMLHttpRequest connection to 'blame_data' view,
  which in turn calls "git blame --incremental" for a file, and
  streams output of git-blame to JavaScript (gitweb.js)
* XMLHttpRequest event handler updates line info in blame view as soon
  as it gets data from 'blame_data' (from server), and it also updates
  progress info
* when 'blame_data' ends, and gitweb.js finishes updating line info,
  it fixes colors to match (as far as possible) ordinary 'blame' view,
  and updates information about how long it took to generate page.

Gitweb deals with streamed 'blame_data' server errors by displaying
them in the progress info area (just in case).

The 'blame_incremental' view tries to be equivalent to 'blame' action;
there are however a few differences in output between 'blame' and
'blame_incremental' view:
* 'blame_incremental' always used query form for this part of link(s)
  which is generated by JavaScript code.  The difference is visible
  if we use path_info link (pass some or all arguments in path_info).
  Changing this would require implementing something akin to href()
  subroutine from gitweb.perl in JavaScript (in gitweb.js).
* 'blame_incremental' always uses "rowspan" attribute, even if
  rowspan="1".  This simplifies code, and is not visible to user.
* The progress bar and progress info are still there even after
  JavaScript part of 'blame_incremental' finishes work.

Note that currently no link generated by gitweb leads to this new
view.


This code is based on patch by Petr Baudis <pasky@suse.cz> patch, which
in turn was tweaked up version of Fredrik Kuivinen <frekui@gmail.com>'s
proof of concept patch.

This patch adds GITWEB_JS compile configuration option, and modifies
git-instaweb.sh to take gitweb.js into account.  The code for
git-instaweb.sh was taken from Pasky's patch.

Signed-off-by: Fredrik Kuivinen <frekui@gmail.com>
Signed-off-by: Petr Baudis <pasky@suse.cz>
Signed-off-by: Jakub Narebski <jnareb@gmail.com>
---
References:
1. Original patch by Frederik Kuivinen
   http://article.gmane.org/gmane.comp.version-control.git/41361
2. Tweaked up version by Petr Baudis
   http://article.gmane.org/gmane.comp.version-control.git/47614
   http://article.gmane.org/gmane.comp.version-control.git/56657
3. New link rewriting and some optimization in Matrin Koegler
   series introducing some JavaScript support in Git
   http://thread.gmane.org/gmane.comp.version-control.git/47902/focus=47905   
4. My earlier patches
   http://thread.gmane.org/gmane.comp.version-control.git/102657/focus=102712
   http://article.gmane.org/gmane.comp.version-control.git/123202
   http://thread.gmane.org/gmane.comp.version-control.git/123957/focus=123968
   http://thread.gmane.org/gmane.comp.version-control.git/125096/focus=125717

Changes compared to last version (v4):
* The file with JavaScript code for 'blame_incremental' got renamed
  from 'blame.js' to 'gitweb.js' -- it is now meant to contain all
  JavaScript code, as per Google and Yahoo! guidelines (there should
  be one file with JavaScript code, to be cached, as JavaScript
  loading is blocking).
* Move coloring rows duting 'blame_data' run to a separate commit.
* The debug(str) function and its use (even commented out) was removed;
  the code is now meant to be in production, and should have leftover
  debugging statements.
* spacePad(input, width) function, which pads with '&nbsp;' got
  replaced by more generic padLeftStr(input, width, padstr) function.
* Added information about global variables used by function via 
  @globals annotation (non in JSDoc standard).
* Make prevDataLength and nextReadPos properties (fields) of
  XMLHttpRequest object, instead of being global variables.
* Pass xhr as parameter to handleError and responseLoaded functions
  (it shadows xhr as a global variable).
* Move setting onreadystatechange handler before xhr.open()
* Add information about GITWEB_JS Makefile configuration variable to
  gitweb/README
* Remove unnecessary 'reminder' comments in gitweb.js

TODO for future commits:
* Use W3C Progress Events (progress, error, load) in addition to
  currently used readystatechange event
    http://www.w3.org/TR/progress-events/
    http://www.w3.org/TR/XMLHttpRequest2/
    http://www.nczonline.net/blog/2009/07/09/firefox-35firebug-xmlhttprequest-and-readystatechange-bug/
  if XMLHttpResponse supports it.
* handleResponse is used both as onreadystatechange and pollTimer;
  if onreadystatechange works for partial responses we can turn off
  the timer.
* JavaScript is single theraded, therefore there is no need for
  critical section; remove inProgress global variable (flag), and
  busy-read while loop.
* Remove (fade out) progress bar and progress info after blame
  incremental finished run, if possible with large amount of code to
  deal with browser incompatibilities.

TODO and possible extensions:
* Profile gitweb.js using YUI Profiler (or other JavaScript profile tool)
    http://developer.yahoo.com/yui/profiler/
  to check whether performance improvements described in articles
  on NCZOnline blog by Nicholas C. Zakas are required, and would help
    http://www.nczonline.net/blog/
* Get rid of global variables, if possible.
* Instead of running startBlame, put it in window.onload handler.

Roads not taken (perhaps that should be part of commit message?):
* Move most (or all) of "git blame --incremental" output parsing to
  server side, and instead of sending direct output in text/plain,
  send processed data in JSON format, e.g.

    {"commit": {
       "sha1": "e83c5163316f89bfbde7d9ab23ca2e25604af290",
       "info": "Kay Sievers, 2005-08-07 21:49:46 +0200",
       "author-initials": "KS",
       ...
     },
     "src-line": 13,
     "dst-line": 16,
     "numlines": 3,
     "filename": "README"
     }

  (line wrapping added for readibility).  This would require however
  taking care on Perl side to send properly formatted JSON, and on
  JavaScript side including json2.js code to read JSON in gitweb.js
  (unless we rely on eval).
* Using some lightweight JavaScript library (framework), like jQuery,
  Prototype, ExtJS, MooTools, etc.  One one hand side this means not
  having to worry about browser incompatibilities as this would be
  taken care of by library; on the other hand side we want gitweb to
  have as few dependences as possible.
* Use 'multipart/x-mixed-replace': first it is not standard, second
  I don't think this would be a good solution for our problem.

 Makefile           |    6 +-
 git-instaweb.sh    |    7 +
 gitweb/README      |    4 +
 gitweb/gitweb.css  |   11 +
 gitweb/gitweb.js   |  726 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 gitweb/gitweb.perl |  272 ++++++++++++++------
 6 files changed, 939 insertions(+), 87 deletions(-)
 create mode 100644 gitweb/gitweb.js

diff --git a/Makefile b/Makefile
index a614347..407b35c 100644
--- a/Makefile
+++ b/Makefile
@@ -261,6 +261,7 @@ GITWEB_HOMETEXT = indextext.html
 GITWEB_CSS = gitweb.css
 GITWEB_LOGO = git-logo.png
 GITWEB_FAVICON = git-favicon.png
+GITWEB_JS = gitweb.js
 GITWEB_SITE_HEADER =
 GITWEB_SITE_FOOTER =
 
@@ -1393,13 +1394,14 @@ gitweb/gitweb.cgi: gitweb/gitweb.perl
 	    -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \
 	    -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \
 	    -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \
+	    -e 's|++GITWEB_JS++|$(GITWEB_JS)|g' \
 	    -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \
 	    -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \
 	    $< >$@+ && \
 	chmod +x $@+ && \
 	mv $@+ $@
 
-git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css
+git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css gitweb/gitweb.js
 	$(QUIET_GEN)$(RM) $@ $@+ && \
 	sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
 	    -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
@@ -1408,6 +1410,8 @@ git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css
 	    -e '/@@GITWEB_CGI@@/d' \
 	    -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \
 	    -e '/@@GITWEB_CSS@@/d' \
+	    -e '/@@GITWEB_JS@@/r gitweb/gitweb.js' \
+	    -e '/@@GITWEB_JS@@/d' \
 	    -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \
 	    $@.sh > $@+ && \
 	chmod +x $@+ && \
diff --git a/git-instaweb.sh b/git-instaweb.sh
index d96eddb..701c0d7 100755
--- a/git-instaweb.sh
+++ b/git-instaweb.sh
@@ -375,8 +375,15 @@ gitweb_css () {
 EOFGITWEB
 }
 
+gitweb_js () {
+	cat > "$1" <<\EOFGITWEB
+@@GITWEB_JS@@
+EOFGITWEB
+}
+
 gitweb_cgi "$GIT_DIR/gitweb/gitweb.cgi"
 gitweb_css "$GIT_DIR/gitweb/gitweb.css"
+gitweb_js  "$GIT_DIR/gitweb/gitweb.js"
 
 case "$httpd" in
 *lighttpd*)
diff --git a/gitweb/README b/gitweb/README
index 66c6a93..b69b0e5 100644
--- a/gitweb/README
+++ b/gitweb/README
@@ -92,6 +92,10 @@ You can specify the following configuration variables when building GIT:
    web browsers that support favicons (website icons) may display them
    in the browser's URL bar and next to site name in bookmarks).  Relative
    to base URI of gitweb.  [Default: git-favicon.png]
+ * GITWEB_JS
+   Points to the localtion where you put gitweb.js on your web server
+   (or to be more generic URI of JavaScript code used by gitweb).
+   Relative to base URI of gitweb.  [Default: gitweb.js]
  * GITWEB_CONFIG
    This Perl file will be loaded using 'do' and can be used to override any
    of the options above as well as some other options -- see the "Runtime
diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css
index 00e2e4c..69ef119 100644
--- a/gitweb/gitweb.css
+++ b/gitweb/gitweb.css
@@ -353,6 +353,17 @@ td.mode {
 	font-family: monospace;
 }
 
+/* progress of blame_interactive */
+div#progress_bar {
+	height: 2px;
+	margin-bottom: -2px;
+	background-color: #d8d9d0;
+}
+div#progress_info {
+	float: right;
+	text-align: right;
+}
+
 /* styling of diffs (patchsets): commitdiff and blobdiff views */
 div.diff.header,
 div.diff.extended_header {
diff --git a/gitweb/gitweb.js b/gitweb/gitweb.js
new file mode 100644
index 0000000..c8411e7
--- /dev/null
+++ b/gitweb/gitweb.js
@@ -0,0 +1,726 @@
+// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>
+//               2007, Petr Baudis <pasky@suse.cz>
+//          2008-2009, Jakub Narebski <jnareb@gmail.com>
+
+/**
+ * @fileOverview JavaScript code for gitweb (git web interface).
+ * @license GPLv2 or later
+ */
+
+/*
+ * This code uses DOM methods instead of (nonstandard) innerHTML
+ * to modify page.
+ *
+ * innerHTML is non-standard IE extension, though supported by most
+ * browsers; however Firefox up to version 1.5 didn't implement it in
+ * a strict mode (application/xml+xhtml mimetype).
+ *
+ * Also my simple benchmarks show that using elem.firstChild.data =
+ * 'content' is slightly faster than elem.innerHTML = 'content'.  It
+ * is however more fragile (text element fragment must exists), and
+ * less feature-rich (we cannot add HTML).
+ *
+ * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the
+ * equivalent using DOM 2 Core is usually shown in comments.
+ */
+
+
+/* ============================================================ */
+/* generic utility functions */
+
+
+/**
+ * pad number N with nonbreakable spaces on the left, to WIDTH characters
+ * example: padLeftStr(12, 3, '&nbsp;') == '&nbsp;12'
+ *          ('&nbsp;' is nonbreakable space)
+ *
+ * @param {Number|String} input: number to pad
+ * @param {Number} width: visible width of output
+ * @param {String} str: string to prefix to string, e.g. '&nbsp;'
+ * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR
+ */
+function padLeftStr(input, width, str) {
+	var prefix = '';
+
+	width -= input.toString().length;
+	while (width > 1) {
+		prefix += str;
+		width--;
+	}
+	return prefix + input;
+}
+
+/**
+ * Pad INPUT on the left to SIZE width, using given padding character CH,
+ * for example padLeft('a', 3, '_') is '__a'.
+ *
+ * @param {String} input: input value converted to string.
+ * @param {Number} width: desired length of output.
+ * @param {String} ch: single character to prefix to string.
+ *
+ * @returns {String} Modified string, at least SIZE length.
+ */
+function padLeft(input, width, ch) {
+	var s = input + "";
+	while (s.length < width) {
+		s = ch + s;
+	}
+	return s;
+}
+
+/**
+ * Create XMLHttpRequest object in cross-browser way
+ * @returns XMLHttpRequest object, or null
+ */
+function createRequestObject() {
+	try {
+		return new XMLHttpRequest();
+	} catch (e) {}
+	try {
+		return window.createRequest();
+	} catch (e) {}
+	try {
+		return new ActiveXObject("Msxml2.XMLHTTP");
+	} catch (e) {}
+	try {
+		return new ActiveXObject("Microsoft.XMLHTTP");
+	} catch (e) {}
+
+	return null;
+}
+
+/* ============================================================ */
+/* utility/helper functions (and variables) */
+
+var xhr;        // XMLHttpRequest object
+var projectUrl; // partial query + separator ('?' or ';')
+
+// 'commits' is an associative map. It maps SHA1s to Commit objects.
+var commits = {};
+
+/**
+ * constructor for Commit objects, used in 'blame'
+ * @class Represents a blamed commit
+ * @param {String} sha1: SHA-1 identifier of a commit
+ */
+function Commit(sha1) {
+	if (this instanceof Commit) {
+		this.sha1 = sha1;
+		this.nprevious = 0; /* number of 'previous', effective parents */
+	} else {
+		return new Commit(sha1);
+	}
+}
+
+/* ............................................................ */
+/* progress info, timing, error reporting */
+
+var blamedLines = 0;
+var totalLines  = '???';
+var div_progress_bar;
+var div_progress_info;
+
+/**
+ * Detects how many lines does a blamed file have,
+ * This information is used in progress info
+ *
+ * @returns {Number|String} Number of lines in file, or string '...'
+ */
+function countLines() {
+	var table =
+		document.getElementById('blame_table') ||
+		document.getElementsByTagName('table')[0];
+
+	if (table) {
+		return table.getElementsByTagName('tr').length - 1; // for header
+	} else {
+		return '...';
+	}
+}
+
+/**
+ * update progress info and length (width) of progress bar
+ *
+ * @globals div_progress_info, div_progress_bar, blamedLines, totalLines
+ */
+function updateProgressInfo() {
+	if (!div_progress_info) {
+		div_progress_info = document.getElementById('progress_info');
+	}
+	if (!div_progress_bar) {
+		div_progress_bar = document.getElementById('progress_bar');
+	}
+	if (!div_progress_info && !div_progress_bar) {
+		return;
+	}
+
+	var percentage = Math.floor(100.0*blamedLines/totalLines);
+
+	if (div_progress_info) {
+		div_progress_info.firstChild.data  = blamedLines + ' / ' + totalLines +
+			' (' + padLeftStr(percentage, 3, '&nbsp;') + '%)';
+	}
+
+	if (div_progress_bar) {
+		//div_progress_bar.setAttribute('style', 'width: '+percentage+'%;');
+		div_progress_bar.style.width = percentage + '%';
+	}
+}
+
+
+var t_interval_server = '';
+var cmds_server = '';
+var t0 = new Date();
+
+/**
+ * write how much it took to generate data, and to run script
+ *
+ * @globals t0, t_interval_server, cmds_server
+ */
+function writeTimeInterval() {
+	var info_time = document.getElementById('generating_time');
+	if (!info_time || !t_interval_server) {
+		return;
+	}
+	var t1 = new Date();
+	info_time.firstChild.data += ' + (' +
+		t_interval_server + ' sec server blame_data / ' +
+		(t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)';
+
+	var info_cmds = document.getElementById('generating_cmd');
+	if (!info_time || !cmds_server) {
+		return;
+	}
+	info_cmds.firstChild.data += ' + ' + cmds_server;
+}
+
+/**
+ * show an error message alert to user within page (in prohress info area)
+ * @param {String} str: plain text error message (no HTML)
+ *
+ * @globals div_progress_info
+ */
+function errorInfo(str) {
+	if (!div_progress_info) {
+		div_progress_info = document.getElementById('progress_info');
+	}
+	if (div_progress_info) {
+		div_progress_info.className = 'error';
+		div_progress_info.firstChild.data = str;
+	}
+}
+
+/* ............................................................ */
+/* coloring rows like 'blame' after 'blame_data' finishes */
+
+/**
+ * returns true if given row element (tr) is first in commit group
+ * to be used only after 'blame_data' finishes (after processing)
+ *
+ * @param {HTMLElement} tr: table row
+ * @returns {Boolean} true if TR is first in commit group
+ */
+function isStartOfGroup(tr) {
+	return tr.firstChild.className === 'sha1';
+}
+
+var colorRe = /(?:light|dark)/;
+
+/**
+ * change colors to use zebra coloring (2 colors) instead of 3 colors
+ * concatenate neighbour commit groups belonging to the same commit
+ *
+ * @globals colorRe
+ */
+function fixColorsAndGroups() {
+	var colorClasses = ['light', 'dark'];
+	var linenum = 1;
+	var tr, prev_group;
+	var colorClass = 0;
+	var table =
+		document.getElementById('blame_table') ||
+		document.getElementsByTagName('table')[0];
+
+	while ((tr = document.getElementById('l'+linenum))) {
+	// index origin is 0, which is table header; start from 1
+	//while ((tr = table.rows[linenum])) { // <- it is slower
+		if (isStartOfGroup(tr, linenum, document)) {
+			if (prev_group &&
+			    prev_group.firstChild.firstChild.href ===
+			            tr.firstChild.firstChild.href) {
+				// we have to concatenate groups
+				var prev_rows = prev_group.firstChild.rowSpan || 1;
+				var curr_rows =         tr.firstChild.rowSpan || 1;
+				prev_group.firstChild.rowSpan = prev_rows + curr_rows;
+				//tr.removeChild(tr.firstChild);
+				tr.deleteCell(0); // DOM2 HTML way
+			} else {
+				colorClass = (colorClass + 1) % 2;
+				prev_group = tr;
+			}
+		}
+		var tr_class = tr.className;
+		tr.className = tr_class.replace(colorRe, colorClasses[colorClass]);
+		linenum++;
+	}
+}
+
+/* ............................................................ */
+/* time and data */
+
+/**
+ * used to extract hours and minutes from timezone info, e.g '-0900'
+ * @constant
+ */
+var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/;
+
+/**
+ * return date in local time formatted in iso-8601 like format
+ * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200'
+ *
+ * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'
+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
+ * @returns {String} date in local time in iso-8601 like format
+ *
+ * @globals tzRe
+ */
+function formatDateISOLocal(epoch, timezoneInfo) {
+	var match = tzRe.exec(timezoneInfo);
+	// date corrected by timezone
+	var localDate = new Date(1000 * (epoch +
+		(parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));
+	var localDateStr = // e.g. '2005-08-07'
+		localDate.getUTCFullYear()                 + '-' +
+		padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' +
+		padLeft(localDate.getUTCDate(),    2, '0');
+	var localTimeStr = // e.g. '21:49:46'
+		padLeft(localDate.getUTCHours(),   2, '0') + ':' +
+		padLeft(localDate.getUTCMinutes(), 2, '0') + ':' +
+		padLeft(localDate.getUTCSeconds(), 2, '0');
+
+	return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;
+}
+
+/* ............................................................ */
+/* unquoting/unescaping filenames */
+
+/**#@+
+ * @constant
+ */
+var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g;
+var octEscRe = /^[0-7]{1,3}$/;
+var maybeQuotedRe = /^\"(.*)\"$/;
+/**#@-*/
+
+/**
+ * unquote maybe git-quoted filename
+ * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a	a'
+ *
+ * @param {String} str: git-quoted string
+ * @returns {String} Unquoted and unescaped string
+ *
+ * @globals escCodeRe, octEscRe, maybeQuotedRe
+ */
+function unquote(str) {
+	function unq(seq) {
+		var es = {
+			// character escape codes, aka escape sequences (from C)
+			// replacements are to some extent JavaScript specific
+			t: "\t",   // tab            (HT, TAB)
+			n: "\n",   // newline        (NL)
+			r: "\r",   // return         (CR)
+			f: "\f",   // form feed      (FF)
+			b: "\b",   // backspace      (BS)
+			a: "\x07", // alarm (bell)   (BEL)
+			e: "\x1B", // escape         (ESC)
+			v: "\v"    // vertical tab   (VT)
+		};
+
+		if (seq.search(octEscRe) !== -1) {
+			// octal char sequence
+			return String.fromCharCode(parseInt(seq, 8));
+		} else if (seq in es) {
+			// C escape sequence, aka character escape code
+			return es[seq];
+		}
+		// quoted ordinary character
+		return seq;
+	}
+
+	var match = str.match(maybeQuotedRe);
+	if (match) {
+		str = match[1];
+		// perhaps str = eval('"'+str+'"'); would be enough?
+		str = str.replace(escCodeRe,
+			function (substr, p1, offset, s) { return unq(p1); });
+	}
+	return str;
+}
+
+/* ============================================================ */
+/* main part: parsing response */
+
+/**
+ * Function called for each blame entry, as soon as it finishes.
+ * It updates page via DOM manipulation, adding sha1 info, etc.
+ *
+ * @param {Commit} commit: blamed commit
+ * @param {Object} group: object representing group of lines,
+ *                        which blame the same commit (blame entry)
+ *
+ * @globals blamedLines
+ */
+function handleLine(commit, group) {
+	/* 
+	   This is the structure of the HTML fragment we are working
+	   with:
+
+	   <tr id="l123" class="">
+	     <td class="sha1" title=""><a href=""> </a></td>
+	     <td class="linenr"><a class="linenr" href="">123</a></td>
+	     <td class="pre"># times (my ext3 doesn&#39;t).</td>
+	   </tr>
+	*/
+
+	var resline = group.resline;
+
+	// format date and time string only once per commit
+	if (!commit.info) {
+		/* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */
+		commit.info = commit.author + ', ' +
+			formatDateISOLocal(commit.authorTime, commit.authorTimezone);
+	}
+
+	// loop over lines in commit group
+	for (var i = 0; i < group.numlines; i++, resline++) {
+		var tr = document.getElementById('l'+resline);
+		if (!tr) {
+			break;
+		}
+		/*
+			<tr id="l123" class="">
+			  <td class="sha1" title=""><a href=""> </a></td>
+			  <td class="linenr"><a class="linenr" href="">123</a></td>
+			  <td class="pre"># times (my ext3 doesn&#39;t).</td>
+			</tr>
+		*/
+		var td_sha1  = tr.firstChild;
+		var a_sha1   = td_sha1.firstChild;
+		var a_linenr = td_sha1.nextSibling.firstChild;
+
+		/* <tr id="l123" class=""> */
+		var tr_class = 'light'; // or tr.className
+		if (commit.boundary) {
+			tr_class += ' boundary';
+		}
+		if (commit.nprevious === 0) {
+			tr_class += ' no-previous';
+		} else if (commit.nprevious > 1) {
+			tr_class += ' multiple-previous';
+		}
+		tr.className = tr_class;
+
+		/* <td class="sha1" title="?" rowspan="?"><a href="?">?</a></td> */
+		if (i === 0) {
+			td_sha1.title = commit.info;
+			td_sha1.rowSpan = group.numlines;
+
+			a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1;
+			a_sha1.firstChild.data = commit.sha1.substr(0, 8);
+			if (group.numlines >= 2) {
+				var fragment = document.createDocumentFragment();
+				var br   = document.createElement("br");
+				var text = document.createTextNode(
+					commit.author.match(/\b([A-Z])\B/g).join(''));
+				if (br && text) {
+					var elem = fragment || td_sha1;
+					elem.appendChild(br);
+					elem.appendChild(text);
+					if (fragment) {
+						td_sha1.appendChild(fragment);
+					}
+				}
+			}
+		} else {
+			//tr.removeChild(td_sha1); // DOM2 Core way
+			tr.deleteCell(0); // DOM2 HTML way
+		}
+
+		/* <td class="linenr"><a class="linenr" href="?">123</a></td> */
+		var linenr_commit =
+			('previous' in commit ? commit.previous : commit.sha1);
+		var linenr_filename =
+			('file_parent' in commit ? commit.file_parent : commit.filename);
+		a_linenr.href = projectUrl + 'a=blame_incremental' +
+			';hb=' + linenr_commit +
+			';f='  + encodeURIComponent(linenr_filename) +
+			'#l' + (group.srcline + i);
+
+		blamedLines++;
+
+		//updateProgressInfo();
+	}
+}
+
+// ----------------------------------------------------------------------
+
+var inProgress = false;   // are we processing response
+
+/**#@+
+ * @constant
+ */
+var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/;
+var infoRe = /^([a-z-]+) ?(.*)/;
+var endRe  = /^END ?([^ ]*) ?(.*)/;
+/**@-*/
+
+var curCommit = new Commit();
+var curGroup  = {};
+
+var pollTimer = null;
+
+/**
+ * Parse output from 'git blame --incremental [...]', received via
+ * XMLHttpRequest from server (blamedataUrl), and call handleLine
+ * (which updates page) as soon as blame entry is completed.
+ *
+ * @param {String[]} lines: new complete lines from blamedata server
+ *
+ * @globals commits, curCommit, curGroup, t_interval_server, cmds_server
+ * @globals sha1Re, infoRe, endRe
+ */
+function processBlameLines(lines) {
+	var match;
+
+	for (var i = 0, len = lines.length; i < len; i++) {
+
+		if ((match = sha1Re.exec(lines[i]))) {
+			var sha1 = match[1];
+			var srcline  = parseInt(match[2], 10);
+			var resline  = parseInt(match[3], 10);
+			var numlines = parseInt(match[4], 10);
+
+			var c = commits[sha1];
+			if (!c) {
+				c = new Commit(sha1);
+				commits[sha1] = c;
+			}
+			curCommit = c;
+
+			curGroup.srcline = srcline;
+			curGroup.resline = resline;
+			curGroup.numlines = numlines;
+
+		} else if ((match = infoRe.exec(lines[i]))) {
+			var info = match[1];
+			var data = match[2];
+			switch (info) {
+			case 'filename':
+				curCommit.filename = unquote(data);
+				// 'filename' information terminates the entry
+				handleLine(curCommit, curGroup);
+				updateProgressInfo();
+				break;
+			case 'author':
+				curCommit.author = data;
+				break;
+			case 'author-time':
+				curCommit.authorTime = parseInt(data, 10);
+				break;
+			case 'author-tz':
+				curCommit.authorTimezone = data;
+				break;
+			case 'previous':
+				curCommit.nprevious++;
+				// store only first 'previous' header
+				if (!'previous' in curCommit) {
+					var parts = data.split(' ', 2);
+					curCommit.previous    = parts[0];
+					curCommit.file_parent = unquote(parts[1]);
+				}
+				break;
+			case 'boundary':
+				curCommit.boundary = true;
+				break;
+			} // end switch
+
+		} else if ((match = endRe.exec(lines[i]))) {
+			t_interval_server = match[1];
+			cmds_server = match[2];
+
+		} else if (lines[i] !== '') {
+			// malformed line
+
+		} // end if (match)
+
+	} // end for (lines)
+}
+
+/**
+ * Process new data and return pointer to end of processed part
+ *
+ * @param {String} unprocessed: new data (from nextReadPos)
+ * @param {Number} nextReadPos: end of last processed data
+ * @return {Number} end of processed data (new value for nextReadPos)
+ */
+function processData(unprocessed, nextReadPos) {
+	var lastLineEnd = unprocessed.lastIndexOf('\n');
+	if (lastLineEnd !== -1) {
+		var lines = unprocessed.substring(0, lastLineEnd).split('\n');
+		nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */;
+
+		processBlameLines(lines);
+	} // end if
+
+	return nextReadPos;
+}
+
+/**
+ * Handle XMLHttpRequest errors
+ *
+ * @param {XMLHttpRequest} xhr: XMLHttpRequest object
+ *
+ * @globals pollTimer, commits, inProgress
+ */
+function handleError(xhr) {
+	errorInfo('Server error: ' +
+		xhr.status + ' - ' + (xhr.statusText || 'Error contacting server'));
+
+	clearInterval(pollTimer);
+	commits = {}; // free memory
+
+	inProgress = false;
+}
+
+/**
+ * Called after XMLHttpRequest finishes (loads)
+ *
+ * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused)
+ *
+ * @globals pollTimer, commits, inProgress
+ */
+function responseLoaded(xhr) {
+	clearInterval(pollTimer);
+
+	fixColorsAndGroups();
+	writeTimeInterval();
+	commits = {}; // free memory
+
+	inProgress = false;
+}
+
+/**
+ * handler for XMLHttpRequest onreadystatechange event
+ * @see startBlame
+ *
+ * @globals xhr, inProgress
+ */
+function handleResponse() {
+
+	/*
+	 * xhr.readyState
+	 *
+	 *  Value  Constant (W3C)    Description
+	 *  -------------------------------------------------------------------
+	 *  0      UNSENT            open() has not been called yet.
+	 *  1      OPENED            send() has not been called yet.
+	 *  2      HEADERS_RECEIVED  send() has been called, and headers
+	 *                           and status are available.
+	 *  3      LOADING           Downloading; responseText holds partial data.
+	 *  4      DONE              The operation is complete.
+	 */
+
+	if (xhr.readyState !== 4 && xhr.readyState !== 3) {
+		return;
+	}
+
+	// the server returned error
+	if (xhr.readyState === 3 && xhr.status !== 200) {
+		return;
+	}
+	if (xhr.readyState === 4 && xhr.status !== 200) {
+		handleError(xhr);
+		return;
+	}
+
+	// In konqueror xhr.responseText is sometimes null here...
+	if (xhr.responseText === null) {
+		return;
+	}
+
+	// in case we were called before finished processing
+	if (inProgress) {
+		return;
+	} else {
+		inProgress = true;
+	}
+
+	// extract new whole (complete) lines, and process them
+	while (xhr.prevDataLength !== xhr.responseText.length) {
+		if (xhr.readyState === 4 &&
+		    xhr.prevDataLength === xhr.responseText.length) {
+			break;
+		}
+
+		xhr.prevDataLength = xhr.responseText.length;
+		var unprocessed = xhr.responseText.substring(xhr.nextReadPos);
+		xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos);
+	} // end while
+
+	// did we finish work?
+	if (xhr.readyState === 4 &&
+	    xhr.prevDataLength === xhr.responseText.length) {
+		responseLoaded(xhr);
+	}
+
+	inProgress = false;
+}
+
+// ============================================================
+// ------------------------------------------------------------
+
+/**
+ * Incrementally update line data in blame_incremental view in gitweb.
+ *
+ * @param {String} blamedataUrl: URL to server script generating blame data.
+ * @param {String} bUrl: partial URL to project, used to generate links.
+ *
+ * Called from 'blame_incremental' view after loading table with
+ * file contents, a base for blame view.
+ *
+ * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer
+*/
+function startBlame(blamedataUrl, bUrl) {
+
+	xhr = createRequestObject();
+	if (!xhr) {
+		errorInfo('ERROR: XMLHttpRequest not supported');
+		return;
+	}
+
+	t0 = new Date();
+	projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';');
+	if ((div_progress_bar = document.getElementById('progress_bar'))) {
+		//div_progress_bar.setAttribute('style', 'width: 100%;');
+		div_progress_bar.style.cssText = 'width: 100%;';
+	}
+	totalLines = countLines();
+	updateProgressInfo();
+
+	/* add extra properties to xhr object to help processing response */
+	xhr.prevDataLength = -1;  // used to detect if we have new data
+	xhr.nextReadPos = 0;      // where unread part of response starts
+
+	xhr.onreadystatechange = handleResponse;
+	//xhr.onreadystatechange = function () { handleResponse(xhr); };
+
+	xhr.open('GET', blamedataUrl);
+	xhr.setRequestHeader('Accept', 'text/plain');
+	xhr.send(null);
+
+	// not all browsers call onreadystatechange event on each server flush
+	// poll response using timer every second to handle this issue
+	pollTimer = setInterval(xhr.onreadystatechange, 1000);
+}
+
+// end of gitweb.js
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index 08d410d..88c91ff 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -96,6 +96,8 @@ our $stylesheet = undef;
 our $logo = "++GITWEB_LOGO++";
 # URI of GIT favicon, assumed to be image/png type
 our $favicon = "++GITWEB_FAVICON++";
+# URI of gitweb.js (JavaScript code for gitweb)
+our $javascript = "++GITWEB_JS++";
 
 # URI and label (title) of GIT logo link
 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
@@ -575,6 +577,8 @@ our %cgi_param_mapping = @cgi_param_mapping;
 # we will also need to know the possible actions, for validation
 our %actions = (
 	"blame" => \&git_blame,
+	"blame_incremental" => \&git_blame_incremental,
+	"blame_data" => \&git_blame_data,
 	"blobdiff" => \&git_blobdiff,
 	"blobdiff_plain" => \&git_blobdiff_plain,
 	"blob" => \&git_blob,
@@ -4800,7 +4804,9 @@ sub git_tag {
 	git_footer_html();
 }
 
-sub git_blame {
+sub git_blame_common {
+	my $format = shift || 'porcelain';
+
 	# permissions
 	gitweb_check_feature('blame')
 		or die_error(403, "Blame view not allowed");
@@ -4822,10 +4828,43 @@ sub git_blame {
 		}
 	}
 
-	# run git-blame --porcelain
-	open my $fd, "-|", git_cmd(), "blame", '-p',
-		$hash_base, '--', $file_name
-		or die_error(500, "Open git-blame failed");
+	my $fd;
+	if ($format eq 'incremental') {
+		# get file contents (as base)
+		open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
+			or die_error(500, "Open git-cat-file failed");
+	} elsif ($format eq 'data') {
+		# run git-blame --incremental
+		open $fd, "-|", git_cmd(), "blame", "--incremental",
+			$hash_base, "--", $file_name
+			or die_error(500, "Open git-blame --incremental failed");
+	} else {
+		# run git-blame --porcelain
+		open $fd, "-|", git_cmd(), "blame", '-p',
+			$hash_base, '--', $file_name
+			or die_error(500, "Open git-blame --porcelain failed");
+	}
+
+	# incremental blame data returns early
+	if ($format eq 'data') {
+		print $cgi->header(
+			-type=>"text/plain", -charset => "utf-8",
+			-status=> "200 OK");
+		local $| = 1; # output autoflush
+		print while <$fd>;
+		close $fd
+			or print "ERROR $!\n";
+
+		print 'END';
+		if (defined $t0 && gitweb_check_feature('timed')) {
+			print ' '.
+			      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
+			      ' '.$number_of_git_cmds;
+		}
+		print "\n";
+
+		return;
+	}
 
 	# page header
 	git_header_html();
@@ -4836,109 +4875,170 @@ sub git_blame {
 		$cgi->a({-href => href(action=>"history", -replay=>1)},
 		        "history") .
 		" | " .
-		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
+		$cgi->a({-href => href(action=>$action, file_name=>$file_name)},
 		        "HEAD");
 	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 	git_print_page_path($file_name, $ftype, $hash_base);
 
 	# page body
+	if ($format eq 'incremental') {
+		print "<noscript>\n<div class=\"error\"><center><b>\n".
+		      "This page requires JavaScript to run.\n Use ".
+		      $cgi->a({-href => href(action=>'blame',-replay=>1)},
+		              'this page').
+		      " instead.\n".
+		      "</b></center></div>\n</noscript>\n";
+
+		print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
+	}
+
+	print qq!<div class="page_body">\n!;
+	print qq!<div id="progress_info">... / ...</div>\n!
+		if ($format eq 'incremental');
+	print qq!<table id="blame_table" class="blame" width="100%">\n!.
+	      #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
+	      qq!<thead>\n!.
+	      qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
+	      qq!</thead>\n!.
+	      qq!<tbody>\n!;
+
 	my @rev_color = qw(light dark);
 	my $num_colors = scalar(@rev_color);
 	my $current_color = 0;
-	my %metainfo = ();
 
-	print <<HTML;
-<div class="page_body">
-<table class="blame">
-<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
-HTML
- LINE:
-	while (my $line = <$fd>) {
-		chomp $line;
-		# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
-		# no <lines in group> for subsequent lines in group of lines
-		my ($full_rev, $orig_lineno, $lineno, $group_size) =
-		   ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
-		if (!exists $metainfo{$full_rev}) {
-			$metainfo{$full_rev} = { 'nprevious' => 0 };
-		}
-		my $meta = $metainfo{$full_rev};
-		my $data;
-		while ($data = <$fd>) {
-			chomp $data;
-			last if ($data =~ s/^\t//); # contents of line
-			if ($data =~ /^(\S+)(?: (.*))?$/) {
-				$meta->{$1} = $2 unless exists $meta->{$1};
+	if ($format eq 'incremental') {
+		my $color_class = $rev_color[$current_color];
+
+		#contents of a file
+		my $linenr = 0;
+	LINE:
+		while (my $line = <$fd>) {
+			chomp $line;
+			$linenr++;
+
+			print qq!<tr id="l$linenr" class="$color_class">!.
+			      qq!<td class="sha1"><a href=""> </a></td>!.
+			      qq!<td class="linenr">!.
+			      qq!<a class="linenr" href="">$linenr</a></td>!;
+			print qq!<td class="pre">! . esc_html($line) . "</td>\n";
+			print qq!</tr>\n!;
+		}
+
+	} else { # porcelain, i.e. ordinary blame
+		my %metainfo = (); # saves information about commits
+
+		# blame data
+	LINE:
+		while (my $line = <$fd>) {
+			chomp $line;
+			# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
+			# no <lines in group> for subsequent lines in group of lines
+			my ($full_rev, $orig_lineno, $lineno, $group_size) =
+			   ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
+			if (!exists $metainfo{$full_rev}) {
+				$metainfo{$full_rev} = { 'nprevious' => 0 };
 			}
-			if ($data =~ /^previous /) {
-				$meta->{'nprevious'}++;
+			my $meta = $metainfo{$full_rev};
+			my $data;
+			while ($data = <$fd>) {
+				chomp $data;
+				last if ($data =~ s/^\t//); # contents of line
+				if ($data =~ /^(\S+)(?: (.*))?$/) {
+					$meta->{$1} = $2 unless exists $meta->{$1};
+				}
+				if ($data =~ /^previous /) {
+					$meta->{'nprevious'}++;
+				}
 			}
-		}
-		my $short_rev = substr($full_rev, 0, 8);
-		my $author = $meta->{'author'};
-		my %date =
-			parse_date($meta->{'author-time'}, $meta->{'author-tz'});
-		my $date = $date{'iso-tz'};
-		if ($group_size) {
-			$current_color = ($current_color + 1) % $num_colors;
-		}
-		my $tr_class = $rev_color[$current_color];
-		$tr_class .= ' boundary' if (exists $meta->{'boundary'});
-		$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
-		$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
-		print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
-		if ($group_size) {
-			print "<td class=\"sha1\"";
-			print " title=\"". esc_html($author) . ", $date\"";
-			print " rowspan=\"$group_size\"" if ($group_size > 1);
-			print ">";
-			print $cgi->a({-href => href(action=>"commit",
-			                             hash=>$full_rev,
-			                             file_name=>$file_name)},
-			              esc_html($short_rev));
-			if ($group_size >= 2) {
-				my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
-				if (@author_initials) {
-					print "<br />" .
-					      esc_html(join('', @author_initials));
-					#           or join('.', ...)
+			my $short_rev = substr($full_rev, 0, 8);
+			my $author = $meta->{'author'};
+			my %date =
+				parse_date($meta->{'author-time'}, $meta->{'author-tz'});
+			my $date = $date{'iso-tz'};
+			if ($group_size) {
+				$current_color = ($current_color + 1) % $num_colors;
+			}
+			my $tr_class = $rev_color[$current_color];
+			$tr_class .= ' boundary' if (exists $meta->{'boundary'});
+			$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
+			$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
+			print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
+			if ($group_size) {
+				print "<td class=\"sha1\"";
+				print " title=\"". esc_html($author) . ", $date\"";
+				print " rowspan=\"$group_size\"" if ($group_size > 1);
+				print ">";
+				print $cgi->a({-href => href(action=>"commit",
+				                             hash=>$full_rev,
+				                             file_name=>$file_name)},
+				              esc_html($short_rev));
+				if ($group_size >= 2) {
+					my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
+					if (@author_initials) {
+						print "<br />" .
+						      esc_html(join('', @author_initials));
+						#           or join('.', ...)
+					}
 				}
+				print "</td>\n";
 			}
-			print "</td>\n";
-		}
-		# 'previous' <sha1 of parent commit> <filename at commit>
-		if (exists $meta->{'previous'} &&
-		    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
-			$meta->{'parent'} = $1;
-			$meta->{'file_parent'} = unquote($2);
-		}
-		my $linenr_commit =
-			exists($meta->{'parent'}) ?
-			$meta->{'parent'} : $full_rev;
-		my $linenr_filename =
-			exists($meta->{'file_parent'}) ?
-			$meta->{'file_parent'} : unquote($meta->{'filename'});
-		my $blamed = href(action => 'blame',
-		                  file_name => $linenr_filename,
-		                  hash_base => $linenr_commit);
-		print "<td class=\"linenr\">";
-		print $cgi->a({ -href => "$blamed#l$orig_lineno",
-		                -class => "linenr" },
-		              esc_html($lineno));
-		print "</td>";
-		print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
-		print "</tr>\n";
+			# 'previous' <sha1 of parent commit> <filename at commit>
+			if (exists $meta->{'previous'} &&
+			    $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
+				$meta->{'parent'} = $1;
+				$meta->{'file_parent'} = unquote($2);
+			}
+			my $linenr_commit =
+				exists($meta->{'parent'}) ?
+				$meta->{'parent'} : $full_rev;
+			my $linenr_filename =
+				exists($meta->{'file_parent'}) ?
+				$meta->{'file_parent'} : unquote($meta->{'filename'});
+			my $blamed = href(action => 'blame',
+			                  file_name => $linenr_filename,
+			                  hash_base => $linenr_commit);
+			print "<td class=\"linenr\">";
+			print $cgi->a({ -href => "$blamed#l$orig_lineno",
+			                -class => "linenr" },
+			              esc_html($lineno));
+			print "</td>";
+			print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
+			print "</tr>\n";
+		} # end while
+
 	}
-	print "</table>\n";
-	print "</div>";
+
+	# footer
+	print "</tbody>\n".
+	      "</table>\n"; # class="blame"
+	print "</div>\n";   # class="blame_body"
 	close $fd
 		or print "Reading blob failed\n";
 
-	# page footer
+	if ($format eq 'incremental') {
+		print qq!<script type="text/javascript" src="$javascript"></script>\n!.
+		      qq!<script type="text/javascript">\n!.
+		      qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
+		      qq!           "!. href() .qq!");\n!.
+		      qq!</script>\n!;
+	}
+
 	git_footer_html();
 }
 
+sub git_blame {
+	git_blame_common();
+}
+
+sub git_blame_incremental {
+	git_blame_common('incremental');
+}
+
+sub git_blame_data {
+	git_blame_common('data');
+}
+
 sub git_tags {
 	my $head = git_get_head_hash($project);
 	git_header_html();
-- 
1.6.3.3

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCHv1 3/5] gitweb: Colorize 'blame_incremental' view during processing
  2009-09-01 11:39   ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Jakub Narebski
@ 2009-09-01 11:39     ` Jakub Narebski
  2009-09-01 11:39       ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Jakub Narebski
  2009-11-05 20:22     ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Petr Baudis
  1 sibling, 1 reply; 12+ messages in thread
From: Jakub Narebski @ 2009-09-01 11:39 UTC (permalink / raw)
  To: git
  Cc: Petr Baudis, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov,
	Martin Koegler, Jakub Narebski

This requires using 3 colors, not only two, to be able to choose color
different from colors of up to 2 neigbours.

gitweb.js select least used color, if more than one color is possible.

Signed-off-by: Jakub Narebski <jnareb@gmail.com>
---
This was earlier part of previous commit (adding 'blame_incremental'
view).

 gitweb/gitweb.css |    5 ++
 gitweb/gitweb.js  |  108 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 110 insertions(+), 3 deletions(-)

diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css
index 69ef119..977a013 100644
--- a/gitweb/gitweb.css
+++ b/gitweb/gitweb.css
@@ -262,6 +262,11 @@ tr.no-previous td.linenr {
 	font-weight: bold;
 }
 
+/* for 'blame_incremental', during processing */
+tr.color1 { background-color: #f6fff6; }
+tr.color2 { background-color: #f6f6ff; }
+tr.color3 { background-color: #fff6f6; }
+
 td {
 	padding: 2px 5px;
 	font-size: 100%;
diff --git a/gitweb/gitweb.js b/gitweb/gitweb.js
index c8411e7..bf38216 100644
--- a/gitweb/gitweb.js
+++ b/gitweb/gitweb.js
@@ -211,6 +211,101 @@ function errorInfo(str) {
 }
 
 /* ............................................................ */
+/* coloring rows during blame_data (git blame --incremental) run */
+
+/**
+ * used to extract N from 'colorN', where N is a number,
+ * @constant
+ */
+var colorRe = /\bcolor([0-9]*)\b/;
+
+/**
+ * return N if <tr class="colorN">, otherwise return null
+ * (some browsers require CSS class names to begin with letter)
+ *
+ * @param {HTMLElement} tr: table row element to check
+ * @param {String} tr.className: 'class' attribute of tr element
+ * @returns {Number|null} N if tr.className == 'colorN', otherwise null
+ *
+ * @globals colorRe
+ */
+function getColorNo(tr) {
+	if (!tr) {
+		return null;
+	}
+	var className = tr.className;
+	if (className) {
+		var match = colorRe.exec(className);
+		if (match) {
+			return parseInt(match[1], 10);
+		}
+	}
+	return null;
+}
+
+var colorsFreq = [0, 0, 0];
+/**
+ * return one of given possible colors (curently least used one)
+ * example: chooseColorNoFrom(2, 3) returns 2 or 3
+ *
+ * @param {Number[]} arguments: one or more numbers
+ *        assumes that  1 <= arguments[i] <= colorsFreq.length
+ * @returns {Number} Least used color number from arguments
+ * @globals colorsFreq
+ */
+function chooseColorNoFrom() {
+	// choose the color which is least used
+	var colorNo = arguments[0];
+	for (var i = 1; i < arguments.length; i++) {
+		if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) {
+			colorNo = arguments[i];
+		}
+	}
+	colorsFreq[colorNo-1]++;
+	return colorNo;
+}
+
+/**
+ * given two neigbour <tr> elements, find color which would be different
+ * from color of both of neighbours; used to 3-color blame table
+ *
+ * @param {HTMLElement} tr_prev
+ * @param {HTMLElement} tr_next
+ * @returns {Number} color number N such that
+ * colorN != tr_prev.className && colorN != tr_next.className
+ */
+function findColorNo(tr_prev, tr_next) {
+	var color_prev = getColorNo(tr_prev);
+	var color_next = getColorNo(tr_next);
+
+
+	// neither of neighbours has color set
+	// THEN we can use any of 3 possible colors
+	if (!color_prev && !color_next) {
+		return chooseColorNoFrom(1,2,3);
+	}
+
+	// either both neighbours have the same color,
+	// or only one of neighbours have color set
+	// THEN we can use any color except given
+	var color;
+	if (color_prev === color_next) {
+		color = color_prev; // = color_next;
+	} else if (!color_prev) {
+		color = color_next;
+	} else if (!color_next) {
+		color = color_prev;
+	}
+	if (color) {
+		return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1);
+	}
+
+	// neighbours have different colors
+	// THEN there is only one color left
+	return (3 - ((color_prev + color_next) % 3));
+}
+
+/* ............................................................ */
 /* coloring rows like 'blame' after 'blame_data' finishes */
 
 /**
@@ -224,8 +319,6 @@ function isStartOfGroup(tr) {
 	return tr.firstChild.className === 'sha1';
 }
 
-var colorRe = /(?:light|dark)/;
-
 /**
  * change colors to use zebra coloring (2 colors) instead of 3 colors
  * concatenate neighbour commit groups belonging to the same commit
@@ -391,6 +484,12 @@ function handleLine(commit, group) {
 			formatDateISOLocal(commit.authorTime, commit.authorTimezone);
 	}
 
+	// color depends on group of lines, not only on blamed commit
+	var colorNo = findColorNo(
+		document.getElementById('l'+(resline-1)),
+		document.getElementById('l'+(resline+group.numlines))
+	);
+
 	// loop over lines in commit group
 	for (var i = 0; i < group.numlines; i++, resline++) {
 		var tr = document.getElementById('l'+resline);
@@ -409,7 +508,10 @@ function handleLine(commit, group) {
 		var a_linenr = td_sha1.nextSibling.firstChild;
 
 		/* <tr id="l123" class=""> */
-		var tr_class = 'light'; // or tr.className
+		var tr_class = '';
+		if (colorNo !== null) {
+			tr_class = 'color'+colorNo;
+		}
 		if (commit.boundary) {
 			tr_class += ' boundary';
 		}
-- 
1.6.3.3

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript
  2009-09-01 11:39     ` [PATCHv1 3/5] gitweb: Colorize 'blame_incremental' view during processing Jakub Narebski
@ 2009-09-01 11:39       ` Jakub Narebski
  2009-09-01 11:39         ` [PATCHv1/RFC 5/5] gitweb: Minify gitweb.js if JSMIN is defined Jakub Narebski
  2009-11-05 20:33         ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Petr Baudis
  0 siblings, 2 replies; 12+ messages in thread
From: Jakub Narebski @ 2009-09-01 11:39 UTC (permalink / raw)
  To: git
  Cc: Petr Baudis, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov,
	Martin Koegler, Jakub Narebski

The new 'blame_incremental' view requires JavaScript to run.  Not all
web browsers implement JavaScript (e.g. text browsers such as Lynx),
and not all users have JavaScript enabled.  Therefore instead of
unconditionally linking to 'blame_incremental' view, we use JavaScript
to convert those links to lead to view utilizing JavaScript, by adding
'js=1' to link.

Currently the only action that takes 'js=1' into account is 'blame',
which then acts as if it was called as 'blame_incremental' action.
Possible enhancement would be to do JavaScript redirect by setting
window.location instead of modifying $format and $action in
git_blame_common() subroutine.

The only JavaScript-aware/using view is currently 'blame_incremental'.
While at it move reading JavaScript to git_footer_html() subroutine.
Note that in this view we do not add 'js=1' currently (even though
perhaps we should; note that for consistency we should also add 'js=1'
in links added by JavaScript part of 'blame_incremental').


This idea was originally implemented by Petr Baudis in
  http://article.gmane.org/gmane.comp.version-control.git/47614
but it added <script> element with fixBlameLinks() function in page
header, to be added as onload event using 'onload' attribute of HTML
'body' element: <body onload="fixBlameLinks();">.  This version adds
script at then end of page (in the page footer), and uses JavaScript
'window.onload=fixLinks();'.  Also in Petr version only links marked
with 'blamelink' class were modified, and they were modified by
replacing "a=blame" by "a=blame_incremental"... which doesn't work for
path_info links, and might replace wrong part if there is "a=blame" in
project name, ref name or file name.

Slightly different solution was implemented by Martin Koegler in
  http://thread.gmane.org/gmane.comp.version-control.git/47902/focus=47905
Here GitAddLinks() function was in gitweb.js file, not as contents of
<script> element.  It was also included in page header (in <head>
element) though, which means waiting for a script to load (and run).
It was smarter in that to "fix" (modify) link, it split URL, modified
value of 'a' parameter, and then recreated modified link.  It avoids
trouble with "a=blame" as substring in project name or file name, but
it doesn't work with path_info URL/link in the way it was written.

Signed-off-by: Jakub Narebski <jnareb@gmail.com>
---
This patch is in RFC stage.

Currently fixLinks() is not invoked for 'blame_incremental', but links
generated by startBlame (or to be more exact by event handler in
gitweb.js) lead to 'blame_incremental' directly.  Also the list of
"JavaScript-aware actions" is currently hardcoded to
'blame_incremental' (and relies on 'blame' + 'js=1' to change
$action).

Also fixLinks() adds 'js=1' to too many links, including $home_link,
and links to feeds (where JavaScript is not supported anyway).
Alternate solution would be to mark links which lead to action which
has JavaScript-requiring alternative, either marking it via 'class'
attribute, or providing for example 'alt_href' or 'jshref' attribute
with link to JavaScript-requiring variant.


Differences from previous version (v2):
* fixLinks() function is put in gitweb.js together with all JavaScript
  code required for 'blame_incremental' view, instead of being defined
  inside <script> element.
* Running startBlame() was moved to git_footer_html, instead of being
  in git_blame_common
* The link to plain 'blame' view (which do not require JavaScript
  support enabled in web browser) inside <noscript> element is marked
  with 'js=0'.
* fixLinks() now doesn't add 'js=1' if links ends with '[?;]js=[01]'
  (so that e.g. 'js=0' means link to view which do not require
  JavaScript).
* performance: calculate number of elements in a list before loop


TODO for future commits:
* Use 'click' event to change links to jave 'js=1' parameter appended;
  this way we would check if JavaScript is enabled at the moment of
  following (clicking) link, not at the moment of loading the page.

  Unfortunately adding event listeners (much better solution than
  providing/adding 'onclick' attribute) is different in different
  browsers.  Some kind of wrapper around browser incompatibilities
  would be required.

 gitweb/gitweb.js   |   34 ++++++++++++++++++++++++++++++++++
 gitweb/gitweb.perl |   28 +++++++++++++++++++---------
 2 files changed, 53 insertions(+), 9 deletions(-)

diff --git a/gitweb/gitweb.js b/gitweb/gitweb.js
index bf38216..66fd64e 100644
--- a/gitweb/gitweb.js
+++ b/gitweb/gitweb.js
@@ -7,6 +7,39 @@
  * @license GPLv2 or later
  */
 
+/* ============================================================ */
+/* functions for generic gitweb actions and views */
+
+/**
+ * used to check if link has 'js' query parameter already (at end),
+ * and other reasons to not add 'js=1' param at the end of link
+ * @constant
+ */
+var jsExceptionsRe = /[;?]js=[01]$/;
+
+/**
+ * Add '?js=1' or ';js=1' to the end of every link in the document
+ * that doesn't have 'js' query parameter set already.
+ *
+ * Links with 'js=1' lead to JavaScript version of given action, if it
+ * exists (currently there is only 'blame_incremental' for 'blame')
+ *
+ * @globals jsExceptionsRe
+ */
+function fixLinks() {
+	var allLinks = document.getElementsByTagName("a") || document.links;
+	for (var i = 0, len = allLinks.length; i < len; i++) {
+		var link = allLinks[i];
+		if (!jsExceptionsRe.test(link)) { // =~ /[;?]js=[01]$/;
+			link.href +=
+				(link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1';
+		}
+	}
+}
+
+
+/* ============================================================ */
+
 /*
  * This code uses DOM methods instead of (nonstandard) innerHTML
  * to modify page.
@@ -89,6 +122,7 @@ function createRequestObject() {
 	return null;
 }
 
+
 /* ============================================================ */
 /* utility/helper functions (and variables) */
 
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index 88c91ff..0adfd3f 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -571,6 +571,8 @@ our @cgi_param_mapping = (
 	snapshot_format => "sf",
 	extra_options => "opt",
 	search_use_regexp => "sr",
+	# this must be last entry (for manipulation from JavaScript)
+	javascript => "js"
 );
 our %cgi_param_mapping = @cgi_param_mapping;
 
@@ -3255,6 +3257,18 @@ sub git_footer_html {
 		insert_file($site_footer);
 	}
 
+	print qq!<script type="text/javascript" src="$javascript"></script>\n!;
+	if ($action eq 'blame_incremental') {
+		print qq!<script type="text/javascript">\n!.
+		      qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
+		      qq!           "!. href() .qq!");\n!.
+		      qq!</script>\n!;
+	} else {
+		print qq!<script type="text/javascript">\n!.
+		      qq!window.onload = fixLinks;\n!.
+		      qq!</script>\n!;
+	}
+
 	print "</body>\n" .
 	      "</html>";
 }
@@ -4806,6 +4820,10 @@ sub git_tag {
 
 sub git_blame_common {
 	my $format = shift || 'porcelain';
+	if ($format eq 'porcelain' && $cgi->param('js')) {
+		$format = 'incremental';
+		$action = 'blame_incremental'; # for page title etc
+	}
 
 	# permissions
 	gitweb_check_feature('blame')
@@ -4885,7 +4903,7 @@ sub git_blame_common {
 	if ($format eq 'incremental') {
 		print "<noscript>\n<div class=\"error\"><center><b>\n".
 		      "This page requires JavaScript to run.\n Use ".
-		      $cgi->a({-href => href(action=>'blame',-replay=>1)},
+		      $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
 		              'this page').
 		      " instead.\n".
 		      "</b></center></div>\n</noscript>\n";
@@ -5016,14 +5034,6 @@ sub git_blame_common {
 	close $fd
 		or print "Reading blob failed\n";
 
-	if ($format eq 'incremental') {
-		print qq!<script type="text/javascript" src="$javascript"></script>\n!.
-		      qq!<script type="text/javascript">\n!.
-		      qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
-		      qq!           "!. href() .qq!");\n!.
-		      qq!</script>\n!;
-	}
-
 	git_footer_html();
 }
 
-- 
1.6.3.3

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCHv1/RFC 5/5] gitweb: Minify gitweb.js if JSMIN is defined
  2009-09-01 11:39       ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Jakub Narebski
@ 2009-09-01 11:39         ` Jakub Narebski
  2009-11-05 20:33         ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Petr Baudis
  1 sibling, 0 replies; 12+ messages in thread
From: Jakub Narebski @ 2009-09-01 11:39 UTC (permalink / raw)
  To: git
  Cc: Petr Baudis, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov,
	Martin Koegler, Jakub Narebski

It requires that $JSMIN command can function as a filter.

Signed-off-by: Jakub Narebski <jnareb@gmail.com>
---
This patch is new in series.  Comments welcome.

I have not tested it extensively, but JSMIN=cat seems to work
correctly.

 Makefile |   20 ++++++++++++++++++++
 1 files changed, 20 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile
index 407b35c..f149a36 100644
--- a/Makefile
+++ b/Makefile
@@ -194,6 +194,9 @@ all::
 # memory allocators with the nedmalloc allocator written by Niall Douglas.
 #
 # Define NO_REGEX if you have no or inferior regex support in your C library.
+#
+# Define JSMIN to point to JavaScript minifier that functions as
+# a filter to have gitweb.js minified.
 
 GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
 	@$(SHELL_PATH) ./GIT-VERSION-GEN
@@ -246,6 +249,9 @@ lib = lib
 # DESTDIR=
 pathsep = :
 
+# JavaScript minifier invocation that can function as filter
+JSMIN =
+
 # default configuration for gitweb
 GITWEB_CONFIG = gitweb_config.perl
 GITWEB_CONFIG_SYSTEM = /etc/gitweb.conf
@@ -261,7 +267,11 @@ GITWEB_HOMETEXT = indextext.html
 GITWEB_CSS = gitweb.css
 GITWEB_LOGO = git-logo.png
 GITWEB_FAVICON = git-favicon.png
+ifdef JSMIN
+GITWEB_JS = gitweb.min.js
+else
 GITWEB_JS = gitweb.js
+endif
 GITWEB_SITE_HEADER =
 GITWEB_SITE_FOOTER =
 
@@ -1374,8 +1384,13 @@ $(patsubst %.perl,%,$(SCRIPT_PERL)): % : %.perl
 	chmod +x $@+ && \
 	mv $@+ $@
 
+ifdef JSMIN
+OTHER_PROGRAMS += gitweb/gitweb.cgi   gitweb/gitweb.min.js
+gitweb/gitweb.cgi: gitweb/gitweb.perl gitweb/gitweb.min.js
+else
 OTHER_PROGRAMS += gitweb/gitweb.cgi
 gitweb/gitweb.cgi: gitweb/gitweb.perl
+endif
 	$(QUIET_GEN)$(RM) $@ $@+ && \
 	sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \
 	    -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \
@@ -1426,6 +1441,11 @@ $(patsubst %.perl,%,$(SCRIPT_PERL)) git-instaweb: % : unimplemented.sh
 	mv $@+ $@
 endif # NO_PERL
 
+ifdef JSMIN
+gitweb/gitweb.min.js: gitweb/gitweb.js
+	$(QUIET_GEN)$(JSMIN) <$< >$@
+endif # JSMIN
+
 configure: configure.ac
 	$(QUIET_GEN)$(RM) $@ $<+ && \
 	sed -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
-- 
1.6.3.3

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript)
  2009-09-01 11:39   ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Jakub Narebski
  2009-09-01 11:39     ` [PATCHv1 3/5] gitweb: Colorize 'blame_incremental' view during processing Jakub Narebski
@ 2009-11-05 20:22     ` Petr Baudis
  2009-11-07 11:04       ` Jakub Narebski
  1 sibling, 1 reply; 12+ messages in thread
From: Petr Baudis @ 2009-11-05 20:22 UTC (permalink / raw)
  To: Jakub Narebski
  Cc: git, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov, Martin Koegler

  Hi!

  Many thanks for nurturing this patch.

On Tue, Sep 01, 2009 at 01:39:17PM +0200, Jakub Narebski wrote:
> Roads not taken (perhaps that should be part of commit message?):
> * Move most (or all) of "git blame --incremental" output parsing to
>   server side, and instead of sending direct output in text/plain,
>   send processed data in JSON format, e.g.
> 
>     {"commit": {
>        "sha1": "e83c5163316f89bfbde7d9ab23ca2e25604af290",
>        "info": "Kay Sievers, 2005-08-07 21:49:46 +0200",
>        "author-initials": "KS",
>        ...
>      },
>      "src-line": 13,
>      "dst-line": 16,
>      "numlines": 3,
>      "filename": "README"
>      }
> 
>   (line wrapping added for readibility).  This would require however
>   taking care on Perl side to send properly formatted JSON, and on
>   JavaScript side including json2.js code to read JSON in gitweb.js
>   (unless we rely on eval).

  I don't know that much about web programming, what is wrong with
relying on eval?

  (BTW, I have always thought that somewhat inevitable course in the
future will be to provide a web API, and basing it on JSON is probably
most natural choice. However, such an initiative needs to come from
actual web programmers.)

> * Using some lightweight JavaScript library (framework), like jQuery,
>   Prototype, ExtJS, MooTools, etc.  One one hand side this means not
>   having to worry about browser incompatibilities as this would be
>   taken care of by library; on the other hand side we want gitweb to
>   have as few dependences as possible.

  Normally, particular version of the library is simply included within
the project. E.g. in Girocco, I use MooTools for the tiny bit of
javascript I do. It is probably overkill to include it just for
incremental blame, but if we ever do much more, I think the much easier
web programming is worth the little trouble.

-- 
				Petr "Pasky" Baudis
A lot of people have my books on their bookshelves.
That's the problem, they need to read them. -- Don Knuth

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript
  2009-09-01 11:39       ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Jakub Narebski
  2009-09-01 11:39         ` [PATCHv1/RFC 5/5] gitweb: Minify gitweb.js if JSMIN is defined Jakub Narebski
@ 2009-11-05 20:33         ` Petr Baudis
  2009-11-06 18:05           ` Jakub Narebski
  1 sibling, 1 reply; 12+ messages in thread
From: Petr Baudis @ 2009-11-05 20:33 UTC (permalink / raw)
  To: Jakub Narebski
  Cc: git, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov, Martin Koegler

  Hi!

On Tue, Sep 01, 2009 at 01:39:19PM +0200, Jakub Narebski wrote:
> @@ -4806,6 +4820,10 @@ sub git_tag {
>  
>  sub git_blame_common {
>  	my $format = shift || 'porcelain';
> +	if ($format eq 'porcelain' && $cgi->param('js')) {
> +		$format = 'incremental';
> +		$action = 'blame_incremental'; # for page title etc
> +	}
>  
>  	# permissions
>  	gitweb_check_feature('blame')

  I'm a bit concerned here. I have somewhat backed out of incremental
blame myself because I have found (in accord with Junio's old findings)
that in most cases, incremental blame can be actually slower than normal
blame because of slow browsers where it takes long to update the page in
each step.

  I'm sorry if I missed this in one of your mails, but how fast is
incremental blame in your implementation? If this still might be an
issue, I think it should be configurable whether to use incremental
blame, or perhaps use some quick heuristic wrt. file size (negative
bias) and history length (positive bias) [not sure if that information
is quickly available].

-- 
				Petr "Pasky" Baudis
A lot of people have my books on their bookshelves.
That's the problem, they need to read them. -- Don Knuth

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript
  2009-11-05 20:33         ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Petr Baudis
@ 2009-11-06 18:05           ` Jakub Narebski
  2009-11-12  8:05             ` Junio C Hamano
  0 siblings, 1 reply; 12+ messages in thread
From: Jakub Narebski @ 2009-11-06 18:05 UTC (permalink / raw)
  To: Petr Baudis
  Cc: git, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov, Martin Koegler

On Thu, Nov 05, 2009, Petr Baudis wrote:
> On Tue, Sep 01, 2009 at 01:39:19PM +0200, Jakub Narebski wrote:
> > @@ -4806,6 +4820,10 @@ sub git_tag {
> >  
> >  sub git_blame_common {
> >  	my $format = shift || 'porcelain';
> > +	if ($format eq 'porcelain' && $cgi->param('js')) {
> > +		$format = 'incremental';
> > +		$action = 'blame_incremental'; # for page title etc
> > +	}
> >  
> >  	# permissions
> >  	gitweb_check_feature('blame')
> 
>   I'm a bit concerned here. I have somewhat backed out of incremental
> blame myself because I have found (in accord with Junio's old findings)
> that in most cases, incremental blame can be actually slower than normal
> blame because of slow browsers where it takes long to update the page in
> each step.
> 
>   I'm sorry if I missed this in one of your mails, but how fast is
> incremental blame in your implementation? If this still might be an
> issue, I think it should be configurable whether to use incremental
> blame, or perhaps use some quick heuristic wrt. file size (negative
> bias) and history length (positive bias) [not sure if that information
> is quickly available].

Unfortunately I can't benchmark the speed of incremental blame well
because of testing it on a single computer.

What I have found that incremental blame spares at least _server time_,
which means that the time to prepare starting view for incremental blame
(with the contents of file in blame format, unblamed) plus the time to
generate incremental blame data is usually about the same or faster
than the time to generate ordinary blame view.  Quite faster if file
have large number of blamed commits:
  $ git blame -p <file> | grep author-time | wc -l

But even if incremental blame turns out to be slower than incremental
blame it still has the advantage of being _incremental_.  You have at 
least some result soon.  Even more with current implementation which
includes progress report for incremental blame.


What needs to be addressed however is to remove totally unnecessary 
critical section / locking code, as JavaScript is single threaded.
We should take care however that JavaScript code of interactive blame
doesn't take all CPU, for example using technique presented in
  "Timed array processing in JavaScript" by Nicholas C. Zakas
  http://www.nczonline.net/blog/2009/08/11/timed-array-processing-in-javascript/


.....................................................................

If you want below there are very simple benchmark of blame and
incremental blame on a _single_ computer:
  AMD Athlon 1 GHz, 512MB RAM, Linux 2.6.14-11.1.aur.2, Apache 2.0.54
I don't remember however if it is for the most current code.

File               | 'blame'[1] | 'blame_incremental'[2]
================================================================	
blob.h             |     2.346s |  0.443s +  (2.244s /   2.921s)
GIT-VERSION-GEN    |     2.449s |  1.346s +  (3.157s /   3.876s)
README             |     2.713s |  0.508s +  (2.952s /   3.659s)
revision.c         |    19.964s |  4.872s + (11.306s /  32.124s)
gitweb/gitweb.perl |    83.912s | 12.069s + (52.922s / 223.133s)

$ git blame --porcelain   gitweb/gitweb.perl >/dev/null
  0m11.437s user + 0m0.740s sys, 66300minor pagefaults
$ git blame --incremental gitweb/gitweb.perl >/dev/null
  0m11.477s user + 0m0.816s sys, 68945minor pagefaults

Footnotes:
~~~~~~~~~~
[1] Total wall-clock time as returned by gitweb in the page footer.
[2] XXs + (XXs server blame_data / XXs client JavaScript).

-- 
Jakub Narebski
Poland

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript)
  2009-11-05 20:22     ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Petr Baudis
@ 2009-11-07 11:04       ` Jakub Narebski
  0 siblings, 0 replies; 12+ messages in thread
From: Jakub Narebski @ 2009-11-07 11:04 UTC (permalink / raw)
  To: Petr Baudis
  Cc: git, Fredrik Kuivinen, Giuseppe Bilotta, Luben Tuikov, Martin Koegler

On Thu, 5 Nov 2009, Petr Baudis wrote:
> 
>   Many thanks for nurturing this patch.

You are welcome.

I have learned quite a bit about JavaScript when working on this
patch...
 
> On Tue, Sep 01, 2009 at 01:39:17PM +0200, Jakub Narebski wrote:
> > Roads not taken (perhaps that should be part of commit message?):
> > * Move most (or all) of "git blame --incremental" output parsing to
> >   server side, and instead of sending direct output in text/plain,
> >   send processed data in JSON format, e.g.
> > 
> >     {"commit": {
> >        "sha1": "e83c5163316f89bfbde7d9ab23ca2e25604af290",
> >        "info": "Kay Sievers, 2005-08-07 21:49:46 +0200",
> >        "author-initials": "KS",
> >        ...
> >      },
> >      "src-line": 13,
> >      "dst-line": 16,
> >      "numlines": 3,
> >      "filename": "README"
> >      }
> > 
> >   (line wrapping added for readibility).  This would require however
> >   taking care on Perl side to send properly formatted JSON, and on
> >   JavaScript side including json2.js code to read JSON in gitweb.js
> >   (unless we rely on eval).
> 
>   I don't know that much about web programming, what is wrong with
> relying on eval?

In general it is insecure.  In this specific situation it shouldn't.

[...]
> > * Using some lightweight JavaScript library (framework), like jQuery,
> >   Prototype, ExtJS, MooTools, etc.  One one hand side this means not
> >   having to worry about browser incompatibilities as this would be
> >   taken care of by library; on the other hand side we want gitweb to
> >   have as few dependences as possible.
> 
>   Normally, particular version of the library is simply included within
> the project. E.g. in Girocco, I use MooTools for the tiny bit of
> javascript I do. It is probably overkill to include it just for
> incremental blame, but if we ever do much more, I think the much easier
> web programming is worth the little trouble.

Alternate solution would be to load for example jQuery for example hosted
on Google as described in http://code.google.com/apis/ajaxlibs/documentation/

-- 
Jakub Narebski
Poland

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript
  2009-11-06 18:05           ` Jakub Narebski
@ 2009-11-12  8:05             ` Junio C Hamano
  2009-11-12  9:22               ` Jakub Narebski
  0 siblings, 1 reply; 12+ messages in thread
From: Junio C Hamano @ 2009-11-12  8:05 UTC (permalink / raw)
  To: Jakub Narebski
  Cc: Petr Baudis, git, Fredrik Kuivinen, Giuseppe Bilotta,
	Luben Tuikov, Martin Koegler

Jakub Narebski <jnareb@gmail.com> writes:

> But even if incremental blame turns out to be slower than incremental
> blame it still has the advantage of being _incremental_.  You have at 
> least some result soon.

It wasn't it was slow that bothered me, but early implementations of
incremental blame I tried didn't _appear_ as incremental.  That was the
dissapointing part.

At the protocol and implementation level it certainly was feeding data
incrementally to the browser, but the end user experience on the screen
was "click....wait...wait...wait...voila the whole blame appears", not
"click...trickle...trickle...trickle...ah everything is filled".  The
latter obviously is what an incremental one should be aiming for.

No I haven't tried your latest code.  Probably I should.

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript
  2009-11-12  8:05             ` Junio C Hamano
@ 2009-11-12  9:22               ` Jakub Narebski
  0 siblings, 0 replies; 12+ messages in thread
From: Jakub Narebski @ 2009-11-12  9:22 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Petr Baudis, git, Fredrik Kuivinen, Giuseppe Bilotta,
	Luben Tuikov, Martin Koegler

On Thu, 12 Nov 2009, Junio C Hamano wrote:
> Jakub Narebski <jnareb@gmail.com> writes:
> 
> > But even if incremental blame turns out to be slower than incremental
> > blame it still has the advantage of being _incremental_.  You have at 
> > least some result soon.
> 
> It wasn't it was slow that bothered me, but early implementations of
> incremental blame I tried didn't _appear_ as incremental.  That was the
> dissapointing part.
> 
> At the protocol and implementation level it certainly was feeding data
> incrementally to the browser, but the end user experience on the screen
> was "click....wait...wait...wait...voila the whole blame appears", not
> "click...trickle...trickle...trickle...ah everything is filled".  The
> latter obviously is what an incremental one should be aiming for.
> 
> No I haven't tried your latest code.  Probably I should.

The problem with earliest versions of incremental blame _in some browsers_
was that 'onreadystatechange' event was not fired as soon as new part of
blame data was available (truth to be said the definition of this event
is a bit underspecified).  That is why newer versions of interactive blame
use timer (alarm) to check every 1 second if there is something new.

Perhaps interactive blame should use 'onprogress' event instead, which is
well defined... but is in _draft_ of standard (XHR 2.0), and not yet in
established standard.

-- 
Jakub Narebski
Poland

^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2009-11-12  9:22 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2009-09-01 11:39 [PATCH 0/5] gitweb: Incremental blame series (1 Sep 09) Jakub Narebski
2009-09-01 11:39 ` [PATCHv2 1/5] gitweb: Add optional "time to generate page" info in footer Jakub Narebski
2009-09-01 11:39   ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Jakub Narebski
2009-09-01 11:39     ` [PATCHv1 3/5] gitweb: Colorize 'blame_incremental' view during processing Jakub Narebski
2009-09-01 11:39       ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Jakub Narebski
2009-09-01 11:39         ` [PATCHv1/RFC 5/5] gitweb: Minify gitweb.js if JSMIN is defined Jakub Narebski
2009-11-05 20:33         ` [PATCHv3/RFC 4/5] gitweb: Create links leading to 'blame_incremental' using JavaScript Petr Baudis
2009-11-06 18:05           ` Jakub Narebski
2009-11-12  8:05             ` Junio C Hamano
2009-11-12  9:22               ` Jakub Narebski
2009-11-05 20:22     ` [PATCHv5 2/5] gitweb: Incremental blame (using JavaScript) Petr Baudis
2009-11-07 11:04       ` Jakub Narebski

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.