Add support to cherry-pick using secondary email in UI

Add an email drop down to the cherry-pick dialog box for single change
cherry-picks so that users can select one of their secondary emails to
be used as the committer email in the new cherry-picked patch-set. This
drop-down is visible only when user has more than one email registered
on their account.

Release-Notes: UI supports cherry-pick using a registered secondary email
Change-Id: If8f52bca76320af8da79a5855daf3ecb53779ea6
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 39929e1..3f50afe 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -280,9 +280,14 @@
 
 ** [[cherry-pick]]`Cherry-Pick`:
 +
-Allows to cherry-pick the change to another branch. The destination
-branch can be selected from a dialog. Cherry-picking a change creates a
-new open change on the selected destination branch.
+Allows to cherry-pick the change to another branch. The destination branch
+can be selected from a dialog. Cherry-picking a change creates a new open
+change on the selected destination branch. 'Cherry-pick committer email'
+drop-down is visible for single change cherry-picks when user has more than
+one email registered to their account. It is possible to select any of the
+registered emails to be used as the cherry-pick committer email. It defaults
+to source commit's committer email if it is a registered email of the calling
+user, else defaults to calling user's preferred email.
 +
 It is also possible to cherry-pick a change to the same branch. This is
 effectively the same as rebasing it to the current tip of the
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 0be80bb..8f835de 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
@@ -1610,6 +1610,7 @@
         base: el.baseCommit ? el.baseCommit : null,
         message: el.message,
         allow_conflicts: conflicts,
+        committer_email: el.committerEmail ? el.committerEmail : null,
       }
     );
   }
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 631d946..15500de 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
@@ -1000,6 +1000,7 @@
             base: null,
             message: 'foo message',
             allow_conflicts: false,
+            committer_email: null,
           },
         ]);
       });
@@ -1048,6 +1049,7 @@
             base: null,
             message: 'foo message',
             allow_conflicts: true,
+            committer_email: null,
           },
         ]);
       });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index b66b2cf..df3db29 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -18,6 +18,8 @@
   ChangeInfoId,
   TopicName,
   ChangeActionDialog,
+  EmailInfo,
+  GitPersonInfo,
 } from '../../../types/common';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {
@@ -30,6 +32,7 @@
   ChangeStatus,
   ProgressStatus,
 } from '../../../constants/constants';
+import {subscribe} from '../../lit/subscription-controller';
 import {fire, fireNoBubble} from '../../../utils/event-util';
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -43,6 +46,7 @@
 import {ParsedChangeInfo} from '../../../types/types';
 import {formStyles} from '../../../styles/form-styles';
 import {branchName} from '../../../utils/patch-set-util';
+import {changeModelToken} from '../../../models/change/change-model';
 
 const SUGGESTIONS_LIMIT = 15;
 const CHANGE_SUBJECT_LIMIT = 50;
@@ -127,13 +131,24 @@
   @state()
   private invalidBranch = false;
 
+  @state()
+  emails: EmailInfo[] = [];
+
   @query('#branchInput')
   branchInput!: GrTypedAutocomplete<BranchName>;
 
+  @state()
+  committerEmail?: string;
+
+  @state()
+  latestCommitter?: GitPersonInfo;
+
   private selectedChangeIds = new Set<ChangeInfoId>();
 
   private readonly restApiService = getAppContext().restApiService;
 
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
   private readonly reporting = getAppContext().reportingService;
 
   private readonly getNavigation = resolve(this, navigationToken);
@@ -142,6 +157,16 @@
     super();
     this.statuses = {};
     this.query = (text: string) => this.getProjectBranchesSuggestions(text);
