| <dom-module id="common"> |
| <script> |
| // Common constants and helper functions used by multiple behaviors below. |
| let PLUGIN = null; |
| const AUTOSUBMIT_LABEL = 'Auto-Submit'; |
| const AS_LABEL_MAX_VALUE = 1; |
| const REVIEW_LABEL = 'Code-Review'; |
| const REVIEW_LABEL_MAX_VALUE = 1; |
| const CQ_LABEL = 'Commit-Queue'; |
| const CQ_LABEL_MAX_VALUE = 2; |
| const STATUS_SUBMITTED = 'MERGED'; |
| |
| // Constants used by the AutoSubmit behaviors below. |
| let AUTOSUBMIT_POPUP = null; |
| let CUSTOM_QUICK_APPROVE_KEY = null; |
| // Below are set in installAutoSubmit function. |
| let _APPROVED_BY_ALL_OTHER_REVIEWERS = false; |
| let _REVIEW_LABEL_MAX_VALUE = 0; |
| let _CQ_LABEL_MAX_VALUE = 0; |
| |
| /** |
| * Calls the server to get an updated change obj and returns its promise. |
| */ |
| const getChangeObj = function() { |
| const changeNum = PLUGIN.changeActions()._el.changeNum; |
| var url = '/changes/' + changeNum + '/detail'; |
| return PLUGIN.restApi().get(url); |
| }; |
| |
| /** |
| * Calls the server to get list of of all draft comments that below to the |
| * calling user. |
| */ |
| const getChangeDrafts = function() { |
| const changeNum = PLUGIN.changeActions()._el.changeNum; |
| var url = '/changes/' + changeNum + '/drafts'; |
| return PLUGIN.restApi().get(url); |
| }; |
| |
| /** |
| * Returns the specified label from the change if it exists else [] is |
| * returned. |
| * @param {object} change gr-change-view |
| * @param {string} labelName: The name of the label we want |
| * @returns {object} gr-change-view.label |
| */ |
| const getChangeLabel = function(change, labelName) { |
| return change.labels[labelName] || []; |
| }; |
| |
| /** |
| * Returns whether the specified vote is permitted on the label. |
| * @param {object} change gr-change-view |
| * @param {string} labelName: The name of the label we want to check |
| * @param {string} vote: The vote we will check (e.g. '+2') |
| * @returns {boolean} |
| */ |
| const isLabelVotePermitted = function(change, labelName, vote) { |
| const permitted = change.permitted_labels[labelName] || []; |
| return permitted.indexOf(vote) !== -1; |
| }; |
| |
| /** |
| * Checks whether the specified label and max value exist on the change. |
| * @param {object} change gr-change-view |
| * @param {string} labelName: The name of the label we want to check |
| * @param {int} maxValue: The max value of the label we want to confirm. |
| * @returns {boolean} |
| */ |
| const checkLabelAndMaxValue = function(change, labelName, maxValue) { |
| if (!change.labels[labelName] || !change.labels[labelName].values) { |
| return false; |
| } |
| return maxValue === getLabelMaxValue(change, labelName); |
| }; |
| |
| /** |
| * Returns the max value of the specified label. Returns 0 if the label |
| * cannot be found or if it has no values. |
| * @param {object} change gr-change-view |
| * @param {string} labelName: The name of the label we want to check |
| * @returns {boolean} |
| */ |
| const getLabelMaxValue = function(change, labelName) { |
| if (!change.labels[labelName] || !change.labels[labelName].values) { |
| return 0; |
| } |
| return Math.max(...Object.keys(change.labels[labelName].values)); |
| }; |
| |
| /** |
| * Sets CR+1 and optionally CQ+2 on the current change. |
| * @param {bool} sendToCQ: Whether the change should also be sent to the CQ. |
| * @returns {promise} Promise from the POST call. |
| */ |
| const setApprovalLabels = function(sendToCQ) { |
| // TODO(rmistry): Add getChangeNum to GrChangeActionsInterface? |
| const changeNum = PLUGIN.changeActions()._el.changeNum; |
| var url = '/changes/' + changeNum + '/revisions/current/review'; |
| var body = {labels: {}}; |
| body['labels'][REVIEW_LABEL] = '+' + _REVIEW_LABEL_MAX_VALUE; |
| body['drafts'] = 'PUBLISH'; |
| if (sendToCQ) { |
| body['labels'][CQ_LABEL] = '+' + _CQ_LABEL_MAX_VALUE; |
| } |
| let changeActions = PLUGIN.changeActions(); |
| return PLUGIN.restApi().post(url, body).then(function() { |
| // TODO(agable): Use real plugin API to reload change when |
| // available. See |
| // https://bugs.chromium.org/p/gerrit/issues/detail?id=6689 |
| changeActions._el.dispatchEvent(new CustomEvent( |
| 'reload-change', |
| {detail: {action: 'reload-change'}, bubbles: false})); |
| }.bind(this)).catch(function(err) { |
| changeActions._el.dispatchEvent(new CustomEvent('show-alert', |
| {detail: {message: err}, bubbles: true})); |
| throw err; |
| }); |
| }; |
| |
| /** |
| * Parses git trailers (and Rietveld tags) from a commit description. |
| * @param {string} message: the commit message to parse |
| * @param {string} token: the LHS of the trailer to parse (e.g. 'Bug') |
| * @returns {string} all found trailers in normalized form (e.g. 'Bug: 1') |
| */ |
| const getTrailer = function(message, token) { |
| // TODO(agable): Remove support for parsing CAPS= style tags. |
| var caps = token.replace(/-/g, '_').toUpperCase(); |
| var trailerRegexp = new RegExp( |
| `^[ \t]*((${token}:|${caps}=)[ \t]*(.*))$`, 'gm'); |
| var collection = new Set(); |
| var collect = function(match, _wholeGroup, _keyGroup, valueGroup) { |
| valueGroup.split(/\s*,+\s*/).forEach(function (value) { |
| if (value) { |
| collection.add(value); |
| } |
| }); |
| }; |
| message.replace(trailerRegexp, collect); |
| if (collection.size > 0) { |
| return token + ': ' + Array.from(collection).join(', ') + '\n'; |
| } |
| return ''; |
| }; |
| </script> |
| </dom-module> |
| |
| |
| <dom-module id="autosubmit-popup"> |
| <template> |
| <gr-dialog |
| id="autosubmitDialog" |
| confirm-label="Trigger Autosubmit" |
| cancel-label="Approve without Autosubmit" |
| on-cancel="_handleCancel" |
| on-confirm="_handleConfirm"> |
| <div class="header" slot="header"> |
| This change's owner has enabled Auto-Submit |
| </div> |
| <div class="main" slot="main"> |
| Click "Trigger Autosubmit" if you would like to send this<br/> |
| change to the CQ, or "Approve without Autosubmit" if not.<br/> |
| <template is="dom-if" if="[[!_isApprovedByAllOtherReviewers()]]"> |
| <br/> |
| <b>Note:</b> Not all reviewers have approved this change! |
| </template> |
| <template is="dom-if" if="[[unresolvedCommentsOrDrafts]]"> |
| <br/> |
| <b>Note:</b> This change has unresolved comments and/or drafts! |
| </template> |
| </div> |
| </gr-dialog> |
| </template> |
| <script> |
| Polymer({ |
| is: 'autosubmit-popup', |
| properties: { |
| unresolvedCommentsOrDrafts: Boolean, |
| }, |
| ready() { |
| this._setUnresolvedCommentsOrDrafts(); |
| }, |
| _handleConfirm() { |
| this._setApprovalLabels(true); |
| AUTOSUBMIT_POPUP.close(); |
| }, |
| _handleCancel() { |
| this._setApprovalLabels(false); |
| AUTOSUBMIT_POPUP.close(); |
| }, |
| _setApprovalLabels(sendToCQ) { |
| setApprovalLabels(sendToCQ).catch(function(err) { |
| // Re-enable the quick approve button if there was an error. |
| PLUGIN.changeActions().setEnabled(CUSTOM_QUICK_APPROVE_KEY, true); |
| }.bind(this)); |
| }, |
| _isApprovedByAllOtherReviewers() { |
| return _APPROVED_BY_ALL_OTHER_REVIEWERS; |
| }, |
| _setUnresolvedCommentsOrDrafts() { |
| getChangeObj().then(function(change) { |
| if (change.unresolved_comment_count !== 0) { |
| this.unresolvedCommentsOrDrafts = true; |
| return; |
| } |
| getChangeDrafts().then(function(drafts) { |
| this.unresolvedCommentsOrDrafts = Object.keys(drafts).length !== 0; |
| }.bind(this)); |
| }.bind(this)); |
| }, |
| }); |
| </script> |
| </dom-module> |
| |
| |
| <dom-module id="autosubmit"> |
| <script> |
| /** |
| * There are two main flows that need to handle the AS+1 vote: |
| * 1. CR+1 in reply dialog should automatically set CQ+2 label without |
| * submitting. An info msg should be displayed to the user in the dialog. |
| * 2. CR+1 button on the main page should display a popup asking if CQ+2 |
| * should be set since AS+1 is set. |
| * |
| * This function adds both behaviors. See crbug.com/631551 for more context. |
| */ |
| const installAutoSubmit = function(change) { |
| if (!change || !change.permitted_labels) { |
| // Could happen if the user is not logged in. |
| return; |
| } |
| if (change.status === STATUS_SUBMITTED) { |
| // Do not need autosubmit behavior for already submitted changes. |
| return; |
| } |
| |
| // Save the change's CR and CQ label max values for easy access from |
| // different functions. |
| _REVIEW_LABEL_MAX_VALUE = getLabelMaxValue(change, REVIEW_LABEL) |
| _CQ_LABEL_MAX_VALUE = getLabelMaxValue(change, CQ_LABEL) |
| |
| if (_REVIEW_LABEL_MAX_VALUE === 0 || _CQ_LABEL_MAX_VALUE === 0 || |
| !checkLabelAndMaxValue(change, AUTOSUBMIT_LABEL, |
| AS_LABEL_MAX_VALUE)) { |
| // Exit early if project does not have the expected labels with expected |
| // max values. |
| return; |
| } |
| |
| if (!isLabelVotePermitted(change, CQ_LABEL, '+' + _CQ_LABEL_MAX_VALUE) || |
| !isLabelVotePermitted(change, REVIEW_LABEL, |
| '+' + _REVIEW_LABEL_MAX_VALUE)) { |
| // Exit early if user does not have necessary permissions to CR+1 |
| // and CQ+2. |
| return; |
| } |
| |
| // Check to see if we are still waiting for any other reviewers to |
| // approve. |
| checkIfApprovedByAllOtherReviewers(change); |
| |
| // Watch for any label value changes to see if CQ+2 should be |
| // automatically added. |
| const replyApi = PLUGIN.changeReply(); |
| replyApi.addLabelValuesChangedCallback( |
| labelValuesCallback(replyApi, change)) |
| |
| // Replace the CR+1 button with a custom handler on the main page. |
| replaceQuickApproveButton(change, replyApi); |
| }; |
| |
| /** |
| * Checks to see if any reviewer, other than the current user and the |
| * change owner, has not approved the change yet. Updates the |
| * _APPROVED_BY_ALL_OTHER_REVIEWERS global var with the result. |
| */ |
| const checkIfApprovedByAllOtherReviewers = function(change) { |
| if (change.reviewers && change.reviewers.REVIEWER) { |
| // Gather account_emails of all approvers. |
| const approver_emails = []; |
| for (let i=0; i<change.labels[REVIEW_LABEL].all.length; i++) { |
| if (change.labels[REVIEW_LABEL].all[i].value === 1) { |
| approver_emails.push(change.labels[REVIEW_LABEL].all[i].email); |
| } |
| } |
| // Get the account_email of the current user and use that to determine |
| // if any other reviewer has not approved the change yet. |
| PLUGIN.restApi().get('/accounts/self/detail').then(function(resp) { |
| let approvedByAllOtherReviewers = true; |
| const current_user_email = resp.email; |
| // Check to see if any reviewer, other than the current user and the |
| // change owner, has not approved the change yet. |
| for (let i=0; i<change.reviewers.REVIEWER.length; i++) { |
| const reviewer_email = change.reviewers.REVIEWER[i].email; |
| if (reviewer_email != current_user_email && |
| reviewer_email != change.owner.email && |
| !approver_emails.includes(reviewer_email)) { |
| approvedByAllOtherReviewers = false; |
| break; |
| } |
| } |
| _APPROVED_BY_ALL_OTHER_REVIEWERS = approvedByAllOtherReviewers; |
| }.bind(this)).catch(function(err) { |
| let changeActions = PLUGIN.changeActions(); |
| changeActions._el.dispatchEvent(new CustomEvent('show-alert', |
| {detail: {message: err}, bubbles: true})); |
| throw err; |
| }); |
| } |
| }; |
| |
| /** |
| * This is the callback used when label values change. Labels can change |
| * from either the reply dialog box or by 'x'ing labels on the main page. |
| * There is no page reload when labels change. |
| * |
| * Algorithm used by the callback: |
| * |
| * ownerDeselectedAutoSubmit = false |
| * |
| * If label name == Code-Review: |
| * Re-evaluate the quick approve button |
| * If CR+1 and AS+1 and not ownerDeselectedAutoSubmit: |
| * Set CQ+2 |
| * Inform user that CQ+2 was automatically set |
| * If label name == Auto-Submit: |
| * Set ownerDeselectedAutoSubmit value |
| * If CR+1 and not ownerDeselectedAutoSubmit: |
| * Set CQ+2 |
| * Inform user that CQ+2 was automatically set |
| */ |
| const labelValuesCallback = function(replyApi, change) { |
| let ownerDeselectedAutoSubmit = false; |
| return ({name, value}) => { |
| const reviewApproved = replyApi.getLabelValue(REVIEW_LABEL) === |
| '+' + _REVIEW_LABEL_MAX_VALUE; |
| const autoSubmitApproved = isAutoSubmitApproved(change, replyApi); |
| replyApi.showMessage(''); |
| |
| if (name === REVIEW_LABEL) { |
| // Always re-evaluate the quick approve button when Code-Review label |
| // changes. Get the latest change object from the server to be sure we |
| // are making the right decisions when displaying the quick approve |
| // button (see crbug.com/820117). |
| getChangeObj().then(function(refreshedChangeObj) {; |
| replaceQuickApproveButton(refreshedChangeObj, replyApi); |
| // If CR+1 and AS+1 then set CQ+2 if there are no unresolved |
| // comments and/or drafts. |
| if (value === '+' + _REVIEW_LABEL_MAX_VALUE && autoSubmitApproved && |
| !ownerDeselectedAutoSubmit) { |
| const unresolved_msg = 'This change has unresolved comments ' + |
| 'and/or drafts. Not triggering ' + |
| 'AutoSubmit.'; |
| if (refreshedChangeObj.unresolved_comment_count !== 0) { |
| replyApi.showMessage(unresolved_msg); |
| } else { |
| getChangeDrafts().then(function(drafts) { |
| if (Object.keys(drafts).length === 0) { |
| replyApi.setLabelValue(CQ_LABEL, '+' + _CQ_LABEL_MAX_VALUE); |
| replyApi.showMessage(getSendToCQReplyDialogMsg()); |
| } else { |
| replyApi.showMessage(unresolved_msg); |
| } |
| }); |
| } |
| } |
| }); |
| } else if (name === AUTOSUBMIT_LABEL) { |
| // The change owner has changed the vote of the Auto-Submit label. |
| ownerDeselectedAutoSubmit = (value !== '+' + AS_LABEL_MAX_VALUE); |
| if (!ownerDeselectedAutoSubmit && reviewApproved) { |
| // If AS+1 and CR+1 then set CQ+2. |
| replyApi.setLabelValue(CQ_LABEL, '+' + _CQ_LABEL_MAX_VALUE); |
| replyApi.showMessage(getSendToCQReplyDialogMsg()); |
| } |
| } |
| }; |
| }; |
| |
| /** |
| * Returns the autosubmit msg that should be displayed in the reply dialog. |
| */ |
| const getSendToCQReplyDialogMsg = function() { |
| let sendToCQMsg = 'This change\'s owner has enabled Auto-Submit. ' + |
| 'Your approval will also set CQ+' + |
| _CQ_LABEL_MAX_VALUE + |
| '. Unselect that if you don\'t want to trigger it.' |
| if (!_APPROVED_BY_ALL_OTHER_REVIEWERS) { |
| sendToCQMsg += '\nNote: Not all reviewers have approved this change!' |
| } |
| return sendToCQMsg; |
| }; |
| |
| /** |
| * Looks at both the replyApi and the change object to determine if |
| * AS+1 is set. This checks the replyApi for the case where the owner sets |
| * their own AS+1 and CR+1 at the same time. In that case we want to also |
| * go ahead and set CQ+1. |
| */ |
| const isAutoSubmitApproved = function(change, replyApi) { |
| return (replyApi.getLabelValue(AUTOSUBMIT_LABEL) === |
| '+' + AS_LABEL_MAX_VALUE || |
| getChangeLabel(change, AUTOSUBMIT_LABEL).approved) |
| }; |
| |
| /** |
| * This function replaces the quick approve button with a new button with |
| * a custom handler that prompts if AS+1 is set. |
| */ |
| const replaceQuickApproveButton = function(change, replyApi) { |
| let changeActions = PLUGIN.changeActions(); |
| |
| // Make sure the old quick approve button is hidden. |
| changeActions.hideQuickApproveAction(); |
| |
| PLUGIN.restApi().get('/accounts/self/detail').then(function(resp) { |
| // See if the current user has approved the change. |
| let approved_by_current_user = false; |
| for (let i=0; i<change.labels[REVIEW_LABEL].all.length; i++) { |
| if (change.labels[REVIEW_LABEL].all[i].value === 1) { |
| if (resp._account_id == |
| change.labels[REVIEW_LABEL].all[i]._account_id) { |
| approved_by_current_user = true; |
| break; |
| } |
| } |
| } |
| |
| if (getChangeLabel(change, REVIEW_LABEL).approved && |
| approved_by_current_user) { |
| if (CUSTOM_QUICK_APPROVE_KEY !== null) { |
| // The custom button is visible but should be now removed because |
| // the user set CR+1. |
| changeActions.remove(CUSTOM_QUICK_APPROVE_KEY); |
| changeActions.removeTapListener(CUSTOM_QUICK_APPROVE_KEY); |
| CUSTOM_QUICK_APPROVE_KEY = null; |
| } |
| // Do not create a new button because this user already set CR+1. |
| return; |
| } else if (CUSTOM_QUICK_APPROVE_KEY !== null) { |
| // There is already a button no need to create a new one. |
| return; |
| } |
| |
| // Create the custom quick approve button. |
| const key = changeActions.add(changeActions.ActionType.CHANGE, 'Code-Review+1'); |
| changeActions.setEnabled(key, true); |
| changeActions.addTapListener( |
| key, quickApproveHandler(changeActions, replyApi, change, key)); |
| changeActions.setActionPriority( |
| changeActions.ActionType.CHANGE, key, -3); |
| CUSTOM_QUICK_APPROVE_KEY = key; |
| }.bind(this)).catch(function(err) { |
| let changeActions = PLUGIN.changeActions(); |
| changeActions._el.dispatchEvent(new CustomEvent('show-alert', |
| {detail: {message: err}, bubbles: true})); |
| throw err; |
| }); |
| }; |
| |
| const quickApproveHandler = function(changeActions, replyApi, change, key) { |
| return () => { |
| // Disable the auto approve button. |
| changeActions.setEnabled(key, false); |
| if (!isAutoSubmitApproved(change, replyApi) || |
| getChangeLabel(change, CQ_LABEL).approved) { |
| // Do not automatically set CQ+2 if AS+1 is not set or if CQ+2 |
| // is already set. |
| setApprovalLabels(false).catch(function(err) { |
| // Re-enable the quick approve button if there was an error. |
| changeActions.setEnabled(key, true); |
| }.bind(this)); |
| } else { |
| if (AUTOSUBMIT_POPUP) { |
| AUTOSUBMIT_POPUP.open(); |
| } else { |
| PLUGIN.popup('autosubmit-popup').then(function(pluginModule) { |
| AUTOSUBMIT_POPUP = pluginModule; |
| }); |
| } |
| } |
| }; |
| }; |
| </script> |
| </dom-module> |
| |
| |
| <dom-module id="lgtm-catcher"> |
| <script> |
| /** |
| * Watches for the reviewer to type "LGTM" (not case sensitive) at the |
| * beginning of their review message, and automatically sets the |
| * Code-Review label to +1 to save the reviewer a click. Doesn't change |
| * the value if the user has manually manipulated it already, or if the |
| * string "LGTM" appears anywhere else. |
| */ |
| const installLgtmWatcher = function() { |
| const replyApi = PLUGIN.changeReply(); |
| let autoAdded = false; |
| replyApi.addReplyTextChangedCallback(text => { |
| const labelValue = replyApi.getLabelValue(REVIEW_LABEL); |
| if (!labelValue) { |
| return; |
| } |
| if (labelValue === ' 0' && autoAdded === false && |
| text.trim().toLowerCase().indexOf('lgtm') === 0) { |
| autoAdded = true; |
| replyApi.setLabelValue(REVIEW_LABEL, '+1'); |
| } else if (labelValue === '+1' && autoAdded === true && |
| text.trim().toLowerCase().indexOf('lgtm') !== 0) { |
| autoAdded = false; |
| replyApi.setLabelValue(REVIEW_LABEL, ' 0'); |
| } |
| }); |
| }; |
| </script> |
| </dom-module> |
| |
| |
| <dom-module id="reland-button"> |
| <script> |
| let _CUSTOM_RELAND_KEY = null; |
| |
| function maybeInstallReland(change) { |
| if (change.status !== STATUS_SUBMITTED) { return; } |
| |
| let changeActions = PLUGIN.changeActions(); |
| |
| if (_CUSTOM_RELAND_KEY !== null) { |
| changeActions.remove(_CUSTOM_RELAND_KEY); |
| } |
| |
| // Show neither the Revert nor Reland actions to non-committer drive-bys. |
| PLUGIN.restApi().get('/accounts/self/detail').then(currentUser => { |
| if (currentUser.email !== change.owner.email && |
| !isLabelVotePermitted(change, REVIEW_LABEL, |
| '+' + REVIEW_LABEL_MAX_VALUE)) { |
| changeActions.setActionHidden(changeActions.ActionType.CHANGE, |
| changeActions.ChangeActions.REVERT, |
| true); |
| } else { |
| installRelandButton(change, changeActions); |
| } |
| }); |
| } |
| |
| /** |
| * Adds a "Reland" button to the page, if appropriate. |
| * @param {ChangeInfo} change: the object for the current change |
| */ |
| const installRelandButton = function(change, changeActions) { |
| let newKey = changeActions.add(changeActions.ActionType.CHANGE, 'Reland'); |
| changeActions.setEnabled(newKey, true); |
| let handler = function() { |
| changeActions.setEnabled(newKey, false); |
| changeActions.setLabel(newKey, 'Reopening...'); |
| let orig = ( |
| change.revisions[change.current_revision].commit.message.trim()); |
| let subject = 'Reland "' + change.subject + '"\n\n'; |
| let disclaimer = ( |
| 'This is a reland of ' + change.current_revision + '\n\n' + |
| 'Original change\'s description:\n'); |
| let quote = orig.replace(/^/gm, '> '); |
| let message = subject + disclaimer + quote; |
| let bugFooter = getTrailer(orig, 'Bug'); |
| let cqFooter = getTrailer(orig, 'Cq-Include-Trybots'); |
| if (bugFooter || cqFooter) { |
| message += '\n\n' + bugFooter + cqFooter; |
| } |
| let url = ( |
| '/changes/' + change._number + '/revisions/' + |
| change.revisions[change.current_revision]._number + |
| '/cherrypick'); |
| let body = { |
| message: message.trim(), |
| destination: change.branch, |
| keep_reviewers: true, |
| }; |
| return PLUGIN.restApi().post(url, body).then(function(response) { |
| window.location.pathname = '/c/' + response._number; |
| }).catch(function(err) { |
| changeActions.setLabel(newKey, 'Can\'t reland'); |
| changeActions.setEnabled(newKey, false); |
| changeActions._el.dispatchEvent(new CustomEvent('show-alert', |
| {detail: {message: 'Cannot reland: ' + err}, bubbles: true})); |
| throw err; |
| }); |
| }; |
| changeActions.addTapListener(newKey, handler); |
| _CUSTOM_RELAND_KEY = newKey; |
| }; |
| </script> |
| </dom-module> |
| |
| |
| <dom-module id="revert-munger"> |
| <script> |
| const _CQ_CHECKBOX_ID = 'cq-checkbox'; |
| |
| /** |
| * Called when the user clicks the "Revert" button to pull up the dialog. |
| * @param {ChangeInfo} change: the object for the current change |
| * @param {string} revertMsg: the default message Gerrit would use |
| * @param {string} originalMsg: the commit message of the original change |
| * @returns {string} used to populate the Revert dialog box |
| */ |
| const interceptRevertMessage = function(change, revertMsg, originalMsg) { |
| if (!change.labels[CQ_LABEL]) { return revertMsg; } |
| |
| // Add CQ checkbox if it does not already exist. |
| if (document.getElementById(_CQ_CHECKBOX_ID) == null) { |
| let newSpan = document.createElement('span'); |
| newSpan.classList.add(Gerrit.css('margin: 8px')); |
| newSpan.textContent = 'Automatically send revert change to CQ:'; |
| |
| let cqCheck = document.createElement('input'); |
| cqCheck.type = 'checkbox'; |
| cqCheck.id = _CQ_CHECKBOX_ID; |
| cqCheck.checked = true; |
| cqCheck.classList.add(Gerrit.css('margin-left: 8px')); |
| newSpan.appendChild(cqCheck); |
| |
| let footer = document.querySelector('#confirmRevertDialog footer'); |
| footer.parentNode.insertBefore(newSpan, footer); |
| } |
| |
| // Add email-style quoting ('> ') in front of the original commit text. |
| let originalCommitText = originalMsg.trim().replace(/^/gm, '> '); |
| |
| // Start constructing the revert change description. |
| |
| // Show reverts-of-reverts as relands instead. |
| revertMsg = revertMsg.replace( |
| /^Revert "Revert "(.*)""/, 'Reland "$1"'); |
| |
| let newDescription = |
| revertMsg + '\n' + |
| 'Original change\'s description:\n' + originalCommitText + '\n\n'; |
| |
| // Construct new Bug: trailer. |
| let bugLine = getTrailer(originalMsg, 'Bug'); |
| bugLine += getTrailer(originalMsg, 'Issue'); |
| |
| // CrOS projects need only the bug line carried over. |
| if (change.project.indexOf('chromiumos/') === 0 || |
| change.project.indexOf('chromeos/') === 0) { |
| newDescription += bugLine; |
| return newDescription.trim(); |
| } |
| |
| // Construct new TBR tag. |
| let reviewers = []; |
| if (change.reviewers.REVIEWER) { |
| reviewers = change.reviewers.REVIEWER.map(function(reviewer) { |
| return reviewer.email; |
| }); |
| } |
| reviewers = reviewers.filter(function(email) { |
| // Do not add Commit bots to TBR. See crbug.com/642739 |
| return email && !email.endsWith('commit-bot@chromium.org'); |
| }); |
| reviewers.push(change.owner.email); |
| reviewers = reviewers.filter(function(item, pos, self) { |
| return self.indexOf(item) === pos; |
| }); |
| let tbrLine = 'TBR=' + reviewers.join(',') + '\n\n'; |
| newDescription += tbrLine; |
| |
| // Carry over original trybots trailer. |
| let includeLine = getTrailer(originalMsg, 'Cq-Include-Trybots'); |
| |
| // Construct new CQ control trailers, but only include them if the |
| // reverted change is recent enough. |
| let cqSkip = 'No-Presubmit: true\nNo-Tree-Checks: true\nNo-Try: true\n'; |
| let submittedDate = new Date(change.submitted + ' UTC'); |
| let diffSeconds = (Date.now() - submittedDate) / 1000; |
| if (diffSeconds > 24 * 60 * 60) { |
| alert('Note: The CL will not be automatically sent to the CQ and ' + |
| 'the CQ checks will not be skipped because this CL landed ' + |
| 'more than 1 day ago.'); |
| document.getElementById(_CQ_CHECKBOX_ID).checked = false; |
| newDescription += '# Not skipping CQ checks because original CL ' + |
| 'landed > 1 day ago.\n\n'; |
| } else { |
| newDescription += cqSkip; |
| } |
| |
| // Add previously-constructed trailers. |
| newDescription += bugLine; |
| newDescription += includeLine; |
| |
| return newDescription.trim(); |
| }; |
| |
| /** |
| * Called when the Revert action has been completed. |
| * @param {ChangeInfo} change: the object representing the new revert |
| * @returns {LabelInfo} the labels to set on the new revert |
| */ |
| const approveAndSubmitRevert = function(change) { |
| let labels = {}; |
| // Auto-approve reverts. |
| labels[REVIEW_LABEL] = REVIEW_LABEL_MAX_VALUE; |
| // Immediately send to CQ only if _CQ_CHECKBOX_ID is checked. |
| if (document.getElementById(_CQ_CHECKBOX_ID).checked) { |
| labels[CQ_LABEL] = CQ_LABEL_MAX_VALUE; |
| } |
| return labels; |
| }; |
| </script> |
| </dom-module> |
| |
| |
| <dom-module id="tbr-warner"> |
| <script> |
| const _TBR_REGEX = /^\s*(TBR=|Tbr:).*$/mg; |
| |
| /** |
| * Warns the user that just adding TBR= to a commit message isn't enough. |
| * @param {ChangeInfo} change: the object for the current change |
| * @param {string} message: the newly-set commit message |
| */ |
| const warnAboutTbr = function(change, message) { |
| let match = _TBR_REGEX.exec(message); |
| if (match && change.labels[REVIEW_LABEL] && |
| change.labels[REVIEW_LABEL].approved == null) { |
| alert('You have added a TBR line. You must also approve ' + |
| 'this change for it to be submittable.'); |
| } |
| }; |
| </script> |
| </dom-module> |
| |
| |
| <dom-module id="install"> |
| <script> |
| // Install all of the pieces of functionality defined above. |
| // We do just one Gerrit.install because the plugin API expects |
| // one install per plugin; if you have multiple installs in a |
| // single plugin, the first showchange event might be fired before |
| // they're all installed. |
| Gerrit.install(plugin => { |
| PLUGIN = plugin; |
| PLUGIN.on('showchange', installLgtmWatcher); |
| PLUGIN.on('showchange', installAutoSubmit); |
| PLUGIN.on('showchange', maybeInstallReland); |
| PLUGIN.on('commitmsgedit', warnAboutTbr); |
| PLUGIN.on('revert', interceptRevertMessage); |
| PLUGIN.on('postrevert', approveAndSubmitRevert); |
| }); |
| </script> |
| </dom-module> |