Merge "Test autoclosing changes for commits that are pushed as merge parents"
diff --git a/.bazelrc b/.bazelrc
index fe7d6dc..9662078 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -7,31 +7,68 @@
 build --action_env=PATH
 build --disk_cache=~/.gerritcodereview/bazel-cache/cas
 
-# Builds using remotejdk_11, executes using remotejdk_11 or local_jdk
-build:java11 --java_language_version=11
-build:java11 --java_runtime_version=remotejdk_11
-build:java11 --tool_java_language_version=11
-build:java11 --tool_java_runtime_version=remotejdk_11
+# Define configuration using remotejdk_17, executes using remotejdk_17 or local_jdk
+build:build_shared --java_language_version=17
+build:build_shared --java_runtime_version=remotejdk_17
+build:build_shared --tool_java_language_version=17
+build:build_shared --tool_java_runtime_version=remotejdk_17
 
 # Builds using remotejdk_17, executes using remotejdk_17 or local_jdk
+# Avoid warnings for non default configurations:
+# build --config=build_shared
 build --java_language_version=17
 build --java_runtime_version=remotejdk_17
 build --tool_java_language_version=17
 build --tool_java_runtime_version=remotejdk_17
 
-# Builds and executes on RBE using remotejdk_11
-build:remote11 --java_language_version=11
-build:remote11 --java_runtime_version=remotejdk_11
-build:remote11 --tool_java_language_version=11
-build:remote11 --tool_java_runtime_version=remotejdk_11
-build:remote11 --config=remote_shared
+# Builds and executes on Google GCP RBE using remotejdk_17
+build:remote --config=config_gcp
+build:remote --config=build_shared
 
-# Builds and executes on RBE using remotejdk_17
-build:remote --java_language_version=17
-build:remote --java_runtime_version=remotejdk_17
-build:remote --tool_java_language_version=17
-build:remote --tool_java_runtime_version=remotejdk_17
-build:remote --config=remote_shared
+# Define remote configuration alias
+build:remote_gcp --config=remote
+
+# Builds and executes on BuildBuddy RBE using remotejdk_17
+build:remote_bb --config=config_bb
+build:remote_bb --config=build_shared
+
+# Define configuration using remotejdk_11, executes using remotejdk_11 or local_jdk
+build:build_java11_shared --java_language_version=11
+build:build_java11_shared --java_runtime_version=remotejdk_11
+build:build_java11_shared --tool_java_language_version=11
+build:build_java11_shared --tool_java_runtime_version=remotejdk_11
+
+build:java11 --config=build_java11_shared
+
+# Builds and executes on Google GCP RBE using remotejdk_11
+build:remote11 --config=config_gcp
+build:remote11 --config=build_java11_shared
+
+# Define remote11 configuration alias
+build:remote11_gcp --config=remote11
+
+# Builds and executes on BuildBuddy RBE using remotejdk_11
+build:remote11_bb --config=config_bb
+build:remote11_bb --config=build_java11_shared
+
+# Builds using remotejdk_21, executes using remotejdk_21 or local_jdk
+build:build_java21_shared --java_language_version=21
+build:build_java21_shared --java_runtime_version=remotejdk_21
+build:build_java21_shared --tool_java_language_version=21
+build:build_java21_shared --tool_java_runtime_version=remotejdk_21
+
+build:java21 --config=build_java21_shared
+
+# Builds and executes on RBE using remotejdk_21
+build:remote21 --config=config_gcp
+build:remote21 --config=build_java21_shared
+
+# Define remote21 configuration alias
+build:remote21_gcp --config=remote21
+
+# Builds and executes on BuildBuddy RBE using remotejdk_11
+build:remote21_bb --config=config_bb
+build:remote21_bb --config=build_java21_shared
 
 # Enable modern C++ features
 build --cxxopt=-std=c++17
@@ -50,3 +87,6 @@
 test --test_output=errors
 
 import %workspace%/tools/remote-bazelrc
