 cdb8a43f76
			
		
	
	cdb8a43f76
	
	
	
		
			
			A nested loop made ChangeComments#getCommentThreads O(n²) in the worst case. Use a map to convert it to O(n). Change-Id: I39f6a462cdd23316a1b513ebf98bbd2c8f9aaec2
		
			
				
	
	
		
			539 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			539 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * 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, opt_patchNum)
 | |
|             .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
 | |
|    * @param {boolean=} opt_includeDrafts
 | |
|    * @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 draft comments in the entire
 | |
|    * change, optionally filtered by path and/or patchNum.
 | |
|    *
 | |
|    * @param {number=} opt_patchNum
 | |
|    * @param {string=} opt_path
 | |
|    * @return {number}
 | |
|    */
 | |
|   ChangeComments.prototype.computeDraftCount = function(opt_patchNum,
 | |
|       opt_path) {
 | |
|     if (opt_path) {
 | |
|       return this.getAllDraftsForPath(opt_path, opt_patchNum).length;
 | |
|     }
 | |
|     const allDrafts = this.getAllDrafts(opt_patchNum);
 | |
|     return this._commentObjToArray(allDrafts).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);
 | |
| 
 | |
|     const threads = this.getCommentThreads(this._sortComments(comments));
 | |
| 
 | |
|     const unresolvedThreads = threads
 | |
|       .filter(thread =>
 | |
|           thread.comments.length &&
 | |
|           thread.comments[thread.comments.length - 1].unresolved);
 | |
| 
 | |
|     return unresolvedThreads.length;
 | |
|   };
 | |
| 
 | |
|   ChangeComments.prototype.getAllThreadsForChange = function() {
 | |
|     const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
 | |
|     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 = [];
 | |
|     const idThreadMap = {};
 | |
|     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 = idThreadMap[comment.in_reply_to];
 | |
|         if (thread) {
 | |
|           thread.comments.push(comment);
 | |
|           idThreadMap[comment.id] = thread;
 | |
|           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);
 | |
|       idThreadMap[comment.id] = 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;
 | |
|       });
 | |
|     },
 | |
|   });
 | |
| })();
 |