Merge "Submit Requiremens - show permission warning in reply dialog"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 248cb8b..5418555 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2103,7 +2103,7 @@
 Gerrit advertises patch set downloads with the `repo download`
 command, assuming that all projects managed by this instance are
 generally worked on with the
-[repo multi-repository tool](https://gerrit.googlesource.com/git-repo).
+https://gerrit.googlesource.com/git-repo[repo multi-repository tool].
 This is not default, as not all instances will deploy repo.
 
 +
@@ -2238,7 +2238,7 @@
 or "http://example.com:8080/gerrit/" so Gerrit can output links that point
 back to itself.
 +
-Setting this is highly recommended, as its necessary for the upload
+Setting this is highly recommended, as it is necessary for the upload
 code invoked by "git push" or "repo upload" to output hyperlinks
 to the newly uploaded changes.
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 35909c7..38ce7b3 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2684,7 +2684,7 @@
 Plugins may also decide not to vote on a given change by returning an
 `Optional.empty()` (ie: the plugin is not enabled for this repository).
 
-If a plugin decides not to vote, it's name will not be displayed in the UI and
+If a plugin decides not to vote, its name will not be displayed in the UI and
 it will not be recoded in the database.
 
 .Gerrit's Pre-submit handling with three plugins
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index a24d80d..4b593ff 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -518,6 +518,7 @@
 +
 Same as <<status,status:'STATE'>>.
 
+[[is-submittable]]
 is:submittable::
 +
 True if the change is submittable according to the submit rules for
@@ -529,8 +530,6 @@
 use the
 link:rest-api-changes.html#get-revision-actions[Get Revision Actions]
 API.
-+
-Equivalent to <<submittable,submittable:ok>>.
 
 [[mergeable]]
 is:mergeable::
@@ -837,7 +836,7 @@
 +
 Matches changes that are ready to be submitted according to one common
 label configuration. (For a more general check, use
-link:#submittable[submittable:ok].)
+link:#is-submittable[is:submittable].)
 
 `is:open (label:Verified-1 OR label:Code-Review-2)`::
 `is:open (label:Verified=reject OR label:Code-Review=reject)`::
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 7ed79c4..ab6d0f4 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -500,6 +500,7 @@
         }
       } catch (Throwable e) {
         logger.atSevere().withCause(e).log(
+            "%s",
             MessageFormat.format(
                 HttpServerText.get().internalErrorDuringUploadPack,
                 ServletUtils.getRepository(req)));
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index cf3562f..fcd16ae 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -178,7 +178,7 @@
         aReq.addExtension(pape);
       }
     } catch (MessageException | ConsumerException e) {
-      logger.atSevere().withCause(e).log("Cannot create OpenID redirect for %s" + openidIdentifier);
+      logger.atSevere().withCause(e).log("Cannot create OpenID redirect for %s", openidIdentifier);
       return new DiscoveryResult(DiscoveryResult.Status.ERROR);
     }
 
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 8395d12..fa9a820 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -89,7 +89,10 @@
           .map(query -> query.replaceAll("\\$\\{user}", "self"))
           .collect(toImmutableList());
   public static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
-      ImmutableSet.of(ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS);
+      ImmutableSet.of(
+          ListChangesOption.LABELS,
+          ListChangesOption.DETAILED_ACCOUNTS,
+          ListChangesOption.SUBMIT_REQUIREMENTS);
 
   public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS =
       ImmutableSet.of(
diff --git a/java/com/google/gerrit/mail/ParserUtil.java b/java/com/google/gerrit/mail/ParserUtil.java
index 4b292f3..40c5a95 100644
--- a/java/com/google/gerrit/mail/ParserUtil.java
+++ b/java/com/google/gerrit/mail/ParserUtil.java
@@ -115,7 +115,8 @@
     int numConsecutiveDigits = 0;
     int maxConsecutiveDigits = 0;
     int numDigitGroups = 0;
-    for (char c : s.toCharArray()) {
+    for (int i = 0; i < s.length(); i++) {
+      char c = s.charAt(i);
       if (c >= '0' && c <= '9') {
         numConsecutiveDigits++;
       } else if (numConsecutiveDigits > 0) {
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCaseSensitivityMigrator.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCaseSensitivityMigrator.java
index a59e935..a6ee366c 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCaseSensitivityMigrator.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCaseSensitivityMigrator.java
@@ -127,11 +127,11 @@
                   isUserNameCaseInsensitive ? "" : "in"));
           extIdNotes.commit(metaDataUpdate);
         } catch (Exception e) {
-          logger.atSevere().withCause(e).log(e.getMessage());
+          logger.atSevere().withCause(e).log("%s", e.getMessage());
         }
       }
     } catch (DuplicateExternalIdKeyException e) {
-      logger.atSevere().withCause(e).log(e.getMessage());
+      logger.atSevere().withCause(e).log("%s", e.getMessage());
       throw e;
     }
   }
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index a718fa4..364f4bf 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -23,7 +23,6 @@
 import com.google.inject.Singleton;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
-import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
@@ -50,7 +49,8 @@
           ImmutableSet.of(
               CoreDownloadSchemes.SSH, CoreDownloadSchemes.HTTP, CoreDownloadSchemes.ANON_HTTP);
     } else {
-      List<String> normalized = new ArrayList<>(allSchemes.length);
+      ImmutableSet.Builder<String> normalized =
+          ImmutableSet.builderWithExpectedSize(allSchemes.length);
       for (String s : allSchemes) {
         String core = toCoreScheme(s);
         if (core == null) {
@@ -59,7 +59,7 @@
         }
         normalized.add(core);
       }
-      downloadSchemes = ImmutableSet.copyOf(normalized);
+      downloadSchemes = normalized.build();
     }
 
     DownloadCommand[] downloadCommandValues = DownloadCommand.values();
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java
index 05c5f4c..d839bce 100644
--- a/java/com/google/gerrit/server/git/DelegateRepository.java
+++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -65,6 +65,10 @@
     this.delegate = delegate;
   }
 
+  Repository delegate() {
+    return delegate;
+  }
+
   @Override
   public void create(boolean bare) throws IOException {
     delegate.create(bare);
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index a1ac6ce..30330eb 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -85,7 +85,12 @@
       try (Repository repo = repoManager.openRepository(p)) {
         logGcConfiguration(p, repo, aggressive);
         print(writer, "collecting garbage for \"" + p + "\":\n");
-        GarbageCollectCommand gc = Git.wrap(repo).gc();
+        GarbageCollectCommand gc =
+            Git.wrap(
+                    repo instanceof DelegateRepository
+                        ? ((DelegateRepository) repo).delegate()
+                        : repo)
+                .gc();
         gc.setAggressive(aggressive);
         logGcInfo(p, "before:", gc.getStatistics());
         gc.setProgressMonitor(
diff --git a/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java b/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java
index 6266925..dfbe663 100644
--- a/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java
+++ b/java/com/google/gerrit/server/git/GitRepositoryManagerModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.ModuleImpl;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager.LocalDiskRepositoryManagerModule;
 import com.google.gerrit.server.git.MultiBaseLocalDiskRepositoryManager.MultiBaseLocalDiskRepositoryManagerModule;
@@ -24,7 +25,9 @@
  * Module to install {@link MultiBaseLocalDiskRepositoryManager} rather than {@link
  * LocalDiskRepositoryManager} if needed.
  */
+@ModuleImpl(name = GitRepositoryManagerModule.MANAGER_MODULE)
 public class GitRepositoryManagerModule extends LifecycleModule {
+  public static final String MANAGER_MODULE = "git-manager";
 
   private final RepositoryConfig repoConfig;
 
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 2ee2e68..d84ce7b 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -307,13 +307,13 @@
     int nameLength = Math.max(oursName.length(), theirsName.length());
     String oursNameFormatted =
         String.format(
-            "%0$-" + nameLength + "s (%s %s)",
+            "%-" + nameLength + "s (%s %s)",
             oursName,
             abbreviateName(ours, NAME_ABBREV_LEN),
             oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
     String theirsNameFormatted =
         String.format(
-            "%0$-" + nameLength + "s (%s %s)",
+            "%-" + nameLength + "s (%s %s)",
             theirsName,
             abbreviateName(theirs, NAME_ABBREV_LEN),
             theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 2a57d3d..52a34d9 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -192,7 +192,7 @@
         volatileTotal.addAndGet(workUnits);
       } else {
         logger.atWarning().log(
-            "Total work has been finalized on sub-task " + getName() + " and cannot be updated");
+            "Total work has been finalized on sub-task %s and cannot be updated", getName());
       }
     }
 
diff --git a/java/com/google/gerrit/server/git/validators/AccountValidator.java b/java/com/google/gerrit/server/git/validators/AccountValidator.java
index 4755f5f..873f421 100644
--- a/java/com/google/gerrit/server/git/validators/AccountValidator.java
+++ b/java/com/google/gerrit/server/git/validators/AccountValidator.java
@@ -28,7 +28,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -77,7 +76,7 @@
       }
     }
 
-    List<String> messages = new ArrayList<>();
+    ImmutableList.Builder<String> messages = ImmutableList.builder();
     Optional<Account> newAccount;
     try {
       newAccount = loadAccount(accountId, allUsersRepo, rw, newId, messages);
@@ -108,7 +107,7 @@
       }
     }
 
-    return ImmutableList.copyOf(messages);
+    return messages.build();
   }
 
   private Optional<Account> loadAccount(
@@ -116,7 +115,7 @@
       Repository allUsersRepo,
       RevWalk rw,
       ObjectId commit,
-      @Nullable List<String> messages)
+      @Nullable ImmutableList.Builder<String> messages)
       throws IOException, ConfigInvalidException {
     rw.reset();
     AccountConfig accountConfig = new AccountConfig(accountId, allUsersName, allUsersRepo);
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index c648d11..dd8534d 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -259,7 +259,7 @@
 
       AccountGroup.UUID uuid = groupRef.get().getUUID();
 
-      List<ConsistencyProblemInfo> problems = new ArrayList<>();
+      ImmutableList.Builder<ConsistencyProblemInfo> problems = ImmutableList.builder();
       if (!Objects.equals(groupUUID, uuid)) {
         problems.add(
             warning(
@@ -273,7 +273,7 @@
         problems.add(
             warning("group note of name '%s' claims to represent name of '%s'", name, actualName));
       }
-      return ImmutableList.copyOf(problems);
+      return problems.build();
     } catch (ConfigInvalidException e) {
       return ImmutableList.of(
           warning("fail to check consistency with group name notes: %s", e.getMessage()));
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 9f14926..6cdc9ae 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -140,7 +140,7 @@
     try {
       futures = new SliceScheduler(index, ok).schedule();
     } catch (ProjectsCollectionFailure e) {
-      logger.atSevere().log(e.getMessage());
+      logger.atSevere().log("%s", e.getMessage());
       return Result.create(sw, false, 0, 0);
     }
 
@@ -181,7 +181,7 @@
       return reindexProject(
           indexer, project, 0, 1, ChangeNotes.Factory.scanChangeIds(repo), done, failed);
     } catch (IOException e) {
-      logger.atSevere().log(e.getMessage());
+      logger.atSevere().log("%s", e.getMessage());
       return null;
     }
   }
diff --git a/java/com/google/gerrit/server/mail/send/CommentFormatter.java b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
index f04ce9d..71f4a90 100644
--- a/java/com/google/gerrit/server/mail/send/CommentFormatter.java
+++ b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
@@ -52,7 +52,7 @@
       return ImmutableList.of();
     }
 
