Merge "Add BEFORE_CHERRY_PICK event"
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index 471e99c..a248db6 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -47,6 +47,7 @@
   BEFORE_PUBLISH_EDIT = 'before-publish-edit',
   PUBLISH_EDIT = 'publish-edit',
   BEFORE_REBASE = 'before-rebase',
+  BEFORE_CHERRY_PICK = 'before-cherry-pick',
 }
 
 export declare interface PluginApi {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index edb2e2f..5b96c09 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1722,18 +1722,27 @@
   }
 
   // private but used in test
-  handleCherrypickConfirm() {
-    this.handleCherryPickRestApi(false);
+  async handleCherrypickConfirm() {
+    await this.handleCherryPickRestApi(false);
   }
 
   // private but used in test
-  handleCherrypickConflictConfirm() {
-    this.handleCherryPickRestApi(true);
+  async handleCherrypickConflictConfirm() {
+    await this.handleCherryPickRestApi(true);
   }
 
-  private handleCherryPickRestApi(conflicts: boolean) {
+  private async handleCherryPickRestApi(conflicts: boolean) {
     assertIsDefined(this.confirmCherrypick, 'confirmCherrypick');
     assertIsDefined(this.actionsModal, 'actionsModal');
+
+    if (
+      !(await this.getPluginLoader().jsApiService.handleBeforeCherryPick(
+        this.change as ChangeInfo
+      ))
+    ) {
+      return;
+    }
+
     const el = this.confirmCherrypick;
     if (!el.branch) {
       fireAlert(this, ERR_BRANCH_EMPTY);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index c522e81..5544e15 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -1146,14 +1146,14 @@
           title: 'Cherry pick change to a different branch',
         };
 
-        element.handleCherrypickConfirm();
+        await element.handleCherrypickConfirm();
         assert.equal(fireActionStub.callCount, 0);
 
         queryAndAssert<GrConfirmCherrypickDialog>(
           element,
           '#confirmCherrypick'
         ).branch = 'master' as BranchName;
-        element.handleCherrypickConfirm();
+        await element.handleCherrypickConfirm();
         assert.equal(fireActionStub.callCount, 0); // Still needs a message.
 
         // Add attributes that are used to determine the message.
@@ -1171,7 +1171,7 @@
         ).commitNum = '123' as CommitId;
         await element.updateComplete;
 
-        element.handleCherrypickConfirm();
+        await element.handleCherrypickConfirm();
         await element.updateComplete;
 
         const autogrowEl = queryAndAssert<GrAutogrowTextarea>(
@@ -1229,7 +1229,7 @@
         ).commitNum = '123' as CommitId;
         await element.updateComplete;
 
-        element.handleCherrypickConflictConfirm();
+        await element.handleCherrypickConflictConfirm();
         await element.updateComplete;
 
         assert.deepEqual(fireActionStub.lastCall.args, [
@@ -1246,6 +1246,30 @@
         ]);
       });
 
+      test('handleBeforeCherryPick blocks action', async () => {
+        const handleBeforeCherryPickStub = sinon
+          .stub(
+            testResolver(pluginLoaderToken).jsApiService,
+            'handleBeforeCherryPick'
+          )
+          .returns(Promise.resolve(false));
+
+        element.handleCherrypickTap();
+        queryAndAssert<GrConfirmCherrypickDialog>(
+          element,
+          '#confirmCherrypick'
+        ).branch = 'master' as BranchName;
+        queryAndAssert<GrConfirmCherrypickDialog>(
+          element,
+          '#confirmCherrypick'
+        ).commitMessage = 'foo message';
+        await element.updateComplete;
+
+        await element.handleCherrypickConfirm();
+        assert.equal(fireActionStub.callCount, 0);
+        assert.isTrue(handleBeforeCherryPickStub.called);
+      });
+
       test('branch name cleared when re-open cherrypick', () => {
         const emptyBranchName = '';
         queryAndAssert<GrConfirmCherrypickDialog>(
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index a0afce1..a2e901a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -107,6 +107,19 @@
     return okay;
   }
 
+  async handleBeforeCherryPick(change: ChangeInfo): Promise<boolean> {
+    await this.waitForPluginsToLoad();
+    let okay = true;
+    for (const cb of this._getEventCallbacks(EventType.BEFORE_CHERRY_PICK)) {
+      try {
+        okay = (await cb(change)) && okay;
+      } catch (err: unknown) {
+        this.reportError(err, EventType.BEFORE_CHERRY_PICK);
+      }
+    }
+    return okay;
+  }
+
   handlePublishEdit(change: ChangeInfo, revision?: RevisionInfo | null) {
     for (const cb of this._getEventCallbacks(EventType.PUBLISH_EDIT)) {
       try {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index 813faf3..220fed3 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -68,6 +68,13 @@
    * @return A promise that resolves to true if the rebase should proceed.
    */
   handleBeforeRebase(change: ChangeInfo): Promise<boolean>;
+  /**
+   * This method is called before a cherry-pick.
+   * It allows plugins to conditionally block the cherry-pick.
+   * @param change The relevant change.
+   * @return A promise that resolves to true if the cherry-pick should proceed.
+   */
+  handleBeforeCherryPick(change: ChangeInfo): Promise<boolean>;
   handlePublishEdit(change: ChangeInfo, revision?: RevisionInfo | null): void;
   handleShowChange(detail: ShowChangeDetail): Promise<void>;
   handleShowRevisionActions(detail: ShowRevisionActionsDetail): void;