+
+# User-specific .bazelrc
+try-import %workspace%/user.bazelrc
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 4a97d9a..4c224b5 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -725,6 +725,21 @@
 ```
 
 
+== BuildBuddy Remote Build Support
+
+To utilize the BuildBuddy Remote Build Execution service, please consult the
+documentation available at the following link: https://www.buildbuddy.io[BuildBuddy].
+
+To use RBE, execute
+
+```
+bazelisk test --config=remote_bb \
+    --remote_instance_name=projects/${PROJECT}/instances/default_instance \
+    --remote_header=x-buildbuddy-api-key=YOUR_API_KEY \
+    javatests/...
+```
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/WORKSPACE b/WORKSPACE
index b2e2d2d..e340eda 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -32,6 +32,14 @@
 load("//tools:deps.bzl", "CAFFEINE_VERS", "java_dependencies")
 
 http_archive(
+    name = "rules_java",
+    sha256 = "4018e97c93f97680f1650ffd2a7530245b864ac543fd24fae8c02ba447cb2864",
+    urls = [
+        "https://github.com/bazelbuild/rules_java/releases/download/7.3.1/rules_java-7.3.1.tar.gz",
+    ],
+)
+
+http_archive(
     name = "platforms",
     sha256 = "3a561c99e7bdbe9173aa653fd579fe849f1d8d67395780ab4770b1f381431d51",
     urls = [
@@ -111,6 +119,8 @@
 
 register_toolchains("//tools:error_prone_warnings_toolchain_java17_definition")
 
+register_toolchains("//tools:error_prone_warnings_toolchain_java21_definition")
+
 # Java-Prettify external repository consumed from git submodule
 local_repository(
     name = "java-prettify",
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index f3881f2..2bdef08 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -87,7 +87,7 @@
     List<ExternalId> extIds = new ArrayList<>(2);
     String httpPass = null;
     if (username != null) {
-      httpPass = "http-pass";
+      httpPass = externalIdFactory.arePasswordsAllowed() ? "http-pass" : null;
       extIds.add(externalIdFactory.createUsername(username, id, httpPass));
     }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index edbb1ee..5295b8f 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -66,7 +66,8 @@
 
   @Override
   public TestAccountCreation.Builder newAccount() {
-    return TestAccountCreation.builder(this::createAccount);
+    return TestAccountCreation.builder(
+        this::createAccount, externalIdFactory.arePasswordsAllowed());
   }
 
   private Account.Id createAccount(TestAccountCreation testAccountCreation) throws Exception {
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
index 042dc9a..16f8903 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -41,10 +41,15 @@
 
   abstract ThrowingFunction<TestAccountCreation, Account.Id> accountCreator();
 
-  public static Builder builder(ThrowingFunction<TestAccountCreation, Account.Id> accountCreator) {
-    return new AutoValue_TestAccountCreation.Builder()
-        .accountCreator(accountCreator)
-        .httpPassword("http-pass");
+  public static Builder builder(
+      ThrowingFunction<TestAccountCreation, Account.Id> accountCreator,
+      boolean arePasswordsAllowed) {
+    TestAccountCreation.Builder builder =
+        new AutoValue_TestAccountCreation.Builder().accountCreator(accountCreator);
+    if (arePasswordsAllowed) {
+      builder.httpPassword("http-pass");
+    }
+    return builder;
   }
 
   @AutoValue.Builder
diff --git a/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java b/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java
index d14aa97..6e8c907 100644
--- a/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java
@@ -29,7 +29,10 @@
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 
-/** Proto converter between {@link HumanComment} and {@link Entities.HumanComment}. */
+/**
+ * Proto converter between {@link HumanComment} and {@link
+ * com.google.gerrit.proto.Entities.HumanComment}.
+ */
 @Immutable
 public enum HumanCommentProtoConverter
     implements ProtoConverter<Entities.HumanComment, HumanComment> {
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index d8fd727..7761a65 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -130,20 +130,12 @@
     setReadyForReview(null);
   }
 
-  /**
-   * Create a new change that reverts this change.
-   *
-   * @see Changes#id(int)
-   */
+  /** Create a new change that reverts this change. */
   default ChangeApi revert() throws RestApiException {
     return revert(new RevertInput());
   }
 
-  /**
-   * Create a new change that reverts this change.
-   *
-   * @see Changes#id(int)
-   */
+  /** Create a new change that reverts this change. */
   ChangeApi revert(RevertInput in) throws RestApiException;
 
   default RevertSubmissionInfo revertSubmission() throws RestApiException {
diff --git a/java/com/google/gerrit/index/query/FilteredSource.java b/java/com/google/gerrit/index/query/FilteredSource.java
index fb31eb6..9746850 100644
--- a/java/com/google/gerrit/index/query/FilteredSource.java
+++ b/java/com/google/gerrit/index/query/FilteredSource.java
@@ -84,6 +84,11 @@
     return cardinality;
   }
 
+  /**
+   * Whether this data source matches with the given object.
+   *
+   * @param object object to be matched
+   */
   protected boolean match(T object) {
     return true;
   }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
index d226565..00a7f6c 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -127,4 +127,7 @@
    * @return the created external ID
    */
   ExternalId createEmail(Account.Id accountId, String email);
+
+  /** Whether this {@link ExternalIdFactory} supports passwords. */
+  boolean arePasswordsAllowed();
 }
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
index 3462c76..d3de715 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
@@ -223,6 +223,11 @@
         blobId);
   }
 
+  @Override
+  public boolean arePasswordsAllowed() {
+    return true;
+  }
+
   private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
       throws ConfigInvalidException {
     String accountIdStr =
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
index edc5fc8..db6fdce 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account.externalids.storage.notedb;
 
-import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static java.util.stream.Collectors.joining;
 
@@ -24,7 +23,6 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.HashedPassword;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -53,14 +51,11 @@
       GitRepositoryManager repoManager,
       AllUsersName allUsers,
       OutgoingEmailValidator validator,
-      ExternalIdFactory externalIdFactory) {
+      ExternalIdFactoryNoteDbImpl externalIdFactory) {
     this.repoManager = repoManager;
     this.allUsers = allUsers;
     this.validator = validator;
-    checkState(
-        externalIdFactory instanceof ExternalIdFactoryNoteDbImpl,
-        "ExternalIdsConsistencyCheckerNoteDbImpl must be initiated with ExternalIdFactoryNoteDbImpl.");
-    this.externalIdFactory = (ExternalIdFactoryNoteDbImpl) externalIdFactory;
+    this.externalIdFactory = externalIdFactory;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
index 265d036..6f0e7aa 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
@@ -156,7 +156,7 @@
     }
 
     @Override
-    public AccountsUpdate create(IdentifiedUser currentUser) {
+    public AccountsUpdateNoteDbImpl create(IdentifiedUser currentUser) {
       PersonIdent serverIdent = serverIdentProvider.get();
       return new AccountsUpdateNoteDbImpl(
           repoManager,
@@ -173,7 +173,7 @@
     }
 
     @Override
-    public AccountsUpdate createWithServerIdent() {
+    public AccountsUpdateNoteDbImpl createWithServerIdent() {
       PersonIdent serverIdent = serverIdentProvider.get();
       return new AccountsUpdateNoteDbImpl(
           repoManager,
diff --git a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index 073f8b1..c76eeeb 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -263,6 +263,7 @@
     input.email = email;
     input.type = type;
     try {
+      @SuppressWarnings("unused")
       var unused = modifyIdentity.apply(changeResource, input);
     } catch (Exception e) {
       throw asRestApiException("Cannot edit identity of change", e);
diff --git a/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
index 878d257..3ca9f99 100644
--- a/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.git.ChangesByProjectCache.UseIndex;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
@@ -95,7 +94,7 @@
     if (projectChanges != null) {
       return projectChanges
           .getUpdatedChangeDatas(
-              project, repo, cdFactory, ChangeNotes.Factory.scanChangeIds(repo), "Updating")
+              project, cdFactory, ChangeNotes.Factory.scanChangeIds(repo), "Updating")
           .stream();
     }
     if (UseIndex.TRUE.equals(useIndex)) {
@@ -115,7 +114,6 @@
     }
     return projectChanges.getUpdatedChangeDatas(
         project,
-        repo,
         cdFactory,
         ChangeNotes.Factory.scanChangeIds(repo),
         ours == projectChanges ? "Scanning" : "Updating");
@@ -152,7 +150,6 @@
 
     public Collection<ChangeData> getUpdatedChangeDatas(
         Project.NameKey project,
-        Repository repo,
         ChangeData.Factory cdFactory,
         Map<Change.Id, ObjectId> metaObjectIdByChange,
         String operation) {
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
index c0c5f56..185a6ac 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
@@ -17,6 +17,8 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.hash.HashCode;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -152,7 +154,7 @@
                 + "\n[[[Original patch trimmed due to size. Decoded string size: "
                 + patchDescription.length()
                 + ". Decoded string SHA1: "
-                + Hashing.sha1().hashString(patchDescription, UTF_8)
+                + sha1(patchDescription)
                 + ".]]]");
       }
     }
@@ -166,7 +168,7 @@
                 + "\n[[[Result patch trimmed due to size. Decoded string size: "
                 + resultPatch.length()
                 + ". Decoded string SHA1: "
-                + Hashing.sha1().hashString(resultPatch, UTF_8)
+                + sha1(resultPatch)
                 + ".]]]");
       }
     }
@@ -219,5 +221,11 @@
     return patch;
   }
 
+  @SuppressWarnings("deprecation")
+  @VisibleForTesting
+  public static HashCode sha1(String s) {
+    return Hashing.sha1().hashString(s, UTF_8);
+  }
+
   private ApplyPatchUtil() {}
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index f242cdb..0367080 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -28,7 +28,6 @@
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.hash.Hashing;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
@@ -57,6 +56,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.restapi.change.ApplyPatchUtil;
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -363,7 +363,7 @@
                 + "\n[[[Original patch trimmed due to size. Decoded string size: "
                 + removePatchHeader(patch).length()
                 + ". Decoded string SHA1: "
-                + Hashing.sha1().hashString(removePatchHeader(patch), UTF_8)
+                + ApplyPatchUtil.sha1(removePatchHeader(patch))
                 + ".]]]"
                 + "\n\nChange-Id: "
                 + result.changeId
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java b/javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java
index fe195f5..80eec84 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java
@@ -73,7 +73,7 @@
    */
   protected RevCommit createChangeWithoutPush(
       String changeId, ImmutableMap<String, String> files, RevCommit... parents) throws Exception {
-    TestRepository.CommitBuilder commitBuilder =
+    TestRepository<?>.CommitBuilder commitBuilder =
         testRepo
             .commit()
             .message("Change " + changeId)
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
index e69d54b..ba881b6 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
@@ -40,10 +40,11 @@
  *  ----->base commit <------
  * }</pre>
  *
- * Tests use only MergeAlways strategy. All other submit strategies (except cherry pick) use the
- * same checks on submit. The {@link ImplicitMergeOnSubmitExperimentsIT} validates that the implicit
- * merge check is applied to all strategies (except cherry pick) and {@link
- * ImplicitMergeOnSubmitCherryPickIT} contains tests for the cherry pick strategy.
+ * Tests use only MergeAlways strategy. All other submit strategies (except cherry pick and rebase
+ * always) use the same checks on submit. The {@link ImplicitMergeOnSubmitExperimentsIT} validates
+ * that the implicit merge check is applied to all strategies (except cherry pick and rebase always)
+ * and {@link ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT} contains tests for the cherry pick
+ * and rebase always strategies.
  */
 public class ImplicitMergeOnSubmitIT extends AbstractImplicitMergeTest {
   private RevCommit masterTip;
diff --git a/javatests/com/google/gerrit/index/query/QueryProcessorTest.java b/javatests/com/google/gerrit/index/query/QueryProcessorTest.java
index cb552a6..38c610a 100644
--- a/javatests/com/google/gerrit/index/query/QueryProcessorTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryProcessorTest.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.index.SchemaDefinitions;
 import java.util.function.IntSupplier;
 import org.junit.Test;
+import org.mockito.Mock;
 
 public class QueryProcessorTest {
 
@@ -38,13 +39,16 @@
 
   private String limitField = null;
 
-  public QueryProcessor createProcessor() {
+  @Mock private SchemaDefinitions<String> schemaDef;
+
+  @Mock private IndexCollection<?, String, ?> indexes;
+
+  @Mock private IndexRewriter<String> rewriter;
+
+  public QueryProcessor<String> createProcessor() {
     QueryProcessor.Metrics metrics = mock(QueryProcessor.Metrics.class);
-    SchemaDefinitions schemaDef = mock(SchemaDefinitions.class);
     IndexConfig indexConfig =
         IndexConfig.builder().maxLimit(maxLimit).defaultLimit(defaultLimit).build();
-    IndexCollection indexes = mock(IndexCollection.class);
-    IndexRewriter rewriter = mock(IndexRewriter.class);
     IntSupplier userQueryLimit =
         new IntSupplier() {
           @Override
@@ -53,8 +57,8 @@
           }
         };
 
-    QueryProcessor processor =
-        new QueryProcessor<String>(
+    QueryProcessor<String> processor =
+        new QueryProcessor<>(
             metrics, schemaDef, indexConfig, indexes, rewriter, limitField, userQueryLimit) {
           @Override
           protected Predicate<String> enforceVisibility(Predicate<String> pred) {
@@ -107,7 +111,7 @@
   @Test
   public void getEffectiveLimit_LimitField() throws QueryParseException {
     limitField = "limit";
-    assertThat(createProcessor().getEffectiveLimit(new LimitPredicate(limitField, 314)))
+    assertThat(createProcessor().getEffectiveLimit(new LimitPredicate<>(limitField, 314)))
         .isEqualTo(314);
   }
 
@@ -116,12 +120,12 @@
     limitField = "limit";
     int[] limits = {271, 314, 499, 666};
 
-    LimitPredicate p = null;
+    LimitPredicate<String> p = null;
     for (int i = 0; i < 4; i++) {
       userProvidedLimit = limits[0];
       userQueryLimit = limits[1];
       maxLimit = limits[2];
-      p = new LimitPredicate(limitField, limits[3]);
+      p = new LimitPredicate<>(limitField, limits[3]);
 
       // "rotate" the array of limits
       int l = limits[0];
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
index 7d6d17d..0af614d 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
@@ -280,7 +280,7 @@
     filterActive = false
   ) {
     return this.restApiService
-      .getSuggestedAccounts(
+      .queryAccounts(
         input,
         SUGGESTIONS_LIMIT,
         canSee,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
index a3c7bbd..c16a108 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
@@ -93,7 +93,7 @@
       },
     ];
 
-    stubRestApi('getSuggestedAccounts').callsFake(input => {
+    stubRestApi('queryAccounts').callsFake(input => {
       if (input.startsWith('test')) {
         return Promise.resolve([
           {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 634fbf8..2ac92af 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -59,7 +59,12 @@
   isSectionSet,
   DisplayRules,
 } from '../../../utils/change-metadata-util';
-import {fireAlert, fire, fireReload} from '../../../utils/event-util';
+import {
+  fireAlert,
+  fire,
+  fireReload,
+  fireError,
+} from '../../../utils/event-util';
 import {
   EditRevisionInfo,
   isDefined,
@@ -89,6 +94,7 @@
 import {changeModelToken} from '../../../models/change/change-model';
 import {relatedChangesModelToken} from '../../../models/change/related-changes-model';
 import {truncatePath} from '../../../utils/path-list-util';
+import {accountEmail, getDisplayName} from '../../../utils/display-name-util';
 
 const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
@@ -134,6 +140,8 @@
 
   @state() revertedChange?: ChangeInfo;
 
+  @state() editMode = false;
+
   @state() account?: AccountDetailInfo;
 
   @state() revision?: RevisionInfo | EditRevisionInfo;
@@ -209,6 +217,11 @@
       () => this.getRelatedChangesModel().revertingChange$,
       revertingChange => (this.revertedChange = revertingChange)
     );
+    subscribe(
+      this,
+      () => this.getChangeModel().editMode$,
+      x => (this.editMode = x)
+    );
     this.queryTopic = (input: string) => this.getTopicSuggestions(input);
     this.queryHashtag = (input: string) => this.getHashtagSuggestions(input);
   }
@@ -231,6 +244,9 @@
         gr-weblink {
           display: block;
         }
+        gr-account-chip {
+          display: inline;
+        }
         gr-account-chip[disabled],
         gr-linked-chip[disabled] {
           opacity: 0;
@@ -471,6 +487,21 @@
             circle-shape
           ></gr-vote-chip>
         </gr-account-chip>
+        ${when(
+          this.editMode &&
+            (role === ChangeRole.AUTHOR || role === ChangeRole.COMMITTER),
+          () => html`
+            <gr-editable-label
+              id="${role}-edit-label"
+              placeholder="Update ${name}"
+              @changed=${(e: CustomEvent<string>) =>
+                this.handleIdentityChanged(e, role)}
+              showAsEditPencil
+              autocomplete
+              .query=${(text: string) => this.getIdentitySuggestions(text)}
+            ></gr-editable-label>
+          `
+        )}
       </span>
     </section>`;
   }
