blob: d1c6c9ddd0ae73dcb3324f0eeaaaabff3fce523f [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/* eslint-disable no-unused-vars */
6
7const COMMENT_TYPE_DESCRIPTION = 'DESCRIPTION';
8
9/**
10 * Fetches the issue from Monorail.
11 * @param {string} issueName The resource name of the issue.
12 * @return {Issue}
13 */
14function getIssue(issueName) {
15 const message = {'name': issueName};
16 const url = URL + 'monorail.v3.Issues/GetIssue';
17 return run_(url, message);
18}
19
20/**
21 * Fetches all the given issues from Monorail.
22 * @param {Array<string>} issueNames The resource names of the issues.
23 * @return {Array<Issue>}
24 */
25function batchGetIssues(issueNames) {
26 const message = {'names': issueNames};
27 const url = URL + 'monorail.v3.Issues/BatchGetIssues';
28 return run_(url, message);
29}
30
31/**
32 * Fetches all the ApprovalValues that belong to the given issue.
33 * @param {string} issueName The resource name of the issue.
34 * @return {Array<ApprovalValue>}
35 */
36function listApprovalValues(issueName) {
37 const message = {'parent': issueName};
38 const url = URL + 'monorail.v3.Issues/ListApprovalValues';
39 return run_(url, message);
40}
41
42/**
43 * Calls SearchIssues with the given parameters.
44 * @param {Array<string>} projectNames resource names of the projects to search.
45 * @param {string} query The query to use to search.
46 * @param {string} orderBy The issue fields to order issues by.
47 * @param {Number} pageSize The maximum issues to return.
48 * @param {string} pageToken The page token from the previous call.
49 * @return {Array<SearchIssuesResponse>}
50 */
51function searchIssuesPagination_(
52 projectNames, query, orderBy, pageSize, pageToken) {
53 const message = {
54 'projects': projectNames,
55 'query': query,
56 'orderBy': orderBy,
57 'pageToken': pageToken};
58 if (pageSize) {
59 message['pageSize'] = pageSize;
60 }
61 const url = URL + 'monorail.v3.Issues/SearchIssues';
62 return run_(url, message);
63}
64
65// TODO(crbug.com/monorail/7143): SearchIssues only accepts one project.
66/**
67 * Searches Monorail for issues using the given query.
68 * NOTE: We currently only accept `projectNames` with one and only one project.
69 * @param {Array<string>} projects Resource names of the projects to search
70 * within.
71 * @param {string=} query The query to use to search.
72 * @param {string=} orderBy The issue fields to order issues by,
73 * e.g. 'EstDays,Opened,-stars'
74 * @return {Array<Issue>}
75 */
76function searchIssues(projects, query, orderBy) {
77 const pageSize = 100;
78 let pageToken;
79
80 issues = [];
81
82 do {
83 const resp = searchIssuesPagination_(
84 projects, query, orderBy, pageSize, pageToken);
85 issues = issues.concat(resp.issues);
86 pageToken = resp.nextPageToken;
87 }
88 while (pageToken);
89
90 return issues;
91}
92
93/**
94 * Calls ListComments with the given parameters.
95 * @param {string} issueName Resource name of the issue.
96 * @param {string} filter The approval filter query.
97 * @param {Number} pageSize The maximum number of comments to return.
98 * @param {string} pageToken The page token from the previous request.
99 * @return {ListCommentsResponse}
100 */
101function listCommentsPagination_(issueName, filter, pageSize, pageToken) {
102 const message = {
103 'parent': issueName,
104 'pageToken': pageToken,
105 'filter': filter,
106 };
107 if (pageSize) {
108 message['pageSize'] = pageSize;
109 }
110 const url = URL + 'monorail.v3.Issues/ListComments';
111 return run_(url, message);
112}
113
114/**
115 * Returns all comments and previous/current descriptions of an issue.
116 * @param {string} issueName Resource name of the Issue.
117 * @param {string=} filter The filter query filtering out comments.
118 * We only accept `approval = "<approvalDef resource name>""`.
119 * e.g. 'approval = "projects/chromium/approvalDefs/34"'
120 * @return {Array<Comment>}
121 */
122function listComments(issueName, filter) {
123 let pageToken;
124
125 let comments = [];
126 do {
127 const resp = listCommentsPagination_(issueName, filter, '', pageToken);
128 comments = comments.concat(resp.comments);
129 pageToken = resp.nextPageToken;
130 }
131 while (pageToken);
132
133 return comments;
134}
135
136/**
137 * Gets the current description of an issue.
138 * @param {string} issueName Resource name of the Issue.
139 * @param {string=} filter The filter query filtering out comments.
140 * We only accept `approval = "<approvalDef resource name>""`.
141 * e.g. 'approval = "projects/chromium/approvalDefs/34"'
142 * @return {Comment}
143 */
144function getCurrentDescription(issueName, filter) {
145 const allComments = listComments(issueName, filter);
146 for (let i = (allComments.length - 1); i > -1; i--) {
147 if (allComments[i].type === COMMENT_TYPE_DESCRIPTION) {
148 return allComments[i];
149 }
150 }
151}
152
153/**
154 * Gets the first (non-description) comment of an issue.
155 * @param {string} issueName Resource name of the Issue.
156 * @param {string=} filter The filter query filtering out comments.
157 * We only accept `approval = "<approvalDef resource name>""`.
158 * e.g. 'approval = "projects/chromium/approvalDefs/34"'
159 * @return {Comment}
160 */
161function getFirstComment(issueName, filter) {
162 const allComments = listComments(issueName, filter);
163 for (let i = 0; i < allComments.length; i++) {
164 if (allComments[i].type !== COMMENT_TYPE_DESCRIPTION) {
165 return allComments[i];
166 }
167 }
168 return null;
169}
170
171/**
172 * Gets the last (non-description) comment of an issue.
173 * @param {string} issueName The resource name of the issue.
174 * @param {string=} filter The filter query filtering out comments.
175 * We only accept `approval = "<approvalDef resource name>""`.
176 * e.g. 'approval = "projects/chromium/approvalDefs/34"'
177 * @return {Issue}
178 */
179function getLastComment(issueName, filter) {
180 const allComments = listComments(issueName, filter);
181 for (let i = (allComments.length - 1); i > -1; i--) {
182 if (allComments[i].type != COMMENT_TYPE_DESCRIPTION) {
183 return allComments[i];
184 }
185 }
186 return null;
187}
188
189/**
190 * Checks if the given label exists in the issue.
191 * @param {Issue} issue The issue to search within for the label.
192 * @param {string} label The label to search for.
193 * @return {boolean}
194 */
195function hasLabel(issue, label) {
196 if (issue.labels) {
197 const testLabel = label.toLowerCase();
198 return issue.labels.some(({label}) => testLabel === label.toLowerCase());
199 }
200 return false;
201}
202
203/**
204 * Checks if the issue has any labels matching the given regex.
205 * @param {Issue} issue The issue to search within for matching labels.
206 * @param {string} regex The regex pattern to use to search for labels.
207 * @return {boolean}
208 */
209function hasLabelMatching(issue, regex) {
210 if (issue.labels) {
211 const re = new RegExp(regex, 'i');
212 return issue.labels.some(({label}) => re.test(label));
213 }
214 return false;
215}
216
217/**
218 * Returns all labels in the issue that match the given regex.
219 * @param {Issue} issue The issue to search within for matching labels.
220 * @param {string} regex The regex pattern to use to search for labels.
221 * @return {Array<string>}
222 */
223function getLabelsMatching(issue, regex) {
224 const labels = [];
225 if (issue.labels) {
226 const re = new RegExp(regex, 'i');
227 for (let i = 0; i < issue.labels.length; i++) {
228 if (re.test(issue.labels[i].label)) {
229 labels.push(issue.labels[i].label);
230 }
231 }
232 }
233 return labels;
234}
235
236/**
237 * Get the comment where the given label was added, if any.
238 * @param {string} issueName The resource name of the issue.
239 * @param {string} label The label that was remove.
240 * @return {Comment}
241 */
242function getLabelSetComment(issueName, label) {
243 const comments = listComments(issueName);
244 for (let i = 0; i < comments.length; i++) {
245 const comment = comments[i];
246 if (comment.amendments) {
247 for (let j = 0; j < comment.amendments.length; j++) {
248 const amendment = comment.amendments[j];
249 if (amendment['fieldName'] === 'Labels' &&
250 amendment['newOrDeltaValue'].toLowerCase() === (
251 label.toLocaleLowerCase())) {
252 return comment;
253 }
254 }
255 }
256 }
257 return null;
258}
259
260/**
261 * Get the comment where the given label was removed, if any.
262 * @param {string} issueName The resource name of the issue.
263 * @param {string} label The label that was remove.
264 * @return {Comment}
265 */
266function getLabelRemoveComment(issueName, label) {
267 const comments = listComments(issueName);
268 for (let i = 0; i < comments.length; i++) {
269 const comment = comments[i];
270 if (comment.amendments) {
271 for (let j = 0; j < comment.amendments.length; j++) {
272 const amendment = comment.amendments[j];
273 if (amendment['fieldName'] === 'Labels' &&
274 amendment[
275 'newOrDeltaValue'].toLowerCase() === (
276 '-' + label.toLocaleLowerCase())) {
277 return comment;
278 }
279 }
280 }
281 }
282 return null;
283}
284
285/**
286 * Updates the issue to have the given label added.
287 * This method does not call Monorail's API to save this change.
288 * Call saveChanges() to send all updates to Monorail.
289 * @param {Issue} issue The issue to update.
290 * @param {string} label The label to add.
291 */
292function addLabel(issue, label) {
293 if (hasLabel(issue, label)) return;
294 maybeCreateDelta_(issue);
295 // Add the label to the issue's delta.labelsAdd.
296 issue.delta.labelsAdd.push(label);
297 // Add the label to the issue.
298 issue.labels.push({label: label});
299 // 'labels' added to updateMask in saveChanges().
300}
301
302/**
303 * Updates the issue to have the given label removed from the issue.
304 * This method does not call Monorail's API to save this change.
305 * Call saveChanges() to send all updates to Monorail.
306 * @param {Issue} issue The issue to update.
307 * @param {string} label The label to remove.
308 */
309function removeLabel(issue, label) {
310 if (!hasLabel(issue, label)) return;
311 maybeCreateDelta_(issue);
312 // Add the label to the issue's delta.labelsRemove.
313 issue.delta.labelsRemove.push(label);
314 // Remove label from issue.
315 for (let i = 0; i < issue.labels.length; i++) {
316 if (issue.labels[i].label.toLowerCase() === label.toLowerCase()) {
317 issue.labels.splice(i, 1);
318 break;
319 }
320 }
321}
322
323/**
324 * Sets the owner of the given issue.
325 * This method does not call Monorail's API to save this change.
326 * Call saveChanges() to send all updates to Monorail.
327 * @param {Issue} issue Issue to change.
328 * @param {string} ownerName The resource name of the new owner,
329 * e.g. 'users/chicken@email.com'
330*/
331function setOwner(issue, ownerName) {
332 maybeCreateDelta_(issue);
333 issue.owner = {'user': ownerName};
334 if (issue.delta.updateMask.indexOf('owner.user') === -1) {
335 issue.delta.updateMask.push('owner.user');
336 }
337}
338
339/**
340 * Sets the summary of the given issue.
341 * This method does not call Monorail's API to save this change.
342 * Call saveChanges() to send all updates to Monorail.
343 * @param {Issue} issue Issue to change.
344 * @param {string} summary The new summary of the issue.
345*/
346function setSummary(issue, summary) {
347 maybeCreateDelta_(issue);
348 issue.summary = summary;
349 if (issue.delta.updateMask.indexOf('summary') === -1) {
350 issue.delta.updateMask.push('summary');
351 }
352}
353
354/**
355 *Sets the status of the given issue.
356 * This method does not call Monorail's API to save this change.
357 * Call saveChanges() to send all updates to Monorail.
358 * @param {Issue} issue Issue to change.
359 * @param {string} status The new status of the issue e.g. 'Available'.
360*/
361function setStatus(issue, status) {
362 maybeCreateDelta_(issue);
363 issue.status.status = status;
364 if (issue.delta.updateMask.indexOf('status.status') === -1) {
365 issue.delta.updateMask.push('status.status');
366 }
367}
368
369/**
370 * Sets the merged into issue for the given issue.
371 * This method does not call Monorail's API to save this change.
372 * Call saveChanges() to send all updates to Monorail.
373 * @param {Issue} issue Issue to change.
374 * @param {IssueRef} mergedIntoRef IssueRef of the issue to merge into.
375 */
376function setMergedInto(issue, mergedIntoRef) {
377 maybeCreateDelta_(issue);
378 issue.mergedIntoIssueRef = mergedIntoRef;
379 if (issue.delta.updateMask.indexOf('mergedIntoIssueRef') === -1) {
380 issue.delta.updateMask.push('mergedIntoIssueRef');
381 }
382}
383
384/**
385 * Checks if target is found in source.
386 * @param {IssueRef} target The IssueRef to look for.
387 * @param {Array<IssueRef>} source the IssueRefs to look in.
388 * @return {number} index of target in source, -1 if not found.
389 */
390function issueRefExists_(target, source) {
391 for (let i = 0; i < source.length; i++) {
392 if ((source[i].issue === target.issue || (!source[i].issue && !target.issue)
393 ) && (source[i].extIdentifier === target.extIdentifier || (
394 !source[i].extIdentifier && !target.extIdentifier))) {
395 return i;
396 }
397 }
398 return -1;
399}
400
401/**
402 * Makes blocking issue ref changes.
403 * blockingIssuesAdd are added before blockingIssuesRemove are removed.
404 * This method does not call Monorail's API to save this change.
405 * Call saveChanges() to send all updates to Monorail.
406 * @param {Issue} issue Issue to change.
407 * @param {Array<IssueRef>} blockingIssuesAdd issues to add as blocking issues.
408 * @param {Array<IssueRef>} blockingIssuesRemove issues to remove from blocking
409 * issues.
410 */
411function addBlockingIssueChanges(
412 issue, blockingIssuesAdd, blockingIssuesRemove) {
413 maybeCreateDelta_(issue);
414 blockingIssuesAdd.forEach((addRef) => {
415 const iInIssue = issueRefExists_(addRef, issue.blockingIssueRefs);
416 if (iInIssue === -1) { // addRef not found in issue
417 issue.blockingIssueRefs.push(addRef);
418 issue.delta.blockingAdd.push(addRef);
419 const iInDeltaRemove = issueRefExists_(
420 addRef, issue.delta.blockingRemove);
421 if (iInDeltaRemove != -1) {
422 // Remove addRef from blckingRemove that may have been added earlier.
423 issue.delta.blockingRemove.splice(iInDeltaRemove, 1);
424 }
425 // issue.delta.updateMask is updated in saveChanges()
426 }
427 });
428 // Add blockingIssuesAdd to issue and issue.delta.blockingAdd if not in
429 // issue.blockingIssues
430 blockingIssuesRemove.forEach((removeRef) => {
431 const iInIssue = issueRefExists_(removeRef, issue.blockingIssueRefs);
432 if (iInIssue > -1) {
433 issue.blockingIssueRefs.splice(iInIssue, 1);
434 issue.delta.blockingRemove.push(removeRef);
435 const iInDeltaAdd = issueRefExists_(removeRef, issue.delta.blockingAdd);
436 if (iInDeltaAdd != -1) {
437 issue.delta.blockingAdd.splice(iInDeltaAdd, 1);
438 }
439 }
440 });
441}
442
443/**
444 * Makes blocked-on issue ref changes.
445 * blockedOnIssuesAdd are added before blockedOnIssuesRemove are removed.
446 * This method does not call Monorail's API to save this change.
447 * Call saveChanges() to send all updates to Monorail.
448 * @param {Issue} issue Issue to change.
449 * @param {Array<IssueRef>} blockedOnIssuesAdd issues to add as blockedon
450 * issues.
451 * @param {Array<IssueRef>} blockedOnIssuesRemove issues to remove from
452 * blockedon issues.
453 */
454function addBlockedOnIssueChanges(
455 issue, blockedOnIssuesAdd, blockedOnIssuesRemove) {
456 maybeCreateDelta_(issue);
457 blockedOnIssuesAdd.forEach((addRef) => {
458 const iInIssue = issueRefExists_(addRef, issue.blockedOnIssueRefs);
459 if (iInIssue === -1) { // addRef not found in issue
460 issue.blockedOnIssueRefs.push(addRef);
461 issue.delta.blockedOnAdd.push(addRef);
462 const iInDeltaRemove = issueRefExists_(
463 addRef, issue.delta.blockedOnRemove);
464 if (iInDeltaRemove != -1) {
465 // Remove addRef from blckingRemove that may have been added earlier.
466 issue.delta.blockedOnRemove.splice(iInDeltaRemove, 1);
467 }
468 // issue.delta.updateMask is updated in saveChanges()
469 }
470 });
471 // Add blockedOnIssuesAdd to issue and issue.delta.blockedOnAdd if not in
472 // issue.blockedOnIssues.
473 blockedOnIssuesRemove.forEach((removeRef) => {
474 const iInIssue = issueRefExists_(removeRef, issue.blockedOnIssueRefs);
475 if (iInIssue > -1) {
476 issue.blockedOnIssueRefs.splice(iInIssue, 1);
477 issue.delta.blockedOnRemove.push(removeRef);
478 const iInDeltaAdd = issueRefExists_(removeRef, issue.delta.blockedOnAdd);
479 if (iInDeltaAdd != -1) {
480 issue.delta.blockedOnAdd.splice(iInDeltaAdd, 1);
481 }
482 }
483 });
484}
485
486
487/**
488 * Looks for a component name in an Array of ComponentValues.
489 * @param {string} compName Resource name of the Component to look for.
490 * @param {Array<ComponentValue>} compArray List of ComponentValues.
491 * @return {number} Index of compName in compArray, -1 if not found.
492 */
493function componentExists_(compName, compArray) {
494 for (let i = 0; i < compArray.length; i++) {
495 if (compArray[i].component === compName) {
496 return i;
497 }
498 }
499 return -1;
500}
501
502/**
503 * Adds the component changes to the issue.
504 * componentNamesAdd are added before componentNamesremove are removed.
505 * This method does not call Monorail's API to save this change.
506 * Call saveChanges() to send all updates to Monorail.
507 * @param {Issue} issue Issue to change.
508 * @param {Array<string>} componentNamesAdd Array of component resource names.
509 * @param {Array<string>} componentNamesRemove Array or component resource
510 * names.
511
512*/
513function addComponentChanges(issue, componentNamesAdd, componentNamesRemove) {
514 maybeCreateDelta_(issue);
515 componentNamesAdd.forEach((compName) => {
516 const iInIssue = componentExists_(compName, issue.components);
517 if (iInIssue === -1) { // compName is not in issue.
518 issue.components.push({'component': compName});
519 issue.delta.componentsAdd.push(compName);
520 const iInDeltaRemove = issue.delta.componentsRemove.indexOf(compName);
521 if (iInDeltaRemove != -1) {
522 // Remove compName from issue.delta.componentsRemove that may have been
523 // added before.
524 issue.delta.componentsRemove.splice(iInDeltaRemove, 1);
525 }
526 // issue.delta.updateMask is updated in saveChanges()
527 }
528 });
529
530 componentNamesRemove.forEach((compName) => {
531 const iInIssue = componentExists_(compName, issue.components);
532 if (iInIssue != -1) { // compName was found in issue.
533 issue.components.splice(iInIssue, 1);
534 issue.delta.componentsRemove.push(compName);
535 const iInDeltaAdd = issue.delta.componentsAdd.indexOf(compName);
536 if (iInDeltaAdd != -1) {
537 // Remove compName from issue.delta.componentsAdd that may have been
538 // added before.
539 issue.delta.componentsAdd.splice(iInDeltaAdd, 1);
540 }
541 }
542 });
543}
544
545/**
546 * Checks if the fieldVal is found in fieldValsArray
547 * @param {FieldValue} fieldVal the field to look for.
548 * @param {Array<FieldValue>} fieldValsArray the Array to look within.
549 * @return {number} the index of fieldVal in fieldValsArray, or -1 if not found.
550 */
551function fieldValueExists_(fieldVal, fieldValsArray) {
552 for (let i = 0; i < fieldValsArray.length; i++) {
553 const currFv = fieldValsArray[i];
554 if (currFv.field === fieldVal.field && currFv.value === fieldVal.value && (
555 currFv.phase === fieldVal.phase || (
556 !currFv.phase && !fieldVal.phase))) {
557 return i;
558 }
559 }
560 return -1;
561}
562
563/**
564 * Adds the FieldValue changes to the issue.
565 * fieldValuesAdd are added before fieldValuesRemove are removed.
566 * This method does not call Monorail's API to save this change.
567 * Call saveChanges() to send all updates to Monorail.
568 * @param {Issue} issue Issue to change.
569 * @param {Array<FieldValue>} fieldValuesAdd Array of FieldValues to add.
570 * @param {Array<FieldValue>} fieldValuesRemove Array of FieldValues to remove.
571*/
572function addFieldValueChanges(issue, fieldValuesAdd, fieldValuesRemove) {
573 maybeCreateDelta_(issue);
574 fieldValuesAdd.forEach((fvAdd) => {
575 const iInIssue = fieldValueExists_(fvAdd, issue.fieldValues);
576 if (iInIssue === -1) { // fvAdd is not already in issue, so we can add it.
577 issue.fieldValues.push(fvAdd);
578 issue.delta.fieldValuesAdd.push(fvAdd);
579 const iInDeltaRemove = fieldValueExists_(
580 fvAdd, issue.delta.fieldValuesRemove);
581 if (iInDeltaRemove != -1) {
582 // fvAdd was added to fieldValuesRemove in a previous call.
583 issue.delta.fieldValuesRemove.splice(iInDeltaRemove, 1);
584 }
585 // issue.delta.updateMask is updated in saveChanges()
586 }
587 });
588 // issue.delta.updateMask is updated in saveChanges()
589 fieldValuesRemove.forEach((fvRemove) => {
590 const iInIssue = fieldValueExists_(fvRemove, issue.fieldValues);
591 if (iInIssue != -1) { // fvRemove is in issue, so we can remove it.
592 issue.fieldValues.splice(iInIssue, 1);
593 issue.delta.fieldValuesRemove.push(fvRemove);
594 const iInDeltaAdd = fieldValueExists_(
595 fvRemove, issue.delta.fieldValuesAdd);
596 if (iInDeltaAdd != -1) {
597 // fvRemove was added to fieldValuesAdd in a previous call.
598 issue.delta.fieldValuesAdd.splice(iInDeltaAdd, 1);
599 }
600 }
601 });
602}
603
604/**
605 * Checks for the existence of userName in userValues
606 * @param {string} userName A user resource name to look for.
607 * @param {Array<UserValue>} userValues UserValues to search through.
608 * @return {number} Index of userName's UserValue in userValues or -1 if not
609 * found.
610 */
611function userValueExists_(userName, userValues) {
612 for (let i = 0; i< userValues.length; i++) {
613 if (userValues[i].user === userName) {
614 return i;
615 }
616 }
617 return -1;
618}
619
620/**
621 * Adds the CC changes to the issue.
622 * ccNamesAdd are added before ccNamesRemove are removed.
623 * This method does not call Monorail's API to save this change.
624 * Call saveChanges() to send all updates to Monorail.
625 * @param {Issue} issue Issue to change.
626 * @param {Array<string>} ccNamesAdd Array if user resource names.
627 * @param {Array<string>} ccNamesRemove Array if user resource names.
628*/
629function addCcChanges(issue, ccNamesAdd, ccNamesRemove) {
630 maybeCreateDelta_(issue);
631 ccNamesAdd.forEach((ccName) => {
632 const iInIssue = userValueExists_(ccName, issue.ccUsers);
633 if (iInIssue === -1) { // User is not in issue, so we can add them.
634 issue.ccUsers.push({'user': ccName});
635 issue.delta.ccsAdd.push(ccName);
636 const iInDeltaRemove = issue.delta.ccsRemove.indexOf(ccName);
637 if (iInDeltaRemove != -1) {
638 // ccName was added to ccsRemove in a previous call.
639 issue.delta.ccsRemove.splice(iInDeltaRemove, 1);
640 }
641 }
642 });
643 ccNamesRemove.forEach((ccName) => {
644 const iInIssue = userValueExists_(ccName, issue.ccUsers);
645 if (iInIssue != -1) { // User is in issue, so we can remove it.
646 issue.ccUsers.splice(iInIssue, 1);
647 issue.delta.ccsRemove.push(ccName);
648 const iInDeltaAdd = issue.delta.ccsAdd.indexOf(ccName);
649 if (iInDeltaAdd != -1) {
650 // ccName was added to delta.ccsAdd in a previous all.
651 issue.delta.ccsAdd.splice(iInDeltaAdd, 1);
652 }
653 }
654 });
655}
656
657/**
658 * Set the pending comment of the issue.
659 * @param {Issue} issue Issue whose comment we want to set.
660 * @param {string} comment Comment that we want for the issue.
661 */
662function setComment(issue, comment) {
663 maybeCreateDelta_(issue);
664 issue.delta.comment = comment;
665}
666
667/**
668 * Get the pending comment for the issue.
669 * @param {Issue} issue Issue whose comment we want.
670 * @return {string}
671 */
672function getPendingComment(issue) {
673 if (issue.delta) {
674 return issue.delta.comment;
675 }
676 return '';
677}
678
679/**
680 * Adds to the existing pending comment
681 * @param {Issue} issue Issue to update.
682 * @param {string} comment The comment string to add to the existing one.
683 */
684function appendComment(issue, comment) {
685 maybeCreateDelta_(issue);
686 issue.delta.comment = issue.delta.comment.concat(comment);
687}
688
689/**
690 * Sets up an issue for pending changes.
691 * @param {Issue} issue The issue that needs to be updated.
692 */
693function maybeCreateDelta_(issue) {
694 if (!issue.delta) {
695 issue.delta = newIssueDelta_();
696 if (!issue.components) {
697 issue.components = [];
698 };
699 if (!issue.blockingIssueRefs) {
700 issue.blockingIssueRefs = [];
701 }
702 if (!issue.blockedOnIssueRefs) {
703 issue.blockedOnIssueRefs = [];
704 }
705 if (!issue.ccUsers) {
706 issue.ccUsers = [];
707 }
708 if (!issue.labels) {
709 issue.labels = [];
710 }
711 if (!issue.fieldValues) {
712 issue.fieldValues = [];
713 }
714 }
715}
716
717/**
718 * Creates an IssueDelta
719 * @return {IssueDelta_}
720 */
721function newIssueDelta_() {
722 return new IssueDelta_();
723}
724
725/** Used to track pending changes to an issue.*/
726function IssueDelta_() {
727 /** Array<string> */ this.updateMask = [];
728
729 // User resource names.
730 /** Array<string> */ this.ccsRemove = [];
731 /** Array<string> */ this.ccsAdd = [];
732
733 /** Array<IssueRef> */ this.blockedOnRemove = [];
734 /** Array<IssueRef> */ this.blockedOnAdd = [];
735 /** Array<IssueRef> */ this.blockingRemove = [];
736 /** Array<IssueRef> */ this.blockingAdd = [];
737
738 // Component resource names.
739 /** Array<string> */ this.componentsRemove = [];
740 /** Array<string> */ this.componentsAdd = [];
741
742 // Label values, e.g. 'Security-Notify'.
743 /** Array<string> */ this.labelsRemove = [];
744 /** Array<string> */ this.labelsAdd = [];
745
746 /** Array<FieldValue> */ this.fieldValuesRemove = [];
747 /** Array<FieldValue> */ this.fieldValuesAdd = [];
748
749 this.comment = '';
750}
751
752/**
753 * Calls Monorail's API to update the issue.
754 * @param {Issue} issue The issue to update where issue['delta'] is expected
755 * to exist.
756 * @param {boolean} sendEmail True if the update should trigger email
757 * notifications.
758 * @return {Issue}
759 */
760function saveChanges(issue, sendEmail) {
761 if (!issue.delta) {
762 throw new Error('No pending changes for issue.');
763 }
764
765 const modifyDelta = {
766 'ccsRemove': issue.delta.ccsRemove,
767 'blockedOnIssuesRemove': issue.delta.blockedOnRemove,
768 'blockingIssuesRemove': issue.delta.blockingRemove,
769 'componentsRemove': issue.delta.componentsRemove,
770 'labelsRemove': issue.delta.labelsRemove,
771 'fieldValsRemove': issue.delta.fieldValuesRemove,
772 'issue': {
773 'name': issue.name,
774 'fieldValues': issue.delta.fieldValuesAdd,
775 'blockedOnIssueRefs': issue.delta.blockedOnAdd,
776 'blockingIssueRefs': issue.delta.blockingAdd,
777 'mergedIntoIssueRef': issue.mergedIntoIssueRef,
778 'summary': issue.summary,
779 'status': issue.status,
780 'owner': issue.owner,
781 'labels': [],
782 'ccUsers': [],
783 'components': [],
784 },
785 };
786
787 if (issue.delta.fieldValuesAdd.length > 0) {
788 issue.delta.updateMask.push('fieldValues');
789 }
790
791 if (issue.delta.blockedOnAdd.length > 0) {
792 issue.delta.updateMask.push('blockedOnIssueRefs');
793 }
794
795 if (issue.delta.blockingAdd.length > 0) {
796 issue.delta.updateMask.push('blockingIssueRefs');
797 }
798
799 if (issue.delta.ccsAdd.length > 0) {
800 issue.delta.updateMask.push('ccUsers');
801 }
802 issue.delta.ccsAdd.forEach((userResourceName) => {
803 modifyDelta.issue['ccUsers'].push({'user': userResourceName});
804 });
805
806 if (issue.delta.labelsAdd.length > 0) {
807 issue.delta.updateMask.push('labels');
808 }
809 issue.delta.labelsAdd.forEach((label) => {
810 modifyDelta.issue['labels'].push({'label': label});
811 });
812
813 if (issue.delta.componentsAdd.length > 0) {
814 issue.delta.updateMask.push('components');
815 }
816 issue.delta.componentsAdd.forEach((compResourceName) => {
817 modifyDelta.issue['components'].push({'component': compResourceName});
818 });
819
820 modifyDelta['updateMask'] = issue.delta.updateMask.join();
821
822 const message = {
823 'deltas': [modifyDelta],
824 'notifyType': sendEmail ? 'EMAIL' : 'NO_NOTIFICATION',
825 'commentContent': issue.delta.comment,
826 };
827
828 const url = URL + 'monorail.v3.Issues/ModifyIssues';
829 response = run_(url, message);
830 if (!response.issues) {
831 Logger.log('All changes Noop');
832 return null;
833 }
834 issue = response.issues[0];
835 return issue;
836}
837
838/**
839 * Creates an Issue.
840 * @param {string} projectName: Resource name of the parent project.
841 * @param {string} summary: Summary of the issue.
842 * @param {string} description: Description of the issue.
843 * @param {string} status: Status of the issue, e.g. "Untriaged".
844 * @param {boolean} sendEmail: True if this should trigger email notifications.
845 * @param {string=} ownerName: Resource name of the issue owner.
846 * @param {Array<string>=} ccNames: Resource names of the users to cc.
847 * @param {Array<string>=} labels: Labels to add to the issue,
848 * e.g. "Restict-View-Google".
849 * @param {Array<string>=} componentNames: Resource names of components to add.
850 * @param {Array<FieldValue>=} fieldValues: FieldValues to add to the issue.
851 * @param {Array<IssueRef>=} blockedOnRefs: IssueRefs for blocked on issues.
852 * @param {Array<IssueRef>=} blockingRefs: IssueRefs for blocking issues.
853 * @return {Issue}
854 */
855function makeIssue(
856 projectName, summary, description, status, sendEmail, ownerName, ccNames,
857 labels, componentNames, fieldValues, blockedOnRefs, blockingRefs) {
858 const issue = {
859 'summary': summary,
860 'status': {'status': status},
861 'ccUsers': [],
862 'components': [],
863 'labels': [],
864 };
865
866 if (ownerName) {
867 issue['owner'] = {'user': ownerName};
868 }
869
870 if (ccNames) {
871 ccNames.forEach((ccName) => {
872 issue['ccUsers'].push({'user': ccName});
873 });
874 };
875
876 if (labels) {
877 labels.forEach((label) => {
878 issue['labels'].push({'label': label});
879 });
880 };
881
882 if (componentNames) {
883 componentNames.forEach((componentName) => {
884 issue['components'].push({'component': componentName});
885 });
886 };
887
888 if (fieldValues) {
889 issue['fieldValues'] = fieldValues;
890 };
891
892 if (blockedOnRefs) {
893 issue['blockedOnIssueRefs'] = blockedOnRefs;
894 };
895
896 if (blockingRefs) {
897 issue['blockingIssueRefs'] = blockingRefs;
898 };
899
900 const message = {
901 'parent': projectName,
902 'issue': issue,
903 'description': description,
904 'notifyType': sendEmail ? 'EMAIL': 'NO_NOTIFICATION',
905 };
906 const url = URL + 'monorail.v3.Issues/MakeIssue';
907 return run_(url, message);
908}