-    List<Block> result = new ArrayList<>();
+    ImmutableList.Builder<Block> result = ImmutableList.builder();
     for (String p : Splitter.on("\n\n").split(source)) {
       if (isQuote(p)) {
         result.add(makeQuote(p));
@@ -64,7 +64,7 @@
         result.add(makeParagraph(p));
       }
     }
-    return ImmutableList.copyOf(result);
+    return result.build();
   }
 
   /**
@@ -91,7 +91,7 @@
    * @param p The block containing the list (as well as potential paragraphs).
    * @param out The list of blocks to append to.
    */
-  private static void makeList(String p, List<Block> out) {
+  private static void makeList(String p, ImmutableList.Builder<Block> out) {
     Block block = null;
     StringBuilder textBuilder = null;
     boolean inList = false;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index bec4b721f..ca636e8 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -31,7 +31,6 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
@@ -275,8 +274,8 @@
 
     public ListMultimap<Project.NameKey, ChangeNotes> create(Predicate<ChangeNotes> predicate)
         throws IOException {
-      ListMultimap<Project.NameKey, ChangeNotes> m =
-          MultimapBuilder.hashKeys().arrayListValues().build();
+      ImmutableListMultimap.Builder<Project.NameKey, ChangeNotes> m =
+          ImmutableListMultimap.builder();
       for (Project.NameKey project : projectCache.all()) {
         try (Repository repo = args.repoManager.openRepository(project)) {
           scan(repo, project)
@@ -286,7 +285,7 @@
               .forEach(n -> m.put(n.getProjectName(), n));
         }
       }
-      return ImmutableListMultimap.copyOf(m);
+      return m.build();
     }
 
     public Stream<ChangeNotesResult> scan(Repository repo, Project.NameKey project)
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 4988406..5d8f57f 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -20,8 +20,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
@@ -124,13 +122,13 @@
             reader,
             NoteMap.read(reader, tipCommit),
             HumanComment.Status.DRAFT);
-    ListMultimap<ObjectId, HumanComment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
+    ImmutableListMultimap.Builder<ObjectId, HumanComment> cs = ImmutableListMultimap.builder();
     for (ChangeRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
       for (HumanComment c : rn.getEntities()) {
         cs.put(c.getCommitId(), c);
       }
     }
-    comments = ImmutableListMultimap.copyOf(cs);
+    comments = cs.build();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index d53b2ca..2ec68f1 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.server.notedb;
 
 import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
@@ -94,13 +92,13 @@
     revisionNoteMap =
         RevisionNoteMap.parseRobotComments(
             args.changeNoteJson, reader, NoteMap.read(reader, tipCommit));
-    ListMultimap<ObjectId, RobotComment> cs = MultimapBuilder.hashKeys().arrayListValues().build();
+    ImmutableListMultimap.Builder<ObjectId, RobotComment> cs = ImmutableListMultimap.builder();
     for (RobotCommentsRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
       for (RobotComment c : rn.getEntities()) {
         cs.put(c.getCommitId(), c);
       }
     }
-    comments = ImmutableListMultimap.copyOf(cs);
+    comments = cs.build();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/patch/DiffUtil.java b/java/com/google/gerrit/server/patch/DiffUtil.java
index e75d50c..70a3208 100644
--- a/java/com/google/gerrit/server/patch/DiffUtil.java
+++ b/java/com/google/gerrit/server/patch/DiffUtil.java
@@ -16,13 +16,13 @@
 package com.google.gerrit.server.patch;
 
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.server.patch.diff.ModifiedFilesCache;
 import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
 import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
@@ -43,8 +43,9 @@
    * patchsets, for example converting a symlink file to a regular file. We identify this case and
    * return a single modified file with changeType = {@link ChangeType#REWRITE}.
    */
-  public static List<ModifiedFile> mergeRewrittenModifiedFiles(List<ModifiedFile> modifiedFiles) {
-    List<ModifiedFile> result = new ArrayList<>();
+  public static ImmutableList<ModifiedFile> mergeRewrittenModifiedFiles(
+      List<ModifiedFile> modifiedFiles) {
+    ImmutableList.Builder<ModifiedFile> result = ImmutableList.builder();
     ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
     modifiedFiles.stream()
         .forEach(
@@ -66,7 +67,7 @@
         result.add(entries.get(0).toBuilder().changeType(ChangeType.REWRITE).build());
       }
     }
-    return result;
+    return result.build();
   }
 
   /**
diff --git a/java/com/google/gerrit/server/patch/MergeListBuilder.java b/java/com/google/gerrit/server/patch/MergeListBuilder.java
index 337d940..8964956 100644
--- a/java/com/google/gerrit/server/patch/MergeListBuilder.java
+++ b/java/com/google/gerrit/server/patch/MergeListBuilder.java
@@ -16,8 +16,6 @@
 
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
@@ -40,11 +38,11 @@
       }
     }
 
-    List<RevCommit> result = new ArrayList<>();
+    ImmutableList.Builder<RevCommit> result = ImmutableList.builder();
     RevCommit c;
     while ((c = rw.next()) != null) {
       result.add(c);
     }
-    return ImmutableList.copyOf(result);
+    return result.build();
   }
 }
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
index 9c4d601..63dd63d 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -139,14 +139,15 @@
               .bTree(bTree)
               .renameScore(key.renameScore())
               .build();
-      List<ModifiedFile> modifiedFiles = DiffUtil.mergeRewrittenModifiedFiles(gitCache.get(gitKey));
+      ImmutableList<ModifiedFile> modifiedFiles =
+          DiffUtil.mergeRewrittenModifiedFiles(gitCache.get(gitKey));
       if (key.aCommit().equals(ObjectId.zeroId())) {
-        return ImmutableList.copyOf(modifiedFiles);
+        return modifiedFiles;
       }
       RevCommit revCommitA = DiffUtil.getRevCommit(rw, key.aCommit());
       RevCommit revCommitB = DiffUtil.getRevCommit(rw, key.bCommit());
       if (DiffUtil.areRelated(revCommitA, revCommitB)) {
-        return ImmutableList.copyOf(modifiedFiles);
+        return modifiedFiles;
       }
       Set<String> touchedFiles =
           getTouchedFilesWithParents(
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index e523d76..39b0f90 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -149,7 +149,8 @@
     // we have to investigate separately (deferred tags) then perform a reachability check starting
     // from all visible branches (refs/heads/*).
     Result initialRefFilter = filterRefs(new ArrayList<>(refs), opts);
-    List<Ref> visibleRefs = new ArrayList<>(initialRefFilter.visibleRefs());
+    ImmutableList.Builder<Ref> visibleRefs = ImmutableList.builder();
+    visibleRefs.addAll(initialRefFilter.visibleRefs());
     if (!initialRefFilter.deferredTags().isEmpty()) {
       try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
         Result allVisibleBranches = filterRefs(getTaggableRefs(repo), opts);
@@ -177,8 +178,9 @@
       }
     }
 
-    logger.atFinest().log("visible refs = %s", visibleRefs);
-    return ImmutableList.copyOf(visibleRefs);
+    ImmutableList<Ref> visibleRefList = visibleRefs.build();
+    logger.atFinest().log("visible refs = %s", visibleRefList);
+    return visibleRefList;
   }
 
   /**
@@ -216,8 +218,8 @@
         permissionBackend
             .user(projectControl.getUser())
             .testOrFalse(GlobalPermission.ACCESS_DATABASE);
-    List<Ref> resultRefs = new ArrayList<>(refs.size());
-    List<Ref> deferredTags = new ArrayList<>();
+    ImmutableList.Builder<Ref> resultRefs = ImmutableList.builderWithExpectedSize(refs.size());
+    ImmutableList.Builder<Ref> deferredTags = ImmutableList.builder();
     for (Ref ref : refs) {
       String refName = ref.getName();
       Change.Id changeId;
@@ -265,9 +267,7 @@
         resultRefs.add(ref);
       }
     }
-    Result result =
-        new AutoValue_DefaultRefFilter_Result(
-            ImmutableList.copyOf(resultRefs), ImmutableList.copyOf(deferredTags));
+    Result result = new AutoValue_DefaultRefFilter_Result(resultRefs.build(), deferredTags.build());
     logger.atFinest().log("Result of ref filtering = %s", result);
     return result;
   }
diff --git a/java/com/google/gerrit/server/permissions/SectionSortCache.java b/java/com/google/gerrit/server/permissions/SectionSortCache.java
index 621f1d0..552d8ee 100644
--- a/java/com/google/gerrit/server/permissions/SectionSortCache.java
+++ b/java/com/google/gerrit/server/permissions/SectionSortCache.java
@@ -128,11 +128,12 @@
     public abstract ImmutableList<String> patterns();
 
     static EntryKey create(String refName, List<AccessSection> sections) {
-      List<String> patterns = new ArrayList<>(sections.size());
+      ImmutableList.Builder<String> patterns =
+          ImmutableList.builderWithExpectedSize(sections.size());
       for (AccessSection s : sections) {
         patterns.add(s.getName());
       }
-      return new AutoValue_SectionSortCache_EntryKey(refName, ImmutableList.copyOf(patterns));
+      return new AutoValue_SectionSortCache_EntryKey(refName, patterns.build());
     }
 
     @Memoized
diff --git a/java/com/google/gerrit/server/project/CommentLinkProvider.java b/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 1b9dc37..c2ac68a 100644
--- a/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.StoredCommentLinkInfo;
@@ -45,7 +44,8 @@
 
   private List<CommentLinkInfo> parseConfig(Config cfg) {
     Set<String> subsections = cfg.getSubsections(ProjectConfig.COMMENTLINK);
-    List<CommentLinkInfo> cls = Lists.newArrayListWithCapacity(subsections.size());
+    ImmutableList.Builder<CommentLinkInfo> cls =
+        ImmutableList.builderWithExpectedSize(subsections.size());
     for (String name : subsections) {
       try {
         StoredCommentLinkInfo cl = ProjectConfig.buildCommentLink(cfg, name, true);
@@ -58,7 +58,7 @@
         logger.atWarning().log("invalid commentlink: %s", e.getMessage());
       }
     }
-    return ImmutableList.copyOf(cls);
+    return cls.build();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 03886a9..4a17b5c 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -901,7 +901,7 @@
       Config rc, String section, String subsection, String varName, boolean useRange) {
     Permission.Builder perm = Permission.builder(varName);
     loadPermissionRules(rc, section, subsection, varName, perm, useRange);
-    return ImmutableList.copyOf(perm.build().getRules());
+    return perm.build().getRules();
   }
 
   private void loadPermissionRules(
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index 39d9aec7..ab4bb70 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -269,7 +269,7 @@
               .call();
 
       // Result for this query that we want to return to the client.
-      List<ChangeInfo> autoCloseableChangesByBranch = new ArrayList<>();
+      ImmutableList.Builder<ChangeInfo> autoCloseableChangesByBranch = ImmutableList.builder();
 
       for (ChangeData autoCloseableChange : queryResult) {
         // Skip changes that we have already processed, either by this query or by
@@ -306,7 +306,7 @@
         }
       }
 
-      return ImmutableList.copyOf(autoCloseableChangesByBranch);
+      return autoCloseableChangesByBranch.build();
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
       throw new StorageException(e);
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
index 2a2e67d..bb99a80 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.SubmitRecord;
@@ -30,7 +31,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -57,32 +57,48 @@
         records.stream().anyMatch(record -> SubmitRecord.Status.FORCED.equals(record.status));
     List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
     ObjectId commitId = cd.currentPatchSet().commitId();
-    return records.stream()
-        // Filter out the "FORCED" submit record. This is a marker submit record that was just used
-        // to indicate that all other records were forced. "FORCED" means that the change was pushed
-        // with the %submit option bypassing submit rules.
-        .filter(r -> !SubmitRecord.Status.FORCED.equals(r.status))
-        .map(r -> createResult(r, labelTypes, commitId, areForced))
-        .flatMap(List::stream)
-        .collect(
-            Collectors.toMap(
-                sr -> sr.submitRequirement(),
-                Function.identity(),
-                (r1, r2) -> {
-                  // We convert submit records to submit requirements by generating a separate
-                  // submit requirement result for each available label in each submit record.
-                  // The SR status is derived from the label status of the submit record.
-                  // This conversion might result in duplicate entries.
-                  // One such example can be a prolog rule emitting the same label name twice.
-                  // Another case might happen if two different submit rules emit the same label
-                  // name. In such cases, we need to merge these entries and return a single submit
-                  // requirement result. If both entries agree in their status, return any of them.
-                  // Otherwise, favour the entry that is blocking submission.
-                  if (r1.fulfilled() == r2.fulfilled()) {
-                    return r1;
-                  }
-                  return r1.fulfilled() ? r2 : r1;
-                }));
+    Map<String, List<SubmitRequirementResult>> srsByName =
+        records.stream()
+            // Filter out the "FORCED" submit record. This is a marker submit record that was just
+            // used to indicate that all other records were forced. "FORCED" means that the change
+            // was pushed with the %submit option bypassing submit rules.
+            .filter(r -> !SubmitRecord.Status.FORCED.equals(r.status))
+            .map(r -> createResult(r, labelTypes, commitId, areForced))
+            .flatMap(List::stream)
+            .collect(Collectors.groupingBy(sr -> sr.submitRequirement().name()));
+
+    // We convert submit records to submit requirements by generating a separate
+    // submit requirement result for each available label in each submit record.
+    // The SR status is derived from the label status of the submit record.
+    // This conversion might result in duplicate entries.
+    // One such example can be a prolog rule emitting the same label name twice.
+    // Another case might happen if two different submit rules emit the same label
+    // name. In such cases, we need to merge these entries and return a single submit
+    // requirement result. If both entries agree in their status, return any of them.
+    // Otherwise, favour the entry that is blocking submission.
+    ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> result =
+        ImmutableMap.builder();
+    for (Map.Entry<String, List<SubmitRequirementResult>> entry : srsByName.entrySet()) {
+      if (entry.getValue().size() == 1) {
+        SubmitRequirementResult srResult = entry.getValue().iterator().next();
+        result.put(srResult.submitRequirement(), srResult);
+        continue;
+      }
+      // If all submit requirements with the same name match in status, return the first one.
+      List<SubmitRequirementResult> resultsSameName = entry.getValue();
+      boolean allNonBlocking = resultsSameName.stream().allMatch(sr -> sr.fulfilled());
+      if (allNonBlocking) {
+        result.put(resultsSameName.get(0).submitRequirement(), resultsSameName.get(0));
+      } else {
+        // Otherwise, return the first submit requirement result that is blocking submission.
+        Optional<SubmitRequirementResult> nonFulfilled =
+            resultsSameName.stream().filter(sr -> !sr.fulfilled()).findFirst();
+        if (nonFulfilled.isPresent()) {
+          result.put(nonFulfilled.get().submitRequirement(), nonFulfilled.get());
+        }
+      }
+    }
+    return result.build();
   }
 
   static List<SubmitRequirementResult> createResult(
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
index 402bb51..df836e0 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
-import java.util.Map;
 
 public interface SubmitRequirementsEvaluator {
   /**
@@ -31,7 +31,7 @@
    * @param includeLegacy if set to true, evaluate legacy {@link
    *     com.google.gerrit.entities.SubmitRecord}s and convert them to submit requirements.
    */
-  Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+  ImmutableMap<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
       ChangeData cd, boolean includeLegacy);
 
   /** Evaluate a single {@link SubmitRequirement} using change data. */
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 64c9a4c..00f6876 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -35,7 +35,6 @@
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Scopes;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Function;
@@ -82,18 +81,17 @@
   }
 
   @Override
