Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

531 lines
16 KiB
JavaScript
Raw Normal View History

// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function() {
'use strict';
const PARENT = 'PARENT';
const Defs = {};
/**
* @typedef {{
* basePatchNum: (string|number),
* patchNum: (number),
* }}
*/
Defs.patchRange;
/**
* @typedef {{
* changeNum: number,
* path: string,
* patchRange: !Defs.patchRange,
* projectConfig: (Object|undefined),
* }}
*/
Defs.commentMeta;
/**
* @typedef {{
* meta: !Defs.commentMeta,
* left: !Array,
* right: !Array,
* }}
*/
Defs.commentsBySide;
/**
* Construct a change comments object, which can be data-bound to child
* elements of that which uses the gr-comment-api.
*
* @param {!Object} comments
* @param {!Object} robotComments
* @param {!Object} drafts
* @param {number} changeNum
* @constructor
*/
function ChangeComments(comments, robotComments, drafts, changeNum) {
this._comments = comments;
this._robotComments = robotComments;
this._drafts = drafts;
this._changeNum = changeNum;
}
ChangeComments.prototype = {
get comments() {
return this._comments;
},
get drafts() {
return this._drafts;
},
get robotComments() {
return this._robotComments;
},
};
ChangeComments.prototype._patchNumEquals =
Gerrit.PatchSetBehavior.patchNumEquals;
ChangeComments.prototype._isMergeParent =
Gerrit.PatchSetBehavior.isMergeParent;
ChangeComments.prototype._getParentIndex =
Gerrit.PatchSetBehavior.getParentIndex;
/**
* Get an object mapping file paths to a boolean representing whether that
* path contains diff comments in the given patch set (including drafts and
* robot comments).
*
* Paths with comments are mapped to true, whereas paths without comments
* are not mapped.
*
* @param {Defs.patchRange=} opt_patchRange The patch-range object containing
* patchNum and basePatchNum properties to represent the range.
* @return {!Object}
*/
ChangeComments.prototype.getPaths = function(opt_patchRange) {
const responses = [this.comments, this.drafts, this.robotComments];
const commentMap = {};
for (const response of responses) {
for (const path in response) {
if (response.hasOwnProperty(path) &&
response[path].some(c => {
// If don't care about patch range, we know that the path exists.
if (!opt_patchRange) { return true; }
return this._isInPatchRange(c, opt_patchRange);
})) {
commentMap[path] = true;
}
}
}
return commentMap;
};
/**
* Gets all the comments and robot comments for the given change.
*
* @param {number=} opt_patchNum
* @return {!Object}
*/
ChangeComments.prototype.getAllPublishedComments = function(opt_patchNum) {
return this.getAllComments(false, opt_patchNum);
};
/**
* Gets all the comments for a particular thread group. Used for refreshing
* comments after the thread group has already been built.
*
* @param {string} rootId
* @return {!Array} an array of comments
*/
ChangeComments.prototype.getCommentsForThread = function(rootId) {
const allThreads = this.getAllThreadsForChange();
const threadMatch = allThreads.find(t => t.rootId === rootId);
// In the event that a single draft comment was removed by the thread-list
// and the diff view is updating comments, there will no longer be a thread
// found. In this case, return null.
return threadMatch ? threadMatch.comments : null;
};
/**
* Filters an array of comments by line and side
*
* @param {!Array} comments
* @param {boolean} parentOnly whether the only comments returned should have
* the side attribute set to PARENT
* @param {string} commentSide whether the comment was left on the left or the
* right side regardless or unified or side-by-side
* @param {number=} opt_line line number, can be undefined if file comment
* @return {!Array} an array of comments
*/
ChangeComments.prototype._filterCommentsBySideAndLine = function(comments,
parentOnly, commentSide, opt_line) {
return comments.filter(c => {
// if parentOnly, only match comments with PARENT for the side.
let sideMatch = parentOnly ? c.side === PARENT : c.side !== PARENT;
if (parentOnly) {
sideMatch = sideMatch && c.side === PARENT;
}
return sideMatch && c.line === opt_line;
}).map(c => {
c.__commentSide = commentSide;
return c;
});
};
/**
* Gets all the comments and robot comments for the given change.
*
* @param {boolean=} opt_includeDrafts
* @param {number=} opt_patchNum
* @return {!Object}
*/
ChangeComments.prototype.getAllComments = function(opt_includeDrafts,
opt_patchNum) {
const paths = this.getPaths();
const publishedComments = {};
for (const path of Object.keys(paths)) {
let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum);
if (opt_includeDrafts) {
const drafts = this.getAllDraftsForPath(path)
.map(d => Object.assign({__draft: true}, d));
commentsToAdd = commentsToAdd.concat(drafts);
}
publishedComments[path] = commentsToAdd;
}
return publishedComments;
};
/**
* Gets all the comments and robot comments for the given change.
*
* @param {number=} opt_patchNum
* @return {!Object}
*/
ChangeComments.prototype.getAllDrafts = function(opt_patchNum) {
const paths = this.getPaths();
const drafts = {};
for (const path of Object.keys(paths)) {
drafts[path] = this.getAllDraftsForPath(path, opt_patchNum);
}
return drafts;
};
/**
* Get the comments (robot comments) for a path and optional patch num.
*
* @param {!string} path
* @param {number=} opt_patchNum
* @return {!Array}
*/
ChangeComments.prototype.getAllCommentsForPath = function(path,
opt_patchNum, opt_includeDrafts) {
const comments = this._comments[path] || [];
const robotComments = this._robotComments[path] || [];
let allComments = comments.concat(robotComments);
if (opt_includeDrafts) {
const drafts = this.getAllDraftsForPath(path)
.map(d => Object.assign({__draft: true}, d));
allComments = allComments.concat(drafts);
}
if (!opt_patchNum) { return allComments; }
return (allComments || []).filter(c =>
this._patchNumEquals(c.patch_set, opt_patchNum)
);
};
/**
* Get the drafts for a path and optional patch num.
*
* @param {!string} path
* @param {number=} opt_patchNum
* @return {!Array}
*/
ChangeComments.prototype.getAllDraftsForPath = function(path,
opt_patchNum) {
const comments = this._drafts[path] || [];
if (!opt_patchNum) { return comments; }
return (comments || []).filter(c =>
this._patchNumEquals(c.patch_set, opt_patchNum)
);
};
/**
* Get the comments (with drafts and robot comments) for a path and
* patch-range. Returns an object with left and right properties mapping to
* arrays of comments in on either side of the patch range for that path.
*
* @param {!string} path
* @param {!Defs.patchRange} patchRange The patch-range object containing patchNum
* and basePatchNum properties to represent the range.
* @param {Object=} opt_projectConfig Optional project config object to
* include in the meta sub-object.
* @return {!Defs.commentsBySide}
*/
ChangeComments.prototype.getCommentsBySideForPath = function(path,
patchRange, opt_projectConfig) {
const comments = this.comments[path] || [];
const drafts = this.drafts[path] || [];
const robotComments = this.robotComments[path] || [];
drafts.forEach(d => { d.__draft = true; });
const all = comments.concat(drafts).concat(robotComments);
const baseComments = all.filter(c =>
this._isInBaseOfPatchRange(c, patchRange));
const revisionComments = all.filter(c =>
this._isInRevisionOfPatchRange(c, patchRange));
return {
meta: {
changeNum: this._changeNum,
path,
patchRange,
projectConfig: opt_projectConfig,
},
left: baseComments,
right: revisionComments,
};
};
/**
* @param {!Object} comments Object keyed by file, with a value of an array
* of comments left on that file.
* @return {!Array} A flattened list of all comments, where each comment
* also includes the file that it was left on, which was the key of the
* originall object.
*/
ChangeComments.prototype._commentObjToArrayWithFile = function(comments) {
let commentArr = [];
for (const file of Object.keys(comments)) {
const commentsForFile = [];
for (const comment of comments[file]) {
commentsForFile.push(Object.assign({__path: file}, comment));
}
commentArr = commentArr.concat(commentsForFile);
}
return commentArr;
};
ChangeComments.prototype._commentObjToArray = function(comments) {
let commentArr = [];
for (const file of Object.keys(comments)) {
commentArr = commentArr.concat(comments[file]);
}
return commentArr;
};
/**
* Computes a string counting the number of commens in a given file and path.
*
* @param {number} patchNum
* @param {string=} opt_path
* @return {number}
*/
ChangeComments.prototype.computeCommentCount = function(patchNum, opt_path) {
if (opt_path) {
return this.getAllCommentsForPath(opt_path, patchNum).length;
}
const allComments = this.getAllPublishedComments(patchNum);
return this._commentObjToArray(allComments).length;
};
/**
* Computes a string counting the number of commens in a given file and path.
*
* @param {number} patchNum
* @param {string=} opt_path
* @return {number}
*/
ChangeComments.prototype.computeDraftCount = function(patchNum, opt_path) {
if (opt_path) {
return this.getAllDraftsForPath(opt_path, patchNum).length;
}
const allComments = this.getAllDrafts(patchNum);
return this._commentObjToArray(allComments).length;
};
/**
* Computes a number of unresolved comment threads in a given file and path.
*
* @param {number} patchNum
* @param {string=} opt_path
* @return {number}
*/
ChangeComments.prototype.computeUnresolvedNum = function(patchNum,
opt_path) {
let comments = [];
let drafts = [];
if (opt_path) {
comments = this.getAllCommentsForPath(opt_path, patchNum);
drafts = this.getAllDraftsForPath(opt_path, patchNum);
} else {
comments = this._commentObjToArray(
this.getAllPublishedComments(patchNum));
}
comments = comments.concat(drafts);
Count unresolved threads within thread groups rather than by leaves Diff comments are threaded together based on the in_reply_to relation (which potentially expresses a tree structure) but are always displayed linearly in the UI. This means that some comments in the middle of a linear thread may be actually stored as leaves of a tree. For example, the following thread of comments can be created if comments two and three are created at nearly the same time. Comment 1: thread root, unresolved, ┣ Comment 2: in reply to comment 1, unresolved, ┗ Comment 3: also in reply to comment 1, unresolved, ┗ Comment 4: in reply to comment 3, resolved Because the thread is flattened, the resolved state of the thread should be determined by the state of the chronologically latest comment (#4), resulting in this thread being considered as resolved. However, in a couple of locations, the resolved state is counted differently. Namely, it finds the "leaf" comments -- that is, the comments that are not marked as the parent of any other comment -- and the number of unresolved threads is determined as the number of unresolved leaves. This approach was used by: - ChangeComments#computeUnresolvedNum in the UI, which determines the string stating the number of unresolved threads in a file row. - ChangeData#unresolvedCommentCount in the Java server code, which determines, among other things, the value of the unresolved_comment_count change detail property, as well as the Prolog fact used by the Prolog recipe that requires all comments to be resolved before a change can be submitted. Instead, the unresolved thread logic is modified to group comments into flat threads, and consider the resolved state of each one based on the chronologically final comment, irregardless of the leaves. Bug: Issue 8472 Change-Id: I2788fdb22ecfd56f0b3da763790a7732ec73be33
2018-03-01 14:25:06 -08:00
const threads = this.getCommentThreads(this._sortComments(comments));
Count unresolved threads within thread groups rather than by leaves Diff comments are threaded together based on the in_reply_to relation (which potentially expresses a tree structure) but are always displayed linearly in the UI. This means that some comments in the middle of a linear thread may be actually stored as leaves of a tree. For example, the following thread of comments can be created if comments two and three are created at nearly the same time. Comment 1: thread root, unresolved, ┣ Comment 2: in reply to comment 1, unresolved, ┗ Comment 3: also in reply to comment 1, unresolved, ┗ Comment 4: in reply to comment 3, resolved Because the thread is flattened, the resolved state of the thread should be determined by the state of the chronologically latest comment (#4), resulting in this thread being considered as resolved. However, in a couple of locations, the resolved state is counted differently. Namely, it finds the "leaf" comments -- that is, the comments that are not marked as the parent of any other comment -- and the number of unresolved threads is determined as the number of unresolved leaves. This approach was used by: - ChangeComments#computeUnresolvedNum in the UI, which determines the string stating the number of unresolved threads in a file row. - ChangeData#unresolvedCommentCount in the Java server code, which determines, among other things, the value of the unresolved_comment_count change detail property, as well as the Prolog fact used by the Prolog recipe that requires all comments to be resolved before a change can be submitted. Instead, the unresolved thread logic is modified to group comments into flat threads, and consider the resolved state of each one based on the chronologically final comment, irregardless of the leaves. Bug: Issue 8472 Change-Id: I2788fdb22ecfd56f0b3da763790a7732ec73be33
2018-03-01 14:25:06 -08:00
const unresolvedThreads = threads
.filter(thread =>
thread.comments.length &&
thread.comments[thread.comments.length - 1].unresolved);
Count unresolved threads within thread groups rather than by leaves Diff comments are threaded together based on the in_reply_to relation (which potentially expresses a tree structure) but are always displayed linearly in the UI. This means that some comments in the middle of a linear thread may be actually stored as leaves of a tree. For example, the following thread of comments can be created if comments two and three are created at nearly the same time. Comment 1: thread root, unresolved, ┣ Comment 2: in reply to comment 1, unresolved, ┗ Comment 3: also in reply to comment 1, unresolved, ┗ Comment 4: in reply to comment 3, resolved Because the thread is flattened, the resolved state of the thread should be determined by the state of the chronologically latest comment (#4), resulting in this thread being considered as resolved. However, in a couple of locations, the resolved state is counted differently. Namely, it finds the "leaf" comments -- that is, the comments that are not marked as the parent of any other comment -- and the number of unresolved threads is determined as the number of unresolved leaves. This approach was used by: - ChangeComments#computeUnresolvedNum in the UI, which determines the string stating the number of unresolved threads in a file row. - ChangeData#unresolvedCommentCount in the Java server code, which determines, among other things, the value of the unresolved_comment_count change detail property, as well as the Prolog fact used by the Prolog recipe that requires all comments to be resolved before a change can be submitted. Instead, the unresolved thread logic is modified to group comments into flat threads, and consider the resolved state of each one based on the chronologically final comment, irregardless of the leaves. Bug: Issue 8472 Change-Id: I2788fdb22ecfd56f0b3da763790a7732ec73be33
2018-03-01 14:25:06 -08:00
return unresolvedThreads.length;
};
ChangeComments.prototype.getAllThreadsForChange = function() {
const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
Count unresolved threads within thread groups rather than by leaves Diff comments are threaded together based on the in_reply_to relation (which potentially expresses a tree structure) but are always displayed linearly in the UI. This means that some comments in the middle of a linear thread may be actually stored as leaves of a tree. For example, the following thread of comments can be created if comments two and three are created at nearly the same time. Comment 1: thread root, unresolved, ┣ Comment 2: in reply to comment 1, unresolved, ┗ Comment 3: also in reply to comment 1, unresolved, ┗ Comment 4: in reply to comment 3, resolved Because the thread is flattened, the resolved state of the thread should be determined by the state of the chronologically latest comment (#4), resulting in this thread being considered as resolved. However, in a couple of locations, the resolved state is counted differently. Namely, it finds the "leaf" comments -- that is, the comments that are not marked as the parent of any other comment -- and the number of unresolved threads is determined as the number of unresolved leaves. This approach was used by: - ChangeComments#computeUnresolvedNum in the UI, which determines the string stating the number of unresolved threads in a file row. - ChangeData#unresolvedCommentCount in the Java server code, which determines, among other things, the value of the unresolved_comment_count change detail property, as well as the Prolog fact used by the Prolog recipe that requires all comments to be resolved before a change can be submitted. Instead, the unresolved thread logic is modified to group comments into flat threads, and consider the resolved state of each one based on the chronologically final comment, irregardless of the leaves. Bug: Issue 8472 Change-Id: I2788fdb22ecfd56f0b3da763790a7732ec73be33
2018-03-01 14:25:06 -08:00
const sortedComments = this._sortComments(comments);
return this.getCommentThreads(sortedComments);
};
ChangeComments.prototype._sortComments = function(comments) {
return comments.slice(0).sort((c1, c2) => {
return util.parseDate(c1.updated) - util.parseDate(c2.updated);
});
};
/**
* Computes all of the comments in thread format.
*
* @param {!Array} comments sorted by updated timestamp.
* @return {!Array}
*/
ChangeComments.prototype.getCommentThreads = function(comments) {
const threads = [];
for (const comment of comments) {
// If the comment is in reply to another comment, find that comment's
// thread and append to it.
if (comment.in_reply_to) {
const thread = threads.find(thread =>
thread.comments.some(c => c.id === comment.in_reply_to));
if (thread) {
thread.comments.push(comment);
continue;
}
}
// Otherwise, this comment starts its own thread.
const newThread = {
comments: [comment],
patchNum: comment.patch_set,
path: comment.__path,
line: comment.line,
rootId: comment.id,
};
if (comment.side) {
newThread.commentSide = comment.side;
}
threads.push(newThread);
}
return threads;
};
/**
* Whether the given comment should be included in the base side of the
* given patch range.
* @param {!Object} comment
* @param {!Defs.patchRange} range
* @return {boolean}
*/
ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
// If the base of the patch range is a parent of a merge, and the comment
// appears on a specific parent then only show the comment if the parent
// index of the comment matches that of the range.
if (comment.parent && comment.side === PARENT) {
return this._isMergeParent(range.basePatchNum) &&
comment.parent === this._getParentIndex(range.basePatchNum);
}
// If the base of the range is the parent of the patch:
if (range.basePatchNum === PARENT &&
comment.side === PARENT &&
this._patchNumEquals(comment.patch_set, range.patchNum)) {
return true;
}
// If the base of the range is not the parent of the patch:
if (range.basePatchNum !== PARENT &&
comment.side !== PARENT &&
this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
return true;
}
return false;
};
/**
* Whether the given comment should be included in the revision side of the
* given patch range.
* @param {!Object} comment
* @param {!Defs.patchRange} range
* @return {boolean}
*/
ChangeComments.prototype._isInRevisionOfPatchRange = function(comment,
range) {
return comment.side !== PARENT &&
this._patchNumEquals(comment.patch_set, range.patchNum);
};
/**
* Whether the given comment should be included in the given patch range.
* @param {!Object} comment
* @param {!Defs.patchRange} range
* @return {boolean|undefined}
*/
ChangeComments.prototype._isInPatchRange = function(comment, range) {
return this._isInBaseOfPatchRange(comment, range) ||
this._isInRevisionOfPatchRange(comment, range);
};
Polymer({
is: 'gr-comment-api',
properties: {
_changeComments: Object,
},
listeners: {
'reload-drafts': 'reloadDrafts',
},
behaviors: [
Gerrit.PatchSetBehavior,
],
/**
* Load all comments (with drafts and robot comments) for the given change
* number. The returned promise resolves when the comments have loaded, but
* does not yield the comment data.
*
* @param {number} changeNum
* @return {!Promise<!Object>}
*/
loadAll(changeNum) {
const promises = [];
promises.push(this.$.restAPI.getDiffComments(changeNum));
promises.push(this.$.restAPI.getDiffRobotComments(changeNum));
promises.push(this.$.restAPI.getDiffDrafts(changeNum));
return Promise.all(promises).then(([comments, robotComments, drafts]) => {
this._changeComments = new ChangeComments(comments,
robotComments, drafts, changeNum);
return this._changeComments;
});
},
/**
* Re-initialize _changeComments with a new ChangeComments object, that
* uses the previous values for comments and robot comments, but fetches
* updated draft comments.
*
* @param {number} changeNum
* @return {!Promise<!Object>}
*/
reloadDrafts(changeNum) {
if (!this._changeComments) {
return this.loadAll(changeNum);
}
return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => {
this._changeComments = new ChangeComments(this._changeComments.comments,
this._changeComments.robotComments, drafts, changeNum);
return this._changeComments;
});
},
});
})();