487 lines
14 KiB
JavaScript
487 lines
14 KiB
JavaScript
// 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 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) {
|
|
commentsToAdd = commentsToAdd.concat(this.getAllDraftsForPath(path));
|
|
}
|
|
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) {
|
|
const comments = this._comments[path] || [];
|
|
const robotComments = this._robotComments[path] || [];
|
|
const allComments = comments.concat(robotComments);
|
|
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);
|
|
|
|
// Create an object where every comment ID is the key of an unresolved
|
|
// comment.
|
|
const idMap = comments.reduce((acc, comment) => {
|
|
if (comment.unresolved) {
|
|
acc[comment.id] = true;
|
|
}
|
|
return acc;
|
|
}, {});
|
|
|
|
// Set false for the comments that are marked as parents.
|
|
for (const comment of comments) {
|
|
idMap[comment.in_reply_to] = false;
|
|
}
|
|
|
|
// The unresolved comments are the comments that still have true.
|
|
const unresolvedLeaves = Object.keys(idMap).filter(key => {
|
|
return idMap[key];
|
|
});
|
|
return unresolvedLeaves.length;
|
|
};
|
|
|
|
ChangeComments.prototype.getAllThreadsForChange = function() {
|
|
const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
|
|
return this.getCommentThreads(comments);
|
|
};
|
|
|
|
/**
|
|
* Computes all of the comments in thread format.
|
|
*
|
|
* @param {!Array} comments
|
|
* @return {!Array}
|
|
*/
|
|
ChangeComments.prototype.getCommentThreads = function(comments) {
|
|
const threads = comments.reduce((groups, comment) => {
|
|
const path = comment.__path;
|
|
const patchset = comment.patch_set;
|
|
const line = comment.line;
|
|
const range = comment.range;
|
|
const side = comment.side;
|
|
let key = `${path}-${patchset}-${line}`;
|
|
if (range) {
|
|
key = `${key}-${range.start_line}-${range.start_character}-` +
|
|
`${range.end_line}-${range.end_character}`;
|
|
}
|
|
if (side) {
|
|
key = `${key}-${side}`;
|
|
}
|
|
const groupObj = {
|
|
comments: [],
|
|
patchNum: patchset,
|
|
path,
|
|
line,
|
|
};
|
|
if (comment.side) {
|
|
groupObj.commentSide = side;
|
|
}
|
|
groups[key] = groups[key] || groupObj;
|
|
groups[key].comments.push(comment);
|
|
return groups;
|
|
}, {});
|
|
|
|
return Object.values(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;
|
|
});
|
|
},
|
|
});
|
|
})();
|