blob: d1c6c9ddd0ae73dcb3324f0eeaaaabff3fce523f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable no-unused-vars */
const COMMENT_TYPE_DESCRIPTION = 'DESCRIPTION';
/**
* Fetches the issue from Monorail.
* @param {string} issueName The resource name of the issue.
* @return {Issue}
*/
function getIssue(issueName) {
const message = {'name': issueName};
const url = URL + 'monorail.v3.Issues/GetIssue';
return run_(url, message);
}
/**
* Fetches all the given issues from Monorail.
* @param {Array<string>} issueNames The resource names of the issues.
* @return {Array<Issue>}
*/
function batchGetIssues(issueNames) {
const message = {'names': issueNames};
const url = URL + 'monorail.v3.Issues/BatchGetIssues';
return run_(url, message);
}
/**
* Fetches all the ApprovalValues that belong to the given issue.
* @param {string} issueName The resource name of the issue.
* @return {Array<ApprovalValue>}
*/
function listApprovalValues(issueName) {
const message = {'parent': issueName};
const url = URL + 'monorail.v3.Issues/ListApprovalValues';
return run_(url, message);
}
/**
* Calls SearchIssues with the given parameters.
* @param {Array<string>} projectNames resource names of the projects to search.
* @param {string} query The query to use to search.
* @param {string} orderBy The issue fields to order issues by.
* @param {Number} pageSize The maximum issues to return.
* @param {string} pageToken The page token from the previous call.
* @return {Array<SearchIssuesResponse>}
*/
function searchIssuesPagination_(
projectNames, query, orderBy, pageSize, pageToken) {
const message = {
'projects': projectNames,
'query': query,
'orderBy': orderBy,
'pageToken': pageToken};
if (pageSize) {
message['pageSize'] = pageSize;
}
const url = URL + 'monorail.v3.Issues/SearchIssues';
return run_(url, message);
}
// TODO(crbug.com/monorail/7143): SearchIssues only accepts one project.
/**
* Searches Monorail for issues using the given query.
* NOTE: We currently only accept `projectNames` with one and only one project.
* @param {Array<string>} projects Resource names of the projects to search
* within.
* @param {string=} query The query to use to search.
* @param {string=} orderBy The issue fields to order issues by,
* e.g. 'EstDays,Opened,-stars'
* @return {Array<Issue>}
*/
function searchIssues(projects, query, orderBy) {
const pageSize = 100;
let pageToken;
issues = [];
do {
const resp = searchIssuesPagination_(
projects, query, orderBy, pageSize, pageToken);
issues = issues.concat(resp.issues);
pageToken = resp.nextPageToken;
}
while (pageToken);
return issues;
}
/**
* Calls ListComments with the given parameters.
* @param {string} issueName Resource name of the issue.
* @param {string} filter The approval filter query.
* @param {Number} pageSize The maximum number of comments to return.
* @param {string} pageToken The page token from the previous request.
* @return {ListCommentsResponse}
*/
function listCommentsPagination_(issueName, filter, pageSize, pageToken) {
const message = {
'parent': issueName,
'pageToken': pageToken,
'filter': filter,
};
if (pageSize) {
message['pageSize'] = pageSize;
}
const url = URL + 'monorail.v3.Issues/ListComments';
return run_(url, message);
}
/**
* Returns all comments and previous/current descriptions of an issue.
* @param {string} issueName Resource name of the Issue.
* @param {string=} filter The filter query filtering out comments.
* We only accept `approval = "<approvalDef resource name>""`.
* e.g. 'approval = "projects/chromium/approvalDefs/34"'
* @return {Array<Comment>}
*/
function listComments(issueName, filter) {
let pageToken;
let comments = [];
do {
const resp = listCommentsPagination_(issueName, filter, '', pageToken);
comments = comments.concat(resp.comments);
pageToken = resp.nextPageToken;
}
while (pageToken);
return comments;
}
/**
* Gets the current description of an issue.
* @param {string} issueName Resource name of the Issue.
* @param {string=} filter The filter query filtering out comments.
* We only accept `approval = "<approvalDef resource name>""`.
* e.g. 'approval = "projects/chromium/approvalDefs/34"'
* @return {Comment}
*/
function getCurrentDescription(issueName, filter) {
const allComments = listComments(issueName, filter);
for (let i = (allComments.length - 1); i > -1; i--) {
if (allComments[i].type === COMMENT_TYPE_DESCRIPTION) {
return allComments[i];
}
}
}
/**
* Gets the first (non-description) comment of an issue.
* @param {string} issueName Resource name of the Issue.
* @param {string=} filter The filter query filtering out comments.
* We only accept `approval = "<approvalDef resource name>""`.
* e.g. 'approval = "projects/chromium/approvalDefs/34"'
* @return {Comment}
*/
function getFirstComment(issueName, filter) {
const allComments = listComments(issueName, filter);
for (let i = 0; i < allComments.length; i++) {
if (allComments[i].type !== COMMENT_TYPE_DESCRIPTION) {
return allComments[i];
}
}
return null;
}
/**
* Gets the last (non-description) comment of an issue.
* @param {string} issueName The resource name of the issue.
* @param {string=} filter The filter query filtering out comments.
* We only accept `approval = "<approvalDef resource name>""`.
* e.g. 'approval = "projects/chromium/approvalDefs/34"'
* @return {Issue}
*/
function getLastComment(issueName, filter) {
const allComments = listComments(issueName, filter);
for (let i = (allComments.length - 1); i > -1; i--) {
if (allComments[i].type != COMMENT_TYPE_DESCRIPTION) {
return allComments[i];
}
}
return null;
}
/**
* Checks if the given label exists in the issue.
* @param {Issue} issue The issue to search within for the label.
* @param {string} label The label to search for.
* @return {boolean}
*/
function hasLabel(issue, label) {
if (issue.labels) {
const testLabel = label.toLowerCase();
return issue.labels.some(({label}) => testLabel === label.toLowerCase());
}
return false;
}
/**
* Checks if the issue has any labels matching the given regex.
* @param {Issue} issue The issue to search within for matching labels.
* @param {string} regex The regex pattern to use to search for labels.
* @return {boolean}
*/
function hasLabelMatching(issue, regex) {
if (issue.labels) {
const re = new RegExp(regex, 'i');
return issue.labels.some(({label}) => re.test(label));
}
return false;
}
/**
* Returns all labels in the issue that match the given regex.
* @param {Issue} issue The issue to search within for matching labels.
* @param {string} regex The regex pattern to use to search for labels.
* @return {Array<string>}
*/
function getLabelsMatching(issue, regex) {
const labels = [];
if (issue.labels) {
const re = new RegExp(regex, 'i');
for (let i = 0; i < issue.labels.length; i++) {
if (re.test(issue.labels[i].label)) {
labels.push(issue.labels[i].label);
}
}
}
return labels;
}
/**
* Get the comment where the given label was added, if any.
* @param {string} issueName The resource name of the issue.
* @param {string} label The label that was remove.
* @return {Comment}
*/
function getLabelSetComment(issueName, label) {
const comments = listComments(issueName);
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (comment.amendments) {
for (let j = 0; j < comment.amendments.length; j++) {
const amendment = comment.amendments[j];
if (amendment['fieldName'] === 'Labels' &&
amendment['newOrDeltaValue'].toLowerCase() === (
label.toLocaleLowerCase())) {
return comment;
}
}
}
}
return null;
}
/**
* Get the comment where the given label was removed, if any.
* @param {string} issueName The resource name of the issue.
* @param {string} label The label that was remove.
* @return {Comment}
*/
function getLabelRemoveComment(issueName, label) {
const comments = listComments(issueName);
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (comment.amendments) {
for (let j = 0; j < comment.amendments.length; j++) {
const amendment = comment.amendments[j];
if (amendment['fieldName'] === 'Labels' &&
amendment[
'newOrDeltaValue'].toLowerCase() === (
'-' + label.toLocaleLowerCase())) {
return comment;
}
}
}
}
return null;
}
/**
* Updates the issue to have the given label added.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue The issue to update.
* @param {string} label The label to add.
*/
function addLabel(issue, label) {
if (hasLabel(issue, label)) return;
maybeCreateDelta_(issue);
// Add the label to the issue's delta.labelsAdd.
issue.delta.labelsAdd.push(label);
// Add the label to the issue.
issue.labels.push({label: label});
// 'labels' added to updateMask in saveChanges().
}
/**
* Updates the issue to have the given label removed from the issue.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue The issue to update.
* @param {string} label The label to remove.
*/
function removeLabel(issue, label) {
if (!hasLabel(issue, label)) return;
maybeCreateDelta_(issue);
// Add the label to the issue's delta.labelsRemove.
issue.delta.labelsRemove.push(label);
// Remove label from issue.
for (let i = 0; i < issue.labels.length; i++) {
if (issue.labels[i].label.toLowerCase() === label.toLowerCase()) {
issue.labels.splice(i, 1);
break;
}
}
}
/**
* Sets the owner of the given issue.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {string} ownerName The resource name of the new owner,
* e.g. 'users/chicken@email.com'
*/
function setOwner(issue, ownerName) {
maybeCreateDelta_(issue);
issue.owner = {'user': ownerName};
if (issue.delta.updateMask.indexOf('owner.user') === -1) {
issue.delta.updateMask.push('owner.user');
}
}
/**
* Sets the summary of the given issue.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {string} summary The new summary of the issue.
*/
function setSummary(issue, summary) {
maybeCreateDelta_(issue);
issue.summary = summary;
if (issue.delta.updateMask.indexOf('summary') === -1) {
issue.delta.updateMask.push('summary');
}
}
/**
*Sets the status of the given issue.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {string} status The new status of the issue e.g. 'Available'.
*/
function setStatus(issue, status) {
maybeCreateDelta_(issue);
issue.status.status = status;
if (issue.delta.updateMask.indexOf('status.status') === -1) {
issue.delta.updateMask.push('status.status');
}
}
/**
* Sets the merged into issue for the given issue.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {IssueRef} mergedIntoRef IssueRef of the issue to merge into.
*/
function setMergedInto(issue, mergedIntoRef) {
maybeCreateDelta_(issue);
issue.mergedIntoIssueRef = mergedIntoRef;
if (issue.delta.updateMask.indexOf('mergedIntoIssueRef') === -1) {
issue.delta.updateMask.push('mergedIntoIssueRef');
}
}
/**
* Checks if target is found in source.
* @param {IssueRef} target The IssueRef to look for.
* @param {Array<IssueRef>} source the IssueRefs to look in.
* @return {number} index of target in source, -1 if not found.
*/
function issueRefExists_(target, source) {
for (let i = 0; i < source.length; i++) {
if ((source[i].issue === target.issue || (!source[i].issue && !target.issue)
) && (source[i].extIdentifier === target.extIdentifier || (
!source[i].extIdentifier && !target.extIdentifier))) {
return i;
}
}
return -1;
}
/**
* Makes blocking issue ref changes.
* blockingIssuesAdd are added before blockingIssuesRemove are removed.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {Array<IssueRef>} blockingIssuesAdd issues to add as blocking issues.
* @param {Array<IssueRef>} blockingIssuesRemove issues to remove from blocking
* issues.
*/
function addBlockingIssueChanges(
issue, blockingIssuesAdd, blockingIssuesRemove) {
maybeCreateDelta_(issue);
blockingIssuesAdd.forEach((addRef) => {
const iInIssue = issueRefExists_(addRef, issue.blockingIssueRefs);
if (iInIssue === -1) { // addRef not found in issue
issue.blockingIssueRefs.push(addRef);
issue.delta.blockingAdd.push(addRef);
const iInDeltaRemove = issueRefExists_(
addRef, issue.delta.blockingRemove);
if (iInDeltaRemove != -1) {
// Remove addRef from blckingRemove that may have been added earlier.
issue.delta.blockingRemove.splice(iInDeltaRemove, 1);
}
// issue.delta.updateMask is updated in saveChanges()
}
});
// Add blockingIssuesAdd to issue and issue.delta.blockingAdd if not in
// issue.blockingIssues
blockingIssuesRemove.forEach((removeRef) => {
const iInIssue = issueRefExists_(removeRef, issue.blockingIssueRefs);
if (iInIssue > -1) {
issue.blockingIssueRefs.splice(iInIssue, 1);
issue.delta.blockingRemove.push(removeRef);
const iInDeltaAdd = issueRefExists_(removeRef, issue.delta.blockingAdd);
if (iInDeltaAdd != -1) {
issue.delta.blockingAdd.splice(iInDeltaAdd, 1);
}
}
});
}
/**
* Makes blocked-on issue ref changes.
* blockedOnIssuesAdd are added before blockedOnIssuesRemove are removed.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {Array<IssueRef>} blockedOnIssuesAdd issues to add as blockedon
* issues.
* @param {Array<IssueRef>} blockedOnIssuesRemove issues to remove from
* blockedon issues.
*/
function addBlockedOnIssueChanges(
issue, blockedOnIssuesAdd, blockedOnIssuesRemove) {
maybeCreateDelta_(issue);
blockedOnIssuesAdd.forEach((addRef) => {
const iInIssue = issueRefExists_(addRef, issue.blockedOnIssueRefs);
if (iInIssue === -1) { // addRef not found in issue
issue.blockedOnIssueRefs.push(addRef);
issue.delta.blockedOnAdd.push(addRef);
const iInDeltaRemove = issueRefExists_(
addRef, issue.delta.blockedOnRemove);
if (iInDeltaRemove != -1) {
// Remove addRef from blckingRemove that may have been added earlier.
issue.delta.blockedOnRemove.splice(iInDeltaRemove, 1);
}
// issue.delta.updateMask is updated in saveChanges()
}
});
// Add blockedOnIssuesAdd to issue and issue.delta.blockedOnAdd if not in
// issue.blockedOnIssues.
blockedOnIssuesRemove.forEach((removeRef) => {
const iInIssue = issueRefExists_(removeRef, issue.blockedOnIssueRefs);
if (iInIssue > -1) {
issue.blockedOnIssueRefs.splice(iInIssue, 1);
issue.delta.blockedOnRemove.push(removeRef);
const iInDeltaAdd = issueRefExists_(removeRef, issue.delta.blockedOnAdd);
if (iInDeltaAdd != -1) {
issue.delta.blockedOnAdd.splice(iInDeltaAdd, 1);
}
}
});
}
/**
* Looks for a component name in an Array of ComponentValues.
* @param {string} compName Resource name of the Component to look for.
* @param {Array<ComponentValue>} compArray List of ComponentValues.
* @return {number} Index of compName in compArray, -1 if not found.
*/
function componentExists_(compName, compArray) {
for (let i = 0; i < compArray.length; i++) {
if (compArray[i].component === compName) {
return i;
}
}
return -1;
}
/**
* Adds the component changes to the issue.
* componentNamesAdd are added before componentNamesremove are removed.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {Array<string>} componentNamesAdd Array of component resource names.
* @param {Array<string>} componentNamesRemove Array or component resource
* names.
*/
function addComponentChanges(issue, componentNamesAdd, componentNamesRemove) {
maybeCreateDelta_(issue);
componentNamesAdd.forEach((compName) => {
const iInIssue = componentExists_(compName, issue.components);
if (iInIssue === -1) { // compName is not in issue.
issue.components.push({'component': compName});
issue.delta.componentsAdd.push(compName);
const iInDeltaRemove = issue.delta.componentsRemove.indexOf(compName);
if (iInDeltaRemove != -1) {
// Remove compName from issue.delta.componentsRemove that may have been
// added before.
issue.delta.componentsRemove.splice(iInDeltaRemove, 1);
}
// issue.delta.updateMask is updated in saveChanges()
}
});
componentNamesRemove.forEach((compName) => {
const iInIssue = componentExists_(compName, issue.components);
if (iInIssue != -1) { // compName was found in issue.
issue.components.splice(iInIssue, 1);
issue.delta.componentsRemove.push(compName);
const iInDeltaAdd = issue.delta.componentsAdd.indexOf(compName);
if (iInDeltaAdd != -1) {
// Remove compName from issue.delta.componentsAdd that may have been
// added before.
issue.delta.componentsAdd.splice(iInDeltaAdd, 1);
}
}
});
}
/**
* Checks if the fieldVal is found in fieldValsArray
* @param {FieldValue} fieldVal the field to look for.
* @param {Array<FieldValue>} fieldValsArray the Array to look within.
* @return {number} the index of fieldVal in fieldValsArray, or -1 if not found.
*/
function fieldValueExists_(fieldVal, fieldValsArray) {
for (let i = 0; i < fieldValsArray.length; i++) {
const currFv = fieldValsArray[i];
if (currFv.field === fieldVal.field && currFv.value === fieldVal.value && (
currFv.phase === fieldVal.phase || (
!currFv.phase && !fieldVal.phase))) {
return i;
}
}
return -1;
}
/**
* Adds the FieldValue changes to the issue.
* fieldValuesAdd are added before fieldValuesRemove are removed.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {Array<FieldValue>} fieldValuesAdd Array of FieldValues to add.
* @param {Array<FieldValue>} fieldValuesRemove Array of FieldValues to remove.
*/
function addFieldValueChanges(issue, fieldValuesAdd, fieldValuesRemove) {
maybeCreateDelta_(issue);
fieldValuesAdd.forEach((fvAdd) => {
const iInIssue = fieldValueExists_(fvAdd, issue.fieldValues);
if (iInIssue === -1) { // fvAdd is not already in issue, so we can add it.
issue.fieldValues.push(fvAdd);
issue.delta.fieldValuesAdd.push(fvAdd);
const iInDeltaRemove = fieldValueExists_(
fvAdd, issue.delta.fieldValuesRemove);
if (iInDeltaRemove != -1) {
// fvAdd was added to fieldValuesRemove in a previous call.
issue.delta.fieldValuesRemove.splice(iInDeltaRemove, 1);
}
// issue.delta.updateMask is updated in saveChanges()
}
});
// issue.delta.updateMask is updated in saveChanges()
fieldValuesRemove.forEach((fvRemove) => {
const iInIssue = fieldValueExists_(fvRemove, issue.fieldValues);
if (iInIssue != -1) { // fvRemove is in issue, so we can remove it.
issue.fieldValues.splice(iInIssue, 1);
issue.delta.fieldValuesRemove.push(fvRemove);
const iInDeltaAdd = fieldValueExists_(
fvRemove, issue.delta.fieldValuesAdd);
if (iInDeltaAdd != -1) {
// fvRemove was added to fieldValuesAdd in a previous call.
issue.delta.fieldValuesAdd.splice(iInDeltaAdd, 1);
}
}
});
}
/**
* Checks for the existence of userName in userValues
* @param {string} userName A user resource name to look for.
* @param {Array<UserValue>} userValues UserValues to search through.
* @return {number} Index of userName's UserValue in userValues or -1 if not
* found.
*/
function userValueExists_(userName, userValues) {
for (let i = 0; i< userValues.length; i++) {
if (userValues[i].user === userName) {
return i;
}
}
return -1;
}
/**
* Adds the CC changes to the issue.
* ccNamesAdd are added before ccNamesRemove are removed.
* This method does not call Monorail's API to save this change.
* Call saveChanges() to send all updates to Monorail.
* @param {Issue} issue Issue to change.
* @param {Array<string>} ccNamesAdd Array if user resource names.
* @param {Array<string>} ccNamesRemove Array if user resource names.
*/
function addCcChanges(issue, ccNamesAdd, ccNamesRemove) {
maybeCreateDelta_(issue);
ccNamesAdd.forEach((ccName) => {
const iInIssue = userValueExists_(ccName, issue.ccUsers);
if (iInIssue === -1) { // User is not in issue, so we can add them.
issue.ccUsers.push({'user': ccName});
issue.delta.ccsAdd.push(ccName);
const iInDeltaRemove = issue.delta.ccsRemove.indexOf(ccName);
if (iInDeltaRemove != -1) {
// ccName was added to ccsRemove in a previous call.
issue.delta.ccsRemove.splice(iInDeltaRemove, 1);
}
}
});
ccNamesRemove.forEach((ccName) => {
const iInIssue = userValueExists_(ccName, issue.ccUsers);
if (iInIssue != -1) { // User is in issue, so we can remove it.
issue.ccUsers.splice(iInIssue, 1);
issue.delta.ccsRemove.push(ccName);
const iInDeltaAdd = issue.delta.ccsAdd.indexOf(ccName);
if (iInDeltaAdd != -1) {
// ccName was added to delta.ccsAdd in a previous all.
issue.delta.ccsAdd.splice(iInDeltaAdd, 1);
}
}
});
}
/**
* Set the pending comment of the issue.
* @param {Issue} issue Issue whose comment we want to set.
* @param {string} comment Comment that we want for the issue.
*/
function setComment(issue, comment) {
maybeCreateDelta_(issue);
issue.delta.comment = comment;
}
/**
* Get the pending comment for the issue.
* @param {Issue} issue Issue whose comment we want.
* @return {string}
*/
function getPendingComment(issue) {
if (issue.delta) {
return issue.delta.comment;
}
return '';
}
/**
* Adds to the existing pending comment
* @param {Issue} issue Issue to update.
* @param {string} comment The comment string to add to the existing one.
*/
function appendComment(issue, comment) {
maybeCreateDelta_(issue);
issue.delta.comment = issue.delta.comment.concat(comment);
}
/**
* Sets up an issue for pending changes.
* @param {Issue} issue The issue that needs to be updated.
*/
function maybeCreateDelta_(issue) {
if (!issue.delta) {
issue.delta = newIssueDelta_();
if (!issue.components) {
issue.components = [];
};
if (!issue.blockingIssueRefs) {
issue.blockingIssueRefs = [];
}
if (!issue.blockedOnIssueRefs) {
issue.blockedOnIssueRefs = [];
}
if (!issue.ccUsers) {
issue.ccUsers = [];
}
if (!issue.labels) {
issue.labels = [];
}
if (!issue.fieldValues) {
issue.fieldValues = [];
}
}
}
/**
* Creates an IssueDelta
* @return {IssueDelta_}
*/
function newIssueDelta_() {
return new IssueDelta_();
}
/** Used to track pending changes to an issue.*/
function IssueDelta_() {
/** Array<string> */ this.updateMask = [];
// User resource names.
/** Array<string> */ this.ccsRemove = [];
/** Array<string> */ this.ccsAdd = [];
/** Array<IssueRef> */ this.blockedOnRemove = [];
/** Array<IssueRef> */ this.blockedOnAdd = [];
/** Array<IssueRef> */ this.blockingRemove = [];
/** Array<IssueRef> */ this.blockingAdd = [];
// Component resource names.
/** Array<string> */ this.componentsRemove = [];
/** Array<string> */ this.componentsAdd = [];
// Label values, e.g. 'Security-Notify'.
/** Array<string> */ this.labelsRemove = [];
/** Array<string> */ this.labelsAdd = [];
/** Array<FieldValue> */ this.fieldValuesRemove = [];
/** Array<FieldValue> */ this.fieldValuesAdd = [];
this.comment = '';
}
/**
* Calls Monorail's API to update the issue.
* @param {Issue} issue The issue to update where issue['delta'] is expected
* to exist.
* @param {boolean} sendEmail True if the update should trigger email
* notifications.
* @return {Issue}
*/
function saveChanges(issue, sendEmail) {
if (!issue.delta) {
throw new Error('No pending changes for issue.');
}
const modifyDelta = {
'ccsRemove': issue.delta.ccsRemove,
'blockedOnIssuesRemove': issue.delta.blockedOnRemove,
'blockingIssuesRemove': issue.delta.blockingRemove,
'componentsRemove': issue.delta.componentsRemove,
'labelsRemove': issue.delta.labelsRemove,
'fieldValsRemove': issue.delta.fieldValuesRemove,
'issue': {
'name': issue.name,
'fieldValues': issue.delta.fieldValuesAdd,
'blockedOnIssueRefs': issue.delta.blockedOnAdd,
'blockingIssueRefs': issue.delta.blockingAdd,
'mergedIntoIssueRef': issue.mergedIntoIssueRef,
'summary': issue.summary,
'status': issue.status,
'owner': issue.owner,
'labels': [],
'ccUsers': [],
'components': [],
},
};
if (issue.delta.fieldValuesAdd.length > 0) {
issue.delta.updateMask.push('fieldValues');
}
if (issue.delta.blockedOnAdd.length > 0) {
issue.delta.updateMask.push('blockedOnIssueRefs');
}
if (issue.delta.blockingAdd.length > 0) {
issue.delta.updateMask.push('blockingIssueRefs');
}
if (issue.delta.ccsAdd.length > 0) {
issue.delta.updateMask.push('ccUsers');
}
issue.delta.ccsAdd.forEach((userResourceName) => {
modifyDelta.issue['ccUsers'].push({'user': userResourceName});
});
if (issue.delta.labelsAdd.length > 0) {
issue.delta.updateMask.push('labels');
}
issue.delta.labelsAdd.forEach((label) => {
modifyDelta.issue['labels'].push({'label': label});
});
if (issue.delta.componentsAdd.length > 0) {
issue.delta.updateMask.push('components');
}
issue.delta.componentsAdd.forEach((compResourceName) => {
modifyDelta.issue['components'].push({'component': compResourceName});
});
modifyDelta['updateMask'] = issue.delta.updateMask.join();
const message = {
'deltas': [modifyDelta],
'notifyType': sendEmail ? 'EMAIL' : 'NO_NOTIFICATION',
'commentContent': issue.delta.comment,
};
const url = URL + 'monorail.v3.Issues/ModifyIssues';
response = run_(url, message);
if (!response.issues) {
Logger.log('All changes Noop');
return null;
}
issue = response.issues[0];
return issue;
}
/**
* Creates an Issue.
* @param {string} projectName: Resource name of the parent project.
* @param {string} summary: Summary of the issue.
* @param {string} description: Description of the issue.
* @param {string} status: Status of the issue, e.g. "Untriaged".
* @param {boolean} sendEmail: True if this should trigger email notifications.
* @param {string=} ownerName: Resource name of the issue owner.
* @param {Array<string>=} ccNames: Resource names of the users to cc.
* @param {Array<string>=} labels: Labels to add to the issue,
* e.g. "Restict-View-Google".
* @param {Array<string>=} componentNames: Resource names of components to add.
* @param {Array<FieldValue>=} fieldValues: FieldValues to add to the issue.
* @param {Array<IssueRef>=} blockedOnRefs: IssueRefs for blocked on issues.
* @param {Array<IssueRef>=} blockingRefs: IssueRefs for blocking issues.
* @return {Issue}
*/
function makeIssue(
projectName, summary, description, status, sendEmail, ownerName, ccNames,
labels, componentNames, fieldValues, blockedOnRefs, blockingRefs) {
const issue = {
'summary': summary,
'status': {'status': status},
'ccUsers': [],
'components': [],
'labels': [],
};
if (ownerName) {
issue['owner'] = {'user': ownerName};
}
if (ccNames) {
ccNames.forEach((ccName) => {
issue['ccUsers'].push({'user': ccName});
});
};
if (labels) {
labels.forEach((label) => {
issue['labels'].push({'label': label});
});
};
if (componentNames) {
componentNames.forEach((componentName) => {
issue['components'].push({'component': componentName});
});
};
if (fieldValues) {
issue['fieldValues'] = fieldValues;
};
if (blockedOnRefs) {
issue['blockedOnIssueRefs'] = blockedOnRefs;
};
if (blockingRefs) {
issue['blockingIssueRefs'] = blockingRefs;
};
const message = {
'parent': projectName,
'issue': issue,
'description': description,
'notifyType': sendEmail ? 'EMAIL': 'NO_NOTIFICATION',
};
const url = URL + 'monorail.v3.Issues/MakeIssue';
return run_(url, message);
}