f17ef185 |
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 | /* ............................................................ */ |
13 | /* Padding */ |
14 | |
15 | /** |
16 | * pad INPUT on the left with STR that is assumed to have visible |
17 | * width of single character (for example nonbreakable spaces), |
18 | * to WIDTH characters |
19 | * |
20 | * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' |
21 | * ('\u00A0' is nonbreakable space) |
22 | * |
23 | * @param {Number|String} input: number to pad |
24 | * @param {Number} width: visible width of output |
25 | * @param {String} str: string to prefix to string, defaults to '\u00A0' |
26 | * @returns {String} INPUT prefixed with STR x (WIDTH - INPUT.length) |
27 | */ |
28 | function padLeftStr(input, width, str) { |
29 | var prefix = ''; |
30 | if (typeof str === 'undefined') { |
31 | ch = '\u00A0'; // using ' ' doesn't work in all browsers |
32 | } |
33 | |
34 | width -= input.toString().length; |
35 | while (width > 0) { |
36 | prefix += str; |
37 | width--; |
38 | } |
39 | return prefix + input; |
40 | } |
41 | |
42 | /** |
43 | * Pad INPUT on the left to WIDTH, using given padding character CH, |
44 | * for example padLeft('a', 3, '_') is '__a' |
45 | * padLeft(4, 2) is '04' (same as padLeft(4, 2, '0')) |
46 | * |
47 | * @param {String} input: input value converted to string. |
48 | * @param {Number} width: desired length of output. |
49 | * @param {String} ch: single character to prefix to string, defaults to '0'. |
50 | * |
51 | * @returns {String} Modified string, at least SIZE length. |
52 | */ |
53 | function padLeft(input, width, ch) { |
54 | var s = input + ""; |
55 | if (typeof ch === 'undefined') { |
56 | ch = '0'; |
57 | } |
58 | |
59 | while (s.length < width) { |
60 | s = ch + s; |
61 | } |
62 | return s; |
63 | } |
64 | |
65 | |
66 | /* ............................................................ */ |
67 | /* Handling browser incompatibilities */ |
68 | |
69 | /** |
70 | * Create XMLHttpRequest object in cross-browser way |
71 | * @returns XMLHttpRequest object, or null |
72 | */ |
73 | function createRequestObject() { |
74 | try { |
75 | return new XMLHttpRequest(); |
76 | } catch (e) {} |
77 | try { |
78 | return window.createRequest(); |
79 | } catch (e) {} |
80 | try { |
81 | return new ActiveXObject("Msxml2.XMLHTTP"); |
82 | } catch (e) {} |
83 | try { |
84 | return new ActiveXObject("Microsoft.XMLHTTP"); |
85 | } catch (e) {} |
86 | |
87 | return null; |
88 | } |
89 | |
90 | |
91 | /** |
92 | * Insert rule giving specified STYLE to given SELECTOR at the end of |
93 | * first CSS stylesheet. |
94 | * |
95 | * @param {String} selector: CSS selector, e.g. '.class' |
96 | * @param {String} style: rule contents, e.g. 'background-color: red;' |
97 | */ |
98 | function addCssRule(selector, style) { |
99 | var stylesheet = document.styleSheets[0]; |
100 | |
101 | var theRules = []; |
102 | if (stylesheet.cssRules) { // W3C way |
103 | theRules = stylesheet.cssRules; |
104 | } else if (stylesheet.rules) { // IE way |
105 | theRules = stylesheet.rules; |
106 | } |
107 | |
108 | if (stylesheet.insertRule) { // W3C way |
109 | stylesheet.insertRule(selector + ' { ' + style + ' }', theRules.length); |
110 | } else if (stylesheet.addRule) { // IE way |
111 | stylesheet.addRule(selector, style); |
112 | } |
113 | } |
114 | |
115 | |
116 | /* ............................................................ */ |
117 | /* Support for legacy browsers */ |
118 | |
119 | /** |
120 | * Provides getElementsByClassName method, if there is no native |
121 | * implementation of this method. |
122 | * |
123 | * NOTE that there are limits and differences compared to native |
124 | * getElementsByClassName as defined by e.g.: |
125 | * https://developer.mozilla.org/en/DOM/document.getElementsByClassName |
126 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-getelementsbyclassname |
127 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-document-getelementsbyclassname |
128 | * |
129 | * Namely, this implementation supports only single class name as |
130 | * argument and not set of space-separated tokens representing classes, |
131 | * it returns Array of nodes rather than live NodeList, and has |
132 | * additional optional argument where you can limit search to given tags |
133 | * (via getElementsByTagName). |
134 | * |
135 | * Based on |
136 | * http://code.google.com/p/getelementsbyclassname/ |
137 | * http://www.dustindiaz.com/getelementsbyclass/ |
138 | * http://stackoverflow.com/questions/1818865/do-we-have-getelementsbyclassname-in-javascript |
139 | * |
140 | * See also http://ejohn.org/blog/getelementsbyclassname-speed-comparison/ |
141 | * |
142 | * @param {String} class: name of _single_ class to find |
143 | * @param {String} [taghint] limit search to given tags |
144 | * @returns {Node[]} array of matching elements |
145 | */ |
146 | if (!('getElementsByClassName' in document)) { |
147 | document.getElementsByClassName = function (classname, taghint) { |
148 | taghint = taghint || "*"; |
149 | var elements = (taghint === "*" && document.all) ? |
150 | document.all : |
151 | document.getElementsByTagName(taghint); |
152 | var pattern = new RegExp("(^|\\s)" + classname + "(\\s|$)"); |
153 | var matches= []; |
154 | for (var i = 0, j = 0, n = elements.length; i < n; i++) { |
155 | var el= elements[i]; |
156 | if (el.className && pattern.test(el.className)) { |
157 | // matches.push(el); |
158 | matches[j] = el; |
159 | j++; |
160 | } |
161 | } |
162 | return matches; |
163 | }; |
164 | } // end if |
165 | |
166 | |
167 | /* ............................................................ */ |
168 | /* unquoting/unescaping filenames */ |
169 | |
170 | /**#@+ |
171 | * @constant |
172 | */ |
173 | var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; |
174 | var octEscRe = /^[0-7]{1,3}$/; |
175 | var maybeQuotedRe = /^\"(.*)\"$/; |
176 | /**#@-*/ |
177 | |
178 | /** |
179 | * unquote maybe C-quoted filename (as used by git, i.e. it is |
180 | * in double quotes '"' if there is any escape character used) |
181 | * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' |
182 | * |
183 | * @param {String} str: git-quoted string |
184 | * @returns {String} Unquoted and unescaped string |
185 | * |
186 | * @globals escCodeRe, octEscRe, maybeQuotedRe |
187 | */ |
188 | function unquote(str) { |
189 | function unq(seq) { |
190 | var es = { |
191 | // character escape codes, aka escape sequences (from C) |
192 | // replacements are to some extent JavaScript specific |
193 | t: "\t", // tab (HT, TAB) |
194 | n: "\n", // newline (NL) |
195 | r: "\r", // return (CR) |
196 | f: "\f", // form feed (FF) |
197 | b: "\b", // backspace (BS) |
198 | a: "\x07", // alarm (bell) (BEL) |
199 | e: "\x1B", // escape (ESC) |
200 | v: "\v" // vertical tab (VT) |
201 | }; |
202 | |
203 | if (seq.search(octEscRe) !== -1) { |
204 | // octal char sequence |
205 | return String.fromCharCode(parseInt(seq, 8)); |
206 | } else if (seq in es) { |
207 | // C escape sequence, aka character escape code |
208 | return es[seq]; |
209 | } |
210 | // quoted ordinary character |
211 | return seq; |
212 | } |
213 | |
214 | var match = str.match(maybeQuotedRe); |
215 | if (match) { |
216 | str = match[1]; |
217 | // perhaps str = eval('"'+str+'"'); would be enough? |
218 | str = str.replace(escCodeRe, |
219 | function (substr, p1, offset, s) { return unq(p1); }); |
220 | } |
221 | return str; |
222 | } |
223 | |
224 | /* end of common-lib.js */ |
225 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> |
226 | // 2007, Petr Baudis <pasky@suse.cz> |
227 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> |
228 | |
229 | /** |
230 | * @fileOverview Datetime manipulation: parsing and formatting |
231 | * @license GPLv2 or later |
232 | */ |
233 | |
234 | |
235 | /* ............................................................ */ |
236 | /* parsing and retrieving datetime related information */ |
237 | |
238 | /** |
239 | * used to extract hours and minutes from timezone info, e.g '-0900' |
240 | * @constant |
241 | */ |
242 | var tzRe = /^([+\-])([0-9][0-9])([0-9][0-9])$/; |
243 | |
244 | /** |
245 | * convert numeric timezone +/-ZZZZ to offset from UTC in seconds |
246 | * |
247 | * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' |
248 | * @returns {Number} offset from UTC in seconds for timezone |
249 | * |
250 | * @globals tzRe |
251 | */ |
252 | function timezoneOffset(timezoneInfo) { |
253 | var match = tzRe.exec(timezoneInfo); |
254 | var tz_sign = (match[1] === '-' ? -1 : +1); |
255 | var tz_hour = parseInt(match[2],10); |
256 | var tz_min = parseInt(match[3],10); |
257 | |
258 | return tz_sign*(((tz_hour*60) + tz_min)*60); |
259 | } |
260 | |
261 | /** |
262 | * return local (browser) timezone as offset from UTC in seconds |
263 | * |
264 | * @returns {Number} offset from UTC in seconds for local timezone |
265 | */ |
266 | function localTimezoneOffset() { |
267 | // getTimezoneOffset returns the time-zone offset from UTC, |
268 | // in _minutes_, for the current locale |
269 | return ((new Date()).getTimezoneOffset() * -60); |
270 | } |
271 | |
272 | /** |
273 | * return local (browser) timezone as numeric timezone '(+|-)HHMM' |
274 | * |
275 | * @returns {String} locat timezone as -/+ZZZZ |
276 | */ |
277 | function localTimezoneInfo() { |
278 | var tzOffsetMinutes = (new Date()).getTimezoneOffset() * -1; |
279 | |
280 | return formatTimezoneInfo(0, tzOffsetMinutes); |
281 | } |
282 | |
283 | |
284 | /** |
285 | * Parse RFC-2822 date into a Unix timestamp (into epoch) |
286 | * |
287 | * @param {String} date: date in RFC-2822 format, e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' |
288 | * @returns {Number} epoch i.e. seconds since '00:00:00 1970-01-01 UTC' |
289 | */ |
290 | function parseRFC2822Date(date) { |
291 | // Date.parse accepts the IETF standard (RFC 1123 Section 5.2.14 and elsewhere) |
292 | // date syntax, which is defined in RFC 2822 (obsoletes RFC 822) |
293 | // and returns number of _milli_seconds since January 1, 1970, 00:00:00 UTC |
294 | return Date.parse(date) / 1000; |
295 | } |
296 | |
297 | |
298 | /* ............................................................ */ |
299 | /* formatting date */ |
300 | |
301 | /** |
302 | * format timezone offset as numerical timezone '(+|-)HHMM' or '(+|-)HH:MM' |
303 | * |
304 | * @param {Number} hours: offset in hours, e.g. 2 for '+0200' |
305 | * @param {Number} [minutes] offset in minutes, e.g. 30 for '-4030'; |
306 | * it is split into hours if not 0 <= minutes < 60, |
307 | * for example 1200 would give '+0100'; |
308 | * defaults to 0 |
309 | * @param {String} [sep] separator between hours and minutes part, |
310 | * default is '', might be ':' for W3CDTF (rfc-3339) |
311 | * @returns {String} timezone in '(+|-)HHMM' or '(+|-)HH:MM' format |
312 | */ |
313 | function formatTimezoneInfo(hours, minutes, sep) { |
314 | minutes = minutes || 0; // to be able to use formatTimezoneInfo(hh) |
315 | sep = sep || ''; // default format is +/-ZZZZ |
316 | |
317 | if (minutes < 0 || minutes > 59) { |
318 | hours = minutes > 0 ? Math.floor(minutes / 60) : Math.ceil(minutes / 60); |
319 | minutes = Math.abs(minutes - 60*hours); // sign of minutes is sign of hours |
320 | // NOTE: this works correctly because there is no UTC-00:30 timezone |
321 | } |
322 | |
323 | var tzSign = hours >= 0 ? '+' : '-'; |
324 | if (hours < 0) { |
325 | hours = -hours; // sign is stored in tzSign |
326 | } |
327 | |
328 | return tzSign + padLeft(hours, 2, '0') + sep + padLeft(minutes, 2, '0'); |
329 | } |
330 | |
331 | /** |
332 | * translate 'utc' and 'local' to numerical timezone |
333 | * @param {String} timezoneInfo: might be 'utc' or 'local' (browser) |
334 | */ |
335 | function normalizeTimezoneInfo(timezoneInfo) { |
336 | switch (timezoneInfo) { |
337 | case 'utc': |
338 | return '+0000'; |
339 | case 'local': // 'local' is browser timezone |
340 | return localTimezoneInfo(); |
341 | } |
342 | return timezoneInfo; |
343 | } |
344 | |
345 | |
346 | /** |
347 | * return date in local time formatted in iso-8601 like format |
348 | * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' |
349 | * |
350 | * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' |
351 | * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' |
352 | * @returns {String} date in local time in iso-8601 like format |
353 | */ |
354 | function formatDateISOLocal(epoch, timezoneInfo) { |
355 | // date corrected by timezone |
356 | var localDate = new Date(1000 * (epoch + |
357 | timezoneOffset(timezoneInfo))); |
358 | var localDateStr = // e.g. '2005-08-07' |
359 | localDate.getUTCFullYear() + '-' + |
360 | padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + |
361 | padLeft(localDate.getUTCDate(), 2, '0'); |
362 | var localTimeStr = // e.g. '21:49:46' |
363 | padLeft(localDate.getUTCHours(), 2, '0') + ':' + |
364 | padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + |
365 | padLeft(localDate.getUTCSeconds(), 2, '0'); |
366 | |
367 | return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; |
368 | } |
369 | |
370 | /** |
371 | * return date in local time formatted in rfc-2822 format |
372 | * e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' |
373 | * |
374 | * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' |
375 | * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' |
376 | * @param {Boolean} [padDay] e.g. 'Sun, 07 Aug' if true, 'Sun, 7 Aug' otherwise |
377 | * @returns {String} date in local time in rfc-2822 format |
378 | */ |
379 | function formatDateRFC2882(epoch, timezoneInfo, padDay) { |
380 | // A short textual representation of a month, three letters |
381 | var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; |
382 | // A textual representation of a day, three letters |
383 | var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; |
384 | // date corrected by timezone |
385 | var localDate = new Date(1000 * (epoch + |
386 | timezoneOffset(timezoneInfo))); |
387 | var localDateStr = // e.g. 'Sun, 7 Aug 2005' or 'Sun, 07 Aug 2005' |
388 | days[localDate.getUTCDay()] + ', ' + |
389 | (padDay ? padLeft(localDate.getUTCDate(),2,'0') : localDate.getUTCDate()) + ' ' + |
390 | months[localDate.getUTCMonth()] + ' ' + |
391 | localDate.getUTCFullYear(); |
392 | var localTimeStr = // e.g. '21:49:46' |
393 | padLeft(localDate.getUTCHours(), 2, '0') + ':' + |
394 | padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + |
395 | padLeft(localDate.getUTCSeconds(), 2, '0'); |
396 | |
397 | return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; |
398 | } |
399 | |
400 | /* end of datetime.js */ |
401 | /** |
402 | * @fileOverview Accessing cookies from JavaScript |
403 | * @license GPLv2 or later |
404 | */ |
405 | |
406 | /* |
407 | * Based on subsection "Cookies in JavaScript" of "Professional |
408 | * JavaScript for Web Developers" by Nicholas C. Zakas and cookie |
409 | * plugin from jQuery (dual licensed under the MIT and GPL licenses) |
410 | */ |
411 | |
412 | |
413 | /** |
414 | * Create a cookie with the given name and value, |
415 | * and other optional parameters. |
416 | * |
417 | * @example |
418 | * setCookie('foo', 'bar'); // will be deleted when browser exits |
419 | * setCookie('foo', 'bar', { expires: new Date(Date.parse('Jan 1, 2012')) }); |
420 | * setCookie('foo', 'bar', { expires: 7 }); // 7 days = 1 week |
421 | * setCookie('foo', 'bar', { expires: 14, path: '/' }); |
422 | * |
423 | * @param {String} sName: Unique name of a cookie (letters, numbers, underscores). |
424 | * @param {String} sValue: The string value stored in a cookie. |
425 | * @param {Object} [options] An object literal containing key/value pairs |
426 | * to provide optional cookie attributes. |
427 | * @param {String|Number|Date} [options.expires] Either literal string to be used as cookie expires, |
428 | * or an integer specifying the expiration date from now on in days, |
429 | * or a Date object to be used as cookie expiration date. |
430 | * If a negative value is specified or a date in the past), |
431 | * the cookie will be deleted. |
432 | * If set to null or omitted, the cookie will be a session cookie |
433 | * and will not be retained when the browser exits. |
434 | * @param {String} [options.path] Restrict access of a cookie to particular directory |
435 | * (default: path of page that created the cookie). |
436 | * @param {String} [options.domain] Override what web sites are allowed to access cookie |
437 | * (default: domain of page that created the cookie). |
438 | * @param {Boolean} [options.secure] If true, the secure attribute of the cookie will be set |
439 | * and the cookie would be accessible only from secure sites |
440 | * (cookie transmission will require secure protocol like HTTPS). |
441 | */ |
442 | function setCookie(sName, sValue, options) { |
443 | options = options || {}; |
444 | if (sValue === null) { |
445 | sValue = ''; |
446 | option.expires = 'delete'; |
447 | } |
448 | |
449 | var sCookie = sName + '=' + encodeURIComponent(sValue); |
450 | |
451 | if (options.expires) { |
452 | var oExpires = options.expires, sDate; |
453 | if (oExpires === 'delete') { |
454 | sDate = 'Thu, 01 Jan 1970 00:00:00 GMT'; |
455 | } else if (typeof oExpires === 'string') { |
456 | sDate = oExpires; |
457 | } else { |
458 | var oDate; |
459 | if (typeof oExpires === 'number') { |
460 | oDate = new Date(); |
461 | oDate.setTime(oDate.getTime() + (oExpires * 24 * 60 * 60 * 1000)); // days to ms |
462 | } else { |
463 | oDate = oExpires; |
464 | } |
465 | sDate = oDate.toGMTString(); |
466 | } |
467 | sCookie += '; expires=' + sDate; |
468 | } |
469 | |
470 | if (options.path) { |
471 | sCookie += '; path=' + (options.path); |
472 | } |
473 | if (options.domain) { |
474 | sCookie += '; domain=' + (options.domain); |
475 | } |
476 | if (options.secure) { |
477 | sCookie += '; secure'; |
478 | } |
479 | document.cookie = sCookie; |
480 | } |
481 | |
482 | /** |
483 | * Get the value of a cookie with the given name. |
484 | * |
485 | * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) |
486 | * @returns {String|null} The string value stored in a cookie |
487 | */ |
488 | function getCookie(sName) { |
489 | var sRE = '(?:; )?' + sName + '=([^;]*);?'; |
490 | var oRE = new RegExp(sRE); |
491 | if (oRE.test(document.cookie)) { |
492 | return decodeURIComponent(RegExp['$1']); |
493 | } else { |
494 | return null; |
495 | } |
496 | } |
497 | |
498 | /** |
499 | * Delete cookie with given name |
500 | * |
501 | * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) |
502 | * @param {Object} [options] An object literal containing key/value pairs |
503 | * to provide optional cookie attributes. |
504 | * @param {String} [options.path] Must be the same as when setting a cookie |
505 | * @param {String} [options.domain] Must be the same as when setting a cookie |
506 | */ |
507 | function deleteCookie(sName, options) { |
508 | options = options || {}; |
509 | options.expires = 'delete'; |
510 | |
511 | setCookie(sName, '', options); |
512 | } |
513 | |
514 | /* end of cookies.js */ |
515 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> |
516 | // 2007, Petr Baudis <pasky@suse.cz> |
517 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> |
518 | |
519 | /** |
520 | * @fileOverview Detect if JavaScript is enabled, and pass it to server-side |
521 | * @license GPLv2 or later |
522 | */ |
523 | |
524 | |
525 | /* ============================================================ */ |
526 | /* Manipulating links */ |
527 | |
528 | /** |
529 | * used to check if link has 'js' query parameter already (at end), |
530 | * and other reasons to not add 'js=1' param at the end of link |
531 | * @constant |
532 | */ |
533 | var jsExceptionsRe = /[;?]js=[01](#.*)?$/; |
534 | |
535 | /** |
536 | * Add '?js=1' or ';js=1' to the end of every link in the document |
537 | * that doesn't have 'js' query parameter set already. |
538 | * |
539 | * Links with 'js=1' lead to JavaScript version of given action, if it |
540 | * exists (currently there is only 'blame_incremental' for 'blame') |
541 | * |
542 | * To be used as `window.onload` handler |
543 | * |
544 | * @globals jsExceptionsRe |
545 | */ |
546 | function fixLinks() { |
547 | var allLinks = document.getElementsByTagName("a") || document.links; |
548 | for (var i = 0, len = allLinks.length; i < len; i++) { |
549 | var link = allLinks[i]; |
550 | if (!jsExceptionsRe.test(link)) { |
551 | link.href = link.href.replace(/(#|$)/, |
552 | (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1$1'); |
553 | } |
554 | } |
555 | } |
556 | |
557 | /* end of javascript-detection.js */ |
558 | // Copyright (C) 2011, John 'Warthog9' Hawley <warthog9@eaglescrag.net> |
559 | // 2011, Jakub Narebski <jnareb@gmail.com> |
560 | |
561 | /** |
562 | * @fileOverview Manipulate dates in gitweb output, adjusting timezone |
563 | * @license GPLv2 or later |
564 | */ |
565 | |
566 | /** |
567 | * Get common timezone, add UI for changing timezones, and adjust |
568 | * dates to use requested common timezone. |
569 | * |
570 | * This function is called during onload event (added to window.onload). |
571 | * |
572 | * @param {String} tzDefault: default timezone, if there is no cookie |
573 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone |
574 | * @param {String} tzCookieInfo.name: name of cookie to store timezone |
575 | * @param {String} tzClassName: denotes elements with date to be adjusted |
576 | */ |
577 | function onloadTZSetup(tzDefault, tzCookieInfo, tzClassName) { |
578 | var tzCookieTZ = getCookie(tzCookieInfo.name, tzCookieInfo); |
579 | var tz = tzDefault; |
580 | |
581 | if (tzCookieTZ) { |
582 | // set timezone to value saved in a cookie |
583 | tz = tzCookieTZ; |
584 | // refresh cookie, so its expiration counts from last use of gitweb |
585 | setCookie(tzCookieInfo.name, tzCookieTZ, tzCookieInfo); |
586 | } |
587 | |
588 | // add UI for changing timezone |
589 | addChangeTZ(tz, tzCookieInfo, tzClassName); |
590 | |
591 | // server-side of gitweb produces datetime in UTC, |
592 | // so if tz is 'utc' there is no need for changes |
593 | var nochange = tz === 'utc'; |
594 | |
595 | // adjust dates to use specified common timezone |
596 | fixDatetimeTZ(tz, tzClassName, nochange); |
597 | } |
598 | |
599 | |
600 | /* ...................................................................... */ |
601 | /* Changing dates to use requested timezone */ |
602 | |
603 | /** |
604 | * Replace RFC-2822 dates contained in SPAN elements with tzClassName |
605 | * CSS class with equivalent dates in given timezone. |
606 | * |
607 | * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local' |
608 | * @param {String} tzClassName: specifies elements to be changed |
609 | * @param {Boolean} nochange: markup for timezone change, but don't change it |
610 | */ |
611 | function fixDatetimeTZ(tz, tzClassName, nochange) { |
612 | // sanity check, method should be ensured by common-lib.js |
613 | if (!document.getElementsByClassName) { |
614 | return; |
615 | } |
616 | |
617 | // translate to timezone in '(-|+)HHMM' format |
618 | tz = normalizeTimezoneInfo(tz); |
619 | |
620 | // NOTE: result of getElementsByClassName should probably be cached |
621 | var classesFound = document.getElementsByClassName(tzClassName, "span"); |
622 | for (var i = 0, len = classesFound.length; i < len; i++) { |
623 | var curElement = classesFound[i]; |
624 | |
625 | curElement.title = 'Click to change timezone'; |
626 | if (!nochange) { |
627 | // we use *.firstChild.data (W3C DOM) instead of *.innerHTML |
628 | // as the latter doesn't always work everywhere in every browser |
629 | var epoch = parseRFC2822Date(curElement.firstChild.data); |
630 | var adjusted = formatDateRFC2882(epoch, tz); |
631 | |
632 | curElement.firstChild.data = adjusted; |
633 | } |
634 | } |
635 | } |
636 | |
637 | |
638 | /* ...................................................................... */ |
639 | /* Adding triggers, generating timezone menu, displaying and hiding */ |
640 | |
641 | /** |
642 | * Adds triggers for UI to change common timezone used for dates in |
643 | * gitweb output: it marks up and/or creates item to click to invoke |
644 | * timezone change UI, creates timezone UI fragment to be attached, |
645 | * and installs appropriate onclick trigger (via event delegation). |
646 | * |
647 | * @param {String} tzSelected: pre-selected timezone, |
648 | * 'utc' or 'local' or '(-|+)HHMM' |
649 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone |
650 | * @param {String} tzClassName: specifies elements to install trigger |
651 | */ |
652 | function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) { |
653 | // make link to timezone UI discoverable |
654 | addCssRule('.'+tzClassName + ':hover', |
655 | 'text-decoration: underline; cursor: help;'); |
656 | |
657 | // create form for selecting timezone (to be saved in a cookie) |
658 | var tzSelectFragment = document.createDocumentFragment(); |
659 | tzSelectFragment = createChangeTZForm(tzSelectFragment, |
660 | tzSelected, tzCookieInfo, tzClassName); |
661 | |
662 | // event delegation handler for timezone selection UI (clicking on entry) |
663 | // see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/ |
664 | // assumes that there is no existing document.onclick handler |
665 | document.onclick = function onclickHandler(event) { |
666 | //IE doesn't pass in the event object |
667 | event = event || window.event; |
668 | |
669 | //IE uses srcElement as the target |
670 | var target = event.target || event.srcElement; |
671 | |
672 | switch (target.className) { |
673 | case tzClassName: |
674 | // don't display timezone menu if it is already displayed |
675 | if (tzSelectFragment.childNodes.length > 0) { |
676 | displayChangeTZForm(target, tzSelectFragment); |
677 | } |
678 | break; |
679 | } // end switch |
680 | }; |
681 | } |
682 | |
683 | /** |
684 | * Create DocumentFragment with UI for changing common timezone in |
685 | * which dates are shown in. |
686 | * |
687 | * @param {DocumentFragment} documentFragment: where attach UI |
688 | * @param {String} tzSelected: default (pre-selected) timezone |
689 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone |
690 | * @returns {DocumentFragment} |
691 | */ |
692 | function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) { |
693 | var div = document.createElement("div"); |
694 | div.className = 'popup'; |
695 | |
696 | /* '<div class="close-button" title="(click on this box to close)">X</div>' */ |
697 | var closeButton = document.createElement('div'); |
698 | closeButton.className = 'close-button'; |
699 | closeButton.title = '(click on this box to close)'; |
700 | closeButton.appendChild(document.createTextNode('X')); |
701 | closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName); |
702 | div.appendChild(closeButton); |
703 | |
704 | /* 'Select timezone: <br clear="all">' */ |
705 | div.appendChild(document.createTextNode('Select timezone: ')); |
706 | var br = document.createElement('br'); |
707 | br.clear = 'all'; |
708 | div.appendChild(br); |
709 | |
710 | /* '<select name="tzoffset"> |
711 | * ... |
712 | * <option value="-0700">UTC-07:00</option> |
713 | * <option value="-0600">UTC-06:00</option> |
714 | * ... |
715 | * </select>' */ |
716 | var select = document.createElement("select"); |
717 | select.name = "tzoffset"; |
718 | //select.style.clear = 'all'; |
719 | select.appendChild(generateTZOptions(tzSelected)); |
720 | select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName); |
721 | div.appendChild(select); |
722 | |
723 | documentFragment.appendChild(div); |
724 | |
725 | return documentFragment; |
726 | } |
727 | |
728 | |
729 | /** |
730 | * Hide (remove from DOM) timezone change UI, ensuring that it is not |
731 | * garbage collected and that it can be re-enabled later. |
732 | * |
733 | * @param {DocumentFragment} documentFragment: contains detached UI |
734 | * @param {HTMLSelectElement} target: select element inside of UI |
735 | * @param {String} tzClassName: specifies element where UI was installed |
736 | * @returns {DocumentFragment} documentFragment |
737 | */ |
738 | function removeChangeTZForm(documentFragment, target, tzClassName) { |
739 | // find containing element, where we appended timezone selection UI |
740 | // `target' is somewhere inside timezone menu |
741 | var container = target.parentNode, popup = target; |
742 | while (container && |
743 | container.className !== tzClassName) { |
744 | popup = container; |
745 | container = container.parentNode; |
746 | } |
747 | // safety check if we found correct container, |
748 | // and if it isn't deleted already |
749 | if (!container || !popup || |
750 | container.className !== tzClassName || |
751 | popup.className !== 'popup') { |
752 | return documentFragment; |
753 | } |
754 | |
755 | // timezone selection UI was appended as last child |
756 | // see also displayChangeTZForm function |
757 | var removed = popup.parentNode.removeChild(popup); |
758 | if (documentFragment.firstChild !== removed) { // the only child |
759 | // re-append it so it would be available for next time |
760 | documentFragment.appendChild(removed); |
761 | } |
762 | // all of inline style was added by this script |
763 | // it is not really needed to remove it, but it is a good practice |
764 | container.removeAttribute('style'); |
765 | |
766 | return documentFragment; |
767 | } |
768 | |
769 | |
770 | /** |
771 | * Display UI for changing common timezone for dates in gitweb output. |
772 | * To be used from 'onclick' event handler. |
773 | * |
774 | * @param {HTMLElement} target: where to install/display UI |
775 | * @param {DocumentFragment} tzSelectFragment: timezone selection UI |
776 | */ |
777 | function displayChangeTZForm(target, tzSelectFragment) { |
778 | // for absolute positioning to be related to target element |
779 | target.style.position = 'relative'; |
780 | target.style.display = 'inline-block'; |
781 | |
782 | // show/display UI for changing timezone |
783 | target.appendChild(tzSelectFragment); |
784 | } |
785 | |
786 | |
787 | /* ...................................................................... */ |
788 | /* List of timezones for timezone selection menu */ |
789 | |
790 | /** |
791 | * Generate list of timezones for creating timezone select UI |
792 | * |
793 | * @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' } |
794 | */ |
795 | function generateTZList() { |
796 | var timezones = [ |
797 | { value: "utc", descr: "UTC/GMT"}, |
798 | { value: "local", descr: "Local (per browser)"} |
799 | ]; |
800 | |
801 | // generate all full hour timezones (no fractional timezones) |
802 | for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) { |
803 | var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2); |
804 | timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'}; |
805 | if (x === 0) { |
806 | timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC±00:00' |
807 | } |
808 | } |
809 | |
810 | return timezones; |
811 | } |
812 | |
813 | /** |
814 | * Generate <options> elements for timezone select UI |
815 | * |
816 | * @param {String} tzSelected: default timezone |
817 | * @returns {DocumentFragment} list of options elements to appendChild |
818 | */ |
819 | function generateTZOptions(tzSelected) { |
820 | var elems = document.createDocumentFragment(); |
821 | var timezones = generateTZList(); |
822 | |
823 | for (var i = 0, len = timezones.length; i < len; i++) { |
824 | var tzone = timezones[i]; |
825 | var option = document.createElement("option"); |
826 | if (tzone.value === tzSelected) { |
827 | option.defaultSelected = true; |
828 | } |
829 | option.value = tzone.value; |
830 | option.appendChild(document.createTextNode(tzone.descr)); |
831 | |
832 | elems.appendChild(option); |
833 | } |
834 | |
835 | return elems; |
836 | } |
837 | |
838 | |
839 | /* ...................................................................... */ |
840 | /* Event handlers and/or their generators */ |
841 | |
842 | /** |
843 | * Create event handler that select timezone and closes timezone select UI. |
844 | * To be used as $('select[name="tzselect"]').onchange handler. |
845 | * |
846 | * @param {DocumentFragment} tzSelectFragment: timezone selection UI |
847 | * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone |
848 | * @param {String} tzCookieInfo.name: name of cookie to save result of selection |
849 | * @param {String} tzClassName: specifies element where UI was installed |
850 | * @returns {Function} event handler |
851 | */ |
852 | function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) { |
853 | //return function selectTZ(event) { |
854 | return function (event) { |
855 | event = event || window.event; |
856 | var target = event.target || event.srcElement; |
857 | |
858 | var selected = target.options.item(target.selectedIndex); |
859 | removeChangeTZForm(tzSelectFragment, target, tzClassName); |
860 | |
861 | if (selected) { |
862 | selected.defaultSelected = true; |
863 | setCookie(tzCookieInfo.name, selected.value, tzCookieInfo); |
864 | fixDatetimeTZ(selected.value, tzClassName); |
865 | } |
866 | }; |
867 | } |
868 | |
869 | /** |
870 | * Create event handler that closes timezone select UI. |
871 | * To be used e.g. as $('.closebutton').onclick handler. |
872 | * |
873 | * @param {DocumentFragment} tzSelectFragment: timezone selection UI |
874 | * @param {String} tzClassName: specifies element where UI was installed |
875 | * @returns {Function} event handler |
876 | */ |
877 | function closeTZFormHandler(tzSelectFragment, tzClassName) { |
878 | //return function closeTZForm(event) { |
879 | return function (event) { |
880 | event = event || window.event; |
881 | var target = event.target || event.srcElement; |
882 | |
883 | removeChangeTZForm(tzSelectFragment, target, tzClassName); |
884 | }; |
885 | } |
886 | |
887 | /* end of adjust-timezone.js */ |
888 | // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com> |
889 | // 2007, Petr Baudis <pasky@suse.cz> |
890 | // 2008-2011, Jakub Narebski <jnareb@gmail.com> |
891 | |
892 | /** |
893 | * @fileOverview JavaScript side of Ajax-y 'blame_incremental' view in gitweb |
894 | * @license GPLv2 or later |
895 | */ |
896 | |
897 | /* ============================================================ */ |
898 | /* |
899 | * This code uses DOM methods instead of (nonstandard) innerHTML |
900 | * to modify page. |
901 | * |
902 | * innerHTML is non-standard IE extension, though supported by most |
903 | * browsers; however Firefox up to version 1.5 didn't implement it in |
904 | * a strict mode (application/xml+xhtml mimetype). |
905 | * |
906 | * Also my simple benchmarks show that using elem.firstChild.data = |
907 | * 'content' is slightly faster than elem.innerHTML = 'content'. It |
908 | * is however more fragile (text element fragment must exists), and |
909 | * less feature-rich (we cannot add HTML). |
910 | * |
911 | * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the |
912 | * equivalent using DOM 2 Core is usually shown in comments. |
913 | */ |
914 | |
915 | |
916 | /* ............................................................ */ |
917 | /* utility/helper functions (and variables) */ |
918 | |
919 | var projectUrl; // partial query + separator ('?' or ';') |
920 | |
921 | // 'commits' is an associative map. It maps SHA1s to Commit objects. |
922 | var commits = {}; |
923 | |
924 | /** |
925 | * constructor for Commit objects, used in 'blame' |
926 | * @class Represents a blamed commit |
927 | * @param {String} sha1: SHA-1 identifier of a commit |
928 | */ |
929 | function Commit(sha1) { |
930 | if (this instanceof Commit) { |
931 | this.sha1 = sha1; |
932 | this.nprevious = 0; /* number of 'previous', effective parents */ |
933 | } else { |
934 | return new Commit(sha1); |
935 | } |
936 | } |
937 | |
938 | /* ............................................................ */ |
939 | /* progress info, timing, error reporting */ |
940 | |
941 | var blamedLines = 0; |
942 | var totalLines = '???'; |
943 | var div_progress_bar; |
944 | var div_progress_info; |
945 | |
946 | /** |
947 | * Detects how many lines does a blamed file have, |
948 | * This information is used in progress info |
949 | * |
950 | * @returns {Number|String} Number of lines in file, or string '...' |
951 | */ |
952 | function countLines() { |
953 | var table = |
954 | document.getElementById('blame_table') || |
955 | document.getElementsByTagName('table')[0]; |
956 | |
957 | if (table) { |
958 | return table.getElementsByTagName('tr').length - 1; // for header |
959 | } else { |
960 | return '...'; |
961 | } |
962 | } |
963 | |
964 | /** |
965 | * update progress info and length (width) of progress bar |
966 | * |
967 | * @globals div_progress_info, div_progress_bar, blamedLines, totalLines |
968 | */ |
969 | function updateProgressInfo() { |
970 | if (!div_progress_info) { |
971 | div_progress_info = document.getElementById('progress_info'); |
972 | } |
973 | if (!div_progress_bar) { |
974 | div_progress_bar = document.getElementById('progress_bar'); |
975 | } |
976 | if (!div_progress_info && !div_progress_bar) { |
977 | return; |
978 | } |
979 | |
980 | var percentage = Math.floor(100.0*blamedLines/totalLines); |
981 | |
982 | if (div_progress_info) { |
983 | div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + |
984 | ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; |
985 | } |
986 | |
987 | if (div_progress_bar) { |
988 | //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); |
989 | div_progress_bar.style.width = percentage + '%'; |
990 | } |
991 | } |
992 | |
993 | |
994 | var t_interval_server = ''; |
995 | var cmds_server = ''; |
996 | var t0 = new Date(); |
997 | |
998 | /** |
999 | * write how much it took to generate data, and to run script |
1000 | * |
1001 | * @globals t0, t_interval_server, cmds_server |
1002 | */ |
1003 | function writeTimeInterval() { |
1004 | var info_time = document.getElementById('generating_time'); |
1005 | if (!info_time || !t_interval_server) { |
1006 | return; |
1007 | } |
1008 | var t1 = new Date(); |
1009 | info_time.firstChild.data += ' + (' + |
1010 | t_interval_server + ' sec server blame_data / ' + |
1011 | (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; |
1012 | |
1013 | var info_cmds = document.getElementById('generating_cmd'); |
1014 | if (!info_time || !cmds_server) { |
1015 | return; |
1016 | } |
1017 | info_cmds.firstChild.data += ' + ' + cmds_server; |
1018 | } |
1019 | |
1020 | /** |
1021 | * show an error message alert to user within page (in progress info area) |
1022 | * @param {String} str: plain text error message (no HTML) |
1023 | * |
1024 | * @globals div_progress_info |
1025 | */ |
1026 | function errorInfo(str) { |
1027 | if (!div_progress_info) { |
1028 | div_progress_info = document.getElementById('progress_info'); |
1029 | } |
1030 | if (div_progress_info) { |
1031 | div_progress_info.className = 'error'; |
1032 | div_progress_info.firstChild.data = str; |
1033 | } |
1034 | } |
1035 | |
1036 | /* ............................................................ */ |
1037 | /* coloring rows during blame_data (git blame --incremental) run */ |
1038 | |
1039 | /** |
1040 | * used to extract N from 'colorN', where N is a number, |
1041 | * @constant |
1042 | */ |
1043 | var colorRe = /\bcolor([0-9]*)\b/; |
1044 | |
1045 | /** |
1046 | * return N if <tr class="colorN">, otherwise return null |
1047 | * (some browsers require CSS class names to begin with letter) |
1048 | * |
1049 | * @param {HTMLElement} tr: table row element to check |
1050 | * @param {String} tr.className: 'class' attribute of tr element |
1051 | * @returns {Number|null} N if tr.className == 'colorN', otherwise null |
1052 | * |
1053 | * @globals colorRe |
1054 | */ |
1055 | function getColorNo(tr) { |
1056 | if (!tr) { |
1057 | return null; |
1058 | } |
1059 | var className = tr.className; |
1060 | if (className) { |
1061 | var match = colorRe.exec(className); |
1062 | if (match) { |
1063 | return parseInt(match[1], 10); |
1064 | } |
1065 | } |
1066 | return null; |
1067 | } |
1068 | |
1069 | var colorsFreq = [0, 0, 0]; |
1070 | /** |
1071 | * return one of given possible colors (currently least used one) |
1072 | * example: chooseColorNoFrom(2, 3) returns 2 or 3 |
1073 | * |
1074 | * @param {Number[]} arguments: one or more numbers |
1075 | * assumes that 1 <= arguments[i] <= colorsFreq.length |
1076 | * @returns {Number} Least used color number from arguments |
1077 | * @globals colorsFreq |
1078 | */ |
1079 | function chooseColorNoFrom() { |
1080 | // choose the color which is least used |
1081 | var colorNo = arguments[0]; |
1082 | for (var i = 1; i < arguments.length; i++) { |
1083 | if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { |
1084 | colorNo = arguments[i]; |
1085 | } |
1086 | } |
1087 | colorsFreq[colorNo-1]++; |
1088 | return colorNo; |
1089 | } |
1090 | |
1091 | /** |
1092 | * given two neighbor <tr> elements, find color which would be different |
1093 | * from color of both of neighbors; used to 3-color blame table |
1094 | * |
1095 | * @param {HTMLElement} tr_prev |
1096 | * @param {HTMLElement} tr_next |
1097 | * @returns {Number} color number N such that |
1098 | * colorN != tr_prev.className && colorN != tr_next.className |
1099 | */ |
1100 | function findColorNo(tr_prev, tr_next) { |
1101 | var color_prev = getColorNo(tr_prev); |
1102 | var color_next = getColorNo(tr_next); |
1103 | |
1104 | |
1105 | // neither of neighbors has color set |
1106 | // THEN we can use any of 3 possible colors |
1107 | if (!color_prev && !color_next) { |
1108 | return chooseColorNoFrom(1,2,3); |
1109 | } |
1110 | |
1111 | // either both neighbors have the same color, |
1112 | // or only one of neighbors have color set |
1113 | // THEN we can use any color except given |
1114 | var color; |
1115 | if (color_prev === color_next) { |
1116 | color = color_prev; // = color_next; |
1117 | } else if (!color_prev) { |
1118 | color = color_next; |
1119 | } else if (!color_next) { |
1120 | color = color_prev; |
1121 | } |
1122 | if (color) { |
1123 | return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); |
1124 | } |
1125 | |
1126 | // neighbors have different colors |
1127 | // THEN there is only one color left |
1128 | return (3 - ((color_prev + color_next) % 3)); |
1129 | } |
1130 | |
1131 | /* ............................................................ */ |
1132 | /* coloring rows like 'blame' after 'blame_data' finishes */ |
1133 | |
1134 | /** |
1135 | * returns true if given row element (tr) is first in commit group |
1136 | * to be used only after 'blame_data' finishes (after processing) |
1137 | * |
1138 | * @param {HTMLElement} tr: table row |
1139 | * @returns {Boolean} true if TR is first in commit group |
1140 | */ |
1141 | function isStartOfGroup(tr) { |
1142 | return tr.firstChild.className === 'sha1'; |
1143 | } |
1144 | |
1145 | /** |
1146 | * change colors to use zebra coloring (2 colors) instead of 3 colors |
1147 | * concatenate neighbor commit groups belonging to the same commit |
1148 | * |
1149 | * @globals colorRe |
1150 | */ |
1151 | function fixColorsAndGroups() { |
1152 | var colorClasses = ['light', 'dark']; |
1153 | var linenum = 1; |
1154 | var tr, prev_group; |
1155 | var colorClass = 0; |
1156 | var table = |
1157 | document.getElementById('blame_table') || |
1158 | document.getElementsByTagName('table')[0]; |
1159 | |
1160 | while ((tr = document.getElementById('l'+linenum))) { |
1161 | // index origin is 0, which is table header; start from 1 |
1162 | //while ((tr = table.rows[linenum])) { // <- it is slower |
1163 | if (isStartOfGroup(tr, linenum, document)) { |
1164 | if (prev_group && |
1165 | prev_group.firstChild.firstChild.href === |
1166 | tr.firstChild.firstChild.href) { |
1167 | // we have to concatenate groups |
1168 | var prev_rows = prev_group.firstChild.rowSpan || 1; |
1169 | var curr_rows = tr.firstChild.rowSpan || 1; |
1170 | prev_group.firstChild.rowSpan = prev_rows + curr_rows; |
1171 | //tr.removeChild(tr.firstChild); |
1172 | tr.deleteCell(0); // DOM2 HTML way |
1173 | } else { |
1174 | colorClass = (colorClass + 1) % 2; |
1175 | prev_group = tr; |
1176 | } |
1177 | } |
1178 | var tr_class = tr.className; |
1179 | tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); |
1180 | linenum++; |
1181 | } |
1182 | } |
1183 | |
1184 | |
1185 | /* ============================================================ */ |
1186 | /* main part: parsing response */ |
1187 | |
1188 | /** |
1189 | * Function called for each blame entry, as soon as it finishes. |
1190 | * It updates page via DOM manipulation, adding sha1 info, etc. |
1191 | * |
1192 | * @param {Commit} commit: blamed commit |
1193 | * @param {Object} group: object representing group of lines, |
1194 | * which blame the same commit (blame entry) |
1195 | * |
1196 | * @globals blamedLines |
1197 | */ |
1198 | function handleLine(commit, group) { |
1199 | /* |
1200 | This is the structure of the HTML fragment we are working |
1201 | with: |
1202 | |
1203 | <tr id="l123" class=""> |
1204 | <td class="sha1" title=""><a href=""> </a></td> |
1205 | <td class="linenr"><a class="linenr" href="">123</a></td> |
1206 | <td class="pre"># times (my ext3 doesn't).</td> |
1207 | </tr> |
1208 | */ |
1209 | |
1210 | var resline = group.resline; |
1211 | |
1212 | // format date and time string only once per commit |
1213 | if (!commit.info) { |
1214 | /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ |
1215 | commit.info = commit.author + ', ' + |
1216 | formatDateISOLocal(commit.authorTime, commit.authorTimezone); |
1217 | } |
1218 | |
1219 | // color depends on group of lines, not only on blamed commit |
1220 | var colorNo = findColorNo( |
1221 | document.getElementById('l'+(resline-1)), |
1222 | document.getElementById('l'+(resline+group.numlines)) |
1223 | ); |
1224 | |
1225 | // loop over lines in commit group |
1226 | for (var i = 0; i < group.numlines; i++, resline++) { |
1227 | var tr = document.getElementById('l'+resline); |
1228 | if (!tr) { |
1229 | break; |
1230 | } |
1231 | /* |
1232 | <tr id="l123" class=""> |
1233 | <td class="sha1" title=""><a href=""> </a></td> |
1234 | <td class="linenr"><a class="linenr" href="">123</a></td> |
1235 | <td class="pre"># times (my ext3 doesn't).</td> |
1236 | </tr> |
1237 | */ |
1238 | var td_sha1 = tr.firstChild; |
1239 | var a_sha1 = td_sha1.firstChild; |
1240 | var a_linenr = td_sha1.nextSibling.firstChild; |
1241 | |
1242 | /* <tr id="l123" class=""> */ |
1243 | var tr_class = ''; |
1244 | if (colorNo !== null) { |
1245 | tr_class = 'color'+colorNo; |
1246 | } |
1247 | if (commit.boundary) { |
1248 | tr_class += ' boundary'; |
1249 | } |
1250 | if (commit.nprevious === 0) { |
1251 | tr_class += ' no-previous'; |
1252 | } else if (commit.nprevious > 1) { |
1253 | tr_class += ' multiple-previous'; |
1254 | } |
1255 | tr.className = tr_class; |
1256 | |
1257 | /* <td class="sha1" title="?" rowspan="?"><a href="?">?</a></td> */ |
1258 | if (i === 0) { |
1259 | td_sha1.title = commit.info; |
1260 | td_sha1.rowSpan = group.numlines; |
1261 | |
1262 | a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; |
1263 | if (a_sha1.firstChild) { |
1264 | a_sha1.firstChild.data = commit.sha1.substr(0, 8); |
1265 | } else { |
1266 | a_sha1.appendChild( |
1267 | document.createTextNode(commit.sha1.substr(0, 8))); |
1268 | } |
1269 | if (group.numlines >= 2) { |
1270 | var fragment = document.createDocumentFragment(); |
1271 | var br = document.createElement("br"); |
1272 | var match = commit.author.match(/\b([A-Z])\B/g); |
1273 | if (match) { |
1274 | var text = document.createTextNode( |
1275 | match.join('')); |
1276 | } |
1277 | if (br && text) { |
1278 | var elem = fragment || td_sha1; |
1279 | elem.appendChild(br); |
1280 | elem.appendChild(text); |
1281 | if (fragment) { |
1282 | td_sha1.appendChild(fragment); |
1283 | } |
1284 | } |
1285 | } |
1286 | } else { |
1287 | //tr.removeChild(td_sha1); // DOM2 Core way |
1288 | tr.deleteCell(0); // DOM2 HTML way |
1289 | } |
1290 | |
1291 | /* <td class="linenr"><a class="linenr" href="?">123</a></td> */ |
1292 | var linenr_commit = |
1293 | ('previous' in commit ? commit.previous : commit.sha1); |
1294 | var linenr_filename = |
1295 | ('file_parent' in commit ? commit.file_parent : commit.filename); |
1296 | a_linenr.href = projectUrl + 'a=blame_incremental' + |
1297 | ';hb=' + linenr_commit + |
1298 | ';f=' + encodeURIComponent(linenr_filename) + |
1299 | '#l' + (group.srcline + i); |
1300 | |
1301 | blamedLines++; |
1302 | |
1303 | //updateProgressInfo(); |
1304 | } |
1305 | } |
1306 | |
1307 | // ---------------------------------------------------------------------- |
1308 | |
1309 | /**#@+ |
1310 | * @constant |
1311 | */ |
1312 | var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; |
1313 | var infoRe = /^([a-z-]+) ?(.*)/; |
1314 | var endRe = /^END ?([^ ]*) ?(.*)/; |
1315 | /**@-*/ |
1316 | |
1317 | var curCommit = new Commit(); |
1318 | var curGroup = {}; |
1319 | |
1320 | /** |
1321 | * Parse output from 'git blame --incremental [...]', received via |
1322 | * XMLHttpRequest from server (blamedataUrl), and call handleLine |
1323 | * (which updates page) as soon as blame entry is completed. |
1324 | * |
1325 | * @param {String[]} lines: new complete lines from blamedata server |
1326 | * |
1327 | * @globals commits, curCommit, curGroup, t_interval_server, cmds_server |
1328 | * @globals sha1Re, infoRe, endRe |
1329 | */ |
1330 | function processBlameLines(lines) { |
1331 | var match; |
1332 | |
1333 | for (var i = 0, len = lines.length; i < len; i++) { |
1334 | |
1335 | if ((match = sha1Re.exec(lines[i]))) { |
1336 | var sha1 = match[1]; |
1337 | var srcline = parseInt(match[2], 10); |
1338 | var resline = parseInt(match[3], 10); |
1339 | var numlines = parseInt(match[4], 10); |
1340 | |
1341 | var c = commits[sha1]; |
1342 | if (!c) { |
1343 | c = new Commit(sha1); |
1344 | commits[sha1] = c; |
1345 | } |
1346 | curCommit = c; |
1347 | |
1348 | curGroup.srcline = srcline; |
1349 | curGroup.resline = resline; |
1350 | curGroup.numlines = numlines; |
1351 | |
1352 | } else if ((match = infoRe.exec(lines[i]))) { |
1353 | var info = match[1]; |
1354 | var data = match[2]; |
1355 | switch (info) { |
1356 | case 'filename': |
1357 | curCommit.filename = unquote(data); |
1358 | // 'filename' information terminates the entry |
1359 | handleLine(curCommit, curGroup); |
1360 | updateProgressInfo(); |
1361 | break; |
1362 | case 'author': |
1363 | curCommit.author = data; |
1364 | break; |
1365 | case 'author-time': |
1366 | curCommit.authorTime = parseInt(data, 10); |
1367 | break; |
1368 | case 'author-tz': |
1369 | curCommit.authorTimezone = data; |
1370 | break; |
1371 | case 'previous': |
1372 | curCommit.nprevious++; |
1373 | // store only first 'previous' header |
1374 | if (!('previous' in curCommit)) { |
1375 | var parts = data.split(' ', 2); |
1376 | curCommit.previous = parts[0]; |
1377 | curCommit.file_parent = unquote(parts[1]); |
1378 | } |
1379 | break; |
1380 | case 'boundary': |
1381 | curCommit.boundary = true; |
1382 | break; |
1383 | } // end switch |
1384 | |
1385 | } else if ((match = endRe.exec(lines[i]))) { |
1386 | t_interval_server = match[1]; |
1387 | cmds_server = match[2]; |
1388 | |
1389 | } else if (lines[i] !== '') { |
1390 | // malformed line |
1391 | |
1392 | } // end if (match) |
1393 | |
1394 | } // end for (lines) |
1395 | } |
1396 | |
1397 | /** |
1398 | * Process new data and return pointer to end of processed part |
1399 | * |
1400 | * @param {String} unprocessed: new data (from nextReadPos) |
1401 | * @param {Number} nextReadPos: end of last processed data |
1402 | * @return {Number} end of processed data (new value for nextReadPos) |
1403 | */ |
1404 | function processData(unprocessed, nextReadPos) { |
1405 | var lastLineEnd = unprocessed.lastIndexOf('\n'); |
1406 | if (lastLineEnd !== -1) { |
1407 | var lines = unprocessed.substring(0, lastLineEnd).split('\n'); |
1408 | nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; |
1409 | |
1410 | processBlameLines(lines); |
1411 | } // end if |
1412 | |
1413 | return nextReadPos; |
1414 | } |
1415 | |
1416 | /** |
1417 | * Handle XMLHttpRequest errors |
1418 | * |
1419 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object |
1420 | * @param {Number} [xhr.pollTimer] ID of the timeout to clear |
1421 | * |
1422 | * @globals commits |
1423 | */ |
1424 | function handleError(xhr) { |
1425 | errorInfo('Server error: ' + |
1426 | xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); |
1427 | |
1428 | if (typeof xhr.pollTimer === "number") { |
1429 | clearTimeout(xhr.pollTimer); |
1430 | delete xhr.pollTimer; |
1431 | } |
1432 | commits = {}; // free memory |
1433 | } |
1434 | |
1435 | /** |
1436 | * Called after XMLHttpRequest finishes (loads) |
1437 | * |
1438 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object |
1439 | * @param {Number} [xhr.pollTimer] ID of the timeout to clear |
1440 | * |
1441 | * @globals commits |
1442 | */ |
1443 | function responseLoaded(xhr) { |
1444 | if (typeof xhr.pollTimer === "number") { |
1445 | clearTimeout(xhr.pollTimer); |
1446 | delete xhr.pollTimer; |
1447 | } |
1448 | |
1449 | fixColorsAndGroups(); |
1450 | writeTimeInterval(); |
1451 | commits = {}; // free memory |
1452 | } |
1453 | |
1454 | /** |
1455 | * handler for XMLHttpRequest onreadystatechange event |
1456 | * @see startBlame |
1457 | * |
1458 | * @param {XMLHttpRequest} xhr: XMLHttpRequest object |
1459 | * @param {Number} xhr.prevDataLength: previous value of xhr.responseText.length |
1460 | * @param {Number} xhr.nextReadPos: start of unread part of xhr.responseText |
1461 | * @param {Number} [xhr.pollTimer] ID of the timeout (to reset or cancel) |
1462 | * @param {Boolean} fromTimer: if handler was called from timer |
1463 | */ |
1464 | function handleResponse(xhr, fromTimer) { |
1465 | |
1466 | /* |
1467 | * xhr.readyState |
1468 | * |
1469 | * Value Constant (W3C) Description |
1470 | * ------------------------------------------------------------------- |
1471 | * 0 UNSENT open() has not been called yet. |
1472 | * 1 OPENED send() has not been called yet. |
1473 | * 2 HEADERS_RECEIVED send() has been called, and headers |
1474 | * and status are available. |
1475 | * 3 LOADING Downloading; responseText holds partial data. |
1476 | * 4 DONE The operation is complete. |
1477 | */ |
1478 | |
1479 | if (xhr.readyState !== 4 && xhr.readyState !== 3) { |
1480 | return; |
1481 | } |
1482 | |
1483 | // the server returned error |
1484 | // try ... catch block is to work around bug in IE8 |
1485 | try { |
1486 | if (xhr.readyState === 3 && xhr.status !== 200) { |
1487 | return; |
1488 | } |
1489 | } catch (e) { |
1490 | return; |
1491 | } |
1492 | if (xhr.readyState === 4 && xhr.status !== 200) { |
1493 | handleError(xhr); |
1494 | return; |
1495 | } |
1496 | |
1497 | // In konqueror xhr.responseText is sometimes null here... |
1498 | if (xhr.responseText === null) { |
1499 | return; |
1500 | } |
1501 | |
1502 | |
1503 | // extract new whole (complete) lines, and process them |
1504 | if (xhr.prevDataLength !== xhr.responseText.length) { |
1505 | xhr.prevDataLength = xhr.responseText.length; |
1506 | var unprocessed = xhr.responseText.substring(xhr.nextReadPos); |
1507 | xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); |
1508 | } |
1509 | |
1510 | // did we finish work? |
1511 | if (xhr.readyState === 4) { |
1512 | responseLoaded(xhr); |
1513 | return; |
1514 | } |
1515 | |
1516 | // if we get from timer, we have to restart it |
1517 | // otherwise onreadystatechange gives us partial response, timer not needed |
1518 | if (fromTimer) { |
1519 | setTimeout(function () { |
1520 | handleResponse(xhr, true); |
1521 | }, 1000); |
1522 | |
1523 | } else if (typeof xhr.pollTimer === "number") { |
1524 | clearTimeout(xhr.pollTimer); |
1525 | delete xhr.pollTimer; |
1526 | } |
1527 | } |
1528 | |
1529 | // ============================================================ |
1530 | // ------------------------------------------------------------ |
1531 | |
1532 | /** |
1533 | * Incrementally update line data in blame_incremental view in gitweb. |
1534 | * |
1535 | * @param {String} blamedataUrl: URL to server script generating blame data. |
1536 | * @param {String} bUrl: partial URL to project, used to generate links. |
1537 | * |
1538 | * Called from 'blame_incremental' view after loading table with |
1539 | * file contents, a base for blame view. |
1540 | * |
1541 | * @globals t0, projectUrl, div_progress_bar, totalLines |
1542 | */ |
1543 | function startBlame(blamedataUrl, bUrl) { |
1544 | |
1545 | var xhr = createRequestObject(); |
1546 | if (!xhr) { |
1547 | errorInfo('ERROR: XMLHttpRequest not supported'); |
1548 | return; |
1549 | } |
1550 | |
1551 | t0 = new Date(); |
1552 | projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); |
1553 | if ((div_progress_bar = document.getElementById('progress_bar'))) { |
1554 | //div_progress_bar.setAttribute('style', 'width: 100%;'); |
1555 | div_progress_bar.style.cssText = 'width: 100%;'; |
1556 | } |
1557 | totalLines = countLines(); |
1558 | updateProgressInfo(); |
1559 | |
1560 | /* add extra properties to xhr object to help processing response */ |
1561 | xhr.prevDataLength = -1; // used to detect if we have new data |
1562 | xhr.nextReadPos = 0; // where unread part of response starts |
1563 | |
1564 | xhr.onreadystatechange = function () { |
1565 | handleResponse(xhr, false); |
1566 | }; |
1567 | |
1568 | xhr.open('GET', blamedataUrl); |
1569 | xhr.setRequestHeader('Accept', 'text/plain'); |
1570 | xhr.send(null); |
1571 | |
1572 | // not all browsers call onreadystatechange event on each server flush |
1573 | // poll response using timer every second to handle this issue |
1574 | xhr.pollTimer = setTimeout(function () { |
1575 | handleResponse(xhr, true); |
1576 | }, 1000); |
1577 | } |
1578 | |
1579 | /* end of blame_incremental.js */ |