Merge "Add slots for post-line content projection"
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index db94db2..34a2623 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -245,6 +245,9 @@
     try (Repository repo = repoManager.openRepository(tipRsrc.getProject());
         RevWalk rw = new RevWalk(repo)) {
       List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
+      if (chain.size() <= 1) {
+        return description;
+      }
       PatchSetData oldestAncestor = chain.get(0);
       if (rebaseUtil.canRebase(
           oldestAncestor.patchSet(), oldestAncestor.data().change().getDest(), repo, rw)) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 5a84f69..b7fe46e 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.entities.RefNames.isConfigRef;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
 import static java.lang.String.format;
 import static org.eclipse.jgit.lib.Constants.R_REFS;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefValidationHelper;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -102,56 +104,58 @@
    */
   public void deleteSingleRef(ProjectState projectState, String ref, @Nullable String prefix)
       throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
-    if (prefix != null && !ref.startsWith(R_REFS)) {
-      ref = prefix + ref;
-    }
+    try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+      if (prefix != null && !ref.startsWith(R_REFS)) {
+        ref = prefix + ref;
+      }
 
-    projectState.checkStatePermitsWrite();
-    permissionBackend
-        .currentUser()
-        .project(projectState.getNameKey())
-        .ref(ref)
-        .check(RefPermission.DELETE);
+      projectState.checkStatePermitsWrite();
+      permissionBackend
+          .currentUser()
+          .project(projectState.getNameKey())
+          .ref(ref)
+          .check(RefPermission.DELETE);
 
-    try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
-      RefUpdate.Result result;
-      RefUpdate u = repository.updateRef(ref);
-      u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
-      u.setNewObjectId(ObjectId.zeroId());
-      u.setForceUpdate(true);
-      refDeletionValidator.validateRefOperation(
-          projectState.getName(),
-          identifiedUser.get(),
-          u,
-          /* pushOptions */ ImmutableListMultimap.of());
-      result = u.delete();
+      try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
+        RefUpdate.Result result;
+        RefUpdate u = repository.updateRef(ref);
+        u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
+        u.setNewObjectId(ObjectId.zeroId());
+        u.setForceUpdate(true);
+        refDeletionValidator.validateRefOperation(
+            projectState.getName(),
+            identifiedUser.get(),
+            u,
+            /* pushOptions */ ImmutableListMultimap.of());
+        result = u.delete();
 
-      switch (result) {
-        case NEW:
-        case NO_CHANGE:
-        case FAST_FORWARD:
-        case FORCED:
-          referenceUpdated.fire(
-              projectState.getNameKey(),
-              u,
-              ReceiveCommand.Type.DELETE,
-              identifiedUser.get().state());
-          break;
+        switch (result) {
+          case NEW:
+          case NO_CHANGE:
+          case FAST_FORWARD:
+          case FORCED:
+            referenceUpdated.fire(
+                projectState.getNameKey(),
+                u,
+                ReceiveCommand.Type.DELETE,
+                identifiedUser.get().state());
+            break;
 
-        case REJECTED_CURRENT_BRANCH:
-          logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
-          throw new ResourceConflictException("cannot delete current branch");
+          case REJECTED_CURRENT_BRANCH:
+            logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
+            throw new ResourceConflictException("cannot delete current branch");
 
-        case LOCK_FAILURE:
-          throw new LockFailureException(String.format("Cannot delete %s", ref), u);
-        case IO_FAILURE:
-        case NOT_ATTEMPTED:
-        case REJECTED:
-        case RENAMED:
-        case REJECTED_MISSING_OBJECT:
-        case REJECTED_OTHER_REASON:
-        default:
-          throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
+          case LOCK_FAILURE:
+            throw new LockFailureException(String.format("Cannot delete %s", ref), u);
+          case IO_FAILURE:
+          case NOT_ATTEMPTED:
+          case REJECTED:
+          case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
+          default:
+            throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/update/context/RefUpdateContext.java b/java/com/google/gerrit/server/update/context/RefUpdateContext.java
index d1c5ff8..3ac0f0d 100644
--- a/java/com/google/gerrit/server/update/context/RefUpdateContext.java
+++ b/java/com/google/gerrit/server/update/context/RefUpdateContext.java
@@ -88,7 +88,7 @@
     ACCOUNTS_UPDATE,
     /** A ref is updated as a part of direct push. */
     DIRECT_PUSH,
-    /** A ref is updated as a part of explicit branch update operation. */
+    /** A ref is updated as a part of explicit branch or ref update operation. */
     BRANCH_MODIFICATION,
     /** A ref is updated as a part of explicit tag update operation. */
     TAG_MODIFICATION,
@@ -106,7 +106,13 @@
     /** A ref is updated as a part of versioned meta data change. */
     VERSIONED_META_DATA_CHANGE,
     /** A ref is updated as a part of commit-ban operation. */
-    BAN_COMMIT
+    BAN_COMMIT,
+    /**
+     * A ref is updated inside a plugin.
+     *
+     * <p>If a plugin updates one of a special refs - it must also open a nested context.
+     */
+    PLUGIN,
   }
 
   /** Opens a provided context. */
diff --git a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
index 4fe6ff7..650c218 100644
--- a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
+++ b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
@@ -26,6 +26,7 @@
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.MERGE_CHANGE;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.OFFLINE_OPERATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.REPO_SEQ;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.TAG_MODIFICATION;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.VERSIONED_META_DATA_CHANGE;
@@ -160,6 +161,8 @@
             RefUpdateContext.hasOpen(MERGE_CHANGE)
                 || RefUpdateContext.hasOpen(RefUpdateType.BRANCH_MODIFICATION)
                 || RefUpdateContext.hasOpen(RefUpdateType.UPDATE_SUPERPROJECT)
+                // Plugin can update any ref
+                || RefUpdateContext.hasOpen(PLUGIN)
                 || isTestRepoCall(),
             "Ordinary ref '%s' is updated outside of the expected operation. Wrap code in the correct RefUpdateContext or add the ref as a special ref.",
             refName);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 342b876..19207bc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -42,6 +42,7 @@
 import {classMap} from 'lit/directives/class-map.js';
 import {createSearchUrl} from '../../../models/views/search';
 import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 
 enum ChangeSize {
@@ -94,9 +95,6 @@
   sectionName?: string;
 
   @property({type: Boolean})
-  showStar = false;
-
-  @property({type: Boolean})
   showNumber = false;
 
   @property({type: String})
@@ -125,6 +123,10 @@
 
   private readonly getNavigation = resolve(this, navigationToken);
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
+  @state() private isLoggedIn = false;
+
   constructor() {
     super();
     subscribe(
@@ -134,6 +136,11 @@
         this.updateCheckedState(selectedChangeNums);
       }
     );
+    subscribe(
+      this,
+      () => this.getUserModel().loggedIn$,
+      isLoggedIn => (this.isLoggedIn = isLoggedIn)
+    );
   }
 
   override connectedCallback() {
@@ -332,6 +339,8 @@
   }
 
   private renderCellSelectionBox() {
+    if (!this.isLoggedIn) return;
+
     return html`
       <td class="cell selection">
         <!--
@@ -352,7 +361,7 @@
   }
 
   private renderCellStar() {
-    if (!this.showStar) return;
+    if (!this.isLoggedIn) return;
 
     return html`
       <td class="cell star">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index c7cb5b8..5e31cc8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -8,9 +8,11 @@
 import {
   SubmitRequirementResultInfo,
   NumericChangeId,
+  Timestamp,
 } from '../../../api/rest-api';
 import '../../../test/common-test-setup';
 import {
+  createAccountWithEmail,
   createAccountWithId,
   createChange,
   createSubmitRequirementExpressionInfo,
@@ -21,7 +23,6 @@
 import {
   query,
   queryAndAssert,
-  stubRestApi,
   waitUntilObserved,
 } from '../../../test/test-utils';
 import {
@@ -43,6 +44,7 @@
   bulkActionsModelToken,
   BulkActionsModel,
 } from '../../../models/bulk-actions/bulk-actions-model';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {createTestAppContext} from '../../../test/test-app-context-init';
 import {ColumnNames} from '../../../constants/constants';
 import {testResolver} from '../../../test/common-test-setup';
@@ -58,13 +60,13 @@
 
   let element: GrChangeListItem;
   let bulkActionsModel: BulkActionsModel;
+  let userModel: UserModel;
 
   setup(async () => {
-    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
-
     bulkActionsModel = new BulkActionsModel(
       createTestAppContext().restApiService
     );
+    userModel = testResolver(userModelToken);
     element = (
       await fixture<DIProviderElement>(
         wrapInProvider(
@@ -105,6 +107,10 @@
     test('bulk actions checkboxes', async () => {
       element.change = {...createChange(), _number: 1 as NumericChangeId};
       bulkActionsModel.sync([element.change]);
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
 
       const checkbox = queryAndAssert<HTMLInputElement>(
@@ -134,6 +140,10 @@
       element.globalIndex = 5;
       element.change = {...createChange(), _number: 1 as NumericChangeId};
       bulkActionsModel.sync([element.change]);
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
 
       const checkbox = queryAndAssert<HTMLInputElement>(
@@ -147,6 +157,10 @@
     });
 
     test('checkbox state updates with model updates', async () => {
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       element.requestUpdate();
       await element.updateComplete;
 
@@ -168,6 +182,10 @@
     });
 
     test('checkbox state updates with change id update', async () => {
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       element.requestUpdate();
       await element.updateComplete;
 
@@ -361,7 +379,10 @@
     const change = createChange();
     bulkActionsModel.sync([change]);
     bulkActionsModel.addSelectedChangeNum(change._number);
-    element.showStar = true;
+    userModel.setAccount({
+      ...createAccountWithEmail('abc@def.com'),
+      registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+    });
     element.showNumber = true;
     element.account = createAccountWithId(1);
     element.config = createServerInfo();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index 8227e11..61b276e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -15,14 +15,15 @@
 import {sharedStyles} from '../../../styles/shared-styles';
 import {Metadata} from '../../../utils/change-metadata-util';
 import {WAITING} from '../../../constants/constants';
-import {provide} from '../../../models/dependency';
+import {provide, resolve} from '../../../models/dependency';
 import {
   bulkActionsModelToken,
   BulkActionsModel,
 } from '../../../models/bulk-actions/bulk-actions-model';
+import {createSearchUrl} from '../../../models/views/search';
+import {userModelToken} from '../../../models/user/user-model';
 import {subscribe} from '../../lit/subscription-controller';
 import {classMap} from 'lit/directives/class-map.js';
-import {createSearchUrl} from '../../../models/views/search';
 
 const NUMBER_FIXED_COLUMNS = 4;
 const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
@@ -52,9 +53,6 @@
   visibleChangeTableColumns?: string[];
 
   @property({type: Boolean})
-  showStar = false;
-
-  @property({type: Boolean})
   showNumber?: boolean; // No default value to prevent flickering.
 
   @property({type: Number})
@@ -104,6 +102,10 @@
     getAppContext().restApiService
   );
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
+  private isLoggedIn = false;
+
   static override get styles() {
     return [
       changeListStyles,
@@ -156,6 +158,11 @@
       () => this.bulkActionsModel.totalChangeCount$,
       totalChangeCount => (this.totalChangeCount = totalChangeCount)
     );
+    subscribe(
+      this,
+      () => this.getUserModel().loggedIn$,
+      isLoggedIn => (this.isLoggedIn = isLoggedIn)
+    );
   }
 
   override willUpdate(changedProperties: PropertyValues) {
@@ -189,8 +196,8 @@
         <td class="leftPadding" aria-hidden="true"></td>
         <td
           class="star"
-          ?aria-hidden=${!this.showStar}
-          ?hidden=${!this.showStar}
+          ?aria-hidden=${!this.isLoggedIn}
+          ?hidden=${!this.isLoggedIn}
         ></td>
         <td class="cell" colspan=${colSpan}>
           ${this.changeSection.emptyStateSlotName
@@ -213,7 +220,7 @@
       <tbody>
         <tr class="groupHeader">
           <td aria-hidden="true" class="leftPadding"></td>
-          <td aria-hidden="true" class="star" ?hidden=${!this.showStar}></td>
+          <td aria-hidden="true" class="star" ?hidden=${!this.isLoggedIn}></td>
           <td class="cell" colspan=${colSpan}>
             <h2 class="heading-3">
               <a
@@ -248,7 +255,7 @@
           : html` <td
                 class="star"
                 aria-label="Star status column"
-                ?hidden=${!this.showStar}
+                ?hidden=${!this.isLoggedIn}
               ></td>
               <td class="number" ?hidden=${!this.showNumber}>#</td>
               ${columns.map(item => this.renderHeaderCell(item))}
@@ -267,7 +274,7 @@
     const indeterminate =
       this.numSelected > 0 && this.numSelected !== this.totalChangeCount;
     return html`
-      <td class="selection">
+      <td class="selection" ?hidden=${!this.isLoggedIn}>
         <!--
           The .checked property must be used rather than the attribute because
           the attribute only controls the default checked state and does not
@@ -322,7 +329,6 @@
         .sectionName=${this.changeSection.name}
         .visibleChangeTableColumns=${columns}
         .showNumber=${this.showNumber}
-        ?showStar=${this.showStar}
         .usp=${this.usp}
         .labelNames=${this.labelNames}
         .globalIndex=${this.startIndex + index}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
index 8dfecdc..63552c7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
@@ -13,9 +13,10 @@
 import {
   createChange,
   createAccountDetailWithId,
+  createAccountWithEmail,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {NumericChangeId, ChangeInfoId} from '../../../api/rest-api';
+import {ChangeInfoId, NumericChangeId, Timestamp} from '../../../api/rest-api';
 import {
   queryAll,
   query,
@@ -27,11 +28,15 @@
 import {ChangeListSection} from '../gr-change-list/gr-change-list';
 import {fixture, html, assert} from '@open-wc/testing';
 import {ColumnNames} from '../../../constants/constants';
+import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 
 suite('gr-change-list section', () => {
   let element: GrChangeListSection;
+  let userModel: UserModel;
 
   setup(async () => {
+    userModel = testResolver(userModelToken);
     const changeSection: ChangeListSection = {
       name: 'test',
       query: 'test',
@@ -193,6 +198,10 @@
         ],
         emptyStateSlotName: 'test',
       };
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
       let rows = queryAll(element, 'gr-change-list-item');
       assert.lengthOf(rows, 2);
@@ -235,6 +244,10 @@
         ],
         emptyStateSlotName: 'test',
       };
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
       const rows = queryAll(element, 'gr-change-list-item');
 
@@ -273,6 +286,31 @@
     });
   });
 
+  test('no checkbox when logged out', async () => {
+    element.changeSection = {
+      name: 'test',
+      query: 'test',
+      results: [
+        {
+          ...createChange(),
+          _number: 1 as NumericChangeId,
+          id: '1' as ChangeInfoId,
+        },
+        {
+          ...createChange(),
+          _number: 2 as NumericChangeId,
+          id: '2' as ChangeInfoId,
+        },
+      ],
+      emptyStateSlotName: 'test',
+    };
+    userModel.setAccount(undefined);
+    await element.updateComplete;
+    const rows = queryAll(element, 'gr-change-list-item');
+    assert.lengthOf(rows, 2);
+    assert.isUndefined(query<HTMLInputElement>(rows[0], 'input'));
+  });
+
   test('colspans', async () => {
     element.visibleChangeTableColumns = [];
     element.changeSection = {results: [{...createChange()}]};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index d2ba2c9..1c86354 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -195,7 +195,6 @@
           .account=${this.account}
           .changes=${this.changes}
           .preferences=${this.preferences}
-          .showStar=${this.loggedIn}
           @toggle-star=${(e: CustomEvent<ChangeStarToggleStarDetail>) => {
             this.handleToggleStar(e);
           }}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 4c43da5..748c2b8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -113,9 +113,6 @@
   showNumber?: boolean; // No default value to prevent flickering.
 
   @property({type: Boolean})
-  showStar = false;
-
-  @property({type: Boolean})
   showReviewedState = false;
 
   @property({type: Array})
@@ -270,7 +267,6 @@
           sectionIndex,
           this.sections
         )}
-        ?showStar=${this.showStar}
         .showNumber=${this.showNumber}
         .visibleChangeTableColumns=${this.visibleChangeTableColumns}
         .usp=${this.usp}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
index bef3166..7e9735f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -23,6 +23,7 @@
 } from '../../../constants/constants';
 import {AccountId, NumericChangeId} from '../../../types/common';
 import {
+  createAccountWithEmail,
   createChange,
   createServerInfo,
   createSubmitRequirementResultInfo,
@@ -32,12 +33,16 @@
 import {fixture, assert} from '@open-wc/testing';
 import {html} from 'lit';
 import {testResolver} from '../../../test/common-test-setup';
+import {Timestamp} from '../../../api/rest-api';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 
 suite('gr-change-list basic tests', () => {
   let element: GrChangeList;
+  let userModel: UserModel;
 
   setup(async () => {
     element = await fixture(html`<gr-change-list></gr-change-list>`);
+    userModel = testResolver(userModelToken);
   });
 
   test('renders', async () => {
@@ -285,6 +290,12 @@
   });
 
   test('toggle checkbox keyboard shortcut', async () => {
+    userModel.setAccount({
+      ...createAccountWithEmail('abc@def.com'),
+      registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+    });
+    await element.updateComplete;
+
     const getCheckbox = (item: GrChangeListItem) =>
       queryAndAssert<HTMLInputElement>(query(item, '.selection'), 'input');
 
@@ -515,7 +526,7 @@
     assert.isTrue(element.isColumnEnabled('Subject'));
   });
 
-  test('showStar and showNumber', async () => {
+  test('loggedIn and showNumber', async () => {
     element.sections = [{results: [{...createChange()}], name: 'a'}];
     element.account = {_account_id: 1001 as AccountId};
     element.preferences = {
@@ -534,6 +545,7 @@
       ],
     };
     element.config = createServerInfo();
+    userModel.setAccount(undefined);
     await element.updateComplete;
     const section = query<GrChangeListSection>(
       element,
@@ -547,7 +559,10 @@
     assert.isNotOk(query(query(section, 'gr-change-list-item'), '.star'));
     assert.isNotOk(query(query(section, 'gr-change-list-item'), '.number'));
 
-    element.showStar = true;
+    userModel.setAccount({
+      ...createAccountWithEmail('abc@def.com'),
+      registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+    });
     await element.updateComplete;
     await section.updateComplete;
     assert.isOk(query(query(section, 'gr-change-list-item'), '.star'));
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index d013654..cd30440 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -284,7 +284,6 @@
         ${this.renderUserHeader()}
         <h1 class="assistive-tech-only">Dashboard</h1>
         <gr-change-list
-          ?showStar=${true}
           .account=${this.account}
           .preferences=${this.preferences}
           .sections=${this.results}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
index 17d7e95..84a3139 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
@@ -82,7 +82,7 @@
         <div class="loading" hidden="">Loading...</div>
         <div>
           <h1 class="assistive-tech-only">Dashboard</h1>
-          <gr-change-list showstar="">
+          <gr-change-list>
             <div id="emptyOutgoing" slot="outgoing-slot">No changes</div>
             <div id="emptyYourTurn" slot="your-turn-slot">
               <span> No changes need your attention &nbsp🎉 </span>
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 6f8bd9a..c2739f3 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
@@ -218,7 +218,6 @@
             <gr-autocomplete
               id="parentInput"
               .query=${this.query}
-              no-debounce
               .text=${this.text}
               @text-changed=${(e: ValueChangedEvent) =>
                 (this.text = e.detail.value)}
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 776e923..2644d81 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
@@ -74,7 +74,6 @@
             <gr-autocomplete
               allow-non-suggested-values=""
               id="parentInput"
-              no-debounce=""
               placeholder="Change number, ref, or commit hash"
             >
             </gr-autocomplete>
@@ -305,7 +304,6 @@
 
     test('input text change triggers function', async () => {
       const recentChangesSpy = sinon.spy(element, 'getRecentChanges');
-      element.parentInput.noDebounce = true;
       pressKey(
         queryAndAssert(queryAndAssert(element, '#parentInput'), '#input'),
         Key.ENTER
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index fbc87ed..314e126 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -278,7 +278,7 @@
     // fallback to gerrit's official doc
     let baseUrl =
       this.docsBaseUrl ||
-      'https://gerrit-review.googlesource.com/documentation/';
+      'https://gerrit-review.googlesource.com/Documentation/';
     if (baseUrl.endsWith('/')) {
       baseUrl = baseUrl.substring(0, baseUrl.length - 1);
     }
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index 43b1f86..0694453 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -319,7 +319,7 @@
       await element.updateComplete;
       assert.equal(
         element.computeHelpDocLink(),
-        'https://gerrit-review.googlesource.com/documentation/' +
+        'https://gerrit-review.googlesource.com/Documentation/' +
           'user-search.html'
       );
     });
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 31283ad..0729f21 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -217,16 +217,20 @@
       assert.isFalse(hideDialogStub.called);
       queryAndAssert<GrButton>(element, '#open').click();
       element.patchNum = 1 as RevisionPatchSetNum;
-      await waitUntilVisible(element.modal!);
+      await showDialogSpy.lastCall.returnValue;
       assert.isTrue(hideDialogStub.called);
       assert.isTrue(element.openDialog!.disabled);
       assert.isFalse(queryStub.called);
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       openAutoComplete.focused = true;
-      openAutoComplete.noDebounce = true;
       openAutoComplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        openAutoComplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await openAutoComplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       await waitUntil(() => !element.openDialog!.disabled);
       queryAndAssert<GrButton>(
@@ -242,7 +246,6 @@
       queryAndAssert<GrButton>(element, '#open').click();
       await waitUntilVisible(element.modal!);
       assert.isTrue(element.openDialog!.disabled);
-      openAutoComplete.noDebounce = true;
       openAutoComplete.text = 'src/test.cpp';
       await element.updateComplete;
       await waitUntil(() => !element.openDialog!.disabled);
@@ -277,9 +280,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       deleteAutocomplete.focused = true;
-      deleteAutocomplete.noDebounce = true;
       deleteAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        deleteAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await deleteAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       await waitUntil(() => !element.deleteDialog!.disabled);
       queryAndAssert<GrButton>(
@@ -304,9 +311,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       deleteAutocomplete.focused = true;
-      deleteAutocomplete.noDebounce = true;
       deleteAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        deleteAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await deleteAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       await waitUntil(() => !element.deleteDialog!.disabled);
       queryAndAssert<GrButton>(
@@ -363,9 +374,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       renameAutocomplete.focused = true;
-      renameAutocomplete.noDebounce = true;
       renameAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        renameAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await renameAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       assert.isTrue(element.renameDialog!.disabled);
 
@@ -395,9 +410,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       renameAutocomplete.focused = true;
-      renameAutocomplete.noDebounce = true;
       renameAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        renameAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await renameAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       assert.isTrue(element.renameDialog!.disabled);
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 6b4d670..cc2723e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -401,8 +401,8 @@
     );
     input.text = 'newTest';
     input.input!.focus();
-    input.noDebounce = true;
     await element.updateComplete;
+    await input.latestSuggestionUpdateComplete;
     assert.isTrue(getSuggestionsStub.calledOnce);
     assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
     await waitUntil(() => makeSuggestionItemSpy.getCalls().length === 2);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index de97741..ab74e8b 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -9,7 +9,11 @@
 import '../../../styles/shared-styles';
 import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 import {fire, fireEvent} from '../../../utils/event-util';
-import {debounce, DelayedTask} from '../../../utils/async-util';
+import {
+  debounce,
+  DelayedTask,
+  ResolvedDelayedTaskStatus,
+} from '../../../utils/async-util';
 import {PropertyType} from '../../../types/common';
 import {modifierPressed} from '../../../utils/dom-util';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -150,12 +154,6 @@
   @property({type: Boolean, attribute: 'warn-uncommitted'})
   warnUncommitted = false;
 
-  /**
-   * When true, querying for suggestions is not debounced w/r/t keypresses
-   */
-  @property({type: Boolean, attribute: 'no-debounce'})
-  noDebounce = false;
-
   @property({type: Boolean, attribute: 'show-blue-focus-border'})
   showBlueFocusBorder = false;
 
@@ -183,6 +181,15 @@
 
   private updateSuggestionsTask?: DelayedTask;
 
+  /**
+   * @return Promise that resolves when suggestions are update.
+   */
+  get latestSuggestionUpdateComplete():
+    | Promise<ResolvedDelayedTaskStatus>
+    | undefined {
+    return this.updateSuggestionsTask?.promise;
+  }
+
   get nativeInput() {
     return (this.input!.inputElement as IronInputElement)
       .inputElement as HTMLInputElement;
@@ -254,11 +261,7 @@
   }
 
   override willUpdate(changedProperties: PropertyValues) {
-    if (
-      changedProperties.has('text') ||
-      changedProperties.has('threshold') ||
-      changedProperties.has('noDebounce')
-    ) {
+    if (changedProperties.has('text') || changedProperties.has('threshold')) {
       this.updateSuggestions();
     }
     if (
@@ -404,12 +407,7 @@
   }
 
   updateSuggestions() {
-    if (
-      this.text === undefined ||
-      this.threshold === undefined ||
-      this.noDebounce === undefined
-    )
-      return;
+    if (this.text === undefined || this.threshold === undefined) return;
 
     // Reset suggestions for every update
     // This will also prevent from carrying over suggestions:
@@ -462,15 +460,11 @@
         });
     };
 
-    if (this.noDebounce) {
-      update();
-    } else {
-      this.updateSuggestionsTask = debounce(
-        this.updateSuggestionsTask,
-        update,
-        DEBOUNCE_WAIT_MS
-      );
-    }
+    this.updateSuggestionsTask = debounce(
+      this.updateSuggestionsTask,
+      update,
+      DEBOUNCE_WAIT_MS
+    );
   }
 
   setFocus(focused: boolean) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index eb1efec..81949c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -391,7 +391,6 @@
 
     element.query = queryStub;
     await element.updateComplete;
-    element.noDebounce = false;
     focusOnInput();
     element.text = 'a';
 
@@ -413,7 +412,6 @@
   test('empty text results in no suggestions', async () => {
     element.text = '';
     element.threshold = 0;
-    element.noDebounce = false;
     await element.updateComplete;
     assert.equal(element.suggestions.length, 0);
   });
@@ -475,7 +473,6 @@
       assert.equal(element.suggestions.length, 1);
       element.text = '';
       element.threshold = 0;
-      element.noDebounce = false;
       await element.updateComplete;
       assert.equal(element.suggestions.length, 0);
     });
@@ -494,7 +491,6 @@
       await waitUntil(() => element.queryErrorMessage === 'Test error');
       element.text = '';
       element.threshold = 0;
-      element.noDebounce = false;
       await element.updateComplete;
       assert.isUndefined(element.queryErrorMessage);
     });
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index cae6319..752de62 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -37,6 +37,11 @@
 
 export const _testOnly_allTasks = new Map<number, DelayedTask>();
 
+export enum ResolvedDelayedTaskStatus {
+  CALLBACK_EXECUTED = 'CALLBACK_EXECUTED',
+  TASK_CANCELLED = 'TASK_CANCELLED',
+}
+
 /**
  * This is just a very simple and small wrapper around setTimeout(). Instead of
  * the usual:
@@ -52,34 +57,54 @@
  * It is just nicer to have an object for this instead of a number as a handle.
  */
 export class DelayedTask {
-  private timer?: number;
+  private timerId?: number;
+
+  /**
+   * Promise that is resolved after the callback is run or the task is
+   * cancelled.
+   */
+  public readonly promise: Promise<ResolvedDelayedTaskStatus>;
+
+  private resolvePromise?: (
+    value: ResolvedDelayedTaskStatus | PromiseLike<ResolvedDelayedTaskStatus>
+  ) => void;
 
   constructor(private callback: () => void, waitMs = 0) {
-    this.timer = window.setTimeout(() => {
-      if (this.timer) _testOnly_allTasks.delete(this.timer);
-      this.timer = undefined;
-      if (this.callback) this.callback();
-    }, waitMs);
-    _testOnly_allTasks.set(this.timer, this);
+    this.promise = new Promise(resolve => {
+      this.resolvePromise = resolve;
+      this.timerId = window.setTimeout(() => {
+        if (this.timerId) _testOnly_allTasks.delete(this.timerId);
+        this.timerId = undefined;
+        if (this.callback) this.callback();
+        resolve(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+      }, waitMs);
+      _testOnly_allTasks.set(this.timerId, this);
+    });
+  }
+
+  private cancelTimer() {
+    window.clearTimeout(this.timerId);
+    if (this.timerId) _testOnly_allTasks.delete(this.timerId);
+    this.timerId = undefined;
   }
 
   cancel() {
     if (this.isActive()) {
-      window.clearTimeout(this.timer);
-      if (this.timer) _testOnly_allTasks.delete(this.timer);
-      this.timer = undefined;
+      this.cancelTimer();
+      this.resolvePromise?.(ResolvedDelayedTaskStatus.TASK_CANCELLED);
     }
   }
 
   flush() {
     if (this.isActive()) {
-      this.cancel();
+      this.cancelTimer();
       if (this.callback) this.callback();
+      this.resolvePromise?.(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
     }
   }
 
   isActive() {
-    return this.timer !== undefined;
+    return this.timerId !== undefined;
   }
 }