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