1 // Copyright (C) 2010 Adam Barth. All rights reserved.
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are met:
6 // 1. Redistributions of source code must retain the above copyright notice,
7 // this list of conditions and the following disclaimer.
9 // 2. Redistributions in binary form must reproduce the above copyright notice,
10 // this list of conditions and the following disclaimer in the documentation
11 // and/or other materials provided with the distribution.
13 // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
14 // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 // DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
17 // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
27 * Create a new function with some of its arguements
29 * Taken from goog.partial in the Closure library.
30 * @param {Function} fn A function to partially apply.
31 * @param {...*} var_args Additional arguments that are partially
33 * @return {!Function} A partially-applied form of the function.
35 function partial(fn, var_args) {
36 var args = Array.prototype.slice.call(arguments, 1);
38 // Prepend the bound arguments to the current arguments.
39 var newArgs = Array.prototype.slice.call(arguments);
40 newArgs.unshift.apply(newArgs, args);
41 return fn.apply(this, newArgs);
45 function determineAttachmentID() {
47 return /id=(\d+)/.exec(window.location.search)[1]
53 // Attempt to activate only in the "Review Patch" context.
54 if (window.top != window)
57 if (!window.location.search.match(/action=review/)
58 && !window.location.toString().match(/bugs\.webkit\.org\/PrettyPatch/))
61 var attachment_id = determineAttachmentID();
63 console.log('No attachment ID');
67 var original_file_contents = {};
68 var patched_file_contents = {};
69 var WEBKIT_BASE_DIR = "http://svn.webkit.org/repository/webkit/trunk/";
70 var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs';
72 function idForLine(number) {
73 return 'line' + number;
76 function nextLineID() {
77 return idForLine(next_line_id++);
80 function forEachLine(callback) {
81 for (var i = 0; i < next_line_id; ++i) {
82 callback($('#' + idForLine(i)));
87 this.id = nextLineID();
91 $(this).hover(function() {
92 $(this).addClass('hot');
95 $(this).removeClass('hot');
99 function fileDiffFor(line) {
100 return line.parents('.FileDiff');
103 function activeCommentFor(line) {
104 // Scope to the diffSection as a performance improvement.
105 return $('textarea[data-comment-for~="' + line[0].id + '"]', fileDiffFor(line));
108 function previousCommentsFor(line) {
109 // Scope to the diffSection as a performance improvement.
110 return $('div[data-comment-for~="' + line[0].id + '"].previousComment', fileDiffFor(line));
113 function findCommentPositionFor(line) {
114 var previous_comments = previousCommentsFor(line);
115 var num_previous_comments = previous_comments.size();
116 if (num_previous_comments)
117 return $(previous_comments[num_previous_comments - 1])
121 function findCommentBlockFor(line) {
122 var comment_block = findCommentPositionFor(line).next();
123 if (!comment_block.hasClass('comment'))
125 return comment_block;
128 function insertCommentFor(line, block) {
129 findCommentPositionFor(line).after(block);
132 function addCommentFor(line) {
133 if (line.attr('data-has-comment')) {
134 // FIXME: This query is overly complex because we place comment blocks
135 // after Lines. Instead, comment blocks should be children of Lines.
136 findCommentPositionFor(line).next().next().filter('.frozenComment').each(unfreezeComment);
139 line.attr('data-has-comment', 'true');
140 line.addClass('commentContext');
142 var comment_block = $('<div class="comment"><textarea data-comment-for="' + line.attr('id') + '"></textarea><div class="actions"><button class="ok">OK</button><button class="discard">Discard</button></div></div>');
143 insertCommentFor(line, comment_block);
144 comment_block.hide().slideDown('fast', function() {
145 $(this).children('textarea').focus();
149 function addCommentField() {
150 var id = $(this).attr('data-comment-for');
153 addCommentFor($('#' + id));
156 function addPreviousComment(line, author, comment_text) {
157 var line_id = line.attr('id');
158 var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>');
159 var author_block = $('<div class="author"></div>').text(author + ':');
160 var text_block = $('<div class="content"></div>').text(comment_text);
161 comment_block.append(author_block).append(text_block).each(hoverify).click(addCommentField);
162 addDataCommentBaseLine(line, line_id);
163 insertCommentFor(line, comment_block);
166 function displayPreviousComments(comments) {
167 for (var i = 0; i < comments.length; ++i) {
168 var author = comments[i].author;
169 var file_name = comments[i].file_name;
170 var line_number = comments[i].line_number;
171 var comment_text = comments[i].comment_text;
173 var file = files[file_name];
175 var query = '.Line .to';
176 if (line_number[0] == '-') {
177 // The line_number represent a removal. We need to adjust the query to
178 // look at the "from" lines.
179 query = '.Line .from';
180 // Trim off the '-' control character.
181 line_number = line_number.substr(1);
184 $(file).find(query).each(function() {
185 if ($(this).text() != line_number)
187 var line = $(this).parent();
188 addPreviousComment(line, author, comment_text);
192 var help_text = 'Scroll though diffs with the "j" and "k" keys.';
193 if (comments.length == 0) {
194 $('#message .commentStatus').text(help_text);
198 descriptor = comments.length + ' comment';
199 if (comments.length > 1)
201 $('#message .commentStatus').text('This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys. ' + help_text);
204 function scanForComments(author, text) {
206 var lines = text.split('\n');
207 for (var i = 0; i < lines.length; ++i) {
208 var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
211 var quote_markers = parts[1];
212 var file_name = parts[2];
213 // FIXME: Store multiple lines for multiline comments and correctly import them here.
214 var line_number = parts[3];
215 if (!file_name in files)
217 while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
219 var comment_lines = [];
220 while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
221 comment_lines.push(lines[i]);
224 --i; // Decrement i because the for loop will increment it again in a second.
225 var comment_text = comment_lines.join('\n').trim();
228 'file_name': file_name,
229 'line_number': line_number,
230 'comment_text': comment_text
236 function isReviewFlag(select) {
237 return $(select).attr('title') == 'Request for patch review.';
240 function isCommitQueueFlag(select) {
241 return $(select).attr('title').match(/commit-queue/);
244 function findControlForFlag(select) {
245 if (isReviewFlag(select))
246 return $('#toolbar .review select');
247 else if (isCommitQueueFlag(select))
248 return $('#toolbar .commitQueue select');
252 function addFlagsForAttachment(details) {
253 var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
254 $('#flagContainer').append(
255 $('<span class="review"> r: ' + flag_control + '</span>')).append(
256 $('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
258 details.find('#flags select').each(function() {
259 var requestee = $(this).parent().siblings('td:first-child').text().trim();
260 if (requestee.length) {
261 // Remove trailing ':'.
262 requestee = requestee.substr(0, requestee.length - 1);
263 requestee = ' (' + requestee + ')';
265 var control = findControlForFlag(this)
266 control.attr('selectedIndex', $(this).attr('selectedIndex'));
267 control.parent().prepend(requestee);
271 window.addEventListener('message', function(e) {
272 if (e.origin != 'https://webkit-commit-queue.appspot.com')
276 $('.statusBubble')[0].style.height = e.data.height;
277 $('.statusBubble')[0].style.width = e.data.width;
281 function handleStatusBubbleLoad(e) {
282 e.target.contentWindow.postMessage('containerMetrics', 'https://webkit-commit-queue.appspot.com');
285 function fetchHistory() {
286 $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
287 var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
288 $.get('show_bug.cgi?id=' + bug_id, function(data) {
290 $(data).find('.bz_comment').each(function() {
291 var author = $(this).find('.email').text();
292 var text = $(this).find('.bz_comment_text').text();
293 var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
294 if (text.match(comment_marker))
295 $.merge(comments, scanForComments(author, text));
297 displayPreviousComments(comments);
300 var details = $(data);
301 addFlagsForAttachment(details);
303 var statusBubble = document.createElement('iframe');
304 statusBubble.className = 'statusBubble';
305 statusBubble.src = 'https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id;
306 statusBubble.scrolling = 'no';
307 // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
308 statusBubble.onload = handleStatusBubbleLoad;
309 $('#statusBubbleContainer').append(statusBubble);
311 $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
315 function firstLine(file_diff) {
316 var container = $('.LineContainer:not(.context)', file_diff)[0];
317 var from = fromLineNumber(container);
318 var to = toLineNumber(container);
322 function crawlDiff() {
323 $('.Line').each(idify).each(hoverify);
324 $('.FileDiff').each(function() {
325 var header = $(this).children('h1');
326 var url_hash = '#L' + firstLine(this);
328 var file_link = $('a', header)[0];
329 file_link.target = "_blank";
330 file_link.href += url_hash;
332 var file_name = header.text();
333 files[file_name] = this;
335 addExpandLinks(file_name);
336 addFileDiffLinks(file_name, url_hash);
340 function addFileDiffLinks(file_name, url_hash) {
341 var diff_links = $('<div class="FileDiffLinkContainer LinkContainer">' +
345 var trac_links = $('<a target="_blank">annotate</a><a target="_blank">revision log</a>');
346 trac_links[0].href = 'http://trac.webkit.org/browser/trunk/' + file_name + '?annotate=blame' + url_hash;
347 trac_links[1].href = 'http://trac.webkit.org/log/trunk/' + file_name;
348 diff_links.append(trac_links);
350 $('h1', files[file_name]).after(diff_links);
351 updateDiffLinkVisibility(files[file_name]);
354 function addExpandLinks(file_name) {
355 if (file_name.indexOf('ChangeLog') != -1)
358 var file_diff = files[file_name];
360 // Don't show the links to expand upwards/downwards if the patch starts/ends without context
361 // lines, i.e. starts/ends with add/remove lines.
362 var first_line = file_diff.querySelector('.LineContainer');
364 // If there is no element with a "Line" class, then this is an image diff.
368 $('.context', file_diff).detach();
370 var expand_bar_index = 0;
371 if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
372 $('h1', file_diff).after(expandBarHtml(file_name, BELOW))
374 $('br').replaceWith(expandBarHtml(file_name));
376 var last_line = file_diff.querySelector('.LineContainer:last-of-type');
377 // Some patches for new files somehow end up with an empty context line at the end
378 // with a from line number of 0. Don't show expand links in that case either.
379 if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
380 $(file_diff).append(expandBarHtml(file_name, ABOVE));
383 function expandBarHtml(file_name, opt_direction) {
384 var html = '<div class="ExpandBar">' +
385 '<div class="ExpandArea Expand' + ABOVE + '"></div>' +
386 '<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>';
388 // FIXME: If there are <100 line to expand, don't show the expand-100 link.
389 // If there are <20 lines to expand, don't show the expand-20 link.
390 if (!opt_direction || opt_direction == ABOVE) {
391 html += expandLinkHtml(ABOVE, 100) +
392 expandLinkHtml(ABOVE, 20);
395 html += expandLinkHtml(ALL);
397 if (!opt_direction || opt_direction == BELOW) {
398 html += expandLinkHtml(BELOW, 20) +
399 expandLinkHtml(BELOW, 100);
402 html += '</div><div class="ExpandArea Expand' + BELOW + '"></div></div>';
406 function expandLinkHtml(direction, amount) {
407 return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
408 (amount ? amount + " " : "") + direction + "</a>";
411 function handleExpandLinkClick() {
412 var expand_bar = $(this).parents('.ExpandBar');
413 var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
414 var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount')));
415 if (file_name in original_file_contents)
418 getWebKitSourceFile(file_name, expand_function, expand_bar);
421 function handleSideBySideLinkClick() {
422 convertDiff('sidebyside', this);
425 function handleUnifyLinkClick() {
426 convertDiff('unified', this);
429 function convertDiff(difftype, convert_link) {
430 var file_diffs = $(convert_link).parents('.FileDiff');
431 if (!file_diffs.size()) {
432 localStorage.setItem('code-review-diffstate', difftype);
433 file_diffs = $('.FileDiff');
436 convertAllFileDiffs(difftype, file_diffs);
439 function patchRevision() {
440 var revision = $('.revision');
441 return revision[0] ? revision.first().text() : null;
444 function getWebKitSourceFile(file_name, onLoad, expand_bar) {
445 function handleLoad(contents) {
446 original_file_contents[file_name] = contents.split('\n');
447 patched_file_contents[file_name] = applyDiff(original_file_contents[file_name], file_name);
451 var revision = patchRevision();
452 var queryParameters = revision ? '?p=' + revision : '';
455 url: WEBKIT_BASE_DIR + file_name + queryParameters,
456 context: document.body,
457 complete: function(xhr, data) {
459 handleLoadError(expand_bar);
461 handleLoad(xhr.responseText);
466 function replaceExpandLinkContainers(expand_bar, text) {
467 $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
470 function handleLoadError(expand_bar) {
471 replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
478 function lineNumbersFromSet(set, is_last) {
482 var size = set.size();
483 var start = is_last ? (size - 1) : 0;
484 var end = is_last ? -1 : size;
485 var offset = is_last ? -1 : 1;
487 for (var i = start; i != end; i += offset) {
488 if (to != -1 && from != -1)
489 return {to: to, from: from};
491 var line_number = set[i];
492 if ($(line_number).hasClass('to')) {
494 to = Number(line_number.textContent);
497 from = Number(line_number.textContent);
502 function expand(expand_bar, file_name, direction, amount) {
503 if (file_name in original_file_contents && !patched_file_contents[file_name]) {
504 // FIXME: In this case, try fetching the source file at the revision the patch was created at.
505 // Might need to modify webkit-patch to include that data in the diff.
506 replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
510 var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
511 var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
513 var above_line_numbers = $('.expansionLineNumber', above_expansion);
514 if (!above_line_numbers[0]) {
515 var diff_section = expand_bar.previousElementSibling;
516 above_line_numbers = $('.lineNumber', diff_section);
519 var above_last_line_num, above_last_from_line_num;
520 if (above_line_numbers[0]) {
521 var above_numbers = lineNumbersFromSet(above_line_numbers, true);
522 above_last_line_num = above_numbers.to;
523 above_last_from_line_num = above_numbers.from;
525 above_last_from_line_num = above_last_line_num = 0;
527 var below_line_numbers = $('.expansionLineNumber', below_expansion);
528 if (!below_line_numbers[0]) {
529 var diff_section = expand_bar.nextElementSibling;
531 below_line_numbers = $('.lineNumber', diff_section);
534 var below_first_line_num, below_first_from_line_num;
535 if (below_line_numbers[0]) {
536 var below_numbers = lineNumbersFromSet(below_line_numbers, false);
537 below_first_line_num = below_numbers.to - 1;
538 below_first_from_line_num = below_numbers.from - 1;
540 below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
542 var start_line_num, start_from_line_num;
545 if (direction == ABOVE) {
546 start_from_line_num = above_last_from_line_num;
547 start_line_num = above_last_line_num;
548 end_line_num = Math.min(start_line_num + amount, below_first_line_num);
549 } else if (direction == BELOW) {
550 end_line_num = below_first_line_num;
551 start_line_num = Math.max(end_line_num - amount, above_last_line_num)
552 start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
553 } else { // direction == ALL
554 start_line_num = above_last_line_num;
555 start_from_line_num = above_last_from_line_num;
556 end_line_num = below_first_line_num;
559 var lines = expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
562 // Filling in all the remaining lines. Overwrite the expand links.
563 if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
564 $('.ExpandLinkContainer', expand_bar).detach();
565 below_expansion.insertBefore(lines, below_expansion.firstChild);
566 } else if (direction == ABOVE) {
567 above_expansion.appendChild(lines);
569 below_expansion.insertBefore(lines, below_expansion.firstChild);
573 function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) {
574 var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line';
576 className += ' ' + opt_className;
578 var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber';
580 var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' +
581 '<span class="from ' + lineNumberClassName + '">' + (from || ' ') +
582 '</span><span class="to ' + lineNumberClassName + '">' + (to || ' ') +
583 '</span> <span class="text"></span>' +
586 $('.text', line).replaceWith(contents);
590 function unifiedExpansionLine(from, to, contents) {
591 return unifiedLine(from, to, contents, true);
594 function sideBySideExpansionLine(from, to, contents) {
595 var line = $('<div class="ExpansionLine"></div>');
596 // Clone the contents so we have two copies we can put back in the DOM.
597 line.append(lineSide('from', contents.clone(true), true, from));
598 line.append(lineSide('to', contents, true, to));
602 function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) {
604 if (opt_attributes || opt_class) {
605 class_name = 'class="';
607 class_name += is_expansion_line ? 'ExpansionLine' : 'Line';
608 class_name += ' ' + (opt_class || '') + '"';
611 var attributes = opt_attributes || '';
613 var line_side = $('<div class="LineSide">' +
614 '<div ' + attributes + ' ' + class_name + '>' +
615 '<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' +
616 (opt_line_number || ' ') +
618 '<span class="text"></span>' +
622 $('.text', line_side).replaceWith(contents);
626 function expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
627 var fragment = document.createDocumentFragment();
628 var is_side_by_side = isDiffSideBySide(files[file_name]);
630 for (var i = 0; i < end_line_num - start_line_num; i++) {
631 var from = start_from_line_num + i + 1;
632 var to = start_line_num + i + 1;
633 var contents = $('<span class="text"></span>');
634 contents.text(patched_file_contents[file_name][start_line_num + i]);
635 var line = is_side_by_side ? sideBySideExpansionLine(from, to, contents) : unifiedExpansionLine(from, to, contents);
636 fragment.appendChild(line[0]);
642 function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
644 var current_line = -1;
645 var last_context_line = context[context.length - 1];
646 if (patched_file[prev_line] == last_context_line)
647 current_line = prev_line + 1;
649 for (var i = prev_line - PATCH_FUZZ; i < prev_line + PATCH_FUZZ; i++) {
650 if (patched_file[i] == last_context_line)
651 current_line = i + 1;
654 if (current_line == -1) {
655 console.log('Hunk #' + hunk_num + ' FAILED.');
660 // For paranoia sake, confirm the rest of the context matches;
661 for (var i = 0; i < context.length - 1; i++) {
662 if (patched_file[current_line - context.length + i] != context[i]) {
663 console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
671 function fromLineNumber(line) {
672 var node = line.querySelector('.from');
673 return node ? Number(node.textContent) : 0;
676 function toLineNumber(line) {
677 var node = line.querySelector('.to');
678 return node ? Number(node.textContent) : 0;
681 function textContentsFor(line) {
682 // Just get the first match since a side-by-side diff has two lines with text inside them for
683 // unmodified lines in the diff.
684 return $('.text', line).first().text();
687 function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
688 if (context.length) {
689 var prev_line_num = fromLineNumber(prev_line) - 1;
690 return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
693 if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
696 console.log('Failed to apply patch. Adds or removes lines before any context lines.');
700 function applyDiff(original_file, file_name) {
701 var diff_sections = files[file_name].getElementsByClassName('DiffSection');
702 var patched_file = original_file.concat([]);
704 // Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
705 for (var i = diff_sections.length - 1; i >= 0; i--) {
706 var section = diff_sections[i];
707 var lines = section.getElementsByClassName('Line');
708 var current_line = -1;
710 var hunk_num = i + 1;
712 for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
714 var line_contents = textContentsFor(line);
715 if ($(line).hasClass('add')) {
716 if (current_line == -1) {
717 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
718 if (current_line == -1)
722 patched_file.splice(current_line, 0, line_contents);
724 } else if ($(line).hasClass('remove')) {
725 if (current_line == -1) {
726 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
727 if (current_line == -1)
731 if (patched_file[current_line] != line_contents) {
732 console.log('Hunk #' + hunk_num + ' FAILED.');
736 patched_file.splice(current_line, 1);
737 } else if (current_line == -1) {
738 context.push(line_contents);
739 } else if (line_contents != patched_file[current_line]) {
740 console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
751 function openOverallComments(e) {
752 $('.overallComments textarea').addClass('open');
753 $('#statusBubbleContainer').addClass('wrap');
756 function onBodyResize() {
757 updateToolbarAnchorState();
760 function updateToolbarAnchorState() {
761 var has_scrollbar = window.innerWidth > document.documentElement.offsetWidth;
762 $('#toolbar').toggleClass('anchored', has_scrollbar);
765 function diffLinksHtml() {
766 return '<a href="javascript:" class="unify-link">unified</a>' +
767 '<a href="javascript:" class="side-by-side-link">side-by-side</a>';
770 $(document).ready(function() {
773 $(document.body).prepend('<div id="message">' +
774 '<div class="help">Select line numbers to add a comment.' +
775 '<div class="DiffLinks LinkContainer">' + diffLinksHtml() + '</div>' +
777 '<div class="commentStatus"></div>' +
779 $(document.body).append('<div id="toolbar">' +
780 '<div class="overallComments">' +
781 '<textarea placeholder="Overall comments"></textarea>' +
784 '<span id="statusBubbleContainer"></span>' +
785 '<span class="actions">' +
786 '<span class="links"><span class="bugLink"></span></span>' +
787 '<span id="flagContainer"></span>' +
788 '<button id="preview_comments">Preview</button>' +
789 '<button id="post_comments">Publish</button> ' +
794 $('.overallComments textarea').bind('click', openOverallComments);
796 $(document.body).prepend('<div id="comment_form" class="inactive"><div class="winter"></div><div class="lightbox"><iframe id="reviewform" src="attachment.cgi?id=' + attachment_id + '&action=reviewform"></iframe></div></div>');
798 // Create a dummy iframe and monitor resizes in it's contentWindow to detect when the top document's body changes size.
799 // FIXME: Should we setTimeout throttle these?
800 var resize_iframe = $('<iframe class="pseudo_resize_event_iframe"></iframe>');
801 $(document.body).append(resize_iframe);
802 $(resize_iframe[0].contentWindow).bind('resize', onBodyResize);
804 updateToolbarAnchorState();
808 function loadDiffState() {
809 var diffstate = localStorage.getItem('code-review-diffstate');
810 if (diffstate != 'sidebyside' && diffstate != 'unified')
813 convertAllFileDiffs(diffstate, $('.FileDiff'));
816 function isDiffSideBySide(file_diff) {
817 return diffState(file_diff) == 'sidebyside';
820 function diffState(file_diff) {
821 var diff_state = $(file_diff).attr('data-diffstate');
822 return diff_state || 'unified';
825 function unifyLine(line, from, to, contents, classNames, attributes, id) {
826 var new_line = unifiedLine(from, to, contents, false, classNames, attributes);
827 var old_line = $(line);
828 if (!old_line.hasClass('LineContainer'))
829 old_line = old_line.parents('.LineContainer');
831 var comments = commentsToTransferFor($(document.getElementById(id)));
832 old_line.after(comments);
833 old_line.replaceWith(new_line);
836 function updateDiffLinkVisibility(file_diff) {
837 if (diffState(file_diff) == 'unified') {
838 $('.side-by-side-link', file_diff).show();
839 $('.unify-link', file_diff).hide();
841 $('.side-by-side-link', file_diff).hide();
842 $('.unify-link', file_diff).show();
846 function convertAllFileDiffs(diff_type, file_diffs) {
847 file_diffs.each(function() {
848 convertFileDiff(diff_type, this);
852 function convertFileDiff(diff_type, file_diff) {
853 if (diffState(file_diff) == diff_type)
856 $(file_diff).removeClass('sidebyside unified');
857 $(file_diff).addClass(diff_type);
859 $(file_diff).attr('data-diffstate', diff_type);
860 updateDiffLinkVisibility(file_diff);
862 $('.shared .Line', file_diff).each(function() {
863 convertLine(diff_type, this);
866 $('.ExpansionLine', file_diff).each(function() {
867 convertExpansionLine(diff_type, this);
871 function convertLine(diff_type, line) {
872 var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine;
873 var from = fromLineNumber(line);
874 var to = toLineNumber(line);
875 var contents = $('.text', line).first();
876 var classNames = classNamesForMovingLine(line);
877 var attributes = attributesForMovingLine(line);
879 convert_function(line, from, to, contents, classNames, attributes, id)
882 function classNamesForMovingLine(line) {
883 var classParts = line.className.split(' ');
884 var classBuffer = [];
885 for (var i = 0; i < classParts.length; i++) {
886 var part = classParts[i];
887 if (part != 'LineContainer' && part != 'Line')
888 classBuffer.push(part);
890 return classBuffer.join(' ');
893 function attributesForMovingLine(line) {
894 var attributesBuffer = ['id=' + line.id];
895 // Make sure to keep all data- attributes.
896 $(line.attributes).each(function() {
897 if (this.name.indexOf('data-') == 0)
898 attributesBuffer.push(this.name + '=' + this.value);
900 return attributesBuffer.join(' ');
903 function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) {
906 var from_attributes = '';
907 var to_attributes = '';
908 // Clone the contents so we have two copies we can put back in the DOM.
909 var from_contents = contents.clone(true);
910 var to_contents = contents;
912 var container_class = 'LineContainer';
913 var container_attributes = '';
915 if (from && !to) { // This is a remove line.
916 from_class = classNames;
917 from_attributes = attributes;
919 } else if (to && !from) { // This is an add line.
920 to_class = classNames;
921 to_attributes = attributes;
924 container_attributes = attributes;
925 container_class += ' Line ' + classNames;
928 var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>');
929 new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class));
930 new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class));
932 $(line).replaceWith(new_line);
934 var line = $(document.getElementById(id));
935 line.after(commentsToTransferFor(line));
938 function convertExpansionLine(diff_type, line) {
939 var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine;
940 var contents = $('.text', line).first();
941 var from = fromLineNumber(line);
942 var to = toLineNumber(line);
943 var new_line = convert_function(from, to, contents);
944 $(line).replaceWith(new_line);
947 function commentsToTransferFor(line) {
948 var fragment = document.createDocumentFragment();
950 previousCommentsFor(line).each(function() {
951 fragment.appendChild(this);
954 var active_comments = activeCommentFor(line);
955 var num_active_comments = active_comments.size();
956 if (num_active_comments > 0) {
957 if (num_active_comments > 1)
958 console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.');
960 var parent = active_comments[0].parentNode;
961 var frozenComment = parent.nextSibling;
962 fragment.appendChild(parent);
963 fragment.appendChild(frozenComment);
969 function discardComment() {
970 var line_id = $(this).parentsUntil('.comment').parent().find('textarea').attr('data-comment-for');
971 var line = $('#' + line_id)
972 findCommentBlockFor(line).slideUp('fast', function() {
974 line.removeAttr('data-has-comment');
975 trimCommentContextToBefore(line, line.attr('data-comment-base-line'));
979 function unfreezeComment() {
980 $(this).prev().show();
984 function showFileDiffLinks() {
985 $('.LinkContainer', this).each(function() { this.style.opacity = 1; });
988 function hideFileDiffLinks() {
989 $('.LinkContainer', this).each(function() { this.style.opacity = 0; });
992 $('.FileDiff').live('mouseenter', showFileDiffLinks);
993 $('.FileDiff').live('mouseleave', hideFileDiffLinks);
994 $('.side-by-side-link').live('click', handleSideBySideLinkClick);
995 $('.unify-link').live('click', handleUnifyLinkClick);
996 $('.ExpandLink').live('click', handleExpandLinkClick);
997 $('.comment .discard').live('click', discardComment);
998 $('.frozenComment').live('click', unfreezeComment);
1000 $('.comment .ok').live('click', function() {
1001 var comment_textarea = $(this).parentsUntil('.comment').parent().find('textarea');
1002 if (comment_textarea.val().trim() == '') {
1003 discardComment.call(this);
1006 var line_id = comment_textarea.attr('data-comment-for');
1007 var line = $('#' + line_id)
1008 findCommentBlockFor(line).hide().after($('<div class="frozenComment"></div>').text(comment_textarea.val()));
1011 function focusOn(node) {
1012 $('.focused').removeClass('focused');
1013 if (node.length == 0)
1015 $(document).scrollTop(node.addClass('focused').position().top - window.innerHeight / 2);
1018 function focusNext(className, is_backward) {
1019 var focusable_nodes = $('.previousComment,.DiffBlock').filter(function() {
1020 return ($(this).hasClass('previousComment') || $('.add,.remove', this).size());
1023 var index = focusable_nodes.index($('.focused'));
1024 if (index == -1 && is_backward)
1025 index = focusable_nodes.length;
1027 var offset = is_backward ? -1 : 1;
1028 var end = is_backward ? -1 : focusable_nodes.size();
1029 for (var i = index + offset; i != end; i = i + offset) {
1030 var node = $(focusable_nodes[i]);
1031 if (node.hasClass(className)) {
1038 var kCharCodeForN = 'n'.charCodeAt(0);
1039 var kCharCodeForP = 'p'.charCodeAt(0);
1040 var kCharCodeForJ = 'j'.charCodeAt(0);
1041 var kCharCodeForK = 'k'.charCodeAt(0);
1043 $('body').live('keypress', function() {
1044 // FIXME: There's got to be a better way to avoid seeing these keypress
1046 if (event.target.nodeName == 'TEXTAREA')
1049 switch (event.charCode) {
1051 focusNext('previousComment', false);
1055 focusNext('previousComment', true);
1059 focusNext('DiffBlock', false);
1063 focusNext('DiffBlock', true);
1068 function contextLinesFor(comment_base_lines, file_diff) {
1069 var base_lines = comment_base_lines.split(' ');
1070 return $('div[data-comment-base-line]', file_diff).filter(function() {
1071 return $(this).attr('data-comment-base-line').split(' ').some(function(item) {
1072 return base_lines.indexOf(item) != -1;
1077 function numberFrom(line_id) {
1078 return Number(line_id.replace('line', ''));
1081 function trimCommentContextToBefore(line, comment_base_line) {
1082 var line_to_trim_to = numberFrom(line.attr('id'));
1083 contextLinesFor(comment_base_line, fileDiffFor(line)).each(function() {
1084 var id = $(this).attr('id');
1085 if (numberFrom(id) > line_to_trim_to)
1088 removeDataCommentBaseLine(this, comment_base_line);
1089 if (!$(this).attr('data-comment-base-line'))
1090 $(this).removeClass('commentContext');
1094 var drag_select_start_index = -1;
1096 function stopDragSelect() {
1097 $('.selected').removeClass('selected');
1098 drag_select_start_index = -1;
1101 function lineOffsetFrom(line, offset) {
1102 var file_diff = line.parents('.FileDiff');
1103 var all_lines = $('.Line', file_diff);
1104 var index = all_lines.index(line);
1105 return $(all_lines[index + offset]);
1108 function previousLineFor(line) {
1109 return lineOffsetFrom(line, -1);
1112 function nextLineFor(line) {
1113 return lineOffsetFrom(line, 1);
1116 $('.lineNumber').live('click', function() {
1117 var line = lineFromLineDescendant($(this));
1118 if (line.hasClass('commentContext'))
1119 trimCommentContextToBefore(previousLineFor(line), line.attr('data-comment-base-line'));
1120 }).live('mousedown', function() {
1121 var line = lineFromLineDescendant($(this));
1122 drag_select_start_index = numberFrom(line.attr('id'));
1123 line.addClass('selected');
1124 event.preventDefault();
1127 function selectTo(focus_index) {
1128 var selected = $('.selected').removeClass('selected');
1129 var is_backward = drag_select_start_index > focus_index;
1130 var current_index = is_backward ? focus_index : drag_select_start_index;
1131 var last_index = is_backward ? drag_select_start_index : focus_index;
1132 while (current_index <= last_index) {
1133 $('#line' + current_index).addClass('selected')
1138 function selectToLineContainer(line_container) {
1139 var line = lineFromLineContainer(line_container);
1140 selectTo(numberFrom(line.attr('id')));
1143 $('.LineContainer').live('mouseenter', function() {
1144 if (drag_select_start_index == -1)
1146 selectToLineContainer(this);
1147 }).live('mouseup', function() {
1148 if (drag_select_start_index == -1)
1151 selectToLineContainer(this);
1153 var selected = $('.selected');
1154 var already_has_comment = selected.last().hasClass('commentContext');
1155 selected.addClass('commentContext');
1157 var comment_base_line;
1158 if (already_has_comment)
1159 comment_base_line = selected.last().attr('data-comment-base-line');
1161 var last = lineFromLineDescendant(selected.last());
1162 addCommentFor($(last));
1163 comment_base_line = last.attr('id');
1166 selected.each(function() {
1167 addDataCommentBaseLine(this, comment_base_line);
1171 function addDataCommentBaseLine(line, id) {
1172 var val = $(line).attr('data-comment-base-line');
1174 var parts = val ? val.split(' ') : [];
1175 for (var i = 0; i < parts.length; i++) {
1181 $(line).attr('data-comment-base-line', parts.join(' '));
1184 function removeDataCommentBaseLine(line, comment_base_lines) {
1185 var val = $(line).attr('data-comment-base-line');
1189 var base_lines = comment_base_lines.split(' ');
1190 var parts = val.split(' ');
1192 for (var i = 0; i < parts.length; i++) {
1193 if (base_lines.indexOf(parts[i]) == -1)
1194 newVal.push(parts[i]);
1197 $(line).attr('data-comment-base-line', newVal.join(' '));
1200 function lineFromLineDescendant(descendant) {
1201 return descendant.hasClass('Line') ? descendant : descendant.parents('.Line');
1204 function lineFromLineContainer(lineContainer) {
1205 var line = $(lineContainer);
1206 if (!line.hasClass('Line'))
1207 line = $('.Line', line);
1211 $('.DiffSection').live('mouseleave', stopDragSelect).live('mouseup', stopDragSelect);
1213 function contextSnippetFor(line, indent) {
1215 contextLinesFor(line.attr('id'), fileDiffFor(line)).each(function() {
1217 if ($(this).hasClass('add'))
1219 else if ($(this).hasClass('remove'))
1221 snippets.push(indent + action + textContentsFor(this));
1223 return snippets.join('\n');
1226 function fileNameFor(line) {
1227 return fileDiffFor(line).find('h1').text();
1230 function indentFor(depth) {
1231 return (new Array(depth + 1)).join('>') + ' ';
1234 function snippetFor(line, indent) {
1235 var file_name = fileNameFor(line);
1236 var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
1237 return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
1240 function quotePreviousComments(comments) {
1241 var quoted_comments = [];
1242 var depth = comments.size();
1243 comments.each(function() {
1244 var indent = indentFor(depth--);
1245 var text = $(this).children('.content').text();
1246 quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
1248 return quoted_comments.join('\n');
1251 $('#comment_form .winter').live('click', function() {
1252 $('#comment_form').addClass('inactive');
1255 function fillInReviewForm() {
1256 var comments_in_context = []
1257 forEachLine(function(line) {
1258 if (line.attr('data-has-comment') != 'true')
1260 var comment = findCommentBlockFor(line).children('textarea').val().trim();
1263 var previous_comments = previousCommentsFor(line);
1264 var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
1265 var quoted_comments = quotePreviousComments(previous_comments);
1266 var comment_with_context = [];
1267 comment_with_context.push(snippet);
1268 if (quoted_comments != '')
1269 comment_with_context.push(quoted_comments);
1270 comment_with_context.push('\n' + comment);
1271 comments_in_context.push(comment_with_context.join('\n'));
1273 var comment = $('.overallComments textarea').val().trim();
1276 comment += comments_in_context.join('\n\n');
1277 if (comments_in_context.length > 0)
1278 comment = 'View in context: ' + window.location + '\n\n' + comment;
1279 var review_form = $('#reviewform').contents();
1280 review_form.find('#comment').val(comment);
1281 review_form.find('#flags select').each(function() {
1282 var control = findControlForFlag(this);
1283 if (!control.size())
1285 $(this).attr('selectedIndex', control.attr('selectedIndex'));
1289 $('#preview_comments').live('click', function() {
1291 $('#comment_form').removeClass('inactive');
1294 $('#post_comments').live('click', function() {
1296 $('#reviewform').contents().find('form').submit();