Show on whose behalf rebase will be done

When a user is not the latest uploader, we show a message that rebase
will be done on behalf of the uploader. It may encourage reviewers to
rebase changes since they will be assured that they will not become a
new uploader - which often means losing voting privileges.

Screenshot: https://imgur.com/a/BESgYBp
Release-Notes: skip
Google-Bug-Id: b/269731839
Change-Id: I966b73ff93f22e4457fc24dbca1fbf7438b08b2a
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 27fde97..dbfe55b 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import '../../shared/gr-account-chip/gr-account-chip';
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {when} from 'lit/directives/when.js';
@@ -10,6 +11,8 @@
   NumericChangeId,
   BranchName,
   ChangeActionDialog,
+  AccountDetailInfo,
+  AccountInfo,
 } from '../../../types/common';
 import '../../shared/gr-dialog/gr-dialog';
 import '../../shared/gr-autocomplete/gr-autocomplete';
@@ -26,6 +29,7 @@
 import {resolve} from '../../../models/dependency';
 import {changeModelToken} from '../../../models/change/change-model';
 import {subscribe} from '../../lit/subscription-controller';
+import {userModelToken} from '../../../models/user/user-model';
 
 export interface RebaseChange {
   name: string;
@@ -86,9 +90,6 @@
   @state()
   allowConflicts = false;
 
-  @state()
-  isOwner = false;
-
   @query('#rebaseOnParentInput')
   private rebaseOnParentInput!: HTMLInputElement;
 
@@ -107,17 +108,30 @@
   @query('#parentInput')
   parentInput!: GrAutocomplete;
 
+  @state()
+  account?: AccountDetailInfo;
+
+  @state()
+  uploader?: AccountInfo;
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
   constructor() {
     super();
     this.query = input => this.getChangeSuggestions(input);
     subscribe(
       this,
-      () => this.getChangeModel().isOwner$,
-      x => (this.isOwner = x)
+      () => this.getUserModel().account$,
+      x => (this.account = x)
+    );
+    subscribe(
+      this,
+      () => this.getChangeModel().latestUploader$,
+      x => (this.uploader = x)
     );
   }
 
@@ -158,6 +172,9 @@
       .rebaseOption {
         margin: var(--spacing-m) 0;
       }
+      .rebaseOnBehalfMsg {
+        margin-top: var(--spacing-m);
+      }
     `,
   ];
 
@@ -255,7 +272,7 @@
             >
           </div>
           ${when(
-            !this.isOwner && this.allowConflicts,
+            !this.isCurrentUserEqualToLatestUploader() && this.allowConflicts,
             () =>
               html`<span class="message"
                 >Rebase cannot be done on behalf of the uploader when allowing
@@ -276,6 +293,16 @@
                 <label for="rebaseChain">Rebase all ancestors</label>
               </div>`
           )}
+          ${when(
+            !this.isCurrentUserEqualToLatestUploader(),
+            () => html`<div class="rebaseOnBehalfMsg">Rebase will be done on behalf of${
+              !this.allowConflicts ? ' the uploader:' : ''
+            } <gr-account-chip
+                .account=${this.allowConflicts ? this.account : this.uploader}
+                .hideHovercard=${true}
+              ></gr-account-chip
+              ><span></div>`
+          )}
         </div>
       </gr-dialog>
     `;
@@ -310,6 +337,11 @@
       });
   }
 
+  isCurrentUserEqualToLatestUploader() {
+    if (!this.account || !this.uploader) return true;
+    return this.account._account_id === this.uploader._account_id;
+  }
+
   getRecentChanges() {
     if (this.recentChanges) {
       return Promise.resolve(this.recentChanges);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index 3064014..8d907c9 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -9,21 +9,34 @@
 import {
   pressKey,
   queryAndAssert,
-  stubFlags,
   stubRestApi,
   waitUntil,
 } from '../../../test/test-utils';
-import {NumericChangeId, BranchName} from '../../../types/common';
-import {createChangeViewChange} from '../../../test/test-data-generators';
+import {NumericChangeId, BranchName, Timestamp} from '../../../types/common';
+import {
+  createAccountWithEmail,
+  createChangeViewChange,
+} from '../../../test/test-data-generators';
 import {fixture, html, assert} from '@open-wc/testing';
 import {Key} from '../../../utils/dom-util';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken} from '../../../models/user/user-model';
+import {
+  changeModelToken,
+  LoadingStatus,
+} from '../../../models/change/change-model';
+import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
 
 suite('gr-confirm-rebase-dialog tests', () => {
   let element: GrConfirmRebaseDialog;
 
   setup(async () => {
-    stubFlags('isEnabled').returns(true);
+    const userModel = testResolver(userModelToken);
+    userModel.setAccount({
+      ...createAccountWithEmail('abc@def.com'),
+      registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+    });
     element = await fixture(
       html`<gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>`
     );
@@ -91,6 +104,57 @@
     );
   });
 
+  suite('on behalf of uploader', () => {
+    let changeModel;
+    const change = {
+      ...createChangeViewChange(),
+    };
+    setup(async () => {
+      element.branch = 'test' as BranchName;
+      await element.updateComplete;
+      changeModel = testResolver(changeModelToken);
+      changeModel.setState({
+        loadingStatus: LoadingStatus.LOADED,
+        change,
+      });
+    });
+    test('for reviewer it shows message about on behalf', () => {
+      const rebaseOnBehalfMsg = queryAndAssert(element, '.rebaseOnBehalfMsg');
+      assert.dom.equal(
+        rebaseOnBehalfMsg,
+        /* HTML */ `<div class="rebaseOnBehalfMsg">
+          Rebase will be done on behalf of the uploader:
+          <gr-account-chip> </gr-account-chip> <span> </span>
+        </div>`
+      );
+      const accountChip: GrAccountChip = queryAndAssert(
+        rebaseOnBehalfMsg,
+        'gr-account-chip'
+      );
+      assert.equal(
+        accountChip.account!,
+        change?.revisions[change.current_revision]?.uploader
+      );
+    });
+    test('allowConflicts', async () => {
+      element.allowConflicts = true;
+      await element.updateComplete;
+      const rebaseOnBehalfMsg = queryAndAssert(element, '.rebaseOnBehalfMsg');
+      assert.dom.equal(
+        rebaseOnBehalfMsg,
+        /* HTML */ `<div class="rebaseOnBehalfMsg">
+          Rebase will be done on behalf of
+          <gr-account-chip> </gr-account-chip> <span> </span>
+        </div>`
+      );
+      const accountChip: GrAccountChip = queryAndAssert(
+        rebaseOnBehalfMsg,
+        'gr-account-chip'
+      );
+      assert.equal(accountChip.account, element.account);
+    });
+  });
+
   test('disableActions property disables dialog confirm', async () => {
     element.disableActions = false;
     await element.updateComplete;
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index ad5b217..b6b9de7 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -196,6 +196,11 @@
     computeLatestPatchNumWithEdit(patchsets)
   );
 
+  public readonly latestUploader$ = select(
+    this.change$,
+    change => change?.revisions[change.current_revision]?.uploader
+  );
+
   /**
    * 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