]>
Commit | Line | Data |
---|---|---|
4af819d4 JN |
1 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> |
2 | // 2007, Petr Baudis <pasky@suse.cz> | |
9a86dd57 | 3 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> |
4af819d4 JN |
4 | |
5 | /** | |
9a86dd57 | 6 | * @fileOverview JavaScript side of Ajax-y 'blame_incremental' view in gitweb |
4af819d4 JN |
7 | * @license GPLv2 or later |
8 | */ | |
9 | ||
e2895de4 | 10 | /* ============================================================ */ |
4af819d4 JN |
11 | /* |
12 | * This code uses DOM methods instead of (nonstandard) innerHTML | |
13 | * to modify page. | |
14 | * | |
15 | * innerHTML is non-standard IE extension, though supported by most | |
16 | * browsers; however Firefox up to version 1.5 didn't implement it in | |
17 | * a strict mode (application/xml+xhtml mimetype). | |
18 | * | |
19 | * Also my simple benchmarks show that using elem.firstChild.data = | |
20 | * 'content' is slightly faster than elem.innerHTML = 'content'. It | |
21 | * is however more fragile (text element fragment must exists), and | |
22 | * less feature-rich (we cannot add HTML). | |
23 | * | |
24 | * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the | |
25 | * equivalent using DOM 2 Core is usually shown in comments. | |
26 | */ | |
27 | ||
28 | ||
e2895de4 | 29 | /* ............................................................ */ |
4af819d4 JN |
30 | /* utility/helper functions (and variables) */ |
31 | ||
4af819d4 JN |
32 | var projectUrl; // partial query + separator ('?' or ';') |
33 | ||
34 | // 'commits' is an associative map. It maps SHA1s to Commit objects. | |
35 | var commits = {}; | |
36 | ||
37 | /** | |
38 | * constructor for Commit objects, used in 'blame' | |
39 | * @class Represents a blamed commit | |
40 | * @param {String} sha1: SHA-1 identifier of a commit | |
41 | */ | |
42 | function Commit(sha1) { | |
43 | if (this instanceof Commit) { | |
44 | this.sha1 = sha1; | |
45 | this.nprevious = 0; /* number of 'previous', effective parents */ | |
46 | } else { | |
47 | return new Commit(sha1); | |
48 | } | |
49 | } | |
50 | ||
51 | /* ............................................................ */ | |
52 | /* progress info, timing, error reporting */ | |
53 | ||
54 | var blamedLines = 0; | |
55 | var totalLines = '???'; | |
56 | var div_progress_bar; | |
57 | var div_progress_info; | |
58 | ||
59 | /** | |
60 | * Detects how many lines does a blamed file have, | |
61 | * This information is used in progress info | |
62 | * | |
63 | * @returns {Number|String} Number of lines in file, or string '...' | |
64 | */ | |
65 | function countLines() { | |
66 | var table = | |
67 | document.getElementById('blame_table') || | |
68 | document.getElementsByTagName('table')[0]; | |
69 | ||
70 | if (table) { | |
71 | return table.getElementsByTagName('tr').length - 1; // for header | |
72 | } else { | |
73 | return '...'; | |
74 | } | |
75 | } | |
76 | ||
77 | /** | |
78 | * update progress info and length (width) of progress bar | |
79 | * | |
80 | * @globals div_progress_info, div_progress_bar, blamedLines, totalLines | |
81 | */ | |
82 | function updateProgressInfo() { | |
83 | if (!div_progress_info) { | |
84 | div_progress_info = document.getElementById('progress_info'); | |
85 | } | |
86 | if (!div_progress_bar) { | |
87 | div_progress_bar = document.getElementById('progress_bar'); | |
88 | } | |
89 | if (!div_progress_info && !div_progress_bar) { | |
90 | return; | |
91 | } | |
92 | ||
93 | var percentage = Math.floor(100.0*blamedLines/totalLines); | |
94 | ||
95 | if (div_progress_info) { | |
96 | div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + | |
6821dee9 | 97 | ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; |
4af819d4 JN |
98 | } |
99 | ||
100 | if (div_progress_bar) { | |
101 | //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); | |
102 | div_progress_bar.style.width = percentage + '%'; | |
103 | } | |
104 | } | |
105 | ||
106 | ||
107 | var t_interval_server = ''; | |
108 | var cmds_server = ''; | |
109 | var t0 = new Date(); | |
110 | ||
111 | /** | |
112 | * write how much it took to generate data, and to run script | |
113 | * | |
114 | * @globals t0, t_interval_server, cmds_server | |
115 | */ | |
116 | function writeTimeInterval() { | |
117 | var info_time = document.getElementById('generating_time'); | |
118 | if (!info_time || !t_interval_server) { | |
119 | return; | |
120 | } | |
121 | var t1 = new Date(); | |
122 | info_time.firstChild.data += ' + (' + | |
123 | t_interval_server + ' sec server blame_data / ' + | |
124 | (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; | |
125 | ||
126 | var info_cmds = document.getElementById('generating_cmd'); | |
127 | if (!info_time || !cmds_server) { | |
128 | return; | |
129 | } | |
130 | info_cmds.firstChild.data += ' + ' + cmds_server; | |
131 | } | |
132 | ||
133 | /** | |
e2895de4 | 134 | * show an error message alert to user within page (in progress info area) |
4af819d4 JN |
135 | * @param {String} str: plain text error message (no HTML) |
136 | * | |
137 | * @globals div_progress_info | |
138 | */ | |
139 | function errorInfo(str) { | |
140 | if (!div_progress_info) { | |
141 | div_progress_info = document.getElementById('progress_info'); | |
142 | } | |
143 | if (div_progress_info) { | |
144 | div_progress_info.className = 'error'; | |
145 | div_progress_info.firstChild.data = str; | |
146 | } | |
147 | } | |
148 | ||
e206d62a JN |
149 | /* ............................................................ */ |
150 | /* coloring rows during blame_data (git blame --incremental) run */ | |
151 | ||
152 | /** | |
153 | * used to extract N from 'colorN', where N is a number, | |
154 | * @constant | |
155 | */ | |
156 | var colorRe = /\bcolor([0-9]*)\b/; | |
157 | ||
158 | /** | |
159 | * return N if <tr class="colorN">, otherwise return null | |
160 | * (some browsers require CSS class names to begin with letter) | |
161 | * | |
162 | * @param {HTMLElement} tr: table row element to check | |
163 | * @param {String} tr.className: 'class' attribute of tr element | |
164 | * @returns {Number|null} N if tr.className == 'colorN', otherwise null | |
165 | * | |
166 | * @globals colorRe | |
167 | */ | |
168 | function getColorNo(tr) { | |
169 | if (!tr) { | |
170 | return null; | |
171 | } | |
172 | var className = tr.className; | |
173 | if (className) { | |
174 | var match = colorRe.exec(className); | |
175 | if (match) { | |
176 | return parseInt(match[1], 10); | |
177 | } | |
178 | } | |
179 | return null; | |
180 | } | |
181 | ||
182 | var colorsFreq = [0, 0, 0]; | |
183 | /** | |
e2895de4 | 184 | * return one of given possible colors (currently least used one) |
e206d62a JN |
185 | * example: chooseColorNoFrom(2, 3) returns 2 or 3 |
186 | * | |
187 | * @param {Number[]} arguments: one or more numbers | |
188 | * assumes that 1 <= arguments[i] <= colorsFreq.length | |
189 | * @returns {Number} Least used color number from arguments | |
190 | * @globals colorsFreq | |
191 | */ | |
192 | function chooseColorNoFrom() { | |
193 | // choose the color which is least used | |
194 | var colorNo = arguments[0]; | |
195 | for (var i = 1; i < arguments.length; i++) { | |
196 | if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { | |
197 | colorNo = arguments[i]; | |
198 | } | |
199 | } | |
200 | colorsFreq[colorNo-1]++; | |
201 | return colorNo; | |
202 | } | |
203 | ||
204 | /** | |
e2895de4 JN |
205 | * given two neighbor <tr> elements, find color which would be different |
206 | * from color of both of neighbors; used to 3-color blame table | |
e206d62a JN |
207 | * |
208 | * @param {HTMLElement} tr_prev | |
209 | * @param {HTMLElement} tr_next | |
210 | * @returns {Number} color number N such that | |
211 | * colorN != tr_prev.className && colorN != tr_next.className | |
212 | */ | |
213 | function findColorNo(tr_prev, tr_next) { | |
214 | var color_prev = getColorNo(tr_prev); | |
215 | var color_next = getColorNo(tr_next); | |
216 | ||
217 | ||
e2895de4 | 218 | // neither of neighbors has color set |
e206d62a JN |
219 | // THEN we can use any of 3 possible colors |
220 | if (!color_prev && !color_next) { | |
221 | return chooseColorNoFrom(1,2,3); | |
222 | } | |
223 | ||
e2895de4 JN |
224 | // either both neighbors have the same color, |
225 | // or only one of neighbors have color set | |
e206d62a JN |
226 | // THEN we can use any color except given |
227 | var color; | |
228 | if (color_prev === color_next) { | |
229 | color = color_prev; // = color_next; | |
230 | } else if (!color_prev) { | |
231 | color = color_next; | |
232 | } else if (!color_next) { | |
233 | color = color_prev; | |
234 | } | |
235 | if (color) { | |
236 | return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); | |
237 | } | |
238 | ||
e2895de4 | 239 | // neighbors have different colors |
e206d62a JN |
240 | // THEN there is only one color left |
241 | return (3 - ((color_prev + color_next) % 3)); | |
242 | } | |
243 | ||
4af819d4 JN |
244 | /* ............................................................ */ |
245 | /* coloring rows like 'blame' after 'blame_data' finishes */ | |
246 | ||
247 | /** | |
248 | * returns true if given row element (tr) is first in commit group | |
249 | * to be used only after 'blame_data' finishes (after processing) | |
250 | * | |
251 | * @param {HTMLElement} tr: table row | |
252 | * @returns {Boolean} true if TR is first in commit group | |
253 | */ | |
254 | function isStartOfGroup(tr) { | |
255 | return tr.firstChild.className === 'sha1'; | |
256 | } | |
257 | ||
4af819d4 JN |
258 | /** |
259 | * change colors to use zebra coloring (2 colors) instead of 3 colors | |
e2895de4 | 260 | * concatenate neighbor commit groups belonging to the same commit |
4af819d4 JN |
261 | * |
262 | * @globals colorRe | |
263 | */ | |
264 | function fixColorsAndGroups() { | |
265 | var colorClasses = ['light', 'dark']; | |
266 | var linenum = 1; | |
267 | var tr, prev_group; | |
268 | var colorClass = 0; | |
269 | var table = | |
270 | document.getElementById('blame_table') || | |
271 | document.getElementsByTagName('table')[0]; | |
272 | ||
273 | while ((tr = document.getElementById('l'+linenum))) { | |
274 | // index origin is 0, which is table header; start from 1 | |
275 | //while ((tr = table.rows[linenum])) { // <- it is slower | |
276 | if (isStartOfGroup(tr, linenum, document)) { | |
277 | if (prev_group && | |
278 | prev_group.firstChild.firstChild.href === | |
279 | tr.firstChild.firstChild.href) { | |
280 | // we have to concatenate groups | |
281 | var prev_rows = prev_group.firstChild.rowSpan || 1; | |
282 | var curr_rows = tr.firstChild.rowSpan || 1; | |
283 | prev_group.firstChild.rowSpan = prev_rows + curr_rows; | |
284 | //tr.removeChild(tr.firstChild); | |
285 | tr.deleteCell(0); // DOM2 HTML way | |
286 | } else { | |
287 | colorClass = (colorClass + 1) % 2; | |
288 | prev_group = tr; | |
289 | } | |
290 | } | |
291 | var tr_class = tr.className; | |
292 | tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); | |
293 | linenum++; | |
294 | } | |
295 | } | |
296 | ||
4af819d4 JN |
297 | |
298 | /* ============================================================ */ | |
299 | /* main part: parsing response */ | |
300 | ||
301 | /** | |
302 | * Function called for each blame entry, as soon as it finishes. | |
303 | * It updates page via DOM manipulation, adding sha1 info, etc. | |
304 | * | |
305 | * @param {Commit} commit: blamed commit | |
306 | * @param {Object} group: object representing group of lines, | |
307 | * which blame the same commit (blame entry) | |
308 | * | |
309 | * @globals blamedLines | |
310 | */ | |
311 | function handleLine(commit, group) { | |
312 | /* | |
313 | This is the structure of the HTML fragment we are working | |
314 | with: | |
315 | ||
316 | <tr id="l123" class=""> | |
317 | <td class="sha1" title=""><a href=""> </a></td> | |
318 | <td class="linenr"><a class="linenr" href="">123</a></td> | |
319 | <td class="pre"># times (my ext3 doesn't).</td> | |
320 | </tr> | |
321 | */ | |
322 | ||
323 | var resline = group.resline; | |
324 | ||
325 | // format date and time string only once per commit | |
326 | if (!commit.info) { | |
327 | /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ | |
328 | commit.info = commit.author + ', ' + | |
329 | formatDateISOLocal(commit.authorTime, commit.authorTimezone); | |
330 | } | |
331 | ||
e206d62a JN |
332 | // color depends on group of lines, not only on blamed commit |
333 | var colorNo = findColorNo( | |
334 | document.getElementById('l'+(resline-1)), | |
335 | document.getElementById('l'+(resline+group.numlines)) | |
336 | ); | |
337 | ||
4af819d4 JN |
338 | // loop over lines in commit group |
339 | for (var i = 0; i < group.numlines; i++, resline++) { | |
340 | var tr = document.getElementById('l'+resline); | |
341 | if (!tr) { | |
342 | break; | |
343 | } | |
344 | /* | |
345 | <tr id="l123" class=""> | |
346 | <td class="sha1" title=""><a href=""> </a></td> | |
347 | <td class="linenr"><a class="linenr" href="">123</a></td> | |
348 | <td class="pre"># times (my ext3 doesn't).</td> | |
349 | </tr> | |
350 | */ | |
351 | var td_sha1 = tr.firstChild; | |
352 | var a_sha1 = td_sha1.firstChild; | |
353 | var a_linenr = td_sha1.nextSibling.firstChild; | |
354 | ||
355 | /* <tr id="l123" class=""> */ | |
e206d62a JN |
356 | var tr_class = ''; |
357 | if (colorNo !== null) { | |
358 | tr_class = 'color'+colorNo; | |
359 | } | |
4af819d4 JN |
360 | if (commit.boundary) { |
361 | tr_class += ' boundary'; | |
362 | } | |
363 | if (commit.nprevious === 0) { | |
364 | tr_class += ' no-previous'; | |
365 | } else if (commit.nprevious > 1) { | |
366 | tr_class += ' multiple-previous'; | |
367 | } | |
368 | tr.className = tr_class; | |
369 | ||
370 | /* <td class="sha1" title="?" rowspan="?"><a href="?">?</a></td> */ | |
371 | if (i === 0) { | |
372 | td_sha1.title = commit.info; | |
373 | td_sha1.rowSpan = group.numlines; | |
374 | ||
375 | a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; | |
6aa2de51 JN |
376 | if (a_sha1.firstChild) { |
377 | a_sha1.firstChild.data = commit.sha1.substr(0, 8); | |
378 | } else { | |
379 | a_sha1.appendChild( | |
380 | document.createTextNode(commit.sha1.substr(0, 8))); | |
381 | } | |
4af819d4 JN |
382 | if (group.numlines >= 2) { |
383 | var fragment = document.createDocumentFragment(); | |
384 | var br = document.createElement("br"); | |
e42a05f7 SB |
385 | var match = commit.author.match(/\b([A-Z])\B/g); |
386 | if (match) { | |
387 | var text = document.createTextNode( | |
388 | match.join('')); | |
389 | } | |
4af819d4 JN |
390 | if (br && text) { |
391 | var elem = fragment || td_sha1; | |
392 | elem.appendChild(br); | |
393 | elem.appendChild(text); | |
394 | if (fragment) { | |
395 | td_sha1.appendChild(fragment); | |
396 | } | |
397 | } | |
398 | } | |
399 | } else { | |
400 | //tr.removeChild(td_sha1); // DOM2 Core way | |
401 | tr.deleteCell(0); // DOM2 HTML way | |
402 | } | |
403 | ||
404 | /* <td class="linenr"><a class="linenr" href="?">123</a></td> */ | |
405 | var linenr_commit = | |
406 | ('previous' in commit ? commit.previous : commit.sha1); | |
407 | var linenr_filename = | |
408 | ('file_parent' in commit ? commit.file_parent : commit.filename); | |
409 | a_linenr.href = projectUrl + 'a=blame_incremental' + | |
410 | ';hb=' + linenr_commit + | |
411 | ';f=' + encodeURIComponent(linenr_filename) + | |
412 | '#l' + (group.srcline + i); | |
413 | ||
414 | blamedLines++; | |
415 | ||
416 | //updateProgressInfo(); | |
417 | } | |
418 | } | |
419 | ||
420 | // ---------------------------------------------------------------------- | |
421 | ||
4af819d4 JN |
422 | /**#@+ |
423 | * @constant | |
424 | */ | |
425 | var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; | |
426 | var infoRe = /^([a-z-]+) ?(.*)/; | |
427 | var endRe = /^END ?([^ ]*) ?(.*)/; | |
428 | /**@-*/ | |
429 | ||
430 | var curCommit = new Commit(); | |
431 | var curGroup = {}; | |
432 | ||
4af819d4 JN |
433 | /** |
434 | * Parse output from 'git blame --incremental [...]', received via | |
435 | * XMLHttpRequest from server (blamedataUrl), and call handleLine | |
436 | * (which updates page) as soon as blame entry is completed. | |
437 | * | |
438 | * @param {String[]} lines: new complete lines from blamedata server | |
439 | * | |
440 | * @globals commits, curCommit, curGroup, t_interval_server, cmds_server | |
441 | * @globals sha1Re, infoRe, endRe | |
442 | */ | |
443 | function processBlameLines(lines) { | |
444 | var match; | |
445 | ||
446 | for (var i = 0, len = lines.length; i < len; i++) { | |
447 | ||
448 | if ((match = sha1Re.exec(lines[i]))) { | |
449 | var sha1 = match[1]; | |
450 | var srcline = parseInt(match[2], 10); | |
451 | var resline = parseInt(match[3], 10); | |
452 | var numlines = parseInt(match[4], 10); | |
453 | ||
454 | var c = commits[sha1]; | |
455 | if (!c) { | |
456 | c = new Commit(sha1); | |
457 | commits[sha1] = c; | |
458 | } | |
459 | curCommit = c; | |
460 | ||
461 | curGroup.srcline = srcline; | |
462 | curGroup.resline = resline; | |
463 | curGroup.numlines = numlines; | |
464 | ||
465 | } else if ((match = infoRe.exec(lines[i]))) { | |
466 | var info = match[1]; | |
467 | var data = match[2]; | |
468 | switch (info) { | |
469 | case 'filename': | |
470 | curCommit.filename = unquote(data); | |
471 | // 'filename' information terminates the entry | |
472 | handleLine(curCommit, curGroup); | |
473 | updateProgressInfo(); | |
474 | break; | |
475 | case 'author': | |
476 | curCommit.author = data; | |
477 | break; | |
478 | case 'author-time': | |
479 | curCommit.authorTime = parseInt(data, 10); | |
480 | break; | |
481 | case 'author-tz': | |
482 | curCommit.authorTimezone = data; | |
483 | break; | |
484 | case 'previous': | |
485 | curCommit.nprevious++; | |
486 | // store only first 'previous' header | |
487 | if (!'previous' in curCommit) { | |
488 | var parts = data.split(' ', 2); | |
489 | curCommit.previous = parts[0]; | |
490 | curCommit.file_parent = unquote(parts[1]); | |
491 | } | |
492 | break; | |
493 | case 'boundary': | |
494 | curCommit.boundary = true; | |
495 | break; | |
496 | } // end switch | |
497 | ||
498 | } else if ((match = endRe.exec(lines[i]))) { | |
499 | t_interval_server = match[1]; | |
500 | cmds_server = match[2]; | |
501 | ||
502 | } else if (lines[i] !== '') { | |
503 | // malformed line | |
504 | ||
505 | } // end if (match) | |
506 | ||
507 | } // end for (lines) | |
508 | } | |
509 | ||
510 | /** | |
511 | * Process new data and return pointer to end of processed part | |
512 | * | |
513 | * @param {String} unprocessed: new data (from nextReadPos) | |
514 | * @param {Number} nextReadPos: end of last processed data | |
515 | * @return {Number} end of processed data (new value for nextReadPos) | |
516 | */ | |
517 | function processData(unprocessed, nextReadPos) { | |
518 | var lastLineEnd = unprocessed.lastIndexOf('\n'); | |
519 | if (lastLineEnd !== -1) { | |
520 | var lines = unprocessed.substring(0, lastLineEnd).split('\n'); | |
521 | nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; | |
522 | ||
523 | processBlameLines(lines); | |
524 | } // end if | |
525 | ||
526 | return nextReadPos; | |
527 | } | |
528 | ||
529 | /** | |
530 | * Handle XMLHttpRequest errors | |
531 | * | |
532 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object | |
42ab5d40 | 533 | * @param {Number} [xhr.pollTimer] ID of the timeout to clear |
4af819d4 | 534 | * |
42ab5d40 | 535 | * @globals commits |
4af819d4 JN |
536 | */ |
537 | function handleError(xhr) { | |
538 | errorInfo('Server error: ' + | |
539 | xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); | |
540 | ||
42ab5d40 JN |
541 | if (typeof xhr.pollTimer === "number") { |
542 | clearTimeout(xhr.pollTimer); | |
543 | delete xhr.pollTimer; | |
544 | } | |
4af819d4 | 545 | commits = {}; // free memory |
4af819d4 JN |
546 | } |
547 | ||
548 | /** | |
549 | * Called after XMLHttpRequest finishes (loads) | |
550 | * | |
42ab5d40 JN |
551 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object |
552 | * @param {Number} [xhr.pollTimer] ID of the timeout to clear | |
4af819d4 | 553 | * |
42ab5d40 | 554 | * @globals commits |
4af819d4 JN |
555 | */ |
556 | function responseLoaded(xhr) { | |
42ab5d40 JN |
557 | if (typeof xhr.pollTimer === "number") { |
558 | clearTimeout(xhr.pollTimer); | |
559 | delete xhr.pollTimer; | |
560 | } | |
4af819d4 JN |
561 | |
562 | fixColorsAndGroups(); | |
563 | writeTimeInterval(); | |
564 | commits = {}; // free memory | |
4af819d4 JN |
565 | } |
566 | ||
567 | /** | |
568 | * handler for XMLHttpRequest onreadystatechange event | |
569 | * @see startBlame | |
570 | * | |
42ab5d40 JN |
571 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object |
572 | * @param {Number} xhr.prevDataLength: previous value of xhr.responseText.length | |
573 | * @param {Number} xhr.nextReadPos: start of unread part of xhr.responseText | |
574 | * @param {Number} [xhr.pollTimer] ID of the timeout (to reset or cancel) | |
575 | * @param {Boolean} fromTimer: if handler was called from timer | |
4af819d4 | 576 | */ |
42ab5d40 | 577 | function handleResponse(xhr, fromTimer) { |
4af819d4 JN |
578 | |
579 | /* | |
580 | * xhr.readyState | |
581 | * | |
582 | * Value Constant (W3C) Description | |
583 | * ------------------------------------------------------------------- | |
584 | * 0 UNSENT open() has not been called yet. | |
585 | * 1 OPENED send() has not been called yet. | |
586 | * 2 HEADERS_RECEIVED send() has been called, and headers | |
587 | * and status are available. | |
588 | * 3 LOADING Downloading; responseText holds partial data. | |
589 | * 4 DONE The operation is complete. | |
590 | */ | |
591 | ||
592 | if (xhr.readyState !== 4 && xhr.readyState !== 3) { | |
593 | return; | |
594 | } | |
595 | ||
596 | // the server returned error | |
b2c2e4c2 JN |
597 | // try ... catch block is to work around bug in IE8 |
598 | try { | |
599 | if (xhr.readyState === 3 && xhr.status !== 200) { | |
600 | return; | |
601 | } | |
602 | } catch (e) { | |
4af819d4 JN |
603 | return; |
604 | } | |
605 | if (xhr.readyState === 4 && xhr.status !== 200) { | |
606 | handleError(xhr); | |
607 | return; | |
608 | } | |
609 | ||
610 | // In konqueror xhr.responseText is sometimes null here... | |
611 | if (xhr.responseText === null) { | |
612 | return; | |
613 | } | |
614 | ||
4af819d4 | 615 | |
e8dd0e40 JN |
616 | // extract new whole (complete) lines, and process them |
617 | if (xhr.prevDataLength !== xhr.responseText.length) { | |
4af819d4 JN |
618 | xhr.prevDataLength = xhr.responseText.length; |
619 | var unprocessed = xhr.responseText.substring(xhr.nextReadPos); | |
620 | xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); | |
e8dd0e40 | 621 | } |
4af819d4 JN |
622 | |
623 | // did we finish work? | |
e8dd0e40 | 624 | if (xhr.readyState === 4) { |
4af819d4 | 625 | responseLoaded(xhr); |
42ab5d40 JN |
626 | return; |
627 | } | |
628 | ||
629 | // if we get from timer, we have to restart it | |
630 | // otherwise onreadystatechange gives us partial response, timer not needed | |
631 | if (fromTimer) { | |
632 | setTimeout(function () { | |
633 | handleResponse(xhr, true); | |
634 | }, 1000); | |
635 | ||
636 | } else if (typeof xhr.pollTimer === "number") { | |
637 | clearTimeout(xhr.pollTimer); | |
638 | delete xhr.pollTimer; | |
4af819d4 | 639 | } |
4af819d4 JN |
640 | } |
641 | ||
642 | // ============================================================ | |
643 | // ------------------------------------------------------------ | |
644 | ||
645 | /** | |
646 | * Incrementally update line data in blame_incremental view in gitweb. | |
647 | * | |
648 | * @param {String} blamedataUrl: URL to server script generating blame data. | |
649 | * @param {String} bUrl: partial URL to project, used to generate links. | |
650 | * | |
651 | * Called from 'blame_incremental' view after loading table with | |
652 | * file contents, a base for blame view. | |
653 | * | |
42ab5d40 | 654 | * @globals t0, projectUrl, div_progress_bar, totalLines |
4af819d4 JN |
655 | */ |
656 | function startBlame(blamedataUrl, bUrl) { | |
657 | ||
42ab5d40 | 658 | var xhr = createRequestObject(); |
4af819d4 JN |
659 | if (!xhr) { |
660 | errorInfo('ERROR: XMLHttpRequest not supported'); | |
661 | return; | |
662 | } | |
663 | ||
664 | t0 = new Date(); | |
665 | projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); | |
666 | if ((div_progress_bar = document.getElementById('progress_bar'))) { | |
667 | //div_progress_bar.setAttribute('style', 'width: 100%;'); | |
668 | div_progress_bar.style.cssText = 'width: 100%;'; | |
669 | } | |
670 | totalLines = countLines(); | |
671 | updateProgressInfo(); | |
672 | ||
673 | /* add extra properties to xhr object to help processing response */ | |
674 | xhr.prevDataLength = -1; // used to detect if we have new data | |
675 | xhr.nextReadPos = 0; // where unread part of response starts | |
676 | ||
42ab5d40 JN |
677 | xhr.onreadystatechange = function () { |
678 | handleResponse(xhr, false); | |
679 | }; | |
4af819d4 JN |
680 | |
681 | xhr.open('GET', blamedataUrl); | |
682 | xhr.setRequestHeader('Accept', 'text/plain'); | |
683 | xhr.send(null); | |
684 | ||
685 | // not all browsers call onreadystatechange event on each server flush | |
686 | // poll response using timer every second to handle this issue | |
42ab5d40 JN |
687 | xhr.pollTimer = setTimeout(function () { |
688 | handleResponse(xhr, true); | |
689 | }, 1000); | |
4af819d4 JN |
690 | } |
691 | ||
9a86dd57 | 692 | /* end of blame_incremental.js */ |