]>
Commit | Line | Data |
---|---|---|
4af819d4 JN |
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 | ||
c4ccf61f JN |
10 | /* ============================================================ */ |
11 | /* functions for generic gitweb actions and views */ | |
12 | ||
13 | /** | |
14 | * used to check if link has 'js' query parameter already (at end), | |
15 | * and other reasons to not add 'js=1' param at the end of link | |
16 | * @constant | |
17 | */ | |
18 | var jsExceptionsRe = /[;?]js=[01]$/; | |
19 | ||
20 | /** | |
21 | * Add '?js=1' or ';js=1' to the end of every link in the document | |
22 | * that doesn't have 'js' query parameter set already. | |
23 | * | |
24 | * Links with 'js=1' lead to JavaScript version of given action, if it | |
25 | * exists (currently there is only 'blame_incremental' for 'blame') | |
26 | * | |
27 | * @globals jsExceptionsRe | |
28 | */ | |
29 | function fixLinks() { | |
30 | var allLinks = document.getElementsByTagName("a") || document.links; | |
31 | for (var i = 0, len = allLinks.length; i < len; i++) { | |
32 | var link = allLinks[i]; | |
33 | if (!jsExceptionsRe.test(link)) { // =~ /[;?]js=[01]$/; | |
34 | link.href += | |
35 | (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1'; | |
36 | } | |
37 | } | |
38 | } | |
39 | ||
40 | ||
41 | /* ============================================================ */ | |
42 | ||
4af819d4 JN |
43 | /* |
44 | * This code uses DOM methods instead of (nonstandard) innerHTML | |
45 | * to modify page. | |
46 | * | |
47 | * innerHTML is non-standard IE extension, though supported by most | |
48 | * browsers; however Firefox up to version 1.5 didn't implement it in | |
49 | * a strict mode (application/xml+xhtml mimetype). | |
50 | * | |
51 | * Also my simple benchmarks show that using elem.firstChild.data = | |
52 | * 'content' is slightly faster than elem.innerHTML = 'content'. It | |
53 | * is however more fragile (text element fragment must exists), and | |
54 | * less feature-rich (we cannot add HTML). | |
55 | * | |
56 | * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the | |
57 | * equivalent using DOM 2 Core is usually shown in comments. | |
58 | */ | |
59 | ||
60 | ||
61 | /* ============================================================ */ | |
62 | /* generic utility functions */ | |
63 | ||
64 | ||
65 | /** | |
66 | * pad number N with nonbreakable spaces on the left, to WIDTH characters | |
6821dee9 SB |
67 | * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' |
68 | * ('\u00A0' is nonbreakable space) | |
4af819d4 JN |
69 | * |
70 | * @param {Number|String} input: number to pad | |
71 | * @param {Number} width: visible width of output | |
6821dee9 | 72 | * @param {String} str: string to prefix to string, e.g. '\u00A0' |
4af819d4 JN |
73 | * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR |
74 | */ | |
75 | function padLeftStr(input, width, str) { | |
76 | var prefix = ''; | |
77 | ||
78 | width -= input.toString().length; | |
6821dee9 | 79 | while (width > 0) { |
4af819d4 JN |
80 | prefix += str; |
81 | width--; | |
82 | } | |
83 | return prefix + input; | |
84 | } | |
85 | ||
86 | /** | |
87 | * Pad INPUT on the left to SIZE width, using given padding character CH, | |
88 | * for example padLeft('a', 3, '_') is '__a'. | |
89 | * | |
90 | * @param {String} input: input value converted to string. | |
91 | * @param {Number} width: desired length of output. | |
92 | * @param {String} ch: single character to prefix to string. | |
93 | * | |
94 | * @returns {String} Modified string, at least SIZE length. | |
95 | */ | |
96 | function padLeft(input, width, ch) { | |
97 | var s = input + ""; | |
98 | while (s.length < width) { | |
99 | s = ch + s; | |
100 | } | |
101 | return s; | |
102 | } | |
103 | ||
104 | /** | |
105 | * Create XMLHttpRequest object in cross-browser way | |
106 | * @returns XMLHttpRequest object, or null | |
107 | */ | |
108 | function createRequestObject() { | |
109 | try { | |
110 | return new XMLHttpRequest(); | |
111 | } catch (e) {} | |
112 | try { | |
113 | return window.createRequest(); | |
114 | } catch (e) {} | |
115 | try { | |
116 | return new ActiveXObject("Msxml2.XMLHTTP"); | |
117 | } catch (e) {} | |
118 | try { | |
119 | return new ActiveXObject("Microsoft.XMLHTTP"); | |
120 | } catch (e) {} | |
121 | ||
122 | return null; | |
123 | } | |
124 | ||
c4ccf61f | 125 | |
4af819d4 JN |
126 | /* ============================================================ */ |
127 | /* utility/helper functions (and variables) */ | |
128 | ||
129 | var xhr; // XMLHttpRequest object | |
130 | var projectUrl; // partial query + separator ('?' or ';') | |
131 | ||
132 | // 'commits' is an associative map. It maps SHA1s to Commit objects. | |
133 | var commits = {}; | |
134 | ||
135 | /** | |
136 | * constructor for Commit objects, used in 'blame' | |
137 | * @class Represents a blamed commit | |
138 | * @param {String} sha1: SHA-1 identifier of a commit | |
139 | */ | |
140 | function Commit(sha1) { | |
141 | if (this instanceof Commit) { | |
142 | this.sha1 = sha1; | |
143 | this.nprevious = 0; /* number of 'previous', effective parents */ | |
144 | } else { | |
145 | return new Commit(sha1); | |
146 | } | |
147 | } | |
148 | ||
149 | /* ............................................................ */ | |
150 | /* progress info, timing, error reporting */ | |
151 | ||
152 | var blamedLines = 0; | |
153 | var totalLines = '???'; | |
154 | var div_progress_bar; | |
155 | var div_progress_info; | |
156 | ||
157 | /** | |
158 | * Detects how many lines does a blamed file have, | |
159 | * This information is used in progress info | |
160 | * | |
161 | * @returns {Number|String} Number of lines in file, or string '...' | |
162 | */ | |
163 | function countLines() { | |
164 | var table = | |
165 | document.getElementById('blame_table') || | |
166 | document.getElementsByTagName('table')[0]; | |
167 | ||
168 | if (table) { | |
169 | return table.getElementsByTagName('tr').length - 1; // for header | |
170 | } else { | |
171 | return '...'; | |
172 | } | |
173 | } | |
174 | ||
175 | /** | |
176 | * update progress info and length (width) of progress bar | |
177 | * | |
178 | * @globals div_progress_info, div_progress_bar, blamedLines, totalLines | |
179 | */ | |
180 | function updateProgressInfo() { | |
181 | if (!div_progress_info) { | |
182 | div_progress_info = document.getElementById('progress_info'); | |
183 | } | |
184 | if (!div_progress_bar) { | |
185 | div_progress_bar = document.getElementById('progress_bar'); | |
186 | } | |
187 | if (!div_progress_info && !div_progress_bar) { | |
188 | return; | |
189 | } | |
190 | ||
191 | var percentage = Math.floor(100.0*blamedLines/totalLines); | |
192 | ||
193 | if (div_progress_info) { | |
194 | div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + | |
6821dee9 | 195 | ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; |
4af819d4 JN |
196 | } |
197 | ||
198 | if (div_progress_bar) { | |
199 | //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); | |
200 | div_progress_bar.style.width = percentage + '%'; | |
201 | } | |
202 | } | |
203 | ||
204 | ||
205 | var t_interval_server = ''; | |
206 | var cmds_server = ''; | |
207 | var t0 = new Date(); | |
208 | ||
209 | /** | |
210 | * write how much it took to generate data, and to run script | |
211 | * | |
212 | * @globals t0, t_interval_server, cmds_server | |
213 | */ | |
214 | function writeTimeInterval() { | |
215 | var info_time = document.getElementById('generating_time'); | |
216 | if (!info_time || !t_interval_server) { | |
217 | return; | |
218 | } | |
219 | var t1 = new Date(); | |
220 | info_time.firstChild.data += ' + (' + | |
221 | t_interval_server + ' sec server blame_data / ' + | |
222 | (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; | |
223 | ||
224 | var info_cmds = document.getElementById('generating_cmd'); | |
225 | if (!info_time || !cmds_server) { | |
226 | return; | |
227 | } | |
228 | info_cmds.firstChild.data += ' + ' + cmds_server; | |
229 | } | |
230 | ||
231 | /** | |
232 | * show an error message alert to user within page (in prohress info area) | |
233 | * @param {String} str: plain text error message (no HTML) | |
234 | * | |
235 | * @globals div_progress_info | |
236 | */ | |
237 | function errorInfo(str) { | |
238 | if (!div_progress_info) { | |
239 | div_progress_info = document.getElementById('progress_info'); | |
240 | } | |
241 | if (div_progress_info) { | |
242 | div_progress_info.className = 'error'; | |
243 | div_progress_info.firstChild.data = str; | |
244 | } | |
245 | } | |
246 | ||
e206d62a JN |
247 | /* ............................................................ */ |
248 | /* coloring rows during blame_data (git blame --incremental) run */ | |
249 | ||
250 | /** | |
251 | * used to extract N from 'colorN', where N is a number, | |
252 | * @constant | |
253 | */ | |
254 | var colorRe = /\bcolor([0-9]*)\b/; | |
255 | ||
256 | /** | |
257 | * return N if <tr class="colorN">, otherwise return null | |
258 | * (some browsers require CSS class names to begin with letter) | |
259 | * | |
260 | * @param {HTMLElement} tr: table row element to check | |
261 | * @param {String} tr.className: 'class' attribute of tr element | |
262 | * @returns {Number|null} N if tr.className == 'colorN', otherwise null | |
263 | * | |
264 | * @globals colorRe | |
265 | */ | |
266 | function getColorNo(tr) { | |
267 | if (!tr) { | |
268 | return null; | |
269 | } | |
270 | var className = tr.className; | |
271 | if (className) { | |
272 | var match = colorRe.exec(className); | |
273 | if (match) { | |
274 | return parseInt(match[1], 10); | |
275 | } | |
276 | } | |
277 | return null; | |
278 | } | |
279 | ||
280 | var colorsFreq = [0, 0, 0]; | |
281 | /** | |
282 | * return one of given possible colors (curently least used one) | |
283 | * example: chooseColorNoFrom(2, 3) returns 2 or 3 | |
284 | * | |
285 | * @param {Number[]} arguments: one or more numbers | |
286 | * assumes that 1 <= arguments[i] <= colorsFreq.length | |
287 | * @returns {Number} Least used color number from arguments | |
288 | * @globals colorsFreq | |
289 | */ | |
290 | function chooseColorNoFrom() { | |
291 | // choose the color which is least used | |
292 | var colorNo = arguments[0]; | |
293 | for (var i = 1; i < arguments.length; i++) { | |
294 | if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { | |
295 | colorNo = arguments[i]; | |
296 | } | |
297 | } | |
298 | colorsFreq[colorNo-1]++; | |
299 | return colorNo; | |
300 | } | |
301 | ||
302 | /** | |
303 | * given two neigbour <tr> elements, find color which would be different | |
304 | * from color of both of neighbours; used to 3-color blame table | |
305 | * | |
306 | * @param {HTMLElement} tr_prev | |
307 | * @param {HTMLElement} tr_next | |
308 | * @returns {Number} color number N such that | |
309 | * colorN != tr_prev.className && colorN != tr_next.className | |
310 | */ | |
311 | function findColorNo(tr_prev, tr_next) { | |
312 | var color_prev = getColorNo(tr_prev); | |
313 | var color_next = getColorNo(tr_next); | |
314 | ||
315 | ||
316 | // neither of neighbours has color set | |
317 | // THEN we can use any of 3 possible colors | |
318 | if (!color_prev && !color_next) { | |
319 | return chooseColorNoFrom(1,2,3); | |
320 | } | |
321 | ||
322 | // either both neighbours have the same color, | |
323 | // or only one of neighbours have color set | |
324 | // THEN we can use any color except given | |
325 | var color; | |
326 | if (color_prev === color_next) { | |
327 | color = color_prev; // = color_next; | |
328 | } else if (!color_prev) { | |
329 | color = color_next; | |
330 | } else if (!color_next) { | |
331 | color = color_prev; | |
332 | } | |
333 | if (color) { | |
334 | return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); | |
335 | } | |
336 | ||
337 | // neighbours have different colors | |
338 | // THEN there is only one color left | |
339 | return (3 - ((color_prev + color_next) % 3)); | |
340 | } | |
341 | ||
4af819d4 JN |
342 | /* ............................................................ */ |
343 | /* coloring rows like 'blame' after 'blame_data' finishes */ | |
344 | ||
345 | /** | |
346 | * returns true if given row element (tr) is first in commit group | |
347 | * to be used only after 'blame_data' finishes (after processing) | |
348 | * | |
349 | * @param {HTMLElement} tr: table row | |
350 | * @returns {Boolean} true if TR is first in commit group | |
351 | */ | |
352 | function isStartOfGroup(tr) { | |
353 | return tr.firstChild.className === 'sha1'; | |
354 | } | |
355 | ||
4af819d4 JN |
356 | /** |
357 | * change colors to use zebra coloring (2 colors) instead of 3 colors | |
358 | * concatenate neighbour commit groups belonging to the same commit | |
359 | * | |
360 | * @globals colorRe | |
361 | */ | |
362 | function fixColorsAndGroups() { | |
363 | var colorClasses = ['light', 'dark']; | |
364 | var linenum = 1; | |
365 | var tr, prev_group; | |
366 | var colorClass = 0; | |
367 | var table = | |
368 | document.getElementById('blame_table') || | |
369 | document.getElementsByTagName('table')[0]; | |
370 | ||
371 | while ((tr = document.getElementById('l'+linenum))) { | |
372 | // index origin is 0, which is table header; start from 1 | |
373 | //while ((tr = table.rows[linenum])) { // <- it is slower | |
374 | if (isStartOfGroup(tr, linenum, document)) { | |
375 | if (prev_group && | |
376 | prev_group.firstChild.firstChild.href === | |
377 | tr.firstChild.firstChild.href) { | |
378 | // we have to concatenate groups | |
379 | var prev_rows = prev_group.firstChild.rowSpan || 1; | |
380 | var curr_rows = tr.firstChild.rowSpan || 1; | |
381 | prev_group.firstChild.rowSpan = prev_rows + curr_rows; | |
382 | //tr.removeChild(tr.firstChild); | |
383 | tr.deleteCell(0); // DOM2 HTML way | |
384 | } else { | |
385 | colorClass = (colorClass + 1) % 2; | |
386 | prev_group = tr; | |
387 | } | |
388 | } | |
389 | var tr_class = tr.className; | |
390 | tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); | |
391 | linenum++; | |
392 | } | |
393 | } | |
394 | ||
395 | /* ............................................................ */ | |
396 | /* time and data */ | |
397 | ||
398 | /** | |
399 | * used to extract hours and minutes from timezone info, e.g '-0900' | |
400 | * @constant | |
401 | */ | |
402 | var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/; | |
403 | ||
404 | /** | |
405 | * return date in local time formatted in iso-8601 like format | |
406 | * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' | |
407 | * | |
408 | * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' | |
409 | * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' | |
410 | * @returns {String} date in local time in iso-8601 like format | |
411 | * | |
412 | * @globals tzRe | |
413 | */ | |
414 | function formatDateISOLocal(epoch, timezoneInfo) { | |
415 | var match = tzRe.exec(timezoneInfo); | |
416 | // date corrected by timezone | |
417 | var localDate = new Date(1000 * (epoch + | |
418 | (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60))); | |
419 | var localDateStr = // e.g. '2005-08-07' | |
420 | localDate.getUTCFullYear() + '-' + | |
421 | padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + | |
422 | padLeft(localDate.getUTCDate(), 2, '0'); | |
423 | var localTimeStr = // e.g. '21:49:46' | |
424 | padLeft(localDate.getUTCHours(), 2, '0') + ':' + | |
425 | padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + | |
426 | padLeft(localDate.getUTCSeconds(), 2, '0'); | |
427 | ||
428 | return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; | |
429 | } | |
430 | ||
431 | /* ............................................................ */ | |
432 | /* unquoting/unescaping filenames */ | |
433 | ||
434 | /**#@+ | |
435 | * @constant | |
436 | */ | |
437 | var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; | |
438 | var octEscRe = /^[0-7]{1,3}$/; | |
439 | var maybeQuotedRe = /^\"(.*)\"$/; | |
440 | /**#@-*/ | |
441 | ||
442 | /** | |
443 | * unquote maybe git-quoted filename | |
444 | * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' | |
445 | * | |
446 | * @param {String} str: git-quoted string | |
447 | * @returns {String} Unquoted and unescaped string | |
448 | * | |
449 | * @globals escCodeRe, octEscRe, maybeQuotedRe | |
450 | */ | |
451 | function unquote(str) { | |
452 | function unq(seq) { | |
453 | var es = { | |
454 | // character escape codes, aka escape sequences (from C) | |
455 | // replacements are to some extent JavaScript specific | |
456 | t: "\t", // tab (HT, TAB) | |
457 | n: "\n", // newline (NL) | |
458 | r: "\r", // return (CR) | |
459 | f: "\f", // form feed (FF) | |
460 | b: "\b", // backspace (BS) | |
461 | a: "\x07", // alarm (bell) (BEL) | |
462 | e: "\x1B", // escape (ESC) | |
463 | v: "\v" // vertical tab (VT) | |
464 | }; | |
465 | ||
466 | if (seq.search(octEscRe) !== -1) { | |
467 | // octal char sequence | |
468 | return String.fromCharCode(parseInt(seq, 8)); | |
469 | } else if (seq in es) { | |
470 | // C escape sequence, aka character escape code | |
471 | return es[seq]; | |
472 | } | |
473 | // quoted ordinary character | |
474 | return seq; | |
475 | } | |
476 | ||
477 | var match = str.match(maybeQuotedRe); | |
478 | if (match) { | |
479 | str = match[1]; | |
480 | // perhaps str = eval('"'+str+'"'); would be enough? | |
481 | str = str.replace(escCodeRe, | |
482 | function (substr, p1, offset, s) { return unq(p1); }); | |
483 | } | |
484 | return str; | |
485 | } | |
486 | ||
487 | /* ============================================================ */ | |
488 | /* main part: parsing response */ | |
489 | ||
490 | /** | |
491 | * Function called for each blame entry, as soon as it finishes. | |
492 | * It updates page via DOM manipulation, adding sha1 info, etc. | |
493 | * | |
494 | * @param {Commit} commit: blamed commit | |
495 | * @param {Object} group: object representing group of lines, | |
496 | * which blame the same commit (blame entry) | |
497 | * | |
498 | * @globals blamedLines | |
499 | */ | |
500 | function handleLine(commit, group) { | |
501 | /* | |
502 | This is the structure of the HTML fragment we are working | |
503 | with: | |
504 | ||
505 | <tr id="l123" class=""> | |
506 | <td class="sha1" title=""><a href=""> </a></td> | |
507 | <td class="linenr"><a class="linenr" href="">123</a></td> | |
508 | <td class="pre"># times (my ext3 doesn't).</td> | |
509 | </tr> | |
510 | */ | |
511 | ||
512 | var resline = group.resline; | |
513 | ||
514 | // format date and time string only once per commit | |
515 | if (!commit.info) { | |
516 | /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ | |
517 | commit.info = commit.author + ', ' + | |
518 | formatDateISOLocal(commit.authorTime, commit.authorTimezone); | |
519 | } | |
520 | ||
e206d62a JN |
521 | // color depends on group of lines, not only on blamed commit |
522 | var colorNo = findColorNo( | |
523 | document.getElementById('l'+(resline-1)), | |
524 | document.getElementById('l'+(resline+group.numlines)) | |
525 | ); | |
526 | ||
4af819d4 JN |
527 | // loop over lines in commit group |
528 | for (var i = 0; i < group.numlines; i++, resline++) { | |
529 | var tr = document.getElementById('l'+resline); | |
530 | if (!tr) { | |
531 | break; | |
532 | } | |
533 | /* | |
534 | <tr id="l123" class=""> | |
535 | <td class="sha1" title=""><a href=""> </a></td> | |
536 | <td class="linenr"><a class="linenr" href="">123</a></td> | |
537 | <td class="pre"># times (my ext3 doesn't).</td> | |
538 | </tr> | |
539 | */ | |
540 | var td_sha1 = tr.firstChild; | |
541 | var a_sha1 = td_sha1.firstChild; | |
542 | var a_linenr = td_sha1.nextSibling.firstChild; | |
543 | ||
544 | /* <tr id="l123" class=""> */ | |
e206d62a JN |
545 | var tr_class = ''; |
546 | if (colorNo !== null) { | |
547 | tr_class = 'color'+colorNo; | |
548 | } | |
4af819d4 JN |
549 | if (commit.boundary) { |
550 | tr_class += ' boundary'; | |
551 | } | |
552 | if (commit.nprevious === 0) { | |
553 | tr_class += ' no-previous'; | |
554 | } else if (commit.nprevious > 1) { | |
555 | tr_class += ' multiple-previous'; | |
556 | } | |
557 | tr.className = tr_class; | |
558 | ||
559 | /* <td class="sha1" title="?" rowspan="?"><a href="?">?</a></td> */ | |
560 | if (i === 0) { | |
561 | td_sha1.title = commit.info; | |
562 | td_sha1.rowSpan = group.numlines; | |
563 | ||
564 | a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; | |
6aa2de51 JN |
565 | if (a_sha1.firstChild) { |
566 | a_sha1.firstChild.data = commit.sha1.substr(0, 8); | |
567 | } else { | |
568 | a_sha1.appendChild( | |
569 | document.createTextNode(commit.sha1.substr(0, 8))); | |
570 | } | |
4af819d4 JN |
571 | if (group.numlines >= 2) { |
572 | var fragment = document.createDocumentFragment(); | |
573 | var br = document.createElement("br"); | |
e42a05f7 SB |
574 | var match = commit.author.match(/\b([A-Z])\B/g); |
575 | if (match) { | |
576 | var text = document.createTextNode( | |
577 | match.join('')); | |
578 | } | |
4af819d4 JN |
579 | if (br && text) { |
580 | var elem = fragment || td_sha1; | |
581 | elem.appendChild(br); | |
582 | elem.appendChild(text); | |
583 | if (fragment) { | |
584 | td_sha1.appendChild(fragment); | |
585 | } | |
586 | } | |
587 | } | |
588 | } else { | |
589 | //tr.removeChild(td_sha1); // DOM2 Core way | |
590 | tr.deleteCell(0); // DOM2 HTML way | |
591 | } | |
592 | ||
593 | /* <td class="linenr"><a class="linenr" href="?">123</a></td> */ | |
594 | var linenr_commit = | |
595 | ('previous' in commit ? commit.previous : commit.sha1); | |
596 | var linenr_filename = | |
597 | ('file_parent' in commit ? commit.file_parent : commit.filename); | |
598 | a_linenr.href = projectUrl + 'a=blame_incremental' + | |
599 | ';hb=' + linenr_commit + | |
600 | ';f=' + encodeURIComponent(linenr_filename) + | |
601 | '#l' + (group.srcline + i); | |
602 | ||
603 | blamedLines++; | |
604 | ||
605 | //updateProgressInfo(); | |
606 | } | |
607 | } | |
608 | ||
609 | // ---------------------------------------------------------------------- | |
610 | ||
611 | var inProgress = false; // are we processing response | |
612 | ||
613 | /**#@+ | |
614 | * @constant | |
615 | */ | |
616 | var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; | |
617 | var infoRe = /^([a-z-]+) ?(.*)/; | |
618 | var endRe = /^END ?([^ ]*) ?(.*)/; | |
619 | /**@-*/ | |
620 | ||
621 | var curCommit = new Commit(); | |
622 | var curGroup = {}; | |
623 | ||
624 | var pollTimer = null; | |
625 | ||
626 | /** | |
627 | * Parse output from 'git blame --incremental [...]', received via | |
628 | * XMLHttpRequest from server (blamedataUrl), and call handleLine | |
629 | * (which updates page) as soon as blame entry is completed. | |
630 | * | |
631 | * @param {String[]} lines: new complete lines from blamedata server | |
632 | * | |
633 | * @globals commits, curCommit, curGroup, t_interval_server, cmds_server | |
634 | * @globals sha1Re, infoRe, endRe | |
635 | */ | |
636 | function processBlameLines(lines) { | |
637 | var match; | |
638 | ||
639 | for (var i = 0, len = lines.length; i < len; i++) { | |
640 | ||
641 | if ((match = sha1Re.exec(lines[i]))) { | |
642 | var sha1 = match[1]; | |
643 | var srcline = parseInt(match[2], 10); | |
644 | var resline = parseInt(match[3], 10); | |
645 | var numlines = parseInt(match[4], 10); | |
646 | ||
647 | var c = commits[sha1]; | |
648 | if (!c) { | |
649 | c = new Commit(sha1); | |
650 | commits[sha1] = c; | |
651 | } | |
652 | curCommit = c; | |
653 | ||
654 | curGroup.srcline = srcline; | |
655 | curGroup.resline = resline; | |
656 | curGroup.numlines = numlines; | |
657 | ||
658 | } else if ((match = infoRe.exec(lines[i]))) { | |
659 | var info = match[1]; | |
660 | var data = match[2]; | |
661 | switch (info) { | |
662 | case 'filename': | |
663 | curCommit.filename = unquote(data); | |
664 | // 'filename' information terminates the entry | |
665 | handleLine(curCommit, curGroup); | |
666 | updateProgressInfo(); | |
667 | break; | |
668 | case 'author': | |
669 | curCommit.author = data; | |
670 | break; | |
671 | case 'author-time': | |
672 | curCommit.authorTime = parseInt(data, 10); | |
673 | break; | |
674 | case 'author-tz': | |
675 | curCommit.authorTimezone = data; | |
676 | break; | |
677 | case 'previous': | |
678 | curCommit.nprevious++; | |
679 | // store only first 'previous' header | |
680 | if (!'previous' in curCommit) { | |
681 | var parts = data.split(' ', 2); | |
682 | curCommit.previous = parts[0]; | |
683 | curCommit.file_parent = unquote(parts[1]); | |
684 | } | |
685 | break; | |
686 | case 'boundary': | |
687 | curCommit.boundary = true; | |
688 | break; | |
689 | } // end switch | |
690 | ||
691 | } else if ((match = endRe.exec(lines[i]))) { | |
692 | t_interval_server = match[1]; | |
693 | cmds_server = match[2]; | |
694 | ||
695 | } else if (lines[i] !== '') { | |
696 | // malformed line | |
697 | ||
698 | } // end if (match) | |
699 | ||
700 | } // end for (lines) | |
701 | } | |
702 | ||
703 | /** | |
704 | * Process new data and return pointer to end of processed part | |
705 | * | |
706 | * @param {String} unprocessed: new data (from nextReadPos) | |
707 | * @param {Number} nextReadPos: end of last processed data | |
708 | * @return {Number} end of processed data (new value for nextReadPos) | |
709 | */ | |
710 | function processData(unprocessed, nextReadPos) { | |
711 | var lastLineEnd = unprocessed.lastIndexOf('\n'); | |
712 | if (lastLineEnd !== -1) { | |
713 | var lines = unprocessed.substring(0, lastLineEnd).split('\n'); | |
714 | nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; | |
715 | ||
716 | processBlameLines(lines); | |
717 | } // end if | |
718 | ||
719 | return nextReadPos; | |
720 | } | |
721 | ||
722 | /** | |
723 | * Handle XMLHttpRequest errors | |
724 | * | |
725 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object | |
726 | * | |
727 | * @globals pollTimer, commits, inProgress | |
728 | */ | |
729 | function handleError(xhr) { | |
730 | errorInfo('Server error: ' + | |
731 | xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); | |
732 | ||
733 | clearInterval(pollTimer); | |
734 | commits = {}; // free memory | |
735 | ||
736 | inProgress = false; | |
737 | } | |
738 | ||
739 | /** | |
740 | * Called after XMLHttpRequest finishes (loads) | |
741 | * | |
742 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused) | |
743 | * | |
744 | * @globals pollTimer, commits, inProgress | |
745 | */ | |
746 | function responseLoaded(xhr) { | |
747 | clearInterval(pollTimer); | |
748 | ||
749 | fixColorsAndGroups(); | |
750 | writeTimeInterval(); | |
751 | commits = {}; // free memory | |
752 | ||
753 | inProgress = false; | |
754 | } | |
755 | ||
756 | /** | |
757 | * handler for XMLHttpRequest onreadystatechange event | |
758 | * @see startBlame | |
759 | * | |
760 | * @globals xhr, inProgress | |
761 | */ | |
762 | function handleResponse() { | |
763 | ||
764 | /* | |
765 | * xhr.readyState | |
766 | * | |
767 | * Value Constant (W3C) Description | |
768 | * ------------------------------------------------------------------- | |
769 | * 0 UNSENT open() has not been called yet. | |
770 | * 1 OPENED send() has not been called yet. | |
771 | * 2 HEADERS_RECEIVED send() has been called, and headers | |
772 | * and status are available. | |
773 | * 3 LOADING Downloading; responseText holds partial data. | |
774 | * 4 DONE The operation is complete. | |
775 | */ | |
776 | ||
777 | if (xhr.readyState !== 4 && xhr.readyState !== 3) { | |
778 | return; | |
779 | } | |
780 | ||
781 | // the server returned error | |
b2c2e4c2 JN |
782 | // try ... catch block is to work around bug in IE8 |
783 | try { | |
784 | if (xhr.readyState === 3 && xhr.status !== 200) { | |
785 | return; | |
786 | } | |
787 | } catch (e) { | |
4af819d4 JN |
788 | return; |
789 | } | |
790 | if (xhr.readyState === 4 && xhr.status !== 200) { | |
791 | handleError(xhr); | |
792 | return; | |
793 | } | |
794 | ||
795 | // In konqueror xhr.responseText is sometimes null here... | |
796 | if (xhr.responseText === null) { | |
797 | return; | |
798 | } | |
799 | ||
800 | // in case we were called before finished processing | |
801 | if (inProgress) { | |
802 | return; | |
803 | } else { | |
804 | inProgress = true; | |
805 | } | |
806 | ||
807 | // extract new whole (complete) lines, and process them | |
808 | while (xhr.prevDataLength !== xhr.responseText.length) { | |
809 | if (xhr.readyState === 4 && | |
810 | xhr.prevDataLength === xhr.responseText.length) { | |
811 | break; | |
812 | } | |
813 | ||
814 | xhr.prevDataLength = xhr.responseText.length; | |
815 | var unprocessed = xhr.responseText.substring(xhr.nextReadPos); | |
816 | xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); | |
817 | } // end while | |
818 | ||
819 | // did we finish work? | |
820 | if (xhr.readyState === 4 && | |
821 | xhr.prevDataLength === xhr.responseText.length) { | |
822 | responseLoaded(xhr); | |
823 | } | |
824 | ||
825 | inProgress = false; | |
826 | } | |
827 | ||
828 | // ============================================================ | |
829 | // ------------------------------------------------------------ | |
830 | ||
831 | /** | |
832 | * Incrementally update line data in blame_incremental view in gitweb. | |
833 | * | |
834 | * @param {String} blamedataUrl: URL to server script generating blame data. | |
835 | * @param {String} bUrl: partial URL to project, used to generate links. | |
836 | * | |
837 | * Called from 'blame_incremental' view after loading table with | |
838 | * file contents, a base for blame view. | |
839 | * | |
840 | * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer | |
841 | */ | |
842 | function startBlame(blamedataUrl, bUrl) { | |
843 | ||
844 | xhr = createRequestObject(); | |
845 | if (!xhr) { | |
846 | errorInfo('ERROR: XMLHttpRequest not supported'); | |
847 | return; | |
848 | } | |
849 | ||
850 | t0 = new Date(); | |
851 | projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); | |
852 | if ((div_progress_bar = document.getElementById('progress_bar'))) { | |
853 | //div_progress_bar.setAttribute('style', 'width: 100%;'); | |
854 | div_progress_bar.style.cssText = 'width: 100%;'; | |
855 | } | |
856 | totalLines = countLines(); | |
857 | updateProgressInfo(); | |
858 | ||
859 | /* add extra properties to xhr object to help processing response */ | |
860 | xhr.prevDataLength = -1; // used to detect if we have new data | |
861 | xhr.nextReadPos = 0; // where unread part of response starts | |
862 | ||
863 | xhr.onreadystatechange = handleResponse; | |
864 | //xhr.onreadystatechange = function () { handleResponse(xhr); }; | |
865 | ||
866 | xhr.open('GET', blamedataUrl); | |
867 | xhr.setRequestHeader('Accept', 'text/plain'); | |
868 | xhr.send(null); | |
869 | ||
870 | // not all browsers call onreadystatechange event on each server flush | |
871 | // poll response using timer every second to handle this issue | |
872 | pollTimer = setInterval(xhr.onreadystatechange, 1000); | |
873 | } | |
874 | ||
875 | // end of gitweb.js |