summaryrefslogtreecommitdiffstats
path: root/scripts/gerrit/cherry-pick_automation/toolbox.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/gerrit/cherry-pick_automation/toolbox.js')
-rw-r--r--scripts/gerrit/cherry-pick_automation/toolbox.js416
1 files changed, 415 insertions, 1 deletions
diff --git a/scripts/gerrit/cherry-pick_automation/toolbox.js b/scripts/gerrit/cherry-pick_automation/toolbox.js
index 121367dc..55989075 100644
--- a/scripts/gerrit/cherry-pick_automation/toolbox.js
+++ b/scripts/gerrit/cherry-pick_automation/toolbox.js
@@ -7,6 +7,7 @@ const safeJsonStringify = require("safe-json-stringify");
const v8 = require('v8');
const postgreSQLClient = require("./postgreSQLClient");
+const { queryBranchesRe, checkBranchAndAccess, queryChange } = require("./gerritRESTTools");
const Logger = require("./logger");
const logger = new Logger();
@@ -17,7 +18,8 @@ let dbListenerCacheUpdateLockout = false;
// Deep copy an object. Useful for forking processing paths with different data
// than originally entered the system.
-exports.deepCopy = function (obj) {
+exports.deepCopy = deepCopy;
+function deepCopy(obj) {
return v8.deserialize(v8.serialize(obj));
}
@@ -43,6 +45,418 @@ exports.findPickToBranches = function (requestUuid, message) {
return branchSet;
};
+// Build a waterfall of cherry picks based on the list of branches.
+// Picks to older feature and release branches must be attached to newer
+// feature branch picks.
+// Example: for input of ["6.5.0", "6.4", "5.15.12", "5.15.11", "5.15"],
+// the output will be:
+// {
+// '6.5.0': [],
+// '6.4': ['5.15.12', '5.15.11', '5.15']
+// }
+exports.waterfallCherryPicks = function (requestUuid, branchList) {
+ branchList = Array.from(branchList); // Might be a set.
+
+ if (branchList.length === 0) {
+ logger.log(`No branches to waterfall.`, 'debug', requestUuid);
+ return {};
+ }
+
+ // If any of the entries of branchList are not numerical,
+ // then return a waterfall object with each branch as a key.
+ if (branchList.some((branch) => !/^(dev|master|\d+\.\d+(\.\d+)?)$/.test(branch))) {
+ let waterfall = {};
+ for (let branch of branchList) {
+ waterfall[branch] = [];
+ }
+ return waterfall;
+ }
+
+ branchList = sortBranches(branchList);
+
+ let waterfall = {};
+ let youngestFeatureBranch = branchList.find(isFeatureBranchOrDev);
+ let remainingBranches = branchList.filter((branch) => branch !== youngestFeatureBranch);
+ if (!youngestFeatureBranch || remainingBranches.length === 0) {
+ waterfall[youngestFeatureBranch || remainingBranches[0]] = [];
+ return waterfall;
+ }
+
+ let children = remainingBranches.filter((branch) => {
+ let result = (isAncestor(branch, youngestFeatureBranch)
+ || isChild(branch, youngestFeatureBranch));
+ return result;
+ });
+
+ waterfall[youngestFeatureBranch] = children;
+
+ remainingBranches = remainingBranches.filter((branch) => !children.includes(branch));
+
+ for (let branch of remainingBranches) {
+ waterfall[branch] = [];
+ }
+
+ return waterfall;
+};
+
+// Determine if a given branch is a child of another branch.
+function isChild(maybeChild, maybeParent) {
+ // Account for dev and master branches.
+ if (maybeChild === "dev" || maybeChild === "master") {
+ return false;
+ }
+ if (maybeParent === "dev" || maybeParent === "master") {
+ return true;
+ }
+ let childParts = maybeChild.split('.').map(Number);
+ let parentParts = maybeParent.split('.').map(Number);
+
+ // Major version less than the parent is always a child.
+ if (childParts[0] < parentParts[0]) {
+ return true;
+ }
+
+ // Feature versions are children if lesser than the parent. This
+ // also catches release versions of the same or newer feature versions.
+ if (childParts[1] <= parentParts[1]) {
+ return true;
+ }
+
+ // Then the release version is newer than the parent.
+ return false;
+}
+
+// Determine if a given branch is an ancestor of another branch.
+function isAncestor(maybeAncestor, reference) {
+ // Account for dev and master branches.
+ if (maybeAncestor === "dev" || maybeAncestor === "master") {
+ return true;
+ }
+ if (reference === "dev" || reference === "master") {
+ return false;
+ }
+ let branchParts = maybeAncestor.split('.').map(Number);
+ let ancestorParts = reference.split('.').map(Number);
+
+ // Release branches like 5.15.0 cannot be ancestors of feature branches like 5.15.
+ if (branchParts.length > ancestorParts.length) {
+ return false;
+ }
+
+ // Major versions that are less than the passed ancestor are ancestors.
+ if (branchParts[0] < reference[0]) {
+ return true;
+ }
+
+ // Feature versions are ancestors if it is greater than the reference.
+ if (branchParts[1] > reference[1]) {
+ return true;
+ }
+
+ return true;
+}
+
+function isFeatureBranchOrDev(branch) {
+ return branch === "dev" || branch === "master" || branch.split('.').length === 2;
+}
+
+// given a list of pick-to branches, determine if any gaps exist in the list.
+// Return a completed list of pick-to targets with a diff.
+exports.findMissingTargets = function (uuid, changeId, project, targets, callback) {
+
+ const isTqtc = /tqtc-/.test(project); // Is the cherry-pick request coming from a tqtc repo?
+ const isLTS = /^(tqtc\/)?lts-/.test(decodeURIComponent(changeId).split('~')[1]); // Is the change on an LTS branch?
+ const prefix = isTqtc && isLTS
+ ? "tqtc/lts-"
+ : isTqtc
+ ? "tqtc/"
+ : isLTS
+ ? "lts-" : "";
+ const bareBranch = decodeURIComponent(changeId).split('~')[1].replace(/^(tqtc\/)?(lts-)?/, '');
+ let highestTarget = "";
+
+ if (Array.from(targets).length === 0) {
+ logger.log(`No targets to check.`, 'debug', uuid);
+ callback(false, undefined, []);
+ return;
+ }
+
+ // targets will always be bare versions, like "5.15".
+ let featureBranches = new Set();
+ for (let target of targets) {
+ // account for dev and master branches.
+ if (target === "dev" || target === "master") {
+ continue;
+ }
+ let parts = target.split('.');
+ featureBranches.add(parts[0] + '.' + parts[1]);
+ }
+ highestTarget = Array.from(featureBranches).sort(sortBranches)[0];
+ if (isChild(highestTarget, bareBranch)) {
+ // If the highest target is a child of the bare branch, then the highest target
+ // should be considered self.
+ highestTarget = bareBranch;
+ }
+
+
+
+ let releaseBranches = {};
+ for (let key of featureBranches) {
+ releaseBranches[key] = [];
+ }
+ for (let target of targets) {
+ // account for dev and master branches.
+ if (target === "dev" || target === "master") {
+ continue;
+ }
+ let parts = target.split('.');
+ if (parts.length === 3) {
+ releaseBranches[parts[0] + '.' + parts[1]].push(target);
+ }
+ }
+
+ // Sort by release version
+ for (let key of featureBranches) {
+ releaseBranches[key].sort((a, b) => {
+ let aParts = a.split('.').map(Number);
+ let bParts = b.split('.').map(Number);
+ return aParts[2] - bParts[2];
+ });
+ }
+
+ // Filter result branches if the originating change had a tqtc and/or an LTS prefix.
+ const searchRe = `(${prefix}(?:${Array.from(featureBranches).join('|')}|[6-9].[0-9]{1,}|${prefix}dev|${prefix}master)).*`
+ queryBranchesRe(uuid, project, false, searchRe, undefined, (success, remoteBranches) => {
+ if (!success) {
+ return;
+ }
+
+ function makeNextFeature(branch) {
+ return branch.split('.').slice(0, 1).join('.')
+ + '.' + (Number(branch.split('.')[1]) + 1); // f.e. 5.15 -> 5.16
+ }
+
+ // Use sanitized branches to determine if any gaps exist.
+ const bareRemoteBranches = remoteBranches.map((branch) => branch.replace(/^(tqtc\/)?(lts-)?/, ''));
+
+ function _finishUp(error, change) {
+ // Always include all gaps in release branches.
+ for (let branch of featureBranches) {
+ for (let release of releaseBranches[branch]) {
+ const nextRelease = release.split('.').slice(0, 2).join('.')
+ + '.' + (Number(release.split('.')[2]) + 1); // f.e. 5.15.12 -> 5.15.13
+ if (releaseBranches[branch].includes(nextRelease)) {
+ continue;
+ }
+ if (bareRemoteBranches.includes(nextRelease)) {
+ missing.push(nextRelease);
+ }
+ }
+ }
+
+ if (missing.length) {
+ logger.log(`Missing branches: ${missing.join(', ')}`, 'info', uuid);
+ }
+ callback(Boolean(error), change, missing);
+ }
+
+ let missing = [];
+ function _findMissingFeatures(branch, start = false) {
+ if (start) {
+ if (bareRemoteBranches.includes(branch)) {
+ if (branch === bareBranch) {
+ // Do not test the branch we're picking from. It must exist.
+ _findMissingFeatures(branch); // recurse normally
+ } else if (!targets.has(branch)) {
+ // Check to see if the next feature branch is still open for new changes.
+ checkBranchAndAccess(uuid, project, prefix + branch, "cherrypickbot", "push", undefined,
+ (success, hasAccess) => {
+ if (success && hasAccess) {
+ const missingChangeId = `${encodeURIComponent(project)}~`
+ + `${encodeURIComponent(prefix + branch)}~${changeId.split('~').pop()}`
+ queryChange(uuid, missingChangeId, undefined, undefined, (success) => {
+ if (!success)
+ missing.push(branch);
+ else
+ logger.log(`Skipping ${branch}. Change already exists.`, "debug", uuid);
+ _findMissingFeatures(branch); // recurse normally
+ });
+ } else {
+ logger.log(`Skipping ${branch} because it is closed.`, "debug", uuid);
+ _findMissingFeatures(branch); // recurse normally
+ }
+ });
+ } else {
+ logger.log(`Skipping ${branch} because it is already in the list.`, "debug", uuid);
+ _findMissingFeatures(branch); // recurse normally
+ }
+ } else {
+ logger.log(`Skipping ${branch} because it does not exist.`, "debug", uuid);
+ _findMissingFeatures(branch); // recurse normally
+ }
+ return;
+ }
+
+ const nextFeature = makeNextFeature(branch);
+ if (bareRemoteBranches.includes(nextFeature)) {
+ if (nextFeature === bareBranch || featureBranches.has(nextFeature)) {
+ _findMissingFeatures(nextFeature); // Recurse to the next feature branch.
+ } else if (isChild(nextFeature, highestTarget)) {
+ // Only test branches which are older than/children to the current branch.
+ // This forces the waterfall to only work downwards from the highest
+ // specified branch.
+ // Check to see if the next feature branch is still open for new changes.
+ checkBranchAndAccess(uuid, project, prefix + nextFeature, "cherrypickbot", "push", undefined,
+ (success, hasAccess) => {
+ if (success && hasAccess) {
+ const missingChangeId = `${encodeURIComponent(project)}~`
+ + `${encodeURIComponent(prefix + nextFeature)}~${changeId.split('~').pop()}`
+ queryChange(uuid, missingChangeId, undefined, undefined, (success, data) => {
+ if (!success)
+ missing.push(prefix + nextFeature);
+ else {
+ if (data.status == "MERGED") {
+ logger.log(`Skipping ${prefix + nextFeature}. Merged change already exists.`, "debug", uuid);
+ } else {
+ const _next = makeNextFeature(nextFeature);
+ if (!bareRemoteBranches.includes(_next)) {
+ // This was the last and highest feature branch, which means it would
+ // be our first pick target (excepting dev). Since the change is open,
+ // we should not touch it.
+ // Call _finishUp() and pick to only any immediate release targets.
+ logger.log(`Missing immediate target ${prefix + nextFeature} has a change in ${data.status}.`, "error", uuid);
+ _finishUp(true, data);
+ return;
+ }
+ }
+ }
+ _findMissingFeatures(nextFeature);
+ });
+ } else {
+ logger.log(`Skipping ${prefix + nextFeature} because it is closed.`, "debug", uuid);
+ _findMissingFeatures(nextFeature);
+ }
+ });
+ } else {
+ // NextFeature exists remotely, but is out of scope based on pick targets and source branch.
+ _finishUp();
+ }
+ } else {
+ // We've reached the end of the feature branches which are older than the highest branch.
+ _finishUp();
+ }
+ }
+
+ // Use the oldest feature version to find any gaps since then.
+ if (Array.from(featureBranches).length === 0) {
+ _finishUp();
+ return;
+ }
+ try {
+ _findMissingFeatures(Array.from(featureBranches).sort(sortBranches)[0], true);
+ } catch (e) {
+ logger.log(`Error finding missing targets: ${e}`, 'error', uuid);
+ }
+ });
+}
+
+exports.sortBranches = sortBranches;
+function sortBranches(branches) {
+ return Array.from(branches).sort((a, b) => {
+ // Account for dev and master branches.
+ if (a === "dev" || a === "master") {
+ return -1;
+ }
+ if (b === "dev" || b === "master") {
+ return 1;
+ }
+ let aParts = a.split('.').map(Number);
+ let bParts = b.split('.').map(Number);
+
+ for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
+ if (aParts[i] !== bParts[i]) {
+ return bParts[i] - aParts[i];
+ }
+ }
+
+ return aParts.length - bParts.length;
+ });
+}
+
+// Take a gerrit Change object and mock a change-merged event.
+// Use the original merge event as the template for the mocked event.
+exports.mockChangeMergedFromOther = mockChangeMergedFromOther;
+function mockChangeMergedFromOther(uuid, originalMerge, targetBranch, remainingPicks, callback) {
+ if (remainingPicks.size == 0) {
+ logger.log(`No remaining picks for existing target on ${targetBranch}. Nothing to do.`,
+ "debug", uuid);
+ callback(null);
+ return;
+ }
+ let mockMerge = deepCopy(originalMerge);
+ // Assemble the fullChangeID from the project and branch.
+ let targetChangeId = encodeURIComponent(mockMerge.change.project) + "~"
+ + encodeURIComponent(targetBranch) + "~" + mockMerge.change.id;
+ // Query the target change from gerrit.
+ queryChange(uuid, targetChangeId, undefined, undefined, function (success, targetChange) {
+ if (!success) {
+ logger.log(`Error mocking change-merged event: ${targetChange}, ${targetChange}`, "error", uuid);
+ callback(null);
+ return;
+ }
+ // Replace the following properties of mockMerge with the targetChange:
+ // newRev
+ // patchSet
+ // change
+ // refName
+ // changeKey
+ // fullChangeID
+ // eventCreatedOn
+ delete mockMerge.uuid;
+ mockMerge.newRev = targetChange.current_revision;
+ mockMerge.patchSet = {
+ number: targetChange.revisions[targetChange.current_revision]._number,
+ revision: targetChange.current_revision,
+ parents: targetChange.revisions[targetChange.current_revision].commit.parents,
+ ref: `refs/changes/${targetChange._number}/${targetChange.current_revision}`,
+ uploader: targetChange.owner,
+ author: targetChange.revisions[targetChange.current_revision].commit.author,
+ createdOn: targetChange.created
+ };
+ targetChange.id = targetChange.change_id;
+ targetChange.number = targetChange._number;
+ // build the url from the project and change number.
+ targetChange.url = `https://codereview.qt-project.org/c/${targetChange.project}/+/`
+ + `${targetChange._number}`;
+ // Replace Pick-to: footer with the remaining picks.
+ // If targetChange did not have a Pick-to footer, add one at the beginning
+ // of the footers. Footers are separated from the commit message body by the last\n\n.
+ const origCommitMessage = targetChange.revisions[targetChange.current_revision].commit.message;
+ let footerIndex = origCommitMessage.lastIndexOf("\n\n");
+ if (footerIndex === -1) {
+ footerIndex = origCommitMessage.length;
+ } else {
+ footerIndex += 2; // Start after the last \n\n.
+ }
+ let footer = origCommitMessage.slice(footerIndex);
+ let pickToFooter = "Pick-to: " + Array.from(remainingPicks).join(" ");
+ if (footer.match(/Pick-to:.*$/m)) {
+ footer = footer.replace(/Pick-to:.*$/m, pickToFooter);
+ } else {
+ footer = pickToFooter + "\n\n" + footer;
+ }
+ targetChange.commitMessage = origCommitMessage.slice(0, footerIndex) + footer;
+ mockMerge.change = targetChange;
+ mockMerge.refName = `refs/heads/${targetChange.branch}`;
+ mockMerge.changeKey = { key: targetChange.change_id };
+ mockMerge.fullChangeID = encodeURIComponent(targetChange.project) + "~"
+ + encodeURIComponent(targetChange.branch) + "~" + targetChange.change_id;
+ mockMerge.eventCreatedOn = Date.now();
+
+ callback(mockMerge);
+ });
+}
+
// Get all database items in the processing_queue with state "processing"
exports.getAllProcessingRequests = getAllProcessingRequests;
function getAllProcessingRequests(callback) {