@matheusbsbs / matheus-bagliano / commits / 4af819d4ca
Fork 0

gitweb: Incremental blame (using JavaScript)

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jakub Narebski committed Sep 1, 2009 at 13:39 UTC 4af819d4cad206648b832f4d6103547981ab8004
6 files changed +940 -88
Makefile
+5 -1
@@ -265,6 +265,7 @@ GITWEB_HOMETEXT = indextext.html
265 GITWEB_CSS = gitweb.css
266 GITWEB_LOGO = git-logo.png
267 GITWEB_FAVICON = git-favicon.png
268 +GITWEB_JS = gitweb.js
269 GITWEB_SITE_HEADER =
270 GITWEB_SITE_FOOTER =
271
@@ -1407,13 +1408,14 @@ gitweb/gitweb.cgi: gitweb/gitweb.perl
1408 -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \
1409 -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \
1410 -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \
1411 + -e 's|++GITWEB_JS++|$(GITWEB_JS)|g' \
1412 -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \
1413 -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \
1414 $< >$@+ && \
1415 chmod +x $@+ && \
1416 mv $@+ $@
1417
1416 -git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css
1418 +git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css gitweb/gitweb.js
1419 $(QUIET_GEN)$(RM) $@ $@+ && \
1420 sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
1421 -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
@@ -1422,6 +1424,8 @@ git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css
1424 -e '/@@GITWEB_CGI@@/d' \
1425 -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \
1426 -e '/@@GITWEB_CSS@@/d' \
1427 + -e '/@@GITWEB_JS@@/r gitweb/gitweb.js' \
1428 + -e '/@@GITWEB_JS@@/d' \
1429 -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \
1430 $@.sh > $@+ && \
1431 chmod +x $@+ && \
git-instaweb.sh
+7
@@ -331,8 +331,15 @@ gitweb_css () {
331 EOFGITWEB
332 }
333
334 +gitweb_js () {
335 + cat > "$1" <<\EOFGITWEB
336 +@@GITWEB_JS@@
337 +EOFGITWEB
338 +}
339 +
340 gitweb_cgi "$GIT_DIR/gitweb/gitweb.cgi"
341 gitweb_css "$GIT_DIR/gitweb/gitweb.css"
342 +gitweb_js "$GIT_DIR/gitweb/gitweb.js"
343
344 case "$httpd" in
345 *lighttpd*)
gitweb/README
+4
@@ -92,6 +92,10 @@ You can specify the following configuration variables when building GIT:
92 web browsers that support favicons (website icons) may display them
93 in the browser's URL bar and next to site name in bookmarks). Relative
94 to base URI of gitweb. [Default: git-favicon.png]
95 + * GITWEB_JS
96 + Points to the localtion where you put gitweb.js on your web server
97 + (or to be more generic URI of JavaScript code used by gitweb).
98 + Relative to base URI of gitweb. [Default: gitweb.js]
99 * GITWEB_CONFIG
100 This Perl file will be loaded using 'do' and can be used to override any
101 of the options above as well as some other options -- see the "Runtime
gitweb/gitweb.css
+11
@@ -348,6 +348,17 @@ td.mode {
348 font-family: monospace;
349 }
350
351 +/* progress of blame_interactive */
352 +div#progress_bar {
353 + height: 2px;
354 + margin-bottom: -2px;
355 + background-color: #d8d9d0;
356 +}
357 +div#progress_info {
358 + float: right;
359 + text-align: right;
360 +}
361 +
362 /* styling of diffs (patchsets): commitdiff and blobdiff views */
363 div.diff.header,
364 div.diff.extended_header {
gitweb/gitweb.js new
+726
@@ -0,0 +1,726 @@
1 +// Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>
2 +// 2007, Petr Baudis <pasky@suse.cz>
3 +// 2008-2009, Jakub Narebski <jnareb@gmail.com>
4 +
5 +/**
6 + * @fileOverview JavaScript code for gitweb (git web interface).
7 + * @license GPLv2 or later
8 + */
9 +
10 +/*
11 + * This code uses DOM methods instead of (nonstandard) innerHTML
12 + * to modify page.
13 + *
14 + * innerHTML is non-standard IE extension, though supported by most
15 + * browsers; however Firefox up to version 1.5 didn't implement it in
16 + * a strict mode (application/xml+xhtml mimetype).
17 + *
18 + * Also my simple benchmarks show that using elem.firstChild.data =
19 + * 'content' is slightly faster than elem.innerHTML = 'content'. It
20 + * is however more fragile (text element fragment must exists), and
21 + * less feature-rich (we cannot add HTML).
22 + *
23 + * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the
24 + * equivalent using DOM 2 Core is usually shown in comments.
25 + */
26 +
27 +
28 +/* ============================================================ */
29 +/* generic utility functions */
30 +
31 +
32 +/**
33 + * pad number N with nonbreakable spaces on the left, to WIDTH characters
34 + * example: padLeftStr(12, 3, '&nbsp;') == '&nbsp;12'
35 + * ('&nbsp;' is nonbreakable space)
36 + *
37 + * @param {Number|String} input: number to pad
38 + * @param {Number} width: visible width of output
39 + * @param {String} str: string to prefix to string, e.g. '&nbsp;'
40 + * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR
41 + */
42 +function padLeftStr(input, width, str) {
43 + var prefix = '';
44 +
45 + width -= input.toString().length;
46 + while (width > 1) {
47 + prefix += str;
48 + width--;
49 + }
50 + return prefix + input;
51 +}
52 +
53 +/**
54 + * Pad INPUT on the left to SIZE width, using given padding character CH,
55 + * for example padLeft('a', 3, '_') is '__a'.
56 + *
57 + * @param {String} input: input value converted to string.
58 + * @param {Number} width: desired length of output.
59 + * @param {String} ch: single character to prefix to string.
60 + *
61 + * @returns {String} Modified string, at least SIZE length.
62 + */
63 +function padLeft(input, width, ch) {
64 + var s = input + "";
65 + while (s.length < width) {
66 + s = ch + s;
67 + }
68 + return s;
69 +}
70 +
71 +/**
72 + * Create XMLHttpRequest object in cross-browser way
73 + * @returns XMLHttpRequest object, or null
74 + */
75 +function createRequestObject() {
76 + try {
77 + return new XMLHttpRequest();
78 + } catch (e) {}
79 + try {
80 + return window.createRequest();
81 + } catch (e) {}
82 + try {
83 + return new ActiveXObject("Msxml2.XMLHTTP");
84 + } catch (e) {}
85 + try {
86 + return new ActiveXObject("Microsoft.XMLHTTP");
87 + } catch (e) {}
88 +
89 + return null;
90 +}
91 +
92 +/* ============================================================ */
93 +/* utility/helper functions (and variables) */
94 +
95 +var xhr; // XMLHttpRequest object
96 +var projectUrl; // partial query + separator ('?' or ';')
97 +
98 +// 'commits' is an associative map. It maps SHA1s to Commit objects.
99 +var commits = {};
100 +
101 +/**
102 + * constructor for Commit objects, used in 'blame'
103 + * @class Represents a blamed commit
104 + * @param {String} sha1: SHA-1 identifier of a commit
105 + */
106 +function Commit(sha1) {
107 + if (this instanceof Commit) {
108 + this.sha1 = sha1;
109 + this.nprevious = 0; /* number of 'previous', effective parents */
110 + } else {
111 + return new Commit(sha1);
112 + }
113 +}
114 +
115 +/* ............................................................ */
116 +/* progress info, timing, error reporting */
117 +
118 +var blamedLines = 0;
119 +var totalLines = '???';
120 +var div_progress_bar;
121 +var div_progress_info;
122 +
123 +/**
124 + * Detects how many lines does a blamed file have,
125 + * This information is used in progress info
126 + *
127 + * @returns {Number|String} Number of lines in file, or string '...'
128 + */
129 +function countLines() {
130 + var table =
131 + document.getElementById('blame_table') ||
132 + document.getElementsByTagName('table')[0];
133 +
134 + if (table) {
135 + return table.getElementsByTagName('tr').length - 1; // for header
136 + } else {
137 + return '...';
138 + }
139 +}
140 +
141 +/**
142 + * update progress info and length (width) of progress bar
143 + *
144 + * @globals div_progress_info, div_progress_bar, blamedLines, totalLines
145 + */
146 +function updateProgressInfo() {
147 + if (!div_progress_info) {
148 + div_progress_info = document.getElementById('progress_info');
149 + }
150 + if (!div_progress_bar) {
151 + div_progress_bar = document.getElementById('progress_bar');
152 + }
153 + if (!div_progress_info && !div_progress_bar) {
154 + return;
155 + }
156 +
157 + var percentage = Math.floor(100.0*blamedLines/totalLines);
158 +
159 + if (div_progress_info) {
160 + div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines +
161 + ' (' + padLeftStr(percentage, 3, '&nbsp;') + '%)';
162 + }
163 +
164 + if (div_progress_bar) {
165 + //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;');
166 + div_progress_bar.style.width = percentage + '%';
167 + }
168 +}
169 +
170 +
171 +var t_interval_server = '';
172 +var cmds_server = '';
173 +var t0 = new Date();
174 +
175 +/**
176 + * write how much it took to generate data, and to run script
177 + *
178 + * @globals t0, t_interval_server, cmds_server
179 + */
180 +function writeTimeInterval() {
181 + var info_time = document.getElementById('generating_time');
182 + if (!info_time || !t_interval_server) {
183 + return;
184 + }
185 + var t1 = new Date();
186 + info_time.firstChild.data += ' + (' +
187 + t_interval_server + ' sec server blame_data / ' +
188 + (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)';
189 +
190 + var info_cmds = document.getElementById('generating_cmd');
191 + if (!info_time || !cmds_server) {
192 + return;
193 + }
194 + info_cmds.firstChild.data += ' + ' + cmds_server;
195 +}
196 +
197 +/**
198 + * show an error message alert to user within page (in prohress info area)
199 + * @param {String} str: plain text error message (no HTML)
200 + *
201 + * @globals div_progress_info
202 + */
203 +function errorInfo(str) {
204 + if (!div_progress_info) {
205 + div_progress_info = document.getElementById('progress_info');
206 + }
207 + if (div_progress_info) {
208 + div_progress_info.className = 'error';
209 + div_progress_info.firstChild.data = str;
210 + }
211 +}
212 +
213 +/* ............................................................ */
214 +/* coloring rows like 'blame' after 'blame_data' finishes */
215 +
216 +/**
217 + * returns true if given row element (tr) is first in commit group
218 + * to be used only after 'blame_data' finishes (after processing)
219 + *
220 + * @param {HTMLElement} tr: table row
221 + * @returns {Boolean} true if TR is first in commit group
222 + */
223 +function isStartOfGroup(tr) {
224 + return tr.firstChild.className === 'sha1';
225 +}
226 +
227 +var colorRe = /(?:light|dark)/;
228 +
229 +/**
230 + * change colors to use zebra coloring (2 colors) instead of 3 colors
231 + * concatenate neighbour commit groups belonging to the same commit
232 + *
233 + * @globals colorRe
234 + */
235 +function fixColorsAndGroups() {
236 + var colorClasses = ['light', 'dark'];
237 + var linenum = 1;
238 + var tr, prev_group;
239 + var colorClass = 0;
240 + var table =
241 + document.getElementById('blame_table') ||
242 + document.getElementsByTagName('table')[0];
243 +
244 + while ((tr = document.getElementById('l'+linenum))) {
245 + // index origin is 0, which is table header; start from 1
246 + //while ((tr = table.rows[linenum])) { // <- it is slower
247 + if (isStartOfGroup(tr, linenum, document)) {
248 + if (prev_group &&
249 + prev_group.firstChild.firstChild.href ===
250 + tr.firstChild.firstChild.href) {
251 + // we have to concatenate groups
252 + var prev_rows = prev_group.firstChild.rowSpan || 1;
253 + var curr_rows = tr.firstChild.rowSpan || 1;
254 + prev_group.firstChild.rowSpan = prev_rows + curr_rows;
255 + //tr.removeChild(tr.firstChild);
256 + tr.deleteCell(0); // DOM2 HTML way
257 + } else {
258 + colorClass = (colorClass + 1) % 2;
259 + prev_group = tr;
260 + }
261 + }
262 + var tr_class = tr.className;
263 + tr.className = tr_class.replace(colorRe, colorClasses[colorClass]);
264 + linenum++;
265 + }
266 +}
267 +
268 +/* ............................................................ */
269 +/* time and data */
270 +
271 +/**
272 + * used to extract hours and minutes from timezone info, e.g '-0900'
273 + * @constant
274 + */
275 +var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/;
276 +
277 +/**
278 + * return date in local time formatted in iso-8601 like format
279 + * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200'
280 + *
281 + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'
282 + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
283 + * @returns {String} date in local time in iso-8601 like format
284 + *
285 + * @globals tzRe
286 + */
287 +function formatDateISOLocal(epoch, timezoneInfo) {
288 + var match = tzRe.exec(timezoneInfo);
289 + // date corrected by timezone
290 + var localDate = new Date(1000 * (epoch +
291 + (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));
292 + var localDateStr = // e.g. '2005-08-07'
293 + localDate.getUTCFullYear() + '-' +
294 + padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' +
295 + padLeft(localDate.getUTCDate(), 2, '0');
296 + var localTimeStr = // e.g. '21:49:46'
297 + padLeft(localDate.getUTCHours(), 2, '0') + ':' +
298 + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' +
299 + padLeft(localDate.getUTCSeconds(), 2, '0');
300 +
301 + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;
302 +}
303 +
304 +/* ............................................................ */
305 +/* unquoting/unescaping filenames */
306 +
307 +/**#@+
308 + * @constant
309 + */
310 +var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g;
311 +var octEscRe = /^[0-7]{1,3}$/;
312 +var maybeQuotedRe = /^\"(.*)\"$/;
313 +/**#@-*/
314 +
315 +/**
316 + * unquote maybe git-quoted filename
317 + * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a'
318 + *
319 + * @param {String} str: git-quoted string
320 + * @returns {String} Unquoted and unescaped string
321 + *
322 + * @globals escCodeRe, octEscRe, maybeQuotedRe
323 + */
324 +function unquote(str) {
325 + function unq(seq) {
326 + var es = {
327 + // character escape codes, aka escape sequences (from C)
328 + // replacements are to some extent JavaScript specific
329 + t: "\t", // tab (HT, TAB)
330 + n: "\n", // newline (NL)
331 + r: "\r", // return (CR)
332 + f: "\f", // form feed (FF)
333 + b: "\b", // backspace (BS)
334 + a: "\x07", // alarm (bell) (BEL)
335 + e: "\x1B", // escape (ESC)
336 + v: "\v" // vertical tab (VT)
337 + };
338 +
339 + if (seq.search(octEscRe) !== -1) {
340 + // octal char sequence
341 + return String.fromCharCode(parseInt(seq, 8));
342 + } else if (seq in es) {
343 + // C escape sequence, aka character escape code
344 + return es[seq];
345 + }
346 + // quoted ordinary character
347 + return seq;
348 + }
349 +
350 + var match = str.match(maybeQuotedRe);
351 + if (match) {
352 + str = match[1];
353 + // perhaps str = eval('"'+str+'"'); would be enough?
354 + str = str.replace(escCodeRe,
355 + function (substr, p1, offset, s) { return unq(p1); });
356 + }
357 + return str;
358 +}
359 +
360 +/* ============================================================ */
361 +/* main part: parsing response */
362 +
363 +/**
364 + * Function called for each blame entry, as soon as it finishes.
365 + * It updates page via DOM manipulation, adding sha1 info, etc.
366 + *
367 + * @param {Commit} commit: blamed commit
368 + * @param {Object} group: object representing group of lines,
369 + * which blame the same commit (blame entry)
370 + *
371 + * @globals blamedLines
372 + */
373 +function handleLine(commit, group) {
374 + /*
375 + This is the structure of the HTML fragment we are working
376 + with:
377 +
378 + <tr id="l123" class="">
379 + <td class="sha1" title=""><a href=""> </a></td>
380 + <td class="linenr"><a class="linenr" href="">123</a></td>
381 + <td class="pre"># times (my ext3 doesn&#39;t).</td>
382 + </tr>
383 + */
384 +
385 + var resline = group.resline;
386 +
387 + // format date and time string only once per commit
388 + if (!commit.info) {
389 + /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */
390 + commit.info = commit.author + ', ' +
391 + formatDateISOLocal(commit.authorTime, commit.authorTimezone);
392 + }
393 +
394 + // loop over lines in commit group
395 + for (var i = 0; i < group.numlines; i++, resline++) {
396 + var tr = document.getElementById('l'+resline);
397 + if (!tr) {
398 + break;
399 + }
400 + /*
401 + <tr id="l123" class="">
402 + <td class="sha1" title=""><a href=""> </a></td>
403 + <td class="linenr"><a class="linenr" href="">123</a></td>
404 + <td class="pre"># times (my ext3 doesn&#39;t).</td>
405 + </tr>
406 + */
407 + var td_sha1 = tr.firstChild;
408 + var a_sha1 = td_sha1.firstChild;
409 + var a_linenr = td_sha1.nextSibling.firstChild;
410 +
411 + /* <tr id="l123" class=""> */
412 + var tr_class = 'light'; // or tr.className
413 + if (commit.boundary) {
414 + tr_class += ' boundary';
415 + }
416 + if (commit.nprevious === 0) {
417 + tr_class += ' no-previous';
418 + } else if (commit.nprevious > 1) {
419 + tr_class += ' multiple-previous';
420 + }
421 + tr.className = tr_class;
422 +
423 + /* <td class="sha1" title="?" rowspan="?"><a href="?">?</a></td> */
424 + if (i === 0) {
425 + td_sha1.title = commit.info;
426 + td_sha1.rowSpan = group.numlines;
427 +
428 + a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1;
429 + a_sha1.firstChild.data = commit.sha1.substr(0, 8);
430 + if (group.numlines >= 2) {
431 + var fragment = document.createDocumentFragment();
432 + var br = document.createElement("br");
433 + var text = document.createTextNode(
434 + commit.author.match(/\b([A-Z])\B/g).join(''));
435 + if (br && text) {
436 + var elem = fragment || td_sha1;
437 + elem.appendChild(br);
438 + elem.appendChild(text);
439 + if (fragment) {
440 + td_sha1.appendChild(fragment);
441 + }
442 + }
443 + }
444 + } else {
445 + //tr.removeChild(td_sha1); // DOM2 Core way
446 + tr.deleteCell(0); // DOM2 HTML way
447 + }
448 +
449 + /* <td class="linenr"><a class="linenr" href="?">123</a></td> */
450 + var linenr_commit =
451 + ('previous' in commit ? commit.previous : commit.sha1);
452 + var linenr_filename =
453 + ('file_parent' in commit ? commit.file_parent : commit.filename);
454 + a_linenr.href = projectUrl + 'a=blame_incremental' +
455 + ';hb=' + linenr_commit +
456 + ';f=' + encodeURIComponent(linenr_filename) +
457 + '#l' + (group.srcline + i);
458 +
459 + blamedLines++;
460 +
461 + //updateProgressInfo();
462 + }
463 +}
464 +
465 +// ----------------------------------------------------------------------
466 +
467 +var inProgress = false; // are we processing response
468 +
469 +/**#@+
470 + * @constant
471 + */
472 +var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/;
473 +var infoRe = /^([a-z-]+) ?(.*)/;
474 +var endRe = /^END ?([^ ]*) ?(.*)/;
475 +/**@-*/
476 +
477 +var curCommit = new Commit();
478 +var curGroup = {};
479 +
480 +var pollTimer = null;
481 +
482 +/**
483 + * Parse output from 'git blame --incremental [...]', received via
484 + * XMLHttpRequest from server (blamedataUrl), and call handleLine
485 + * (which updates page) as soon as blame entry is completed.
486 + *
487 + * @param {String[]} lines: new complete lines from blamedata server
488 + *
489 + * @globals commits, curCommit, curGroup, t_interval_server, cmds_server
490 + * @globals sha1Re, infoRe, endRe
491 + */
492 +function processBlameLines(lines) {
493 + var match;
494 +
495 + for (var i = 0, len = lines.length; i < len; i++) {
496 +
497 + if ((match = sha1Re.exec(lines[i]))) {
498 + var sha1 = match[1];
499 + var srcline = parseInt(match[2], 10);
500 + var resline = parseInt(match[3], 10);
501 + var numlines = parseInt(match[4], 10);
502 +
503 + var c = commits[sha1];
504 + if (!c) {
505 + c = new Commit(sha1);
506 + commits[sha1] = c;
507 + }
508 + curCommit = c;
509 +
510 + curGroup.srcline = srcline;
511 + curGroup.resline = resline;
512 + curGroup.numlines = numlines;
513 +
514 + } else if ((match = infoRe.exec(lines[i]))) {
515 + var info = match[1];
516 + var data = match[2];
517 + switch (info) {
518 + case 'filename':
519 + curCommit.filename = unquote(data);
520 + // 'filename' information terminates the entry
521 + handleLine(curCommit, curGroup);
522 + updateProgressInfo();
523 + break;
524 + case 'author':
525 + curCommit.author = data;
526 + break;
527 + case 'author-time':
528 + curCommit.authorTime = parseInt(data, 10);
529 + break;
530 + case 'author-tz':
531 + curCommit.authorTimezone = data;
532 + break;
533 + case 'previous':
534 + curCommit.nprevious++;
535 + // store only first 'previous' header
536 + if (!'previous' in curCommit) {
537 + var parts = data.split(' ', 2);
538 + curCommit.previous = parts[0];
539 + curCommit.file_parent = unquote(parts[1]);
540 + }
541 + break;
542 + case 'boundary':
543 + curCommit.boundary = true;
544 + break;
545 + } // end switch
546 +
547 + } else if ((match = endRe.exec(lines[i]))) {
548 + t_interval_server = match[1];
549 + cmds_server = match[2];
550 +
551 + } else if (lines[i] !== '') {
552 + // malformed line
553 +
554 + } // end if (match)
555 +
556 + } // end for (lines)
557 +}
558 +
559 +/**
560 + * Process new data and return pointer to end of processed part
561 + *
562 + * @param {String} unprocessed: new data (from nextReadPos)
563 + * @param {Number} nextReadPos: end of last processed data
564 + * @return {Number} end of processed data (new value for nextReadPos)
565 + */
566 +function processData(unprocessed, nextReadPos) {
567 + var lastLineEnd = unprocessed.lastIndexOf('\n');
568 + if (lastLineEnd !== -1) {
569 + var lines = unprocessed.substring(0, lastLineEnd).split('\n');
570 + nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */;
571 +
572 + processBlameLines(lines);
573 + } // end if
574 +
575 + return nextReadPos;
576 +}
577 +
578 +/**
579 + * Handle XMLHttpRequest errors
580 + *
581 + * @param {XMLHttpRequest} xhr: XMLHttpRequest object
582 + *
583 + * @globals pollTimer, commits, inProgress
584 + */
585 +function handleError(xhr) {
586 + errorInfo('Server error: ' +
587 + xhr.status + ' - ' + (xhr.statusText || 'Error contacting server'));
588 +
589 + clearInterval(pollTimer);
590 + commits = {}; // free memory
591 +
592 + inProgress = false;
593 +}
594 +
595 +/**
596 + * Called after XMLHttpRequest finishes (loads)
597 + *
598 + * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused)
599 + *
600 + * @globals pollTimer, commits, inProgress
601 + */
602 +function responseLoaded(xhr) {
603 + clearInterval(pollTimer);
604 +
605 + fixColorsAndGroups();
606 + writeTimeInterval();
607 + commits = {}; // free memory
608 +
609 + inProgress = false;
610 +}
611 +
612 +/**
613 + * handler for XMLHttpRequest onreadystatechange event
614 + * @see startBlame
615 + *
616 + * @globals xhr, inProgress
617 + */
618 +function handleResponse() {
619 +
620 + /*
621 + * xhr.readyState
622 + *
623 + * Value Constant (W3C) Description
624 + * -------------------------------------------------------------------
625 + * 0 UNSENT open() has not been called yet.
626 + * 1 OPENED send() has not been called yet.
627 + * 2 HEADERS_RECEIVED send() has been called, and headers
628 + * and status are available.
629 + * 3 LOADING Downloading; responseText holds partial data.
630 + * 4 DONE The operation is complete.
631 + */
632 +
633 + if (xhr.readyState !== 4 && xhr.readyState !== 3) {
634 + return;
635 + }
636 +
637 + // the server returned error
638 + if (xhr.readyState === 3 && xhr.status !== 200) {
639 + return;
640 + }
641 + if (xhr.readyState === 4 && xhr.status !== 200) {
642 + handleError(xhr);
643 + return;
644 + }
645 +
646 + // In konqueror xhr.responseText is sometimes null here...
647 + if (xhr.responseText === null) {
648 + return;
649 + }
650 +
651 + // in case we were called before finished processing
652 + if (inProgress) {
653 + return;
654 + } else {
655 + inProgress = true;
656 + }
657 +
658 + // extract new whole (complete) lines, and process them
659 + while (xhr.prevDataLength !== xhr.responseText.length) {
660 + if (xhr.readyState === 4 &&
661 + xhr.prevDataLength === xhr.responseText.length) {
662 + break;
663 + }
664 +
665 + xhr.prevDataLength = xhr.responseText.length;
666 + var unprocessed = xhr.responseText.substring(xhr.nextReadPos);
667 + xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos);
668 + } // end while
669 +
670 + // did we finish work?
671 + if (xhr.readyState === 4 &&
672 + xhr.prevDataLength === xhr.responseText.length) {
673 + responseLoaded(xhr);
674 + }
675 +
676 + inProgress = false;
677 +}
678 +
679 +// ============================================================
680 +// ------------------------------------------------------------
681 +
682 +/**
683 + * Incrementally update line data in blame_incremental view in gitweb.
684 + *
685 + * @param {String} blamedataUrl: URL to server script generating blame data.
686 + * @param {String} bUrl: partial URL to project, used to generate links.
687 + *
688 + * Called from 'blame_incremental' view after loading table with
689 + * file contents, a base for blame view.
690 + *
691 + * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer
692 +*/
693 +function startBlame(blamedataUrl, bUrl) {
694 +
695 + xhr = createRequestObject();
696 + if (!xhr) {
697 + errorInfo('ERROR: XMLHttpRequest not supported');
698 + return;
699 + }
700 +
701 + t0 = new Date();
702 + projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';');
703 + if ((div_progress_bar = document.getElementById('progress_bar'))) {
704 + //div_progress_bar.setAttribute('style', 'width: 100%;');
705 + div_progress_bar.style.cssText = 'width: 100%;';
706 + }
707 + totalLines = countLines();
708 + updateProgressInfo();
709 +
710 + /* add extra properties to xhr object to help processing response */
711 + xhr.prevDataLength = -1; // used to detect if we have new data
712 + xhr.nextReadPos = 0; // where unread part of response starts
713 +
714 + xhr.onreadystatechange = handleResponse;
715 + //xhr.onreadystatechange = function () { handleResponse(xhr); };
716 +
717 + xhr.open('GET', blamedataUrl);
718 + xhr.setRequestHeader('Accept', 'text/plain');
719 + xhr.send(null);
720 +
721 + // not all browsers call onreadystatechange event on each server flush
722 + // poll response using timer every second to handle this issue
723 + pollTimer = setInterval(xhr.onreadystatechange, 1000);
724 +}
725 +
726 +// end of gitweb.js
gitweb/gitweb.perl
+187 -87
@@ -96,6 +96,8 @@ our $stylesheet = undef;
96 our $logo = "++GITWEB_LOGO++";
97 # URI of GIT favicon, assumed to be image/png type
98 our $favicon = "++GITWEB_FAVICON++";
99 +# URI of gitweb.js (JavaScript code for gitweb)
100 +our $javascript = "++GITWEB_JS++";
101
102 # URI and label (title) of GIT logo link
103 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
@@ -564,6 +566,8 @@ our %cgi_param_mapping = @cgi_param_mapping;
566 # we will also need to know the possible actions, for validation
567 our %actions = (
568 "blame" => \&git_blame,
569 + "blame_incremental" => \&git_blame_incremental,
570 + "blame_data" => \&git_blame_data,
571 "blobdiff" => \&git_blobdiff,
572 "blobdiff_plain" => \&git_blobdiff_plain,
573 "blob" => \&git_blob,
@@ -4787,7 +4791,9 @@ sub git_tag {
4791 git_footer_html();
4792 }
4793
4790 -sub git_blame {
4794 +sub git_blame_common {
4795 + my $format = shift || 'porcelain';
4796 +
4797 # permissions
4798 gitweb_check_feature('blame')
4799 or die_error(403, "Blame view not allowed");
@@ -4809,10 +4815,43 @@ sub git_blame {
4815 }
4816 }
4817
4812 - # run git-blame --porcelain
4813 - open my $fd, "-|", git_cmd(), "blame", '-p',
4814 - $hash_base, '--', $file_name
4815 - or die_error(500, "Open git-blame failed");
4818 + my $fd;
4819 + if ($format eq 'incremental') {
4820 + # get file contents (as base)
4821 + open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
4822 + or die_error(500, "Open git-cat-file failed");
4823 + } elsif ($format eq 'data') {
4824 + # run git-blame --incremental
4825 + open $fd, "-|", git_cmd(), "blame", "--incremental",
4826 + $hash_base, "--", $file_name
4827 + or die_error(500, "Open git-blame --incremental failed");
4828 + } else {
4829 + # run git-blame --porcelain
4830 + open $fd, "-|", git_cmd(), "blame", '-p',
4831 + $hash_base, '--', $file_name
4832 + or die_error(500, "Open git-blame --porcelain failed");
4833 + }
4834 +
4835 + # incremental blame data returns early
4836 + if ($format eq 'data') {
4837 + print $cgi->header(
4838 + -type=>"text/plain", -charset => "utf-8",
4839 + -status=> "200 OK");
4840 + local $| = 1; # output autoflush
4841 + print while <$fd>;
4842 + close $fd
4843 + or print "ERROR $!\n";
4844 +
4845 + print 'END';
4846 + if (defined $t0 && gitweb_check_feature('timed')) {
4847 + print ' '.
4848 + Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
4849 + ' '.$number_of_git_cmds;
4850 + }
4851 + print "\n";
4852 +
4853 + return;
4854 + }
4855
4856 # page header
4857 git_header_html();
@@ -4823,109 +4862,170 @@ sub git_blame {
4862 $cgi->a({-href => href(action=>"history", -replay=>1)},
4863 "history") .
4864 " | " .
4826 - $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4865 + $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
4866 "HEAD");
4867 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4868 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4869 git_print_page_path($file_name, $ftype, $hash_base);
4870
4871 # page body
4872 + if ($format eq 'incremental') {
4873 + print "<noscript>\n<div class=\"error\"><center><b>\n".
4874 + "This page requires JavaScript to run.\n Use ".
4875 + $cgi->a({-href => href(action=>'blame',-replay=>1)},
4876 + 'this page').
4877 + " instead.\n".
4878 + "</b></center></div>\n</noscript>\n";
4879 +
4880 + print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
4881 + }
4882 +
4883 + print qq!<div class="page_body">\n!;
4884 + print qq!<div id="progress_info">... / ...</div>\n!
4885 + if ($format eq 'incremental');
4886 + print qq!<table id="blame_table" class="blame" width="100%">\n!.
4887 + #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
4888 + qq!<thead>\n!.
4889 + qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
4890 + qq!</thead>\n!.
4891 + qq!<tbody>\n!;
4892 +
4893 my @rev_color = qw(light dark);
4894 my $num_colors = scalar(@rev_color);
4895 my $current_color = 0;
4836 - my %metainfo = ();
4896
4838 - print <<HTML;
4839 -<div class="page_body">
4840 -<table class="blame">
4841 -<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4842 -HTML
4843 - LINE:
4844 - while (my $line = <$fd>) {
4845 - chomp $line;
4846 - # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4847 - # no <lines in group> for subsequent lines in group of lines
4848 - my ($full_rev, $orig_lineno, $lineno, $group_size) =
4849 - ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4850 - if (!exists $metainfo{$full_rev}) {
4851 - $metainfo{$full_rev} = { 'nprevious' => 0 };
4852 - }
4853 - my $meta = $metainfo{$full_rev};
4854 - my $data;
4855 - while ($data = <$fd>) {
4856 - chomp $data;
4857 - last if ($data =~ s/^\t//); # contents of line
4858 - if ($data =~ /^(\S+)(?: (.*))?$/) {
4859 - $meta->{$1} = $2 unless exists $meta->{$1};
4897 + if ($format eq 'incremental') {
4898 + my $color_class = $rev_color[$current_color];
4899 +
4900 + #contents of a file
4901 + my $linenr = 0;
4902 + LINE:
4903 + while (my $line = <$fd>) {
4904 + chomp $line;
4905 + $linenr++;
4906 +
4907 + print qq!<tr id="l$linenr" class="$color_class">!.
4908 + qq!<td class="sha1"><a href=""> </a></td>!.
4909 + qq!<td class="linenr">!.
4910 + qq!<a class="linenr" href="">$linenr</a></td>!;
4911 + print qq!<td class="pre">! . esc_html($line) . "</td>\n";
4912 + print qq!</tr>\n!;
4913 + }
4914 +
4915 + } else { # porcelain, i.e. ordinary blame
4916 + my %metainfo = (); # saves information about commits
4917 +
4918 + # blame data
4919 + LINE:
4920 + while (my $line = <$fd>) {
4921 + chomp $line;
4922 + # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4923 + # no <lines in group> for subsequent lines in group of lines
4924 + my ($full_rev, $orig_lineno, $lineno, $group_size) =
4925 + ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4926 + if (!exists $metainfo{$full_rev}) {
4927 + $metainfo{$full_rev} = { 'nprevious' => 0 };
4928 }
4861 - if ($data =~ /^previous /) {
4862 - $meta->{'nprevious'}++;
4929 + my $meta = $metainfo{$full_rev};
4930 + my $data;
4931 + while ($data = <$fd>) {
4932 + chomp $data;
4933 + last if ($data =~ s/^\t//); # contents of line
4934 + if ($data =~ /^(\S+)(?: (.*))?$/) {
4935 + $meta->{$1} = $2 unless exists $meta->{$1};
4936 + }
4937 + if ($data =~ /^previous /) {
4938 + $meta->{'nprevious'}++;
4939 + }
4940 }
4864 - }
4865 - my $short_rev = substr($full_rev, 0, 8);
4866 - my $author = $meta->{'author'};
4867 - my %date =
4868 - parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4869 - my $date = $date{'iso-tz'};
4870 - if ($group_size) {
4871 - $current_color = ($current_color + 1) % $num_colors;
4872 - }
4873 - my $tr_class = $rev_color[$current_color];
4874 - $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4875 - $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4876 - $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4877 - print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4878 - if ($group_size) {
4879 - print "<td class=\"sha1\"";
4880 - print " title=\"". esc_html($author) . ", $date\"";
4881 - print " rowspan=\"$group_size\"" if ($group_size > 1);
4882 - print ">";
4883 - print $cgi->a({-href => href(action=>"commit",
4884 - hash=>$full_rev,
4885 - file_name=>$file_name)},
4886 - esc_html($short_rev));
4887 - if ($group_size >= 2) {
4888 - my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
4889 - if (@author_initials) {
4890 - print "<br />" .
4891 - esc_html(join('', @author_initials));
4892 - # or join('.', ...)
4941 + my $short_rev = substr($full_rev, 0, 8);
4942 + my $author = $meta->{'author'};
4943 + my %date =
4944 + parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4945 + my $date = $date{'iso-tz'};
4946 + if ($group_size) {
4947 + $current_color = ($current_color + 1) % $num_colors;
4948 + }
4949 + my $tr_class = $rev_color[$current_color];
4950 + $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4951 + $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4952 + $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4953 + print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4954 + if ($group_size) {
4955 + print "<td class=\"sha1\"";
4956 + print " title=\"". esc_html($author) . ", $date\"";
4957 + print " rowspan=\"$group_size\"" if ($group_size > 1);
4958 + print ">";
4959 + print $cgi->a({-href => href(action=>"commit",
4960 + hash=>$full_rev,
4961 + file_name=>$file_name)},
4962 + esc_html($short_rev));
4963 + if ($group_size >= 2) {
4964 + my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
4965 + if (@author_initials) {
4966 + print "<br />" .
4967 + esc_html(join('', @author_initials));
4968 + # or join('.', ...)
4969 + }
4970 }
4971 + print "</td>\n";
4972 }
4895 - print "</td>\n";
4896 - }
4897 - # 'previous' <sha1 of parent commit> <filename at commit>
4898 - if (exists $meta->{'previous'} &&
4899 - $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
4900 - $meta->{'parent'} = $1;
4901 - $meta->{'file_parent'} = unquote($2);
4902 - }
4903 - my $linenr_commit =
4904 - exists($meta->{'parent'}) ?
4905 - $meta->{'parent'} : $full_rev;
4906 - my $linenr_filename =
4907 - exists($meta->{'file_parent'}) ?
4908 - $meta->{'file_parent'} : unquote($meta->{'filename'});
4909 - my $blamed = href(action => 'blame',
4910 - file_name => $linenr_filename,
4911 - hash_base => $linenr_commit);
4912 - print "<td class=\"linenr\">";
4913 - print $cgi->a({ -href => "$blamed#l$orig_lineno",
4914 - -class => "linenr" },
4915 - esc_html($lineno));
4916 - print "</td>";
4917 - print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4918 - print "</tr>\n";
4973 + # 'previous' <sha1 of parent commit> <filename at commit>
4974 + if (exists $meta->{'previous'} &&
4975 + $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
4976 + $meta->{'parent'} = $1;
4977 + $meta->{'file_parent'} = unquote($2);
4978 + }
4979 + my $linenr_commit =
4980 + exists($meta->{'parent'}) ?
4981 + $meta->{'parent'} : $full_rev;
4982 + my $linenr_filename =
4983 + exists($meta->{'file_parent'}) ?
4984 + $meta->{'file_parent'} : unquote($meta->{'filename'});
4985 + my $blamed = href(action => 'blame',
4986 + file_name => $linenr_filename,
4987 + hash_base => $linenr_commit);
4988 + print "<td class=\"linenr\">";
4989 + print $cgi->a({ -href => "$blamed#l$orig_lineno",
4990 + -class => "linenr" },
4991 + esc_html($lineno));
4992 + print "</td>";
4993 + print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4994 + print "</tr>\n";
4995 + } # end while
4996 +
4997 }
4920 - print "</table>\n";
4921 - print "</div>";
4998 +
4999 + # footer
5000 + print "</tbody>\n".
5001 + "</table>\n"; # class="blame"
5002 + print "</div>\n"; # class="blame_body"
5003 close $fd
5004 or print "Reading blob failed\n";
5005
4925 - # page footer
5006 + if ($format eq 'incremental') {
5007 + print qq!<script type="text/javascript" src="$javascript"></script>\n!.
5008 + qq!<script type="text/javascript">\n!.
5009 + qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
5010 + qq! "!. href() .qq!");\n!.
5011 + qq!</script>\n!;
5012 + }
5013 +
5014 git_footer_html();
5015 }
5016
5017 +sub git_blame {
5018 + git_blame_common();
5019 +}
5020 +
5021 +sub git_blame_incremental {
5022 + git_blame_common('incremental');
5023 +}
5024 +
5025 +sub git_blame_data {
5026 + git_blame_common('data');
5027 +}
5028 +
5029 sub git_tags {
5030 my $head = git_get_head_hash($project);
5031 git_header_html();