Merge "Cache rest api for getAccountDetails"
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index 66ffa42..e79c530 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -195,7 +195,7 @@
     return ref.startsWith(REFS_TAGS);
   }
 
-  /** True if the provided ref is {@link REFS_EXTERNAL_IDS}. */
+  /** True if the provided ref is {@link #REFS_EXTERNAL_IDS}. */
   public static boolean isExternalIdRef(String ref) {
     return REFS_EXTERNAL_IDS.equals(ref);
   }
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 72bfe40..92788b7 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -121,7 +121,7 @@
       data.put("userIsAuthenticated", true);
       if (page == RequestedPage.DASHBOARD) {
         data.put("defaultDashboardHex", ListOption.toHex(IndexPreloadingUtil.DASHBOARD_OPTIONS));
-        data.put("dashboardQuery", IndexPreloadingUtil.computeDashboardQueryList(serverApi));
+        data.put("dashboardQuery", IndexPreloadingUtil.computeDashboardQueryList());
       }
     } catch (AuthException e) {
       logger.atFine().log("Can't inline account-related data because user is unauthenticated");
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index bb3b6d5..afaeaf6 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -22,9 +22,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.UsedAt.Project;
-import com.google.gerrit.extensions.api.config.Server;
 import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.Url;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -188,7 +186,7 @@
     return Optional.empty();
   }
 