-  public Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+  public ImmutableMap<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
       ChangeData cd, boolean includeLegacy) {
-    Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements = getRequirements(cd);
-    Map<SubmitRequirement, SubmitRequirementResult> result = projectConfigRequirements;
-    if (includeLegacy) {
-      Map<SubmitRequirement, SubmitRequirementResult> legacyReqs =
-          SubmitRequirementsAdapter.getLegacyRequirements(cd);
-      result =
-          submitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
-              projectConfigRequirements, legacyReqs, cd.project());
+    ImmutableMap<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements =
+        getRequirements(cd);
+    if (!includeLegacy) {
+      return projectConfigRequirements;
     }
-    return ImmutableMap.copyOf(result);
+    Map<SubmitRequirement, SubmitRequirementResult> legacyReqs =
+        SubmitRequirementsAdapter.getLegacyRequirements(cd);
+    return submitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
+        projectConfigRequirements, legacyReqs, cd.project());
   }
 
   @Override
@@ -147,7 +145,7 @@
    * <p>The behaviour in case of the name match is controlled by {@link
    * SubmitRequirement#allowOverrideInChildProjects} of global {@link SubmitRequirement}.
    */
-  private Map<SubmitRequirement, SubmitRequirementResult> getRequirements(ChangeData cd) {
+  private ImmutableMap<SubmitRequirement, SubmitRequirementResult> getRequirements(ChangeData cd) {
     Map<String, SubmitRequirement> globalRequirements = getGlobalRequirements();
 
     ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
@@ -167,11 +165,12 @@
                         globalSubmitRequirement.allowOverrideInChildProjects()
                             ? projectConfigRequirement
                             : globalSubmitRequirement));
-    Map<SubmitRequirement, SubmitRequirementResult> results = new HashMap<>();
+    ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> results =
+        ImmutableMap.builder();
     for (SubmitRequirement requirement : requirements.values()) {
       results.put(requirement, evaluateRequirement(requirement, cd));
     }
-    return results;
+    return results.build();
   }
 
   /**
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
index e34ab1d..65508ae 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementResult;
@@ -95,10 +96,12 @@
    * @return a map that is the result of merging both input maps, while eliminating requirements
    *     with the same name and status.
    */
-  public Map<SubmitRequirement, SubmitRequirementResult> mergeLegacyAndNonLegacyRequirements(
-      Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements,
-      Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements,
-      Project.NameKey project) {
+  public ImmutableMap<SubmitRequirement, SubmitRequirementResult>
+      mergeLegacyAndNonLegacyRequirements(
+          Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements,
+          Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements,
+          Project.NameKey project) {
+    // Cannot use ImmutableMap.Builder here since entries in the map may be overridden.
     Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
     result.putAll(projectConfigRequirements);
     Map<String, SubmitRequirementResult> requirementsByName =
@@ -116,7 +119,7 @@
       metrics.submitRequirementsMismatchingWithLegacy.increment(project.get(), srName);
       result.put(legacy.getKey(), legacy.getValue());
     }
-    return result;
+    return ImmutableMap.copyOf(result);
   }
 
   /** Returns true if both input results are equal in allowing/disallowing change submission. */
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 3f8bfda..e7b25fb 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -315,7 +315,7 @@
           querySupplier, byProjectGroupsPredicate(indexConfig, project, groups));
     }
     Set<Change.Id> seen = new HashSet<>();
-    List<ChangeData> result = new ArrayList<>();
+    ImmutableList.Builder<ChangeData> result = ImmutableList.builder();
     for (List<String> part : Iterables.partition(groups, batchSize)) {
       for (ChangeData cd :
           queryExhaustively(querySupplier, byProjectGroupsPredicate(indexConfig, project, part))) {
@@ -324,6 +324,6 @@
         }
       }
     }
-    return ImmutableList.copyOf(result);
+    return result.build();
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index 983d9b4..83535c9 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.ResultSet;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -52,7 +51,7 @@
         getChildren().stream().map(p -> ((ChangeDataSource) p).read()).collect(toImmutableList());
     return new LazyResultSet<>(
         () -> {
-          List<ChangeData> r = new ArrayList<>();
+          ImmutableList.Builder<ChangeData> r = ImmutableList.builder();
           Set<Change.Id> have = new HashSet<>();
           for (ResultSet<ChangeData> resultSet : results) {
             for (ChangeData result : resultSet) {
@@ -61,7 +60,7 @@
               }
             }
           }
-          return ImmutableList.copyOf(r);
+          return r.build();
         });
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 4f181a4..536aae2 100644
--- a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -24,8 +24,6 @@
 import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Optional;
 
 public class ParentProjectPredicate extends OrPredicate<ChangeData> {
@@ -46,7 +44,7 @@
       return ImmutableList.of();
     }
 