+    subscribe(
+      this,
+      () => this.getChangeModel().latestCommitter$,
+      x => (this.latestCommitter = x)
+    );
+  }
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.loadEmails();
   }
 
   override willUpdate(changedProperties: PropertyValues) {
@@ -341,6 +366,17 @@
         @bind-value-changed=${(e: BindValueChangeEvent) =>
           (this.message = e.detail.value ?? '')}
       ></iron-autogrow-textarea>
+      ${when(
+        this.canShowEmailDropdown(),
+        () => html`<div id="cherryPickEmailDropdown">Cherry Pick Committer Email
+            <gr-dropdown-list
+                .items=${this.getEmailDropdownItems()}
+                .value=${this.committerEmail}
+                @value-change=${this.setCommitterEmail}
+            >
+            </gr-dropdown-list>
+            <span></div>`
+      )}
     `;
   }
 
@@ -580,6 +616,7 @@
         topic,
         allow_conflicts: true,
         allow_empty: true,
+        committer_email: this.committerEmail ? this.committerEmail : null,
       };
       const handleError = (response?: Response | null) => {
         this.handleCherryPickFailed(change, response);
@@ -654,4 +691,40 @@
         return branches;
       });
   }
+
+  async loadEmails() {
+    let accountEmails: EmailInfo[] = [];
+    await this.restApiService.getAccountEmails().then(emails => {
+      accountEmails = emails ?? [];
+    });
+    let selectedEmail: string | undefined;
+    accountEmails.forEach(e => {
+      if (e.preferred) {
+        selectedEmail = e.email;
+      }
+    });
+
+    if (accountEmails.some(e => e.email === this.latestCommitter?.email)) {
+      selectedEmail = this.latestCommitter?.email;
+    }
+    this.emails = accountEmails;
+    this.committerEmail = selectedEmail;
+  }
+
+  private canShowEmailDropdown() {
+    return this.emails.length > 1;
+  }
+
+  private getEmailDropdownItems() {
+    return this.emails.map(e => {
+      return {
+        text: e.email,
+        value: e.email,
+      };
+    });
+  }
+
+  private setCommitterEmail(e: CustomEvent<{value: string}>) {
+    this.committerEmail = e.detail.value;
+  }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
index dc8dba9..83e6f9e 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
@@ -5,7 +5,12 @@
  */
 import '../../../test/common-test-setup';
 import './gr-confirm-cherrypick-dialog';
-import {queryAll, queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {
+  query,
+  queryAll,
+  queryAndAssert,
+  stubRestApi,
+} from '../../../test/test-utils';
 import {GrConfirmCherrypickDialog} from './gr-confirm-cherrypick-dialog';
 import {
   BranchName,
@@ -24,11 +29,53 @@
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {ProgressStatus} from '../../../constants/constants';
 import {fixture, html, assert} from '@open-wc/testing';
+import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 
 const CHERRY_PICK_TYPES = {
   SINGLE_CHANGE: 1,
   TOPIC: 2,
 };
+
+const changes: ChangeInfo[] = [
+  {
+    ...createChange(),
+    id: '1234' as ChangeInfoId,
+    change_id: '12345678901234' as ChangeId,
+    topic: 'T' as TopicName,
+    subject: 'random',
+    project: 'A' as RepoName,
+    _number: 1 as NumericChangeId,
+    revisions: {
+      a: createRevision(),
+    },
+    current_revision: 'a' as CommitId,
+  },
+  {
+    ...createChange(),
+    id: '5678' as ChangeInfoId,
+    change_id: '23456' as ChangeId,
+    topic: 'T' as TopicName,
+    subject: 'a'.repeat(100),
+    project: 'B' as RepoName,
+    _number: 2 as NumericChangeId,
+    revisions: {
+      a: createRevision(),
+    },
+    current_revision: 'a' as CommitId,
+  },
+];
+
+const emails = [
+  {
+    email: 'primary@email.com',
+    preferred: true,
+  },
+  {
+    email: 'secondary@email.com',
+    preferred: false,
+  },
+];
+
 suite('gr-confirm-cherrypick-dialog tests', () => {
   let element: GrConfirmCherrypickDialog;
 
@@ -149,34 +196,6 @@
   });
 
   suite('cherry pick topic', () => {
-    const changes: ChangeInfo[] = [
-      {
-        ...createChange(),
-        id: '1234' as ChangeInfoId,
-        change_id: '12345678901234' as ChangeId,
-        topic: 'T' as TopicName,
-        subject: 'random',
-        project: 'A' as RepoName,
-        _number: 1 as NumericChangeId,
-        revisions: {
-          a: createRevision(),
-        },
-        current_revision: 'a' as CommitId,
-      },
-      {
-        ...createChange(),
-        id: '5678' as ChangeInfoId,
-        change_id: '23456' as ChangeId,
-        topic: 'T' as TopicName,
-        subject: 'a'.repeat(100),
-        project: 'B' as RepoName,
-        _number: 2 as NumericChangeId,
-        revisions: {
-          a: createRevision(),
-        },
-        current_revision: 'a' as CommitId,
-      },
-    ];
     setup(async () => {
       element.updateChanges(changes);
       element.cherryPickType = CHERRY_PICK_TYPES.TOPIC;
@@ -290,4 +309,36 @@
     assert.equal(branches.length, 1);
     assert.equal(branches[0].name, 'test-branch');
   });
+
+  suite('cherry pick single change with committer email', () => {
+    test('hide email dropdown when user has one email', async () => {
+      element.emails = emails.slice(0, 1);
+      await element.updateComplete;
+      assert.notExists(query(element, '#cherryPickEmailDropdown'));
+    });
+
+    test('show email dropdown when user has more than one email', async () => {
+      element.emails = emails;
+      await element.updateComplete;
+      const cherryPickEmailDropdown = queryAndAssert(
+        element,
+        '#cherryPickEmailDropdown'
+      );
+      assert.dom.equal(
+        cherryPickEmailDropdown,
+        `<div id="cherryPickEmailDropdown">Cherry Pick Committer Email
+        <gr-dropdown-list></gr-dropdown-list>
+        <span></span>
+        </div>`
+      );
+      const emailDropdown = queryAndAssert<GrDropdownList>(
+        cherryPickEmailDropdown,
+        'gr-dropdown-list'
+      );
+      assert.deepEqual(
+        emailDropdown.items?.map(e => e.value),
+        emails.map(e => e.email)
+      );
+    });
+  });
 });
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index e6f8512..dedbe9d 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -255,6 +255,11 @@
     change => change?.revisions[change.current_revision]?.uploader
   );
 
+  public readonly latestCommitter$ = select(
+    this.change$,
+    change => change?.revisions[change.current_revision]?.commit?.committer
+  );
+
   /**
    * Emits the current patchset number. If the route does not define the current
    * patchset num, then this selector waits for the change to be defined and