@@ -865,6 +896,31 @@
   }
 
   // private but used in test
+  async handleIdentityChanged(e: CustomEvent<string>, role: ChangeRole) {
+    assertIsDefined(this.change, 'change');
+    const input = e.detail.length ? e.detail.trim() : undefined;
+    if (!input?.length) return;
+    const reg = /(\w+.*)\s<(\S+@\S+.\S+)>/;
+    const [, name, email] = input.match(reg) ?? [];
+    if (!name || !email) {
+      fireError(
+        this,
+        'Invalid input format, valid identity format is "FullName <user@example.com>"'
+      );
+      return;
+    }
+    fireAlert(this, 'Saving identity and reloading ...');
+    await this.restApiService.updateIdentityInChangeEdit(
+      this.change._number,
+      name,
+      email,
+      role.toUpperCase()
+    );
+    fire(this, 'hide-alert', {});
+    fireReload(this);
+  }
+
+  // private but used in test
   computeTopicReadOnly() {
     return !this.mutable || !this.change?.actions?.topic?.enabled;
   }
@@ -1140,7 +1196,7 @@
     if (
       role === ChangeRole.AUTHOR &&
       rev.commit?.author &&
-      this.change.owner.email !== rev.commit.author.email
+      (this.editMode || this.change.owner.email !== rev.commit.author.email)
     ) {
       return rev.commit.author;
     }