-    List<Predicate<ChangeData>> r = new ArrayList<>();
+    ImmutableList.Builder<Predicate<ChangeData>> r = ImmutableList.builder();
     r.add(ChangePredicates.project(projectState.get().getNameKey()));
     try {
       for (ProjectInfo p : childProjects.list(projectState.get().getNameKey())) {
@@ -55,7 +53,7 @@
     } catch (PermissionBackendException e) {
       logger.atWarning().withCause(e).log("cannot check permissions to expand child projects");
     }
-    return ImmutableList.copyOf(r);
+    return r.build();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
index ebec3295..e3ab135 100644
--- a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
+++ b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
@@ -22,8 +22,6 @@
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Set;
 
 @Singleton
@@ -33,14 +31,14 @@
 
   @Inject
   AllowedFormats(DownloadConfig cfg) {
-    Map<String, ArchiveFormatInternal> exts = new HashMap<>();
+    ImmutableMap.Builder<String, ArchiveFormatInternal> exts = ImmutableMap.builder();
     for (ArchiveFormatInternal format : cfg.getArchiveFormats()) {
       for (String ext : format.getSuffixes()) {
         exts.put(ext, format);
       }
       exts.put(format.name().toLowerCase(), format);
     }
-    extensions = ImmutableMap.copyOf(exts);
+    extensions = exts.build();
 
     // Zip is not supported because it may be interpreted by a Java plugin as a
     // valid JAR file, whose code would have access to cookies on the domain.
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 6a89247..4605d7c 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -1318,11 +1318,12 @@
         return ImmutableList.of();
       }
 
-      List<FixSuggestion> fixSuggestions = new ArrayList<>(fixSuggestionInfos.size());
+      ImmutableList.Builder<FixSuggestion> fixSuggestions =
+          ImmutableList.builderWithExpectedSize(fixSuggestionInfos.size());
       for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
         fixSuggestions.add(createFixSuggestionFromInput(fixSuggestionInfo));
       }
-      return ImmutableList.copyOf(fixSuggestions);
+      return fixSuggestions.build();
     }
 
     private FixSuggestion createFixSuggestionFromInput(FixSuggestionInfo fixSuggestionInfo) {
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index 74ddae1..2ce82ab 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -43,7 +43,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
@@ -176,11 +175,11 @@
     }
 
     // Perform more expensive walk-sort.
-    List<ChangeData> sorted = new ArrayList<>(cds.size());
+    ImmutableList.Builder<ChangeData> sorted = ImmutableList.builderWithExpectedSize(cds.size());
     for (PatchSetData psd : sorter.get().sort(cds)) {
       sorted.add(psd.data());
     }
-    return ImmutableList.copyOf(sorted);
+    return sorted.build();
   }
 
   private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 9029e11..8cedd60 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -109,7 +109,7 @@
     PermissionBackend.ForProject perm = permissionBackend.currentUser().project(state.getNameKey());
     try (Repository git = gitManager.openRepository(state.getNameKey());
         RevWalk rw = new RevWalk(git)) {
-      List<DashboardInfo> all = new ArrayList<>();
+      ImmutableList.Builder<DashboardInfo> all = ImmutableList.builder();
       for (Ref ref : git.getRefDatabase().getRefsByPrefix(REFS_DASHBOARDS)) {
         try {
           perm.ref(ref.getName()).check(RefPermission.READ);
@@ -118,7 +118,7 @@
           // Do nothing.
         }
       }
-      return ImmutableList.copyOf(all);
+      return all.build();
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException(project, e);
     }
@@ -132,7 +132,7 @@
       String project,
       boolean setDefault)
       throws IOException {
-    List<DashboardInfo> list = new ArrayList<>();
+    ImmutableList.Builder<DashboardInfo> list = ImmutableList.builder();
     try (TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
       tw.addTree(rw.parseTree(ref.getObjectId()));
       tw.setRecursive(true);
@@ -155,6 +155,6 @@
         }
       }
     }
-    return ImmutableList.copyOf(list);
+    return list.build();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 5706016..cd68a2f 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -666,7 +666,7 @@
       } catch (IllegalArgumentException e) {
         throw new BadRequestException(e.getMessage());
       }
-      return searcher.search(ImmutableList.copyOf(projectCache.all()));
+      return searcher.search(projectCache.all().asList());
     } else {
       return projectCache.all().stream();
     }
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index 0a9503f..205420c 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -42,7 +42,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -72,7 +71,8 @@
       return ImmutableList.of();
     }
 
-    List<AccessSection> sections = new ArrayList<>(sectionInfos.size());
+    ImmutableList.Builder<AccessSection> sections =
+        ImmutableList.builderWithExpectedSize(sectionInfos.size());
     for (Map.Entry<String, AccessSectionInfo> entry : sectionInfos.entrySet()) {
       if (entry.getValue().permissions == null) {
         continue;
@@ -120,7 +120,7 @@
       }
       sections.add(accessSection.build());
     }
-    return ImmutableList.copyOf(sections);
+    return sections.build();
   }
 
   /**
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index 84b0ab7..b218347 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
@@ -47,7 +46,8 @@
   @Override
   public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
-    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
+    ImmutableList.Builder<SubmitStrategyOp> ops =
+        ImmutableList.builderWithExpectedSize(sorted.size());
     boolean first = true;
     while (!sorted.isEmpty()) {
       CodeReviewCommit n = sorted.remove(0);
@@ -62,7 +62,7 @@
       }
       first = false;
     }
-    return ImmutableList.copyOf(ops);
+    return ops.build();
   }
 
   private class CherryPickRootOp extends SubmitStrategyOp {
diff --git a/java/com/google/gerrit/server/submit/FastForwardOnly.java b/java/com/google/gerrit/server/submit/FastForwardOnly.java
index ad01d31..ee8fec8 100644
--- a/java/com/google/gerrit/server/submit/FastForwardOnly.java
+++ b/java/com/google/gerrit/server/submit/FastForwardOnly.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.update.RepoContext;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -47,7 +46,8 @@
       }
     }
 
-    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
+    ImmutableList.Builder<SubmitStrategyOp> ops =
+        ImmutableList.builderWithExpectedSize(sorted.size());
     CodeReviewCommit newTipCommit =
         args.mergeUtil.getFirstFastForward(args.mergeTip.getInitialTip(), args.rw, sorted);
     if (!newTipCommit.equals(args.mergeTip.getInitialTip())) {
@@ -57,7 +57,7 @@
         ops.add(new NotFastForwardOp(c));
       }
     }
-    return ImmutableList.copyOf(ops);
+    return ops.build();
   }
 
   private class NotFastForwardOp extends SubmitStrategyOp {
diff --git a/java/com/google/gerrit/server/submit/MergeAlways.java b/java/com/google/gerrit/server/submit/MergeAlways.java
index 7258448..1118a29 100644
--- a/java/com/google/gerrit/server/submit/MergeAlways.java
+++ b/java/com/google/gerrit/server/submit/MergeAlways.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
@@ -28,7 +27,8 @@
   @Override
   public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
-    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
+    ImmutableList.Builder<SubmitStrategyOp> ops =
+        ImmutableList.builderWithExpectedSize(sorted.size());
     if (args.mergeTip.getInitialTip() == null && !sorted.isEmpty()) {
       // The branch is unborn. Take a fast-forward resolution to
       // create the branch.
@@ -39,7 +39,7 @@
       CodeReviewCommit n = sorted.remove(0);
       ops.add(new MergeOneOp(args, n));
     }
-    return ImmutableList.copyOf(ops);
+    return ops.build();
   }
 
   static boolean dryRun(
diff --git a/java/com/google/gerrit/server/submit/MergeIfNecessary.java b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
index 29fc240..75136f5 100644
--- a/java/com/google/gerrit/server/submit/MergeIfNecessary.java
+++ b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
@@ -28,7 +27,8 @@
   @Override
   public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
-    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
+    ImmutableList.Builder<SubmitStrategyOp> ops =
+        ImmutableList.builderWithExpectedSize(sorted.size());
 
     if (args.mergeTip.getInitialTip() == null
         || !args.subscriptionGraph.hasSubscription(args.destBranch)) {
@@ -44,7 +44,7 @@
       CodeReviewCommit n = sorted.remove(0);
       ops.add(new MergeOneOp(args, n));
     }
-    return ImmutableList.copyOf(ops);
+    return ops.build();
   }
 
   static boolean dryRun(
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 1409775..cfb2f88 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.update.PostUpdateContext;
 import com.google.gerrit.server.update.RepoContext;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
@@ -97,7 +96,8 @@
       foundNonMerge = true;
     }
 
-    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
+    ImmutableList.Builder<SubmitStrategyOp> ops =
+        ImmutableList.builderWithExpectedSize(sorted.size());
     boolean first = true;
     while (!sorted.isEmpty()) {
       CodeReviewCommit n = sorted.remove(0);
@@ -114,7 +114,7 @@
       }
       first = false;
     }
-    return ImmutableList.copyOf(ops);
+    return ops.build();
   }
 
   private class RebaseRootOp extends SubmitStrategyOp {
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 9edfdc4..f26882a 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -620,7 +620,8 @@
         return ImmutableList.of();
       }
       logDebug("Reindexing %d changes", results.size());
-      List<ListenableFuture<ChangeData>> indexFutures = new ArrayList<>(results.size());
+      ImmutableList.Builder<ListenableFuture<ChangeData>> indexFutures =
+          ImmutableList.builderWithExpectedSize(results.size());
       for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
         Change.Id id = e.getKey();
         switch (e.getValue()) {
@@ -636,7 +637,7 @@
             throw new IllegalStateException("unexpected result: " + e.getValue());
         }
       }
-      return ImmutableList.copyOf(indexFutures);
+      return indexFutures.build();
     }
   }
 
diff --git a/java/com/google/gerrit/server/util/AccountTemplateUtil.java b/java/com/google/gerrit/server/util/AccountTemplateUtil.java
index 1b39bef..93d7086 100644
--- a/java/com/google/gerrit/server/util/AccountTemplateUtil.java
+++ b/java/com/google/gerrit/server/util/AccountTemplateUtil.java
@@ -24,9 +24,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.HashSet;
 import java.util.Optional;
-import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -62,7 +60,7 @@
       return ImmutableSet.of();
     }
     Matcher matcher = ACCOUNT_TEMPLATE_PATTERN.matcher(textTemplate);
-    Set<Account.Id> accountsInTemplate = new HashSet<>();
+    ImmutableSet.Builder<Account.Id> accountsInTemplate = ImmutableSet.builder();
     while (matcher.find()) {
       String accountId = matcher.group(1);
       Optional<Account.Id> parsedAccountId = Account.Id.tryParse(accountId);
@@ -72,7 +70,7 @@
         logger.atFine().log("Failed to parse accountId from template %s", matcher.group());
       }
     }
-    return ImmutableSet.copyOf(accountsInTemplate);
+    return accountsInTemplate.build();
   }
 
   public static String getAccountTemplate(Account.Id accountId) {
diff --git a/java/com/google/gerrit/testing/IndexVersions.java b/java/com/google/gerrit/testing/IndexVersions.java
index f245665..3810707 100644
--- a/java/com/google/gerrit/testing/IndexVersions.java
+++ b/java/com/google/gerrit/testing/IndexVersions.java
@@ -96,7 +96,7 @@
         return ImmutableList.copyOf(schemas.keySet());
       }
 
-      List<Integer> versions = new ArrayList<>();
+      ImmutableList.Builder<Integer> versions = ImmutableList.builder();
       for (String s : Splitter.on(',').trimResults().split(value)) {
         if (CURRENT.equals(s)) {
           versions.add(schemaDef.getLatest().getVersion());
@@ -115,15 +115,15 @@
           versions.add(version);
         }
       }
-      return ImmutableList.copyOf(versions);
+      return versions.build();
     }
 
-    List<Integer> schemaVersions = new ArrayList<>(2);
+    ImmutableList.Builder<Integer> schemaVersions = ImmutableList.builderWithExpectedSize(2);
     if (schemaDef.getPrevious() != null) {
       schemaVersions.add(schemaDef.getPrevious().getVersion());
     }
     schemaVersions.add(schemaDef.getLatest().getVersion());
-    return ImmutableList.copyOf(schemaVersions);
+    return schemaVersions.build();
   }
 
   public static <V> Map<String, Config> asConfigMap(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 66dbe80..273737b 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -5581,6 +5581,7 @@
         Status.SATISFIED,
         /* isLegacy= */ false,
         /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
