gitweb: Incremental blame (WIP)
[git/jnareb-git.git] / gitweb / blame.js
blob197d61578fe1b791f73849c2c07ecc941f5248b3
1 // Copyright (C) 2007, Fredrik Kuivinen <frekui@gmail.com>
3 var DEBUG = 0;
4 function debug(str) {
5         if (DEBUG)
6                 alert(str);
9 function createRequestObject() {
10         var ro;
11         if (window.XMLHttpRequest) {
12                 ro = new XMLHttpRequest();
13         } else {
14                 ro = new ActiveXObject("Microsoft.XMLHTTP");
15         }
16         if (!ro)
17                 debug("Couldn't start XMLHttpRequest object");
18         return ro;
21 var http;
22 var baseUrl;
24 // 'commits' is an associative map. It maps SHA1s to Commit objects.
25 var commits = new Object();
27 function Commit(sha1) {
28         this.sha1 = sha1;
31 // convert month or day of the month to string, padding it with
32 // '0' (zero) to two characters width if necessary
33 function zeroPad(n) {
34         if (n < 10)
35                 return '0' + n;
36         else
37                 return n.toString();
40 function spacePad(n,width) {
41         var scale = 1;
42         var str = '';
44         while (width > 1) {
45                 scale *= 10;
46                 if (n < scale)
47                         str += '&nbsp;';
48                 width--;
49         }
50         return str + n.toString();
54 var blamedLines = 0;
55 var totalLines  = '???';
56 var div_progress_bar;
57 var div_progress_info;
59 function countLines() {
60         var table = document.getElementById('blame_table');
61         if (!table)
62                 table = document.getElementsByTagName('table').item(0);
64         if (table)
65                 return table.getElementsByTagName('tr').length - 1; // for header
66         else
67                 return '...';
70 function updateProgressInfo() {
71         if (!div_progress_info)
72                 div_progress_info = document.getElementById('progress_info');
73         if (!div_progress_bar)
74                 div_progress_bar = document.getElementById('progress_bar');
75         if (!div_progress_info && !div_progress_bar)
76                 return;
78         var percentage = Math.floor(100.0*blamedLines/totalLines);
80         if (div_progress_info) {
81                 div_progress_info.innerHTML  = blamedLines + ' / ' + totalLines
82                         + ' ('+spacePad(percentage,3)+'%)';
83         }
85         if (div_progress_bar) {
86                 div_progress_bar.setAttribute('style', 'width: '+percentage+'%;');
87         }
90 var colorRe = new RegExp('color([0-9]*)');
91 /* return N if <tr class="colorN">, otherwise return null */
92 function getColorNo(tr) {
93         var className = tr.getAttribute('class');
94         if (className) {
95                 match = colorRe.exec(className);
96                 if (match)
97                         return parseInt(match[1]);
98         }
99         return null;
102 function findColorNo(tr_prev, tr_next) {
103         var color_prev;
104         var color_next;
106         if (tr_prev)
107                 color_prev = getColorNo(tr_prev);
108         if (tr_next)
109                 color_next = getColorNo(tr_next);
111         if (!color_prev && !color_next)
112                 return 1;
113         if (color_prev == color_next)
114                 return ((color_prev % 3) + 1);
115         if (!color_prev)
116                 return ((color_next % 3) + 1);
117         if (!color_next)
118                 return ((color_prev % 3) + 1);
119         return (3 - ((color_prev + color_next) % 3));
122 var tzRe = new RegExp('^([+-][0-9][0-9])([0-9][0-9])$');
123 // called for each blame entry, as soon as it finishes
124 function handleLine(commit) {
125         /* This is the structure of the HTML fragment we are working
126            with:
127            
128            <tr id="l123" class="">
129              <td class="sha1" title=""><a href=""></a></td>
130              <td class="linenr"><a class="linenr" href="">123</a></td>
131              <td class="pre"># times (my ext3 doesn&#39;t).</td>
132            </tr>
133         */
135         var resline = commit.resline;
137         if (!commit.info) {
138                 var date = new Date();
139                 date.setTime(commit.authorTime * 1000); // milliseconds
140                 var dateStr =
141                         date.getUTCFullYear() + '-'
142                         + zeroPad(date.getUTCMonth()+1) + '-'
143                         + zeroPad(date.getUTCDate());
144                 var timeStr =
145                         zeroPad(date.getUTCHours()) + ':'
146                         + zeroPad(date.getUTCMinutes()) + ':'
147                         + zeroPad(date.getUTCSeconds());
149                 var localDate = new Date();
150                 var match = tzRe.exec(commit.authorTimezone);
151                 localDate.setTime(1000 * (commit.authorTime
152                         + (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60)));
153                 var localTimeStr =
154                         zeroPad(localDate.getUTCHours()) + ':'
155                         + zeroPad(localDate.getUTCMinutes()) + ':'
156                         + zeroPad(localDate.getUTCSeconds());
158                 /* e.g. 'Kay Sievers, 2005-08-07 19:49:46 +0000 (21:49:46 +0200)' */
159                 commit.info = commit.author + ', ' + dateStr + ' '
160                         + timeStr + ' +0000'
161                         + ' (' + localTimeStr + ' ' + commit.authorTimezone + ')';
162         }
164         var color = findColorNo(
165                 document.getElementById('l'+(resline-1)),
166                 document.getElementById('l'+(resline+commit.numlines))
167         );
170         for (var i = 0; i < commit.numlines; i++) {
171                 var tr = document.getElementById('l'+resline);
172                 if (!tr) {
173                         debug('tr is null! resline: ' + resline);
174                         break;
175                 }
176                 /*
177                         <tr id="l123" class="">
178                           <td class="sha1" title=""><a href=""></a></td>
179                           <td class="linenr"><a class="linenr" href="">123</a></td>
180                           <td class="pre"># times (my ext3 doesn&#39;t).</td>
181                         </tr>
182                 */
183                 var td_sha1  = tr.firstChild;
184                 var a_sha1   = td_sha1.firstChild;
185                 var a_linenr = td_sha1.nextSibling.firstChild;
187                 /* <tr id="l123" class=""> */
188                 if (color) {
189                         tr.setAttribute('class', 'color'+color.toString());
190                         // Internet Explorer needs this
191                         //tr.setAttribute('className', color.toString);
192                 }
193                 /* <td class="sha1" title="?" rowspan="?"><a href="?">?</a></td> */
194                 if (i == 0) {
195                         td_sha1.title = commit.info;
196                         td_sha1.setAttribute('rowspan', commit.numlines);
198                         a_sha1.href = baseUrl + '?a=commit;h=' + commit.sha1;
199                         a_sha1.innerHTML = commit.sha1.substr(0, 8);
200                         if (commit.numlines >= 2) {
201                                 var br   = document.createElement("br");
202                                 var text = document.createTextNode(commit.author.match(/\b([A-Z])\B/g).join(''));
203                                 if (br && text) {
204                                         td_sha1.appendChild(br);
205                                         td_sha1.appendChild(text);
206                                 }
207                         }
208                 } else {
209                         tr.removeChild(td_sha1);
210                 }
212                 /* <td class="linenr"><a class="linenr" href="?">123</a></td> */
213                 a_linenr.href = baseUrl + '?a=blame;hb=' + commit.sha1
214                         + ';f=' + commit.filename + '#l' + (commit.srcline + i);
216                 resline++;
217                 blamedLines++;
219                 //updateProgressInfo();
220         }
223 function startOfGroup(tr) {
224         return tr.firstChild.getAttribute('class') == 'sha1';
227 function fixColors() {
228         var colorClasses = ['light2', 'dark2'];
229         var linenum = 1;
230         var tr;
231         var colorClass = 0;
233         while ((tr = document.getElementById('l'+linenum))) {
234                 if (startOfGroup(tr, linenum, document)) {
235                         colorClass = (colorClass + 1) % 2;
236                 }
237                 tr.setAttribute('class', colorClasses[colorClass]);
238                 // Internet Explorer needs this
239                 tr.setAttribute('className', colorClasses[colorClass]);
240                 linenum++;
241         }
244 var t_interval_server = '';
245 var t0 = new Date();
247 function writeTimeInterval() {
248         var info = document.getElementById('generate_time');
249         if (!info)
250                 return;
251         var t1 = new Date();
253         info.innerHTML += ' + '
254                 + t_interval_server+'s server (blame_data) / '
255                 + (t1.getTime() - t0.getTime())/1000 + 's client (JavaScript)';
258 // ----------------------------------------------------------------------
260 var prevDataLength = -1;
261 var nextLine = 0;
262 var inProgress = false;
264 var sha1Re = new RegExp('([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)');
265 var infoRe = new RegExp('([a-z-]+) ?(.*)');
266 var endRe = new RegExp('END ?(.*)');
267 var curCommit = new Commit();
269 var pollTimer = null;
271 function handleResponse() {
272         debug('handleResp ready: ' + http.readyState
273               + ' respText null?: ' + (http.responseText === null)
274               + ' progress: ' + inProgress);
276         if (http.readyState != 4 && http.readyState != 3)
277                 return;
279         // the server stream is incorrect
280         if (http.readyState == 3 && http.status != 200)
281                 return;
282         if (http.readyState == 4 && http.status != 200) {
283                 if (!div_progress_info)
284                         div_progress_info = document.getElementById('progress_info');
286                 if (div_progress_info) {
287                         div_progress_info.setAttribute('class', 'error');
288                         div_progress_info.innerHTML =
289                                 http.status+' - Error contacting server\n';
290                 } else {
291                         document.write("<b>ERROR:</b> HTTP status is "+http.status+"<br />\n");
292                 }
294                 clearInterval(pollTimer);
295                 inProgress = false;
296         }
298         // In konqueror http.responseText is sometimes null here...
299         if (http.responseText === null)
300                 return;
302         /*
303         var resp = document.getElementById('state');
304         if (resp) {
305                 resp.innerHTML = http.readyState + ' : ' + http.status
306                         + '<br />len = ' + http.responseText.length
307                         + '; inProgress='+inProgress;
308                 //inProgress = true;
309         }
310         */
312         if (inProgress)
313                 return;
314         else
315                 inProgress = true;
317         while (prevDataLength != http.responseText.length) {
318                 if (http.readyState == 4
319                     && prevDataLength == http.responseText.length) {
320                         break;
321                 }
323                 prevDataLength = http.responseText.length;
324                 var response = http.responseText.substring(nextLine);
325                 var lines = response.split('\n');
326                 nextLine = nextLine + response.lastIndexOf('\n') + 1;
327                 if (response[response.length-1] != '\n') {
328                         lines.pop();
329                 }
331                 for (var i = 0; i < lines.length; i++) {
332                         var match = sha1Re.exec(lines[i]);
333                         if (match) {
334                                 var sha1 = match[1];
335                                 var srcline = parseInt(match[2]);
336                                 var resline = parseInt(match[3]);
337                                 var numlines = parseInt(match[4]);
338                                 var c = commits[sha1];
339                                 if (!c) {
340                                         c = new Commit(sha1);
341                                         commits[sha1] = c;
342                                 }
344                                 c.srcline = srcline;
345                                 c.resline = resline;
346                                 c.numlines = numlines;
347                                 curCommit = c;
348                         } else if ((match = infoRe.exec(lines[i]))) {
349                                 var info = match[1];
350                                 var data = match[2];
351                                 if (info == 'filename') {
352                                         curCommit.filename = data;
353                                         handleLine(curCommit);
354                                         updateProgressInfo();
355                                 } else if (info == 'author') {
356                                         curCommit.author = data;
357                                 } else if (info == 'author-time') {
358                                         curCommit.authorTime = parseInt(data);
359                                 } else if (info == 'author-tz') {
360                                         curCommit.authorTimezone = data;
361                                 }
362                         } else if ((match = endRe.exec(lines[i]))) {
363                                 t_interval_server = match[1];
364                                 debug('END: '+lines[i]);
365                         } else if (lines[i] != '') {
366                                 debug('malformed line: ' + lines[i]);
367                         }
368                 }
369         }
371         if (http.readyState == 4 &&
372             prevDataLength == http.responseText.length) {
373                 clearInterval(pollTimer);
375                 fixColors();
376                 writeTimeInterval();
377         }
379         inProgress = false;
382 function startBlame(blamedataUrl, bUrl) {
383         debug('startBlame('+blamedataUrl+', '+bUrl+')');
385         t0 = new Date();
386         baseUrl = bUrl;
387         totalLines = countLines();
388         updateProgressInfo();
390         http = createRequestObject();
391         http.open('get', blamedataUrl);
392         http.onreadystatechange = handleResponse;
393         http.send(null);
395         pollTimer = setInterval(handleResponse, 1000);
398 // end of blame.js