@@ -1148,10 +1204,12 @@
     if (
       role === ChangeRole.COMMITTER &&
       rev.commit?.committer &&
-      this.change.owner.email !== rev.commit.committer.email &&
-      !(
-        rev.uploader?.email && rev.uploader.email === rev.commit.committer.email
-      )
+      (this.editMode ||
+        (this.change.owner.email !== rev.commit.committer.email &&
+          !(
+            rev.uploader?.email &&
+            rev.uploader.email === rev.commit.committer.email
+          )))
     ) {
       return rev.commit.committer;
     }
@@ -1227,6 +1285,25 @@
       );
   }
 
+  private async getIdentitySuggestions(
+    input: string
+  ): Promise<AutocompleteSuggestion[]> {
+    const suggestions = await this.restApiService.getAccountSuggestions(input);
+    if (!suggestions) return [];
+    const identitySuggestions: AutocompleteSuggestion[] = [];
+    suggestions.forEach(account => {
+      const name = getDisplayName(this.serverConfig, account);
+      const emails: string[] = [];
+      account.email && emails.push(account.email);
+      account.secondary_emails && emails.push(...account.secondary_emails);
+      emails.forEach(email => {
+        const identity = name + ' ' + accountEmail(email);
+        identitySuggestions.push({name: identity});
+      });
+    });
+    return identitySuggestions;
+  }
+
   private computeVoteForRole(role: ChangeRole) {
     const reviewer = this.getNonOwnerRole(role);
     if (reviewer && isAccount(reviewer)) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 93ef3e3..309ad2d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -407,6 +407,15 @@
         element.change = change;
         assert.isNotOk(element.getNonOwnerRole(ChangeRole.COMMITTER));
       });