-  public static List<String> computeDashboardQueryList(Server serverApi) throws RestApiException {
+  public static List<String> computeDashboardQueryList() {
     List<String> queryList = new ArrayList<>();
     queryList.add(SELF_DASHBOARD_HAS_UNPUBLISHED_DRAFTS_QUERY);
     queryList.add(SELF_YOUR_TURN);
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 8e443f82..7984737 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -505,9 +505,11 @@
 
   /** The user assigned to the change. */
   // The getter always returns NO_ASSIGNEE, since assignee field is deprecated.
+  @Deprecated
   public static final IndexedField<ChangeData, Integer> ASSIGNEE_FIELD =
       IndexedField.<ChangeData>integerBuilder("Assignee").build(changeGetter(c -> NO_ASSIGNEE));
 
+  @Deprecated
   public static final IndexedField<ChangeData, Integer>.SearchSpec ASSIGNEE_SPEC =
       ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE);
 
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 6ddf7a3..82b8f18 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -240,6 +240,7 @@
           .build();
 
   /** Remove assignee field. */
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V82 =
       new Schema.Builder<ChangeData>()
           .add(V81)
diff --git a/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java b/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
index 60dff84..e91f7b7 100644
--- a/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
+++ b/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
@@ -72,13 +72,15 @@
     if (!ready) {
       synchronized (dataDir) {
         if (!ready) {
-          try {
-            Files.createDirectories(dataDir);
-          } catch (IOException e) {
-            throw new ProvisionException(
-                String.format(
-                    "Cannot create %s for plugin %s", dataDir.toAbsolutePath(), plugin.getName()),
-                e);
+          if (!Files.isDirectory(dataDir)) {
+            try {
+              Files.createDirectories(dataDir);
+            } catch (IOException e) {
+              throw new ProvisionException(
+                  String.format(
+                      "Cannot create %s for plugin %s", dataDir.toAbsolutePath(), plugin.getName()),
+                  e);
+            }
           }
           ready = true;
         }
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 62da2f2..dd0ec78d 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -34,6 +34,7 @@
         "//lib/auto:auto-factory",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/commons:codec",
         "//lib/commons:compress",
         "//lib/commons:lang3",
         "//lib/errorprone:annotations",
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
index d4f549a..4021f77 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
@@ -23,6 +23,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.api.errors.PatchApplyException;
 import org.eclipse.jgit.api.errors.PatchFormatException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -51,8 +52,12 @@
       throws IOException, RestApiException {
     checkNotNull(mergeTip);
     RevTree tip = mergeTip.getTree();
-    InputStream patchStream =
-        new ByteArrayInputStream(input.patch.getBytes(StandardCharsets.UTF_8));
+    InputStream patchStream;
+    if (Base64.isBase64(input.patch)) {
+      patchStream = new ByteArrayInputStream(org.eclipse.jgit.util.Base64.decode(input.patch));
+    } else {
+      patchStream = new ByteArrayInputStream(input.patch.getBytes(StandardCharsets.UTF_8));
+    }
     try {
       PatchApplier applier = new PatchApplier(repo, tip, oi);
       PatchApplier.Result applyResult = applier.applyPatch(patchStream);
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..1144e4f 100644
--- a/java/com/google/gerrit/server/update/context/RefUpdateContext.java
+++ b/java/com/google/gerrit/server/update/context/RefUpdateContext.java
@@ -22,7 +22,7 @@
 import java.util.Deque;
 
 /**
- * Passes additional information about an operation to the {@link BatchRefUpdate#execute} method.
+ * Passes additional information about an operation to the {@code BatchRefUpdate#execute} method.
  *
  * <p>To pass the additional information {@link RefUpdateContext}, wraps a code into an open
  * RefUpdateContext, e.g.:
@@ -34,7 +34,7 @@
  * }
  * }</pre>
  *
- * When the {@link BatchRefUpdate#execute} method is executed, it can get all opened contexts and
+ * When the {@code BatchRefUpdate#execute} method is executed, it can get all opened contexts and
  * use it for an additional actions, e.g. it can put it in the reflog.
  *
  * <p>The information provided by this class is used internally in google.
@@ -42,7 +42,7 @@
  * <p>The InMemoryRepositoryManager file makes some validation to ensure that RefUpdateContext is
  * used correctly within the code (see thee validateRefUpdateContext method).
  *
- * <p>The class includes only operations from open-source gerrit and can be extended (see {@link
+ * <p>The class includes only operations from open-source gerrit and can be extended (see {@code
  * TestActionRefUpdateContext} for example how to extend it).
  */
 public class RefUpdateContext implements AutoCloseable {
@@ -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..8d1130c 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);
@@ -168,7 +171,7 @@
       private RefUpdateContextValidator addSpecialRef(
           Predicate<String> refNamePredicate, RefUpdateType... validRefUpdateTypes) {
         specialRefs.add(
-            new SimpleImmutableEntry<Predicate<String>, ImmutableList<RefUpdateType>>(
+            new SimpleImmutableEntry<>(
                 refNamePredicate, ImmutableList.copyOf(validRefUpdateTypes)));
         return this;
       }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index 898e1ff..0b55563 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -213,6 +213,29 @@
   }
 
   @Test
+  public void applyGerritBasedPatchUsingRestWithEncodedPatch_success() throws Exception {
+    String head = getHead(repo(), HEAD).name();
+    createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
+    PushOneCommit.Result baseCommit = createChange("Add file", ADDED_FILE_NAME, ADDED_FILE_CONTENT);
+    baseCommit.assertOkStatus();
+    createBranchWithRevision(BranchNameKey.create(project, DESTINATION_BRANCH), head);
+    RestResponse patchResp =
+        userRestSession.get("/changes/" + baseCommit.getChangeId() + "/revisions/current/patch");
+    patchResp.assertOK();
+    String originalEncodedPatch = patchResp.getEntityContent();
+    String originalDecodedPatch = new String(Base64.decode(patchResp.getEntityContent()), UTF_8);
+    ApplyPatchPatchSetInput in = buildInput(originalEncodedPatch);
+    PushOneCommit.Result destChange = createChange();
+
+    RestResponse resp =
+        adminRestSession.post("/changes/" + destChange.getChangeId() + "/patch:apply", in);
+
+    resp.assertOK();
+    BinaryResult resultPatch = gApi.changes().id(destChange.getChangeId()).current().patch();
+    assertThat(removeHeader(resultPatch)).isEqualTo(removeHeader(originalDecodedPatch));
+  }
+
+  @Test
   public void applyPatchWithConflict_fails() throws Exception {
     initBaseWithFile(MODIFIED_FILE_NAME, "Unexpected base content");
     ApplyPatchPatchSetInput in = buildInput(MODIFIED_FILE_DIFF);
@@ -404,6 +427,6 @@
   }
 
   private String removeHeader(String s) {
-    return s.substring(s.indexOf("\ndiff --git"), s.length() - 1);
+    return s.substring(s.lastIndexOf("\ndiff --git"), s.length() - 1);
   }
 }
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/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index b096f76..e5991f6 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -500,11 +500,12 @@
   }
 
   private renderEditorView() {
-    // The `cache()` is required for re-using the editor view when switching
-    // back and forth between change, diff and editor views.
-    return cache(
-      this.isEditorView() ? html`<gr-editor-view></gr-editor-view>` : nothing
-    );
+    // For some reason caching the editor view caused an issue (b/269308770).
+    // We did not bother to root cause that issue, but instead let's forgo
+    // caching of the editor view. It does not help much anyway.
+    return this.isEditorView()
+      ? html`<gr-editor-view></gr-editor-view>`
+      : nothing;
   }
 
   private isEditorView() {
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..f8f7f9d 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:
@@ -437,10 +435,11 @@
       return;
     }
 
+    const requestText = this.text;
     const update = () => {
       query(this.text)
         .then(suggestions => {
-          if (this.text !== this.text) {
+          if (requestText !== this.text) {
             // Late response.
             return;
           }
@@ -462,15 +461,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/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
index 13a6b00..b97ae59 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -160,6 +160,8 @@
         ${this.renderLineNumberCell(Side.RIGHT)}
         ${this.renderSignCell(Side.RIGHT)} ${this.renderContentCell(Side.RIGHT)}
       </tr>
+      ${this.renderPostLineSlot(Side.LEFT)}
+      ${this.renderPostLineSlot(Side.RIGHT)}
     `;
     if (this.addTableWrapperForTesting) {
       return html`<table>
@@ -456,6 +458,13 @@
         id=${this.contentId(side)}
       >${textElement}</div>`;
   }
+
+  private renderPostLineSlot(side: Side) {
+    const lineNumber = this.lineNumber(side);
+    return lineNumber && Number.isInteger(lineNumber)
+      ? html`<slot name="post-${side}-line-${lineNumber}"></slot>`
+      : nothing;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
index 1c7b311..42d30aa 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
@@ -87,6 +87,8 @@
                 </div>
               </td>
             </tr>
+            <slot name="post-left-line-1"></slot>
+            <slot name="post-right-line-1"></slot>
           </tbody>
         </table>
       `
@@ -147,6 +149,8 @@
                 </div>
               </td>
             </tr>
+            <slot name="post-left-line-1"></slot>
+            <slot name="post-right-line-1"></slot>
           </tbody>
         </table>
       `
@@ -201,6 +205,7 @@
                   <slot name="right-1"> </slot>
                 </div>
               </td>
+              <slot name="post-right-line-1"></slot>
             </tr>
           </tbody>
         </table>
@@ -257,6 +262,7 @@
                 <div class="contentText gr-diff" data-side="right"></div>
               </td>
             </tr>
+            <slot name="post-left-line-1"></slot>
           </tbody>
         </table>
       `
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
index 14f4536..381f9b2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
@@ -126,8 +126,14 @@
       element,
       /* HTML */ `
         <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+        <slot name="post-left-line-1"></slot>
+        <slot name="post-right-line-1"></slot>
         <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+        <slot name="post-left-line-1"></slot>
+        <slot name="post-right-line-1"></slot>
         <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+        <slot name="post-left-line-1"></slot>
+        <slot name="post-right-line-1"></slot>
         <table>
           <tbody class="both gr-diff section">
             <tr
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index a4bc98e..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:
@@ -55,24 +60,23 @@
   private timerId?: number;
 
   /**
-   * Promise that is resolved after the callback is run.
-   * If the task is cancelled the promise is rejected instead.
+   * Promise that is resolved after the callback is run or the task is
+   * cancelled.
    */
-  public readonly promise: Promise<void>;
+  public readonly promise: Promise<ResolvedDelayedTaskStatus>;
 
-  private rejectPromise?: () => void;
-
-  private resolvePromise?: () => void;
+  private resolvePromise?: (
+    value: ResolvedDelayedTaskStatus | PromiseLike<ResolvedDelayedTaskStatus>
+  ) => void;
 
   constructor(private callback: () => void, waitMs = 0) {
-    this.promise = new Promise((resolve, reject) => {
-      this.rejectPromise = reject;
+    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();
+        resolve(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
       }, waitMs);
       _testOnly_allTasks.set(this.timerId, this);
     });
@@ -87,7 +91,7 @@
   cancel() {
     if (this.isActive()) {
       this.cancelTimer();
-      this.rejectPromise?.();
+      this.resolvePromise?.(ResolvedDelayedTaskStatus.TASK_CANCELLED);
     }
   }
 
@@ -95,7 +99,7 @@
     if (this.isActive()) {
       this.cancelTimer();
       if (this.callback) this.callback();
-      this.resolvePromise?.();
+      this.resolvePromise?.(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
     }
   }