blob: 64d85c4c6d2f055e1e0bbe81b17b520f21f89206 [file] [log] [blame]
<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 CQ_LABEL = 'Commit-Queue';
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);
};
/**
* 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 &&
change.labels[REVIEW_LABEL].all) {
// 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 ===
_REVIEW_LABEL_MAX_VALUE) {
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);
// 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) {
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;
if (change.labels[REVIEW_LABEL].all) {
for (let i=0; i<change.labels[REVIEW_LABEL].all.length; i++) {
if (change.labels[REVIEW_LABEL].all[i].value ===
_REVIEW_LABEL_MAX_VALUE) {
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, 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, key) {
return () => {
getChangeObj().then(function(refreshedChangeObj) {
// Disable the auto approve button.
changeActions.setEnabled(key, false);
if (!getChangeLabel(refreshedChangeObj, AUTOSUBMIT_LABEL).approved ||
getChangeLabel(refreshedChangeObj, 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) {
// Make sure the popup is completely closed before we open it.
AUTOSUBMIT_POPUP.close();
AUTOSUBMIT_POPUP.open();
} else {
PLUGIN.popup('autosubmit-popup').then(function(pluginModule) {
AUTOSUBMIT_POPUP = pluginModule;
});
}
// Re-enable the key in case the user clicks outside the popup.
changeActions.setEnabled(key, true);
}
});
};
};
</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 => {
const reviewLabelMaxValue = getLabelMaxValue(change, REVIEW_LABEL);
if (currentUser.email !== change.owner.email &&
!isLabelVotePermitted(change, REVIEW_LABEL,
'+' + reviewLabelMaxValue)) {
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] = getLabelMaxValue(change, REVIEW_LABEL);
// Immediately send to CQ only if _CQ_CHECKBOX_ID is checked.
if (document.getElementById(_CQ_CHECKBOX_ID).checked) {
labels[CQ_LABEL] = getLabelMaxValue(change, CQ_LABEL);
}
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>