+    assertThat(change.submittable).isTrue();
 
     // Merge the change. Submit requirements are still the same.
     gApi.changes().id(changeId).current().submit();
@@ -5647,6 +5648,7 @@
         Status.UNSATISFIED,
         /* isLegacy= */ false,
         /* submittabilityCondition= */ "label:build-cop-override=MIN");
+    assertThat(change.submittable).isFalse();
   }
 
   @Test
@@ -6271,6 +6273,39 @@
   }
 
   @Test
+  public void legacySubmitRequirementWithIgnoreSelfApproval() throws Exception {
+    LabelType verified =
+        label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+    verified = verified.toBuilder().setIgnoreSelfApproval(true).build();
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().upsertLabelType(verified);
+      u.save();
+    }
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(verified.getName())
+                .ref(RefNames.REFS_HEADS + "*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
+
+    // The DefaultSubmitRule emits an "OK" submit record for Verified, while the
+    // ignoreSelfApprovalRule emits a "NEED" submit record. The "submit requirements" adapter merges
+    // both results and returns the blocking one only.
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    gApi.changes().id(changeId).addReviewer(user.id().toString());
+
+    voteLabel(changeId, verified.getName(), +1);
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    Collection<SubmitRequirementResultInfo> submitRequirements = changeInfo.submitRequirements;
+    assertSubmitRequirementStatus(
+        submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ true);
+  }
+
+  @Test
   public void legacySubmitRequirementDuplicatesGlobal_statusDoesNotMatch_bothRecordsReturned()
       throws Exception {
     // The behaviour does not depend on AllowOverrideInChildProject in global submit requirement.
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
index 31381dd..3bfb573 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
@@ -371,7 +371,7 @@
   }
 
   private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
