/** * @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(window) { 'use strict'; // Prevent redefinition. if (window.GrReviewerUpdatesParser) { return; } function GrReviewerUpdatesParser(change) { // TODO (viktard): Polyfill Object.assign for IE. this.result = Object.assign({}, change); this._lastState = {}; } GrReviewerUpdatesParser.parse = function(change) { if (!change || !change.messages || !change.reviewer_updates || !change.reviewer_updates.length) { return change; } const parser = new GrReviewerUpdatesParser(change); parser._filterRemovedMessages(); parser._groupUpdates(); parser._formatUpdates(); parser._advanceUpdates(); return parser.result; }; GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS = 500; GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000; GrReviewerUpdatesParser.prototype.result = null; GrReviewerUpdatesParser.prototype._batch = null; GrReviewerUpdatesParser.prototype._updateItems = null; GrReviewerUpdatesParser.prototype._lastState = null; /** * Removes messages that describe removed reviewers, since reviewer_updates * are used. */ GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() { this.result.messages = this.result.messages.filter(message => { return message.tag !== 'autogenerated:gerrit:deleteReviewer'; }); }; /** * Is a part of _groupUpdates(). Creates a new batch of updates. * @param {Object} update instance of ReviewerUpdateInfo */ GrReviewerUpdatesParser.prototype._startBatch = function(update) { this._updateItems = []; return { author: update.updated_by, date: update.updated, type: 'REVIEWER_UPDATE', }; }; /** * Is a part of _groupUpdates(). Validates current batch: * - filters out updates that don't change reviewer state. * - updates current reviewer state. * @param {Object} update instance of ReviewerUpdateInfo */ GrReviewerUpdatesParser.prototype._completeBatch = function(update) { const items = []; for (const accountId in this._updateItems) { if (!this._updateItems.hasOwnProperty(accountId)) continue; const updateItem = this._updateItems[accountId]; if (this._lastState[accountId] !== updateItem.state) { this._lastState[accountId] = updateItem.state; items.push(updateItem); } } if (items.length) { this._batch.updates = items; } }; /** * Groups reviewer updates. Sequential updates are grouped if: * - They were performed within short timeframe (6 seconds) * - Made by the same person * - Non-change updates are discarded within a group * - Groups with no-change updates are discarded (eg CC -> CC) */ GrReviewerUpdatesParser.prototype._groupUpdates = function() { const updates = this.result.reviewer_updates; const newUpdates = updates.reduce((newUpdates, update) => { if (!this._batch) { this._batch = this._startBatch(update); } const updateDate = util.parseDate(update.updated).getTime(); const batchUpdateDate = util.parseDate(this._batch.date).getTime(); const reviewerId = update.reviewer._account_id.toString(); if (updateDate - batchUpdateDate > GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS || update.updated_by._account_id !== this._batch.author._account_id) { // Next sequential update should form new group. this._completeBatch(); if (this._batch.updates && this._batch.updates.length) { newUpdates.push(this._batch); } this._batch = this._startBatch(update); } this._updateItems[reviewerId] = { reviewer: update.reviewer, state: update.state, }; if (this._lastState[reviewerId]) { this._updateItems[reviewerId].prev_state = this._lastState[reviewerId]; } return newUpdates; }, []); this._completeBatch(); if (this._batch.updates && this._batch.updates.length) { newUpdates.push(this._batch); } this.result.reviewer_updates = newUpdates; }; /** * Generates update message for reviewer state change. * @param {string} prev previous reviewer state. * @param {string} state current reviewer state. * @return {string} */ GrReviewerUpdatesParser.prototype._getUpdateMessage = function(prev, state) { if (prev === 'REMOVED' || !prev) { return 'added to ' + state + ': '; } else if (state === 'REMOVED') { if (prev) { return 'removed from ' + prev + ': '; } else { return 'removed : '; } } else { return 'moved from ' + prev + ' to ' + state + ': '; } }; /** * Groups updates for same category (eg CC->CC) into a hash arrays of * reviewers. * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo. * @return {!Object} Hash of arrays of AccountInfo, message as key. */ GrReviewerUpdatesParser.prototype._groupUpdatesByMessage = function(updates) { return updates.reduce((result, item) => { const message = this._getUpdateMessage(item.prev_state, item.state); if (!result[message]) { result[message] = []; } result[message].push(item.reviewer); return result; }, {}); }; /** * Generates text messages for grouped reviewer updates. * Formats reviewer updates to a (not yet implemented) EventInfo instance. * @see https://gerrit-review.googlesource.com/c/94490/ */ GrReviewerUpdatesParser.prototype._formatUpdates = function() { for (const update of this.result.reviewer_updates) { const grouppedReviewers = this._groupUpdatesByMessage(update.updates); const newUpdates = []; for (const message in grouppedReviewers) { if (grouppedReviewers.hasOwnProperty(message)) { newUpdates.push({ message, reviewers: grouppedReviewers[message], }); } } update.updates = newUpdates; } }; /** * Moves reviewer updates that are within short time frame of change messages * back in time so they would come before change messages. * TODO(viktard): Remove when server-side serves reviewer updates like so. */ GrReviewerUpdatesParser.prototype._advanceUpdates = function() { const updates = this.result.reviewer_updates; const messages = this.result.messages; messages.forEach((message, index) => { const messageDate = util.parseDate(message.date).getTime(); const nextMessageDate = index === messages.length - 1 ? null : util.parseDate(messages[index + 1].date).getTime(); for (const update of updates) { const date = util.parseDate(update.date).getTime(); if (date >= messageDate && (!nextMessageDate || date < nextMessageDate)) { const timestamp = util.parseDate(update.date).getTime() - GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS; update.date = new Date(timestamp) .toISOString().replace('T', ' ').replace('Z', '000000'); } if (nextMessageDate && date > nextMessageDate) { break; } } }); }; window.GrReviewerUpdatesParser = GrReviewerUpdatesParser; })(window);