blob: fafec9d13e36892cb7cb959a14d5f06ef78c279b [file] [log] [blame]
/**
* @license
* Copyright (C) 2016 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.
*/
// Tags identifying ChangeMessages that move change into WIP state.
const WIP_TAGS = [
'autogenerated:gerrit:newWipPatchSet',
'autogenerated:gerrit:setWorkInProgress',
];
// Tags identifying ChangeMessages that move change out of WIP state.
const READY_TAGS = [
'autogenerated:gerrit:setReadyForReview',
];
/** @polymerBehavior Gerrit.PatchSetBehavior*/
export const PatchSetBehavior = {
EDIT_NAME: 'edit',
PARENT_NAME: 'PARENT',
/**
* As patchNum can be either a string (e.g. 'edit', 'PARENT') OR a number,
* this function checks for patchNum equality.
*
* @param {string|number} a
* @param {string|number|undefined} b Undefined sometimes because
* computeLatestPatchNum can return undefined.
* @return {boolean}
*/
patchNumEquals(a, b) {
return a + '' === b + '';
},
/**
* Whether the given patch is a numbered parent of a merge (i.e. a negative
* number).
*
* @param {string|number} n
* @return {boolean}
*/
isMergeParent(n) {
return (n + '')[0] === '-';
},
/**
* Given an object of revisions, get a particular revision based on patch
* num.
*
* @param {Object} revisions The object of revisions given by the API
* @param {number|string} patchNum The number index of the revision
* @return {Object} The correspondent revision obj from {revisions}
*/
getRevisionByPatchNum(revisions, patchNum) {
for (const rev of Object.values(revisions || {})) {
if (PatchSetBehavior.patchNumEquals(rev._number, patchNum)) {
return rev;
}
}
},
/**
* Find change edit base revision if change edit exists.
*
* @param {!Array<!Object>} revisions The revisions array.
* @return {Object} change edit parent revision or null if change edit
* doesn't exist.
*/
findEditParentRevision(revisions) {
const editInfo =
revisions.find(info => info._number ===
PatchSetBehavior.EDIT_NAME);
if (!editInfo) { return null; }
return revisions.find(info => info._number === editInfo.basePatchNum) ||
null;
},
/**
* Find change edit base patch set number if change edit exists.
*
* @param {!Array<!Object>} revisions The revisions array.
* @return {number} Change edit patch set number or -1.
*/
findEditParentPatchNum(revisions) {
const revisionInfo =
PatchSetBehavior.findEditParentRevision(revisions);
return revisionInfo ? revisionInfo._number : -1;
},
/**
* Sort given revisions array according to the patch set number, in
* descending order.
* The sort algorithm is change edit aware. Change edit has patch set number
* equals 'edit', but must appear after the patch set it was based on.
* Example: change edit is based on patch set 2, and another patch set was
* uploaded after change edit creation, the sorted order should be:
* 3, edit, 2, 1.
*
* @param {!Array<!Object>} revisions The revisions array
* @return {!Array<!Object>} The sorted {revisions} array
*/
sortRevisions(revisions) {
const editParent =
PatchSetBehavior.findEditParentPatchNum(revisions);
// Map a normal patchNum to 2 * (patchNum - 1) + 1... I.e. 1 -> 1,
// 2 -> 3, 3 -> 5, etc.
// Map an edit to the patchNum of parent*2... I.e. edit on 2 -> 4.
const num = r => (r._number === PatchSetBehavior.EDIT_NAME ?
2 * editParent :
2 * (r._number - 1) + 1);
return revisions.sort((a, b) => num(b) - num(a));
},
/**
* Construct a chronological list of patch sets derived from change details.
* Each element of this list is an object with the following properties:
*
* * num {number} The number identifying the patch set
* * desc {!string} Optional patch set description
* * wip {boolean} If true, this patch set was never subject to review.
* * sha {string} hash of the commit
*
* The wip property is determined by the change's current work_in_progress
* property and its log of change messages.
*
* @param {!Object} change The change details
* @return {!Array<!Object>} Sorted list of patch set objects, as described
* above
*/
computeAllPatchSets(change) {
if (!change) { return []; }
let patchNums = [];
if (change.revisions && Object.keys(change.revisions).length) {
const revisions = Object.keys(change.revisions)
.map(sha => Object.assign({sha}, change.revisions[sha]));
patchNums =
PatchSetBehavior.sortRevisions(revisions)
.map(e => {
// TODO(kaspern): Mark which patchset an edit was made on, if an
// edit exists -- perhaps with a temporary description.
return {
num: e._number,
desc: e.description,
sha: e.sha,
};
});
}
return PatchSetBehavior._computeWipForPatchSets(change, patchNums);
},
/**
* Populate the wip properties of the given list of patch sets.
*
* @param {!Object} change The change details
* @param {!Array<!Object>} patchNums Sorted list of patch set objects, as
* generated by computeAllPatchSets
* @return {!Array<!Object>} The given list of patch set objects, with the
* wip property set on each of them
*/
_computeWipForPatchSets(change, patchNums) {
if (!change.messages || !change.messages.length) {
return patchNums;
}
const psWip = {};
let wip = change.work_in_progress;
for (let i = 0; i < change.messages.length; i++) {
const msg = change.messages[i];
if (WIP_TAGS.includes(msg.tag)) {
wip = true;
} else if (READY_TAGS.includes(msg.tag)) {
wip = false;
}
if (psWip[msg._revision_number] !== false) {
psWip[msg._revision_number] = wip;
}
}
for (let i = 0; i < patchNums.length; i++) {
patchNums[i].wip = psWip[patchNums[i].num];
}
return patchNums;
},
/** @return {number|undefined} */
computeLatestPatchNum(allPatchSets) {
if (!allPatchSets || !allPatchSets.length) { return undefined; }
if (allPatchSets[0].num === PatchSetBehavior.EDIT_NAME) {
return allPatchSets[1].num;
}
return allPatchSets[0].num;
},
/** @return {boolean} */
hasEditBasedOnCurrentPatchSet(allPatchSets) {
if (!allPatchSets || allPatchSets.length < 2) { return false; }
return allPatchSets[0].num === PatchSetBehavior.EDIT_NAME;
},
/** @return {boolean} */
hasEditPatchsetLoaded(patchRangeRecord) {
const patchRange = patchRangeRecord.base;
if (!patchRange) { return false; }
return patchRange.patchNum === PatchSetBehavior.EDIT_NAME ||
patchRange.basePatchNum === PatchSetBehavior.EDIT_NAME;
},
/**
* Check whether there is no newer patch than the latest patch that was
* available when this change was loaded.
*
* @return {Promise<!Object>} A promise that yields true if the latest patch
* has been loaded, and false if a newer patch has been uploaded in the
* meantime. The promise is rejected on network error.
*/
fetchChangeUpdates(change, restAPI) {
const knownLatest = PatchSetBehavior.computeLatestPatchNum(
PatchSetBehavior.computeAllPatchSets(change));
return restAPI.getChangeDetail(change._number)
.then(detail => {
if (!detail) {
const error = new Error('Unable to check for latest patchset.');
return Promise.reject(error);
}
const actualLatest = PatchSetBehavior.computeLatestPatchNum(
PatchSetBehavior.computeAllPatchSets(detail));
return {
isLatest: actualLatest <= knownLatest,
newStatus: change.status !== detail.status ? detail.status : null,
newMessages: change.messages.length < detail.messages.length,
};
});
},
/**
* @param {number|string} patchNum
* @param {!Array<!Object>} revisions A sorted array of revisions.
*
* @return {number} The index of the revision with the given patchNum.
*/
findSortedIndex(patchNum, revisions) {
revisions = revisions || [];
const findNum = rev => rev._number + '' === patchNum + '';
return revisions.findIndex(findNum);
},
/**
* Convert parent indexes from patch range expressions to numbers.
* For example, in a patch range expression `"-3"` becomes `3`.
*
* @param {number|string} rangeBase
* @return {number}
*/
getParentIndex(rangeBase) {
return -parseInt(rangeBase + '', 10);
},
};
// eslint-disable-next-line no-unused-vars
function defineEmptyMixin() {
// This is a temporary function.
// Polymer linter doesn't process correctly the following code:
// class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
// To workaround this issue, the mock mixin is declared in this method.
// In the following changes, legacy behaviors will be converted to mixins.
/**
* @polymer
* @mixinFunction
*/
const PatchSetMixin = base => // eslint-disable-line no-unused-vars
class extends base {
computeLatestPatchNum(allPatchSets) {}
hasEditPatchsetLoaded(patchRangeRecord) {}
hasEditBasedOnCurrentPatchSet(allPatchSets) {}
computeAllPatchSets(change) {}
};
}
// TODO(dmfilippov) Remove the following lines with assignments
// Plugins can use the behavior because it was accessible with
// the global Gerrit... variable. To avoid breaking changes in plugins
// temporary assign global variables.
window.Gerrit = window.Gerrit || {};
window.Gerrit.PatchSetBehavior = PatchSetBehavior;