blob: 0e06a0dcae43123ac73078e8f886804e913393ed [file] [log] [blame]
/**
* @license
* Copyright 2021 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.
*/
import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
import {PopupPluginApi} from '@gerritcodereview/typescript-api/popup';
import {
AccountInfo,
ChangeInfo,
} from '@gerritcodereview/typescript-api/rest-api';
import {
ActionType,
ChangeActions,
ChangeActionsPluginApi,
} from '@gerritcodereview/typescript-api/change-actions';
import {html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators';
import {
REVIEW_LABEL,
STATUS_SUBMITTED,
getLabelMaxValue,
getTrailer,
isLabelVotePermitted,
} from './common';
let _CUSTOM_RELAND_KEY: string | null = null;
let _NEW_KEY: string | null = null;
let _CURR_CHANGE: ChangeInfo | null = null;
let _RELAND_POPUP: PopupPluginApi | null = null;
/**
* Adds a "Reland" button to the page if appropriate.
*
* @param plugin - the plugin object
* @param change - the object for the current change
*/
export async function maybeInstallReland(
plugin: PluginApi,
change: ChangeInfo
) {
if (change.status !== STATUS_SUBMITTED || change.revert_of) {
return;
}
const changeActions = plugin.changeActions();
if (_CUSTOM_RELAND_KEY !== null) {
changeActions.remove(_CUSTOM_RELAND_KEY);
}
installRelandButton(plugin, change, changeActions);
// Show neither the Revert nor Reland actions to non-committer drive-bys.
const currentUser: AccountInfo = await plugin
.restApi()
.get('/accounts/self/detail');
const reviewLabelMaxValue = getLabelMaxValue(change, REVIEW_LABEL);
if (
currentUser.email !== change.owner.email &&
!isLabelVotePermitted(change, REVIEW_LABEL, `+${reviewLabelMaxValue}`)
) {
changeActions.removePrimaryActionKey(ChangeActions.REVERT);
changeActions.setActionOverflow(
ActionType.CHANGE,
ChangeActions.REVERT,
true
);
if (_CUSTOM_RELAND_KEY) {
changeActions.removePrimaryActionKey(_CUSTOM_RELAND_KEY);
changeActions.setActionOverflow(
ActionType.CHANGE,
_CUSTOM_RELAND_KEY,
true
);
}
}
}
/**
* Adds a "Reland" button to the page.
*
* @param plugin - the plugin object
* @param change - the object for the current change
* @param changeActions - the object that contains requested
* action changes
*/
function installRelandButton(
plugin: PluginApi,
change: ChangeInfo,
changeActions: ChangeActionsPluginApi
) {
_CURR_CHANGE = change;
_NEW_KEY = changeActions.add(ActionType.CHANGE, 'Create Reland');
changeActions.setEnabled(_NEW_KEY, true);
const handler = async function () {
if (_RELAND_POPUP) {
// Make sure the popup is completely closed before we open it.
_RELAND_POPUP.close();
await _RELAND_POPUP.open();
} else {
_RELAND_POPUP = await plugin.popup(RelandPopup.is);
}
};
changeActions.addTapListener(_NEW_KEY, handler);
_CUSTOM_RELAND_KEY = _NEW_KEY;
}
/**
* createReland relands a change with a given notification level.
*
* @param plugin - the plugin object
* @param notify - the notification level
*/
async function createReland(plugin: PluginApi, notify: string) {
if (!_NEW_KEY || !_CURR_CHANGE) {
console.error('New key or current change is not set');
return;
}
if (!_CURR_CHANGE.revisions || !_CURR_CHANGE.current_revision) {
console.error('No revision / current revision found.');
return;
}
const currentRevision = _CURR_CHANGE.revisions[_CURR_CHANGE.current_revision];
if (!currentRevision) {
console.error('No current revision in revisions.');
return;
}
const changeActions = plugin.changeActions();
changeActions.setEnabled(_NEW_KEY, false);
changeActions.setLabel(_NEW_KEY, 'Reopening...');
let orig = '';
if (currentRevision.commit) {
orig = currentRevision.commit.message.trim();
}
const subject = 'Reland "' + String(_CURR_CHANGE.subject) + '"\n\n';
const disclaimer =
`This is a reland of commit ${_CURR_CHANGE.current_revision}` +
"\n\nOriginal change's description:\n";
const quote = orig.replace(/^/gm, '> ').replace(/^> $/gm, '>');
let message = subject + disclaimer + quote;
const bugFooter = getTrailer(orig, 'Bug');
const cqFooter = getTrailer(orig, 'Cq-Include-Trybots');
if (bugFooter || cqFooter) {
message += '\n\n' + bugFooter + cqFooter;
}
const url =
'/changes/' +
String(_CURR_CHANGE._number) +
'/revisions/' +
String(_CURR_CHANGE.revisions[_CURR_CHANGE.current_revision]._number) +
'/cherrypick';
const body = {
message: message.trim(),
destination: _CURR_CHANGE.branch,
notify,
keep_reviewers: true,
};
try {
const response: ChangeInfo = await plugin.restApi().post(url, body);
window.location.pathname = `/c/${response._number}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
changeActions.setLabel(_NEW_KEY, "Can't reland");
changeActions.setEnabled(_NEW_KEY, false);
changeActions.ensureEl().dispatchEvent(
new CustomEvent('show-alert', {
detail: {message: `Cannot reland: ${err.message}`},
composed: true,
bubbles: true,
})
);
throw err;
}
}
@customElement('reland-popup')
class RelandPopup extends LitElement {
// Guaranteed to be provided by the 'popup'.
@property()
plugin!: PluginApi;
override render() {
return html` <gr-dialog
id="relandDialog"
confirm-label="Notify Reviewers"
@confirm=${this.handleConfirm}
cancel-label="Don't Notify Reviewers"
@cancel=${this.handleCancel}
>
<div class="main" slot="main">
Click "Notify Reviewers" if you would like to send this<br />
reland to reviewers, or "Don't Notify reviewers" if not.<br />
</div>
</gr-dialog>`;
}
static get is() {
return 'reland-popup';
}
private handleConfirm() {
this.createReland('ALL');
if (_RELAND_POPUP) {
_RELAND_POPUP.close();
}
}
private handleCancel() {
this.createReland('OWNER');
if (_RELAND_POPUP) {
_RELAND_POPUP.close();
}
}
private async createReland(notify: string) {
try {
await createReland(this.plugin, notify);
} catch (err) {
console.error(err);
}
}
}