-    for (AccessSection s : ImmutableList.copyOf(cfg.getAccessSections())) {
+    for (AccessSection s : cfg.getAccessSections()) {
       if (s.getName().startsWith("refs/heads/")
           || s.getName().startsWith("refs/for/")
           || s.getName().equals("refs/*")) {
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 9d1bdaa..e76e2f6 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -379,7 +379,7 @@
   }
 
   private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
-    for (AccessSection s : ImmutableList.copyOf(cfg.getAccessSections())) {
+    for (AccessSection s : cfg.getAccessSections()) {
       if (s.getName().startsWith("refs/heads/")
           || s.getName().startsWith("refs/for/")
           || s.getName().equals("refs/*")) {
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index ef9f004..0905587 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -125,7 +125,7 @@
     // Remove read permissions for all users besides admin.
     try (ProjectConfigUpdate u = updateProject(allProjects)) {
 
-      for (AccessSection sec : ImmutableList.copyOf(u.getConfig().getAccessSections())) {
+      for (AccessSection sec : u.getConfig().getAccessSections()) {
         u.getConfig()
             .upsertAccessSection(
                 sec.getName(),
@@ -142,7 +142,7 @@
 
     // Remove all read permissions on All-Users.
     try (ProjectConfigUpdate u = updateProject(allUsers)) {
-      for (AccessSection sec : ImmutableList.copyOf(u.getConfig().getAccessSections())) {
+      for (AccessSection sec : u.getConfig().getAccessSections()) {
         u.getConfig()
             .upsertAccessSection(
                 sec.getName(),
diff --git a/javatests/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java b/javatests/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java
index 9cdcb40..7d8794b 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java
@@ -22,8 +22,6 @@
 import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.CountDownLatch;
 
 class IndexUpgradeController implements OnlineUpgradeListener {
@@ -45,18 +43,18 @@
   private final CountDownLatch started;
   private final CountDownLatch finished;
 
-  private final List<UpgradeAttempt> startedAttempts;
-  private final List<UpgradeAttempt> succeededAttempts;
-  private final List<UpgradeAttempt> failedAttempts;
+  private final ImmutableList.Builder<UpgradeAttempt> startedAttempts;
+  private final ImmutableList.Builder<UpgradeAttempt> succeededAttempts;
+  private final ImmutableList.Builder<UpgradeAttempt> failedAttempts;
 
   IndexUpgradeController(int numExpected) {
     this.numExpected = numExpected;
     readyToStart = new CountDownLatch(1);
     started = new CountDownLatch(numExpected);
     finished = new CountDownLatch(numExpected);
-    startedAttempts = new ArrayList<>();
-    succeededAttempts = new ArrayList<>();
-    failedAttempts = new ArrayList<>();
+    startedAttempts = ImmutableList.builder();
+    succeededAttempts = ImmutableList.builder();
+    failedAttempts = ImmutableList.builder();
   }
 
   Module module() {
@@ -93,7 +91,7 @@
     finish(UpgradeAttempt.create(name, oldVersion, newVersion), failedAttempts);
   }
 
-  private synchronized void finish(UpgradeAttempt a, List<UpgradeAttempt> out) {
+  private synchronized void finish(UpgradeAttempt a, ImmutableList.Builder<UpgradeAttempt> out) {
     checkState(readyToStart.getCount() == 0, "shouldn't be finishing upgrade before starting");
     checkState(
         finished.getCount() > 0, "already finished %s upgrades, can't finish %s", numExpected, a);
@@ -108,14 +106,14 @@
   }
 
   synchronized ImmutableList<UpgradeAttempt> getStartedAttempts() {
-    return ImmutableList.copyOf(startedAttempts);
+    return startedAttempts.build();
   }
 
   synchronized ImmutableList<UpgradeAttempt> getSucceededAttempts() {
-    return ImmutableList.copyOf(succeededAttempts);
+    return succeededAttempts.build();
   }
 
   synchronized ImmutableList<UpgradeAttempt> getFailedAttempts() {
-    return ImmutableList.copyOf(failedAttempts);
+    return failedAttempts.build();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index c9c26f9..e69f781 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableSet;
@@ -143,7 +143,7 @@
   public void listProjectsToOutputStream() throws Exception {
     int numInitialProjects = gApi.projects().list().get().size();
     int numTestProjects = 5;
-    List<String> testProjects = createProjects("zzz_testProject", numTestProjects);
+    ImmutableSet<String> testProjects = createProjects("zzz_testProject", numTestProjects);
     try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
 
       listProjects.setStart(numInitialProjects);
@@ -153,7 +153,7 @@
           Splitter.on("\n")
               .omitEmptyStrings()
               .splitToList(new String(displayOut.toByteArray(), UTF_8));
-      assertThat(lines).isEqualTo(testProjects);
+      assertThat(lines).isEqualTo(testProjects.asList());
     }
   }
 
@@ -173,8 +173,7 @@
 
     int numInitialProjects = gApi.projects().list().get().size();
     int numTestProjects = 5;
-    Set<String> testProjects =
-        ImmutableSet.copyOf(createProjects("zzz_testProject", numTestProjects));
+    ImmutableSet<String> testProjects = createProjects("zzz_testProject", numTestProjects);
     try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
 
       listProjects.setStart(numInitialProjects);
@@ -191,11 +190,11 @@
     }
   }
 
-  private List<String> createProjects(String prefix, int numProjects) {
+  private ImmutableSet<String> createProjects(String prefix, int numProjects) {
     return IntStream.range(0, numProjects)
         .mapToObj(i -> projectOperations.newProject().name(prefix + i).create())
         .map(Project.NameKey::get)
-        .collect(toList());
+        .collect(toImmutableSet());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 8bf70f7..044da19 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -480,7 +480,7 @@
   }
 
   private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
-    for (AccessSection accessSection : ImmutableList.copyOf(cfg.getAccessSections())) {
+    for (AccessSection accessSection : cfg.getAccessSections()) {
       cfg.upsertAccessSection(
           accessSection.getName(),
           updatedAccessSection -> {
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
index 46687e3..3508112 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
@@ -53,6 +53,7 @@
 import com.google.inject.Module;
 import java.util.Collection;
 import java.util.Set;
+import java.util.stream.StreamSupport;
 import javax.inject.Inject;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -144,13 +145,14 @@
 
                       @Override
                       public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
-                        return ImmutableList.copyOf(groupIds).stream().anyMatch(g -> contains(g));
+                        return StreamSupport.stream(groupIds.spliterator(), /* parallel= */ false)
+                            .anyMatch(g -> contains(g));
                       }
 
                       @Override
                       public Set<AccountGroup.UUID> intersection(
                           Iterable<AccountGroup.UUID> groupIds) {
-                        return ImmutableList.copyOf(groupIds).stream()
+                        return StreamSupport.stream(groupIds.spliterator(), /* parallel= */ false)
                             .filter(g -> contains(g))
                             .collect(toImmutableSet());
                       }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
index ae45d90..ce5cff7 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -31,8 +31,6 @@
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
-import java.util.ArrayList;
-import java.util.List;
 import org.junit.Test;
 
 @UseSsh
@@ -120,7 +118,7 @@
   }
 
   private static class TestPerformanceLogger implements PerformanceLogger {
-    private List<PerformanceLogEntry> logEntries = new ArrayList<>();
+    private ImmutableList.Builder<PerformanceLogEntry> logEntries = ImmutableList.builder();
 
     @Override
     public void log(String operation, long durationMs, Metadata metadata) {
@@ -128,7 +126,7 @@
     }
 
     ImmutableList<PerformanceLogEntry> logEntries() {
-      return ImmutableList.copyOf(logEntries);
+      return logEntries.build();
     }
   }
 
diff --git a/javatests/com/google/gerrit/mail/data/NonUTF8Message.java b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
index 60368eb..86a0b56 100644
--- a/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
+++ b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
@@ -45,7 +45,8 @@
   public int[] rawChars() {
     int[] arr = new int[raw.length()];
     int i = 0;
-    for (char c : raw.toCharArray()) {
+    for (int j = 0; j < raw.length(); j++) {
+      char c = raw.charAt(j);
       arr[i++] = c;
     }
     return arr;
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index 38e50b5..b048163 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -100,7 +100,7 @@
   private void configureProject() throws Exception {
     ProjectConfig pc = loadAllProjects();
 
-    for (AccessSection sec : ImmutableList.copyOf(pc.getAccessSections())) {
+    for (AccessSection sec : pc.getAccessSections()) {
       pc.upsertAccessSection(
           sec.getName(),
           updatedSection -> {
diff --git a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
index cb6de34..7316074 100644
--- a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
@@ -24,7 +24,8 @@
 
   @Test
   public void validPathSeparator() {
-    for (char c : VALID_CHARACTERS.toCharArray()) {
+    for (int i = 0; i < VALID_CHARACTERS.length(); i++) {
+      char c = VALID_CHARACTERS.charAt(i);
       assertWithMessage("valid character rejected: " + c)
           .that(GitwebConfig.isValidPathSeparator(c))
           .isTrue();
@@ -33,7 +34,8 @@
 
   @Test
   public void inalidPathSeparator() {
-    for (char c : SOME_INVALID_CHARACTERS.toCharArray()) {
+    for (int i = 0; i < SOME_INVALID_CHARACTERS.length(); i++) {
+      char c = SOME_INVALID_CHARACTERS.charAt(i);
       assertWithMessage("invalid character accepted: " + c)
           .that(GitwebConfig.isValidPathSeparator(c))
           .isFalse();
diff --git a/javatests/com/google/gerrit/server/git/GarbageCollectionTest.java b/javatests/com/google/gerrit/server/git/GarbageCollectionTest.java
new file mode 100644
index 0000000..41b5d79
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/GarbageCollectionTest.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.GcConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import java.io.IOException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class GarbageCollectionTest {
+  private static final Project.NameKey FOO = Project.nameKey("foo");
+
+  @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  @Mock private GcConfig gcConfig;
+  @Mock private DelegateRepository wrapper;
+
+  private SitePaths site;
+  private Config cfg;
+
+  @Before
+  public void setup() throws Exception {
+    site = new SitePaths(temporaryFolder.newFolder().toPath());
+    site.resolve("git").toFile().mkdir();
+    cfg = new Config();
+    cfg.setString("gerrit", null, "basePath", "git");
+  }
+
+  @Test
+  public void shouldCallGcOnDelegatedRepositoryWhenDelegateRepositoryIsPassed() throws IOException {
+    // given
+    GarbageCollection objectUnderTest = prepareObjectForTesting();
+
+    // when
+    objectUnderTest.run(ImmutableList.of(FOO), false, null);
+
+    // then
+    verify(wrapper).delegate();
+  }
+
+  private GarbageCollection prepareObjectForTesting() throws IOException {
+    LocalDiskRepositoryManager repoManager = new DelegatedRepositoryManager(site, cfg, wrapper);
+    try (Repository repo = repoManager.createRepository(FOO)) {
+      assertThat(repo).isNotNull();
+    }
+    return new GarbageCollection(
+        repoManager,
+        new GarbageCollectionQueue(),
+        gcConfig,
+        new PluginSetContext<>(new DynamicSet<>(), PluginMetrics.DISABLED_INSTANCE));
+  }
+
+  private static final class DelegatedRepositoryManager extends LocalDiskRepositoryManager {
+    private final DelegateRepository wrapper;
+
+    private DelegatedRepositoryManager(SitePaths site, Config cfg, DelegateRepository wrapper) {
+      super(site, cfg);
+      this.wrapper = wrapper;
+    }
+
+    @Override
+    public Repository openRepository(NameKey name) throws RepositoryNotFoundException {
+      Repository opened = super.openRepository(name);
+      when(wrapper.delegate()).thenReturn(opened);
+      when(wrapper.getConfig()).thenReturn(opened.getConfig());
+      return wrapper;
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index fefa066..e60d6b4 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -32,8 +32,6 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 import org.eclipse.jgit.lib.Config;
 import org.junit.After;
@@ -360,7 +358,7 @@
   }
 
   private static class TestPerformanceLogger implements PerformanceLogger {
-    private List<PerformanceLogEntry> logEntries = new ArrayList<>();
+    private ImmutableList.Builder<PerformanceLogEntry> logEntries = ImmutableList.builder();
 
     @Override
     public void log(String operation, long durationMs, Metadata metadata) {
@@ -368,7 +366,7 @@
     }
 
     ImmutableList<PerformanceLogEntry> logEntries() {
-      return ImmutableList.copyOf(logEntries);
+      return logEntries.build();
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
index 646f0cd..767ac28 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -34,8 +34,6 @@
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestUpdateUI;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.junit.TestRepository;
@@ -84,7 +82,7 @@
     protected final NoteDbSchemaUpdater updater;
     protected final GitRepositoryManager repoManager;
     protected final NoteDbSchemaVersion.Arguments args;
-    private final List<String> messages;
+    private final ImmutableList.Builder<String> messages;
 
     TestUpdate(Optional<Integer> initialVersion) {
       cfg = new Config();
@@ -106,7 +104,7 @@
               versionManager,
               args,
               ImmutableSortedMap.of(10, TestSchema_10.class, 11, TestSchema_11.class));
-      messages = new ArrayList<>();
+      messages = ImmutableList.builder();
     }
 
     private class TestSchemaCreator implements SchemaCreator {
@@ -173,7 +171,7 @@
     }
 
     ImmutableList<String> getMessages() {
-      return ImmutableList.copyOf(messages);
+      return messages.build();
     }
 
     Optional<Integer> readVersion() throws Exception {
diff --git a/plugins/gitiles b/plugins/gitiles
index 97ce60f..a0709a4 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 97ce60f8bb4dbf40dde79cf56db6425c384dabcf
+Subproject commit a0709a402ee1d4fe3921fd81e575ec48a053cc9f
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index d1c320f..17b0ba0 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -97,6 +97,11 @@
   actions?: Action[];
 
   /**
+   * Shown prominently in the change summary below the run chips.
+   */
+  summaryMessage?: string;
+
+  /**
    * Top-level links that are not associated with a specific run or result.
    * Will be shown as icons in the header of the Checks tab.
    */
diff --git a/polygerrit-ui/app/api/reporting.ts b/polygerrit-ui/app/api/reporting.ts
index c3655bb..40474e1 100644
--- a/polygerrit-ui/app/api/reporting.ts
+++ b/polygerrit-ui/app/api/reporting.ts
@@ -18,6 +18,27 @@
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export type EventDetails = any;
 
+export enum Deduping {
+  /**
+   * Only report the event once per session, even if the event details are
+   * different.
+   */
+  EVENT_ONCE_PER_SESSION = 'EVENT_ONCE_PER_SESSION',
+  /**
+   * Only report the event once per change, even if the event details are
+   * different.
+   */
+  EVENT_ONCE_PER_CHANGE = 'EVENT_ONCE_PER_CHANGE',
+  /** Only report these exact event details once per session. */
+  DETAILS_ONCE_PER_SESSION = 'DETAILS_ONCE_PER_SESSION',
+  /** Only report these exact event details once per change. */
+  DETAILS_ONCE_PER_CHANGE = 'DETAILS_ONCE_PER_CHANGE',
+}
+export declare interface ReportingOptions {
+  /** Set this, if you don't want to report *every* time. */
+  deduping?: Deduping;
+}
+
 export declare interface ReportingPluginApi {
   reportInteraction(eventName: string, details?: EventDetails): void;
 
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index ca3f2d7..acb8679 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -726,6 +726,7 @@
 
 export declare interface LabelCommonInfo {
   optional?: boolean; // not set if false
+  description?: string;
 }
 
 export type LabelNameToInfoMap = {[labelName: string]: LabelInfo};
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index 78fffe3..52747b3 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -108,4 +108,5 @@
   COMMENT_SAVED = 'comment-saved',
   DISCARD_COMMENT = 'discard-comment',
   COMMENT_DISCARDED = 'comment-discarded',
+  CHECKS_TAB_RENDERED = 'checks-tab-rendered',
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index cf5d952..f8b36c4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -260,7 +260,7 @@
   }
 
   _computeShowInherit(inheritsFrom?: ProjectInfo) {
-    return inheritsFrom?.id?.length ? 'show' : '';
+    return this._editing || inheritsFrom?.id?.length ? 'show' : '';
   }
 
   // TODO(TS): Unclear what is model here, provide a better explanation
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
index 84cf6d9..b4fa7cc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
@@ -75,10 +75,10 @@
     ></gr-user-header>
     <h1 class="assistive-tech-only">Dashboard</h1>
     <gr-change-list
-      show-star=""
+      showstar=""
       account="[[account]]"
       preferences="[[preferences]]"
-      selected-index="[[_selectedChangeIndex]]"
+      selectedindex="[[_selectedChangeIndex]]"
       sections="[[_results]]"
       on-selected-index-changed="_handleSelectedIndexChanged"
       on-toggle-star="_handleToggleStar"
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index c90359e..3298e53 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -405,6 +405,9 @@
   @state()
   actions: Action[] = [];
 
+  @state()
+  messages: string[] = [];
+
   private showAllChips = new Map<RunStatus | Category, boolean>();
 
   private getCommentsModel = resolve(this, commentsModelToken);
@@ -447,6 +450,11 @@
     );
     subscribe(
       this,
+      this.checksModel.topLevelMessagesLatest$,
+      x => (this.messages = x)
+    );
+    subscribe(
+      this,
       this.getCommentsModel().changeComments$,
       x => (this.changeComments = x)
     );
@@ -556,10 +564,18 @@
         .actions #moreMessage {
           display: none;
         }
+        .summaryMessage {
+          line-height: var(--line-height-normal);
+          color: var(--primary-text-color);
+        }
       `,
     ];
   }
 
+  private renderSummaryMessage() {
+    return this.messages.map(m => html`<div class="summaryMessage">${m}</div>`);
+  }
+
   private renderActions() {
     const actions = this.actions ?? [];
     const summaryActions = actions.filter(a => a.summary).slice(0, 2);
@@ -794,7 +810,8 @@
                   class="loadingSpin"
                   ?hidden="${!this.someProvidersAreLoading}"
                 ></span>
-                ${this.renderErrorMessages()}${this.renderChecksLogin()}${this.renderActions()}
+                ${this.renderErrorMessages()} ${this.renderChecksLogin()}
+                ${this.renderSummaryMessage()} ${this.renderActions()}
               </div>
             </td>
           </tr>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 18eda12..0f19016 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -2152,29 +2152,28 @@
 
     if (isLocationChange) {
       this._editingCommitMessage = false;
-      const relatedChangesLoaded = coreDataPromise.then(() => {
-        let relatedChangesPromise:
-          | Promise<RelatedChangesInfo | undefined>
-          | undefined;
-        const patchNum = this._computeLatestPatchNum(this._allPatchSets);
-        if (this._change && patchNum) {
-          relatedChangesPromise = this.restApiService
-            .getRelatedChanges(this._change._number, patchNum)
-            .then(response => {
-              if (this._change && response) {
-                this.hasParent = this._calculateHasParent(
-                  this._change.change_id,
-                  response.changes
-                );
-              }
-              return response;
-            });
-        }
-        // TODO: use returned Promise
-        this.getRelatedChangesList()?.reload(relatedChangesPromise);
-      });
-      allDataPromises.push(relatedChangesLoaded);
     }
+    const relatedChangesLoaded = coreDataPromise.then(() => {
+      let relatedChangesPromise:
+        | Promise<RelatedChangesInfo | undefined>
+        | undefined;
+      const patchNum = this._computeLatestPatchNum(this._allPatchSets);
+      if (this._change && patchNum) {
+        relatedChangesPromise = this.restApiService
+          .getRelatedChanges(this._change._number, patchNum)
+          .then(response => {
+            if (this._change && response) {
+              this.hasParent = this._calculateHasParent(
+                this._change.change_id,
+                response.changes
+              );
+            }
+            return response;
+          });
+      }
+      return this.getRelatedChangesList()?.reload(relatedChangesPromise);
+    });
+    allDataPromises.push(relatedChangesLoaded);
 
     Promise.all(allDataPromises).then(() => {
       // Loading of commments data is no longer part of this reporting
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 4ca593a..c432276 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -1448,15 +1448,27 @@
     assert.isTrue(recreateSpy.calledOnce);
   });
 
-  test('related changes are not updated after other action', async () => {
-    sinon.stub(element, 'loadData').callsFake(() => Promise.resolve());
+  test('related changes are updated when loadData is called', async () => {
     await flush();
     const relatedChanges = element.shadowRoot!.querySelector(
       '#relatedChanges'
     ) as GrRelatedChangesList;
-    sinon.stub(relatedChanges, 'reload');
+    const reloadStub = sinon.stub(relatedChanges, 'reload');
+    stubRestApi('getMergeable').returns(
+      Promise.resolve({...createMergeable(), mergeable: true})
+    );
+
+    element.params = createAppElementChangeViewParams();
+    element.changeModel.setState({
+      loadingStatus: LoadingStatus.LOADED,
+      change: {
+        ...createChangeViewChange(),
+      },
+    });
+
     await element.loadData(true);
     assert.isFalse(navigateToChangeStub.called);
+    assert.isTrue(reloadStub.called);
   });
 
   test('_computeCopyTextForTitle', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index a0c6b83..59dcd0e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -29,11 +29,6 @@
     :host([disabled]) .container {
       opacity: 0.5;
     }
-    .container {
-      display: flex;
-      flex-direction: column;
-      max-height: 100%;
-    }
     section {
       border-top: 1px solid var(--border-color);
       flex-shrink: 0;
@@ -261,7 +256,7 @@
     }
 
   </style>
-  <div class$="container" tabindex="-1">
+  <div tabindex="-1">
     <section class="peopleContainer">
       <gr-endpoint-decorator name="reply-reviewers">
         <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
@@ -404,7 +399,7 @@
         Saving comments...
       </span>
     </section>
-    <div class$="stickyBottom newReplyDialog">
+    <div class="stickyBottom newReplyDialog">
       <gr-endpoint-decorator name="reply-bottom">
         <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
         <section
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 4294e3f..0d0088f 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -171,17 +171,17 @@
           </tr>
         </thead>
         <tbody>
-          ${submit_requirements.map(requirement =>
-            this.renderRequirement(requirement)
+          ${submit_requirements.map((requirement, index) =>
+            this.renderRequirement(requirement, index)
           )}
         </tbody>
       </table>
       ${this.disableHovercards
         ? ''
         : submit_requirements.map(
-            requirement => html`
+            (requirement, index) => html`
               <gr-submit-requirement-hovercard
-                for="requirement-${charsOnly(requirement.name)}"
+                for="requirement-${index}-${charsOnly(requirement.name)}"
                 .requirement="${requirement}"
                 .change="${this.change}"
                 .account="${this.account}"
@@ -192,9 +192,9 @@
       ${this.renderTriggerVotes()}`;
   }
 
-  renderRequirement(requirement: SubmitRequirementResultInfo) {
+  renderRequirement(requirement: SubmitRequirementResultInfo, index: number) {
     return html`
-      <tr id="requirement-${charsOnly(requirement.name)}">
+      <tr id="requirement-${index}-${charsOnly(requirement.name)}">
         <td>${this.renderStatus(requirement.status)}</td>
         <td class="name">
           <gr-limited-text
@@ -423,7 +423,10 @@
     if (!this.labelInfo) return;
     return html`
       <div class="container">
-        <gr-trigger-vote-hovercard .labelName=${this.label}>
+        <gr-trigger-vote-hovercard
+          .labelName=${this.label}
+          .labelInfo=${this.labelInfo}
+        >
           <gr-label-info
             slot="label-info"
             .change=${this.change}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index 5a094ef..323c70f 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -85,7 +85,7 @@
         </tr>
       </thead>
       <tbody>
-        <tr id="requirement-Verified">
+        <tr id="requirement-0-Verified">
           <td>
             <iron-icon
               aria-label="satisfied"
@@ -111,7 +111,7 @@
         </tr>
       </tbody>
     </table>
-    <gr-submit-requirement-hovercard for="requirement-Verified">
+    <gr-submit-requirement-hovercard for="requirement-0-Verified">
     </gr-submit-requirement-hovercard>
   `);
   });
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
index 552cc69..33c2eac 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
@@ -18,6 +18,7 @@
 import {css, html, LitElement} from 'lit';
 import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
 import {fontStyles} from '../../../styles/gr-font-styles';
+import {LabelInfo} from '../../../api/rest-api';
 
 // This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
 const base = HovercardMixin(LitElement);
@@ -27,6 +28,9 @@
   @property()
   labelName?: string;
 
+  @property({type: Object})
+  labelInfo?: LabelInfo;
+
   static override get styles() {
     return [
       fontStyles,
@@ -83,6 +87,18 @@
           </div>
         </div>
       </div>
+      ${this.renderDescription()}
+    </div>`;
+  }
+
+  private renderDescription() {
+    const description = this.labelInfo?.description;
+    if (!description) return;
+    return html`<div class="section description">
+      <div class="sectionIcon">
+        <iron-icon icon="gr-icons:description"></iron-icon>
+      </div>
+      <div class="sectionContent">${description}</div>
     </div>`;
   }
 }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 4929b7c..f4bbc5d 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -676,6 +676,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -683,6 +684,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -690,6 +692,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -697,6 +700,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -704,6 +708,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -711,6 +716,7 @@
       [],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
   }
@@ -721,6 +727,7 @@
       [fakeRun0],
       fakeActions,
       fakeLinks,
+      'ETA: 1 min',
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -728,6 +735,7 @@
       [fakeRun1],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -735,6 +743,7 @@
       [fakeRun2],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -742,6 +751,7 @@
       [fakeRun3],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -749,6 +759,7 @@
       fakeRun4Att,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     this.checksModel.updateStateSetResults(
@@ -756,6 +767,7 @@
       [fakeRun5],
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
   }
@@ -764,7 +776,8 @@
     plugin: string,
     runs: CheckRun[],
     actions: Action[] = [],
-    links: Link[] = []
+    links: Link[] = [],
+    summaryMessage: string | undefined = undefined
   ) {
     const newRuns = this.runs.includes(runs[0]) ? [] : runs;
     this.checksModel.updateStateSetResults(
@@ -772,6 +785,7 @@
       newRuns,
       actions,
       links,
+      summaryMessage,
       ChecksPatchset.LATEST
     );
   }
@@ -843,7 +857,13 @@
         <gr-button
           link
           @click="${() =>
-            this.toggle('f0', [fakeRun0], fakeActions, fakeLinks)}"
+            this.toggle(
+              'f0',
+              [fakeRun0],
+              fakeActions,
+              fakeLinks,
+              'ETA: 1 min'
+            )}"
           >0</gr-button
         >
         <gr-button link @click="${() => this.toggle('f1', [fakeRun1])}"
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index a9c30c5..0ffde6f 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -26,6 +26,8 @@
 import {ChecksTabState} from '../../types/events';
 import {getAppContext} from '../../services/app-context';
 import {subscribe} from '../lit/subscription-controller';
+import {Deduping} from '../../api/reporting';
+import {Interaction} from '../../constants/reporting';
 
 /**
  * The "Checks" tab on the Gerrit change page. Gets its data from plugins that
@@ -65,6 +67,8 @@
 
   private readonly checksModel = getAppContext().checksModel;
 
+  private readonly reporting = getAppContext().reportingService;
+
   constructor() {
     super();
     subscribe(
@@ -113,6 +117,11 @@
   }
 
   override render() {
+    this.reporting.reportInteraction(
+      Interaction.CHECKS_TAB_RENDERED,
+      this.tabState,
+      {deduping: Deduping.DETAILS_ONCE_PER_CHANGE}
+    );
     return html`
       <div class="container">
         <gr-checks-runs
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.ts
index 10476cd..0edc57b 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.ts
@@ -45,7 +45,7 @@
     <div class="main" slot="main">[[text]]</div>
     <gr-button
       id="signIn"
-      class$="signInLink"
+      class="signInLink"
       hidden$="[[!showSignInButton]]"
       link=""
       slot="footer"
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index 0a9fbbf..bfe396c 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -64,7 +64,11 @@
     if (!config || !config.change) return true;
     if (column === 'Comments')
       return this.flagsService.isEnabled('comments-column');
-    if (column === 'Requirements')
+    if (column === 'Status')
+      return !this.flagsService.isEnabled(
+        KnownExperimentId.SUBMIT_REQUIREMENTS_UI
+      );
+    if (column === ' Status ')
       return this.flagsService.isEnabled(
         KnownExperimentId.SUBMIT_REQUIREMENTS_UI
       );
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index ee40ad5..151556c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -72,6 +72,7 @@
 import {notDeepEqual} from '../../../utils/deep-util';
 import {resolve} from '../../../models/dependency';
 import {commentsModelToken} from '../../../models/comments/comments-model';
+import {whenRendered} from '../../../utils/dom-util';
 
 const NEWLINE_PATTERN = /\n/g;
 
@@ -626,8 +627,11 @@
 
   override firstUpdated() {
     if (this.shouldScrollIntoView) {
-      this.commentBox?.focus();
-      this.scrollIntoView();
+      whenRendered(this, () => {
+        this.expandCollapseComments(false);
+        this.commentBox?.focus();
+        this.scrollIntoView({block: 'center'});
+      });
     }
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index a0b3274..eda0438 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -221,11 +221,19 @@
     const labelInfo = this.labelInfo;
     if (!labelInfo) return;
     const reviewers = (this.change?.reviewers['REVIEWER'] ?? [])
-      .filter(
-        reviewer =>
-          (this.showAllReviewers && canVote(labelInfo, reviewer)) ||
-          (!this.showAllReviewers && hasVoted(labelInfo, reviewer))
-      )
+      .filter(reviewer => {
+        if (this.showAllReviewers) {
+          if (isDetailedLabelInfo(labelInfo)) {
+            return canVote(labelInfo, reviewer);
+          } else {
+            // isQuickLabelInfo
+            return hasVoted(labelInfo, reviewer);
+          }
+        } else {
+          // !showAllReviewers
+          return hasVoted(labelInfo, reviewer);
+        }
+      })
       .sort((r1, r2) => sortReviewers(r1, r2, this.change, this.account));
     return html`<div>
       ${reviewers.map(reviewer => this.renderReviewerVote(reviewer))}
@@ -251,10 +259,14 @@
 
   renderReviewerVote(reviewer: AccountInfo) {
     const labelInfo = this.labelInfo;
-    if (!labelInfo || !isDetailedLabelInfo(labelInfo)) return;
-    const approvalInfo = getApprovalInfo(labelInfo, reviewer);
+    if (!labelInfo) return;
+    const approvalInfo = isDetailedLabelInfo(labelInfo)
+      ? getApprovalInfo(labelInfo, reviewer)
+      : undefined;
     const noVoteYet =
-      !approvalInfo || hasNeutralStatus(labelInfo, approvalInfo);
+      !hasVoted(labelInfo, reviewer) ||
+      (isDetailedLabelInfo(labelInfo) &&
+        hasNeutralStatus(labelInfo, approvalInfo));
     return html`<div class="reviewer-row">
       <gr-account-chip .account="${reviewer}" .change="${this.change}">
         <gr-vote-chip
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 2bdee51..e58e594 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -122,6 +122,7 @@
   errorMessage?: string;
   /** Presence of loginCallback implicitly means that the provider is in NOT_LOGGED_IN state. */
   loginCallback?: () => void;
+  summaryMessage?: string;
   runs: CheckRun[];
   actions: Action[];
   links: Link[];
@@ -239,6 +240,13 @@
     )
   );
 
+  public topLevelMessagesLatest$ = select(this.checksLatest$, state => {
+    const messages = Object.values(state).map(
+      providerState => providerState.summaryMessage
+    );
+    return messages.filter(m => m !== undefined) as string[];
+  });
+
   public topLevelActionsSelected$ = select(this.checksSelected$, state =>
     Object.values(state).reduce(
       (allActions: Action[], providerState: ChecksProviderState) => [
@@ -444,6 +452,7 @@
     runs: CheckRunApi[],
     actions: Action[] = [],
     links: Link[] = [],
+    summaryMessage: string | undefined,
     patchset: ChecksPatchset
   ) {
     const attemptMap = createAttemptMap(runs);
@@ -483,6 +492,7 @@
       }),
       actions: [...actions],
       links: [...links],
+      summaryMessage,
     };
     this.subject$.next(nextState);
   }
@@ -701,6 +711,7 @@
                 response.runs ?? [],
                 response.actions ?? [],
                 response.links ?? [],
+                response.summaryMessage,
                 patchset
               );
               break;
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts
index 1bb0f8a..c2588fe 100644
--- a/polygerrit-ui/app/services/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -120,6 +120,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.isFalse(current.loading);
@@ -132,6 +133,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.isFalse(current.loading);
@@ -144,6 +146,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.lengthOf(current.runs, 1);
@@ -156,6 +159,7 @@
       RUNS,
       [],
       [],
+      undefined,
       ChecksPatchset.LATEST
     );
     assert.equal(
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 518716b..0da8b4c 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -16,7 +16,7 @@
  */
 import {Finalizable} from '../registry';
 import {NumericChangeId} from '../../types/common';
-import {EventDetails} from '../../api/reporting';
+import {EventDetails, ReportingOptions} from '../../api/reporting';
 import {PluginApi} from '../../api/plugin';
 import {
   Execution,
@@ -113,7 +113,8 @@
   ): void;
   reportInteraction(
     eventName: string | Interaction,
-    details?: EventDetails
+    details?: EventDetails,
+    options?: ReportingOptions
   ): void;
   reportErrorDialog(message: string): void;
   setRepoName(repoName: string): void;
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index a01e9db..bf10da9 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -18,7 +18,7 @@
 import {EventValue, ReportingService, Timer} from './gr-reporting';
 import {hasOwnProperty} from '../../utils/common-util';
 import {NumericChangeId} from '../../types/common';
-import {EventDetails} from '../../api/reporting';
+import {Deduping, EventDetails, ReportingOptions} from '../../api/reporting';
 import {PluginApi} from '../../api/plugin';
 import {Finalizable} from '../registry';
 import {
@@ -285,10 +285,10 @@
   private slowRpcList: SlowRpcCall[] = [];
 
   /**
-   * Keeps track of which ids were already reported to have been executed.
-   * Execution ids should only be reported once per session.
+   * Keeps track of which ids were already reported for events that should only
+   * be reported once per session.
    */
-  private executionReported = new Set<string>();
+  private reportedIds = new Set<string>();
 
   public readonly hiddenDurationTimer = new HiddenDurationTimer();
 
@@ -815,7 +815,43 @@
     );
   }
 
-  reportInteraction(eventName: string | Interaction, details: EventDetails) {
+  /**
+   * Returns true when the event was deduped and thus should not be reported.
+   */
+  _dedup(
+    eventName: string | Interaction,
+    details: EventDetails,
+    deduping?: Deduping
+  ): boolean {
+    if (!deduping) return false;
+    let id = '';
+    switch (deduping) {
+      case Deduping.DETAILS_ONCE_PER_CHANGE:
+        id = `${eventName}-${this.reportChangeId}-${JSON.stringify(details)}`;
+        break;
+      case Deduping.DETAILS_ONCE_PER_SESSION:
+        id = `${eventName}-${JSON.stringify(details)}`;
+        break;
+      case Deduping.EVENT_ONCE_PER_CHANGE:
+        id = `${eventName}-${this.reportChangeId}`;
+        break;
+      case Deduping.EVENT_ONCE_PER_SESSION:
+        id = `${eventName}`;
+        break;
+      default:
+        throw new Error(`Invalid 'deduping' option '${deduping}'.`);
+    }
+    if (this.reportedIds.has(id)) return true;
+    this.reportedIds.add(id);
+    return false;
+  }
+
+  reportInteraction(
+    eventName: string | Interaction,
+    details: EventDetails,
+    options?: ReportingOptions
+  ) {
+    if (this._dedup(eventName, details, options?.deduping)) return;
     this.reporter(
       INTERACTION.TYPE,
       INTERACTION.CATEGORY.DEFAULT,
@@ -827,9 +863,7 @@
   }
 
   reportExecution(name: Execution, details?: EventDetails) {
-    const id = `${name}${JSON.stringify(details)}`;
-    if (this.executionReported.has(id)) return;
-    this.executionReported.add(id);
+    if (this._dedup(name, details, Deduping.DETAILS_ONCE_PER_SESSION)) return;
     this.reporter(
       LIFECYCLE.TYPE,
       LIFECYCLE.CATEGORY.EXECUTION,
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
index 8068dc00..990a5c8 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
@@ -18,6 +18,7 @@
 import '../../test/common-test-setup-karma.js';
 import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting_impl.js';
 import {getAppContext} from '../app-context.js';
+import {Deduping} from '../../api/reporting.js';
 suite('gr-reporting tests', () => {
   let service;
 
@@ -347,6 +348,44 @@
     assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
   });
 
+  test('dedup', () => {
+    assert.isFalse(service._dedup('a', undefined, undefined));
+    assert.isFalse(service._dedup('a', undefined, undefined));
+
+    let deduping = Deduping.EVENT_ONCE_PER_SESSION;
+    assert.isFalse(service._dedup('b', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('b', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('b', {x: 'bar'}, deduping));
+
+    deduping = Deduping.DETAILS_ONCE_PER_SESSION;
+    assert.isFalse(service._dedup('c', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('c', {x: 'foo'}, deduping));
+    assert.isFalse(service._dedup('c', {x: 'bar'}, deduping));
+    assert.isTrue(service._dedup('c', {x: 'bar'}, deduping));
+
+    deduping = Deduping.EVENT_ONCE_PER_CHANGE;
+    service.setChangeId(1);
+    assert.isFalse(service._dedup('d', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('d', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('d', {x: 'bar'}, deduping));
+    service.setChangeId(2);
+    assert.isFalse(service._dedup('d', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('d', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('d', {x: 'bar'}, deduping));
+
+    deduping = Deduping.DETAILS_ONCE_PER_CHANGE;
+    service.setChangeId(1);
+    assert.isFalse(service._dedup('e', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('e', {x: 'foo'}, deduping));
+    assert.isFalse(service._dedup('e', {x: 'bar'}, deduping));
+    assert.isTrue(service._dedup('e', {x: 'bar'}, deduping));
+    service.setChangeId(2);
+    assert.isFalse(service._dedup('e', {x: 'foo'}, deduping));
+    assert.isTrue(service._dedup('e', {x: 'foo'}, deduping));
+    assert.isFalse(service._dedup('e', {x: 'bar'}, deduping));
+    assert.isTrue(service._dedup('e', {x: 'bar'}, deduping));
+  });
+
   suite('plugins', () => {
     setup(() => {
       service.reporter.restore();
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index b96ebe6..34f0bc1 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -490,3 +490,18 @@
   }
   return false;
 }
+
+/** Executes the given callback when the element's height is > 0. */
+export function whenRendered(el: HTMLElement, callback: () => void) {
+  if (el.clientHeight > 0) {
+    callback();
+    return;
+  }
+  const obs = new ResizeObserver(() => {
+    if (el.clientHeight > 0) {
+      callback();
+      obs.unobserve(el);
+    }
+  });
+  obs.observe(el);
+}
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 437258c..82380849 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -148,7 +148,10 @@
   if (isDetailedLabelInfo(label)) {
     return !hasNeutralStatus(label, getApprovalInfo(label, account));
   } else if (isQuickLabelInfo(label)) {
-    return label.approved === account || label.rejected === account;
+    return (
+      label.approved?._account_id === account._account_id ||
+      label.rejected?._account_id === account._account_id
+    );
   }
   return false;
 }
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
index 7febbac..8f63d08 100644
--- a/tools/bzl/BUILD
+++ b/tools/bzl/BUILD
@@ -7,4 +7,5 @@
 sh_test(
     name = "always_pass_test",
     srcs = ["always_pass_test.sh"],
+    tags = ["no_rbe"],
 )