+
+      test('getNonOwnerRole returns committer with same email as owner in edit mode', () => {
+        // Set the committer email to be the same as the owner.
+        change!.revisions.rev1.commit!.committer.email =
+          'abc@def' as EmailAddress;
+        element.change = change;
+        element.editMode = true;
+        assert.isOk(element.getNonOwnerRole(ChangeRole.COMMITTER));
+      });
     });
 
     suite('role=author', () => {
@@ -430,6 +439,14 @@
         element.change = change;
         assert.isNotOk(element.getNonOwnerRole(ChangeRole.AUTHOR));
       });
+
+      test('getNonOwnerRole returns author with same email as owner in edit mode', () => {
+        // Set the author email to be the same as the owner.
+        change!.revisions.rev1.commit!.author.email = 'abc@def' as EmailAddress;
+        element.change = change;
+        element.editMode = true;
+        assert.isOk(element.getNonOwnerRole(ChangeRole.AUTHOR));
+      });
     });
   });
 
@@ -938,6 +955,35 @@
     });
   });
 
+  test('update author identity', async () => {
+    const change = createParsedChange();
+    element.change = change;
+    element.editMode = true;
+    await element.updateComplete;
+    const updateIdentityInChangeEditStub = stubRestApi(
+      'updateIdentityInChangeEdit'
+    ).resolves();
+    const alertStub = sinon.stub();
+    element.addEventListener('show-alert', alertStub);
+    queryAndAssert(element, '#author-edit-label').dispatchEvent(
+      new CustomEvent('changed', {detail: 'user <user@example.com>'})
+    );
+    assert.isTrue(
+      updateIdentityInChangeEditStub.calledWith(
+        42 as NumericChangeId,
+        'user',
+        'user@example.com',
+        'AUTHOR'
+      )
+    );
+    await updateIdentityInChangeEditStub.lastCall.returnValue;
+    await waitUntilCalled(alertStub, 'alertStub');
+    assert.deepEqual(alertStub.lastCall.args[0].detail, {
+      message: 'Saving identity and reloading ...',
+      showDismiss: true,
+    });
+  });
+
   test('editTopic', async () => {
     element.account = createAccountDetailWithId();
     element.change = {
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
index 8b4b52f..48f5a05 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -172,7 +172,7 @@
       return Promise.resolve([]);
     }
     return this.restApiService
-      .getSuggestedAccounts(
+      .queryAccounts(
         expression,
         MAX_AUTOCOMPLETE_RESULTS,
         /* canSee=*/ undefined,
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
index 7e3b896..0551f23 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
@@ -25,7 +25,7 @@
   });
 
   test('Autocompletes accounts', () => {
-    stubRestApi('getSuggestedAccounts').callsFake(() =>
+    stubRestApi('queryAccounts').callsFake(() =>
       Promise.resolve([
         {
           name: 'fred',
@@ -39,7 +39,7 @@
   });
 
   test('Inserts self as option when valid', () => {
-    stubRestApi('getSuggestedAccounts').callsFake(() =>
+    stubRestApi('queryAccounts').callsFake(() =>
       Promise.resolve([
         {
           name: 'fred',
@@ -60,7 +60,7 @@
   });
 
   test('Inserts me as option when valid', () => {
-    stubRestApi('getSuggestedAccounts').callsFake(() =>
+    stubRestApi('queryAccounts').callsFake(() =>
       Promise.resolve([
         {
           name: 'fred',
@@ -118,7 +118,7 @@
   });
 
   test('Autocompletes accounts with no email', () => {
-    stubRestApi('getSuggestedAccounts').callsFake(() =>
+    stubRestApi('queryAccounts').callsFake(() =>
       Promise.resolve([{name: 'fred'}])
     );
     return element.fetchAccounts('owner', 'fr').then(s => {
@@ -127,7 +127,7 @@
   });
 
   test('Autocompletes accounts with email', () => {
-    stubRestApi('getSuggestedAccounts').callsFake(() =>
+    stubRestApi('queryAccounts').callsFake(() =>
       Promise.resolve([{email: 'fred@goog.co' as EmailAddress}])
     );
     return element.fetchAccounts('owner', 'fr').then(s => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 91fd7e3..405f476 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -936,13 +936,12 @@
           <gr-endpoint-param
             name="onTrigger"
             .value=${(pluginName: string) => {
-              this.shownSidebar =
-                this.shownSidebar === pluginName ? undefined : pluginName;
+              const closeSidebar = this.shownSidebar === pluginName;
+              this.shownSidebar = closeSidebar ? undefined : pluginName;
               this.getUserModel().updatePreferences({
-                diff_page_sidebar:
-                  this.shownSidebar === pluginName
-                    ? 'NONE'
-                    : `plugin-${pluginName}`,
+                diff_page_sidebar: closeSidebar
+                  ? 'NONE'
+                  : `plugin-${pluginName}`,
               });
             }}
           ></gr-endpoint-param>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 7972cc1..7f70911 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -606,7 +606,7 @@
   // TODO(dhruvsri): merge with getAccountSuggestions in account-util
   async computeReviewerSuggestions(): Promise<Item[]> {
     return (
-      (await this.restApiService.getSuggestedAccounts(
+      (await this.restApiService.queryAccounts(
         this.currentSearchString ?? '',
         /* number= */ 15,
         this.changeNum,
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 4aef66e..d84f5a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -59,7 +59,7 @@
       // updated.
       const listenerStub = sinon.stub();
       element.addEventListener('text-changed', listenerStub);
-      stubRestApi('getSuggestedAccounts').returns(
+      stubRestApi('queryAccounts').returns(
         Promise.resolve([
           createAccountWithEmail('abc@google.com'),
           createAccountWithEmail('abcdef@google.com'),
@@ -93,7 +93,7 @@
     });
 
     test('mention selector opens when previous char is \n', async () => {
-      stubRestApi('getSuggestedAccounts').returns(
+      stubRestApi('queryAccounts').returns(
         Promise.resolve([
           {
             ...createAccountWithEmail('abc@google.com'),
@@ -130,7 +130,7 @@
 
     test('mention suggestions cleared before request returns', async () => {
       const promise = mockPromise<Item[]>();
-      stubRestApi('getSuggestedAccounts').returns(promise);
+      stubRestApi('queryAccounts').returns(promise);
       element.textarea!.focus();
       await waitUntil(() => element.textarea!.focused === true);
 
@@ -164,7 +164,7 @@
     test('mention dropdown shows suggestion for latest text', async () => {
       const promise1 = mockPromise<Item[]>();
       const promise2 = mockPromise<Item[]>();
-      const suggestionStub = stubRestApi('getSuggestedAccounts');
+      const suggestionStub = stubRestApi('queryAccounts');
       suggestionStub.returns(promise1);
       element.textarea!.focus();
       await waitUntil(() => element.textarea!.focused === true);
@@ -221,7 +221,7 @@
     });
 
     test('selecting mentions from dropdown', async () => {
-      stubRestApi('getSuggestedAccounts').returns(
+      stubRestApi('queryAccounts').returns(
         Promise.resolve([
           createAccountWithEmail('abc@google.com'),
           createAccountWithEmail('abcdef@google.com'),
@@ -254,7 +254,7 @@
       const listenerStub = sinon.stub();
       element.addEventListener('text-changed', listenerStub);
       const resetSpy = sinon.spy(element, 'resetDropdown');
-      stubRestApi('getSuggestedAccounts').returns(
+      stubRestApi('queryAccounts').returns(
         Promise.resolve([
           createAccountWithEmail('abc@google.com'),
           createAccountWithEmail('abcdef@google.com'),
@@ -348,7 +348,7 @@
     });
 
     test('mention dropdown is cleared if @ is deleted', async () => {
-      stubRestApi('getSuggestedAccounts').returns(
+      stubRestApi('queryAccounts').returns(
         Promise.resolve([
           createAccountWithEmail('abc@google.com'),
           createAccountWithEmail('abcdef@google.com'),
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index bbb47c2..18ba3c1 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -1778,7 +1778,7 @@
     });
   }
 
-  async getSuggestedAccounts(
+  async queryAccounts(
     inputVal: string,
     n?: number,
     canSee?: NumericChangeId,
@@ -1815,6 +1815,19 @@
     }) as Promise<AccountInfo[] | undefined>;
   }
 
+  getAccountSuggestions(inputVal: string): Promise<AccountInfo[] | undefined> {
+    const params: QueryAccountsParams = {suggest: undefined, q: ''};
+    inputVal = inputVal?.trim() ?? '';
+    if (inputVal.length > 0) {
+      params.q = inputVal;
+    }
+    if (!params.q) return Promise.resolve([]);
+    return this._restApiHelper.fetchJSON({
+      url: '/accounts/',
+      params,
+    }) as Promise<AccountInfo[] | undefined>;
+  }
+
   addChangeReviewer(
     changeNum: NumericChangeId,
     reviewerID: AccountId | EmailAddress | GroupId
@@ -2313,6 +2326,21 @@
     });
   }
 
+  updateIdentityInChangeEdit(
+    changeNum: NumericChangeId,
+    name: string,
+    email: string,
+    type: string
+  ) {
+    return this._getChangeURLAndSend({
+      changeNum,
+      method: HttpMethod.PUT,
+      endpoint: '/edit:identity',
+      body: {name, email, type},
+      reportEndpointAsIs: true,
+    });
+  }
+
   deleteChangeCommitMessage(
     changeNum: NumericChangeId,
     messageId: ChangeMessageId
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
index fe52529..1018d00 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
@@ -347,7 +347,7 @@
     assert.isFalse(element._cache.has(cacheKey));
   });
 
-  suite('getAccountSuggestions', () => {
+  suite('queryAccounts', () => {
     let fetchStub: sinon.SinonStub;
     const testProject = 'testproject';
     const testChangeNumber = 341682;
@@ -362,7 +362,7 @@
     });
 
     test('url with just email', async () => {
-      await element.getSuggestedAccounts('bro');
+      await element.queryAccounts('bro');
       assert.isTrue(fetchStub.calledOnce);
       assert.equal(
         fetchStub.firstCall.args[0].url,
@@ -371,7 +371,7 @@
     });
 
     test('url with email and canSee changeId', async () => {
-      await element.getSuggestedAccounts(
+      await element.queryAccounts(
         'bro',
         undefined,
         testChangeNumber as NumericChangeId
@@ -384,7 +384,7 @@
     });
 
     test('url with email and canSee changeId and isActive', async () => {
-      await element.getSuggestedAccounts(
+      await element.queryAccounts(
         'bro',
         undefined,
         testChangeNumber as NumericChangeId,
@@ -398,6 +398,18 @@
     });
   });
 
+  test('getAccountSuggestions using suggest query param', () => {
+    const fetchStub = sinon
+      .stub(element._restApiHelper, 'fetch')
+      .resolves(new Response());
+    element.getAccountSuggestions('user');
+    assert.isTrue(fetchStub.calledOnce);
+    assert.equal(
+      fetchStub.firstCall.args[0].url,
+      `${getBaseUrl()}/accounts/?suggest&q=user`
+    );
+  });
+
   test('getAccount when resp is undefined clears cache', async () => {
     const cacheKey = '/accounts/self/detail';
     const account = createAccountDetailWithId();
@@ -758,6 +770,27 @@
     });
   });
 
+  test('updateIdentityInChangeEdit', async () => {
+    element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+    const change_num = 1 as NumericChangeId;
+    const name = 'user';
+    const email = 'user@example.com';
+    const type = 'AUTHOR';
+    const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+    await element.updateIdentityInChangeEdit(change_num, name, email, type);
+    assert.isTrue(sendStub.calledOnce);
+    assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+    assert.equal(
+      sendStub.lastCall.args[0].url,
+      '/changes/test~1/edit:identity'
+    );
+    assert.deepEqual(sendStub.lastCall.args[0].body, {
+      email: 'user@example.com',
+      name: 'user',
+      type: 'AUTHOR',
+    });
+  });
+
   test('deleteChangeCommitMessage', async () => {
     element._projectLookup = {1: Promise.resolve('test' as RepoName)};
     const change_num = 1 as NumericChangeId;
@@ -1046,18 +1079,18 @@
     assert(fetchStub.called);
   });
 
-  test('getSuggestedAccounts does not return fetchJSON', async () => {
+  test('queryAccounts does not return fetchJSON', async () => {
     const fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
-    const accts = await element.getSuggestedAccounts('');
+    const accts = await element.queryAccounts('');
     assert.isFalse(fetchJSONSpy.called);
     assert.equal(accts!.length, 0);
   });
 
-  test('fetchJSON gets called by getSuggestedAccounts', async () => {
+  test('fetchJSON gets called by queryAccounts', async () => {
     const fetchJSONStub = sinon
       .stub(element._restApiHelper, 'fetchJSON')
       .resolves();
-    await element.getSuggestedAccounts('own');
+    await element.queryAccounts('own');
     assert.deepEqual(fetchJSONStub.lastCall.args[0].params, {
       q: '"own"',
       o: 'DETAILS',
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index 546f06a0..a147e26 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -165,13 +165,14 @@
    * Request list of accounts via https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#query-account
    * Operators defined here https://gerrit-review.googlesource.com/Documentation/user-search-accounts.html#_search_operators
    */
-  getSuggestedAccounts(
+  queryAccounts(
     input: string,
     n?: number,
     canSee?: NumericChangeId,
     filterActive?: boolean,
     errFn?: ErrorCallback
   ): Promise<AccountInfo[] | undefined>;
+  getAccountSuggestions(input: string): Promise<AccountInfo[] | undefined>;
   getSuggestedGroups(
     input: string,
     project?: RepoName,
@@ -821,6 +822,13 @@
     message: string
   ): Promise<Response>;
 
+  updateIdentityInChangeEdit(
+    changeNum: NumericChangeId,
+    name: string,
+    email: string,
+    type: string
+  ): Promise<Response | undefined>;
+
   getChangeCommitInfo(
     changeNum: NumericChangeId,
     patchNum: PatchSetNum
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 7969264..5928ebf 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -413,7 +413,10 @@
   getRobotCommentFixPreview(): Promise<FilePathToDiffInfoMap | undefined> {
     return Promise.resolve({});
   },
-  getSuggestedAccounts(): Promise<AccountInfo[] | undefined> {
+  queryAccounts(): Promise<AccountInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getAccountSuggestions(): Promise<AccountInfo[] | undefined> {
     return Promise.resolve([]);
   },
   getSuggestedGroups(): Promise<GroupNameToGroupInfoMap | undefined> {
@@ -554,4 +557,7 @@
   setRepoHead(): Promise<Response> {
     return Promise.resolve(new Response());
   },
+  updateIdentityInChangeEdit(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
 };
diff --git a/polygerrit-ui/app/utils/display-name-util.ts b/polygerrit-ui/app/utils/display-name-util.ts
index 850509f..5f56e51 100644
--- a/polygerrit-ui/app/utils/display-name-util.ts
+++ b/polygerrit-ui/app/utils/display-name-util.ts
@@ -63,7 +63,7 @@
     .join(' ');
 }
 
-function accountEmail(email?: string) {
+export function accountEmail(email?: string) {
   if (typeof email !== 'undefined') {
     return '<' + email + '>';
   }
diff --git a/tools/BUILD b/tools/BUILD
index 01f1a8f..182e4dc1 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -18,17 +18,20 @@
     visibility = ["//visibility:public"],
 )
 
-default_java_toolchain(
-    name = "error_prone_warnings_toolchain_java17",
+[default_java_toolchain(
+    name = "error_prone_warnings_toolchain_java" + VERSION,
     configuration = dict(),
-    java_runtime = "@bazel_tools//tools/jdk:remotejdk_17",
+    java_runtime = "@rules_java//toolchains:remotejdk_" + VERSION,
     package_configuration = [
         ":error_prone",
     ],
-    source_version = "17",
-    target_version = "17",
+    source_version = VERSION,
+    target_version = VERSION,
     visibility = ["//visibility:public"],
-)
+) for VERSION in [
+    "17",
+    "21",
+]]
 
 # Error Prone errors enabled by default; see ../.bazelrc for how this is
 # enabled. This warnings list is originally based on:
diff --git a/tools/remote-bazelrc b/tools/remote-bazelrc
index ef81af0..d2a18bc 100644
--- a/tools/remote-bazelrc
+++ b/tools/remote-bazelrc
@@ -27,6 +27,7 @@
 # for a remote machine to execute them.
 build:remote_shared --jobs=200
 build:remote_shared --disk_cache=
+build:remote_shared --remote_download_minimal
 
 # Set several flags related to specifying the platform, toolchain and java
 # properties.
@@ -42,24 +43,39 @@
 # machine exactly match the host machine.
 build:remote_shared --define=EXECUTOR=remote
 
-# Enable the remote cache so action results can be shared across machines,
-# developers, and workspaces.
-build:remote_shared --remote_cache=remotebuildexecution.googleapis.com
-
-# Enable remote execution so actions are performed on the remote systems.
-build:remote_shared --remote_executor=remotebuildexecution.googleapis.com
-
 # Set a higher timeout value, just in case.
 build:remote_shared --remote_timeout=3600
 
+# Configuration flags for remote settings in Google GCP RBE
+# Enable the remote cache so action results can be shared across machines,
+# developers, and workspaces.
+build:config_gcp --remote_cache=remotebuildexecution.googleapis.com
+
+# Enable remote execution so actions are performed on the remote systems.
+build:config_gcp --remote_executor=remotebuildexecution.googleapis.com
+
 # Enable authentication. This will pick up application default credentials by
 # default. You can use --auth_credentials=some_file.json to use a service
 # account credential instead.
-build:remote_shared --google_default_credentials
+build:config_gcp --google_default_credentials
+build:config_gcp --config=remote_shared
 
-# The following flags enable the remote cache so action results can be shared
-# across machines, developers, and workspaces.
-build:remote-cache --remote_cache=remotebuildexecution.googleapis.com
-build:remote-cache --tls_enabled=true
-build:remote-cache --remote_timeout=3600
-build:remote-cache --auth_enabled=true
+# Configuration flags for remote settings in BuildBuddy RBE
+# Enable the remote cache so action results can be shared across machines,
+# developers, and workspaces.
+build:config_bb --remote_cache=grpcs://remote.buildbuddy.io
+
+# Enable remote execution so actions are performed on the remote systems.
+build:config_bb --remote_executor=grpcs://remote.buildbuddy.io
+
+# The results from each Bazel command are viewable with BuildBuddy
+build:config_bb --bes_results_url=https://app.buildbuddy.io/invocation/
+
+# The results of a local build will be uploaded to the BuildBuddy server,
+# providing visibility and collaboration features for the build.
+build:config_bb --remote_upload_local_results
+
+# Define the Build Event Service (BES) backend to use for remote caching and
+# build event storage.
+build:config_bb --bes_backend=grpcs://remote.buildbuddy.io
+build:config_bb --config=remote_shared