Merge "Nicer error messages for unresolved dependencies"
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
index 832933b..d8999e3 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.extensions.registration;
 
+import com.google.common.collect.ImmutableList;
 import com.google.inject.Binding;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -38,11 +38,12 @@
     return new DynamicSet<>(find(injector, type));
   }
 
-  private static <T> List<AtomicReference<Extension<T>>> find(Injector src, TypeLiteral<T> type) {
+  private static <T> ImmutableList<AtomicReference<Extension<T>>> find(
+      Injector src, TypeLiteral<T> type) {
     List<Binding<T>> bindings = src.findBindingsByType(type);
     int cnt = bindings != null ? bindings.size() : 0;
     if (cnt == 0) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
     List<AtomicReference<Extension<T>>> r = new ArrayList<>(cnt);
     for (Binding<T> b : bindings) {
@@ -50,6 +51,6 @@
         r.add(new AtomicReference<>(new Extension<>(PluginName.GERRIT, b.getProvider())));
       }
     }
-    return r;
+    return ImmutableList.copyOf(r);
   }
 }
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
index fd31fcd..5b528cb 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.registration;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.Binding;
 import com.google.inject.Inject;
@@ -80,10 +81,10 @@
     return Collections.unmodifiableMap(m);
   }
 
-  public static List<RegistrationHandle> attachItems(
+  public static ImmutableList<RegistrationHandle> attachItems(
       Injector src, String pluginName, Map<TypeLiteral<?>, DynamicItem<?>> items) {
     if (src == null || items == null || items.isEmpty()) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     List<RegistrationHandle> handles = new ArrayList<>(4);
@@ -103,13 +104,13 @@
       remove(handles);
       throw e;
     }
-    return handles;
+    return ImmutableList.copyOf(handles);
   }
 
-  public static List<RegistrationHandle> attachSets(
+  public static ImmutableList<RegistrationHandle> attachSets(
       Injector src, String pluginName, Map<TypeLiteral<?>, DynamicSet<?>> sets) {
     if (src == null || sets == null || sets.isEmpty()) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     List<RegistrationHandle> handles = new ArrayList<>(4);
@@ -131,13 +132,13 @@
       remove(handles);
       throw e;
     }
-    return handles;
+    return ImmutableList.copyOf(handles);
   }
 
-  public static List<RegistrationHandle> attachMaps(
+  public static ImmutableList<RegistrationHandle> attachMaps(
       Injector src, String pluginName, Map<TypeLiteral<?>, DynamicMap<?>> maps) {
     if (src == null || maps == null || maps.isEmpty()) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     List<RegistrationHandle> handles = new ArrayList<>(4);
@@ -160,7 +161,7 @@
       remove(handles);
       throw e;
     }
-    return handles;
+    return ImmutableList.copyOf(handles);
   }
 
   public static LifecycleListener registerInParentInjectors() {
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index d46b344..5dc6d01 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -164,7 +164,7 @@
     }
   }
 
-  private Map<ExternalId, Fingerprint> readKeysToRemove(
+  private ImmutableMap<ExternalId, Fingerprint> readKeysToRemove(
       GpgKeysInput input, Collection<ExternalId> existingExtIds) {
     if (input.delete == null || input.delete.isEmpty()) {
       return ImmutableMap.of();
@@ -179,10 +179,11 @@
         // Skip removal.
       }
     }
-    return fingerprints;
+    return ImmutableMap.copyOf(fingerprints);
   }
 
-  private List<PGPPublicKeyRing> readKeysToAdd(GpgKeysInput input, Collection<Fingerprint> toRemove)
+  private ImmutableList<PGPPublicKeyRing> readKeysToAdd(
+      GpgKeysInput input, Collection<Fingerprint> toRemove)
       throws BadRequestException, IOException {
     if (input.add == null || input.add.isEmpty()) {
       return ImmutableList.of();
@@ -206,7 +207,7 @@
         throw new BadRequestException("Failed to parse GPG keys", e);
       }
     }
-    return keyRings;
+    return ImmutableList.copyOf(keyRings);
   }
 
   private void storeKeys(
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 369ea29..ed694e6 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -1714,7 +1714,7 @@
   private static List<IdString> splitPath(HttpServletRequest req) {
     String path = RequestUtil.getEncodedPathInfo(req);
     if (Strings.isNullOrEmpty(path)) {
-      return Collections.emptyList();
+      return new ArrayList<>();
     }
     List<IdString> out = new ArrayList<>();
     for (String p : Splitter.on('/').split(path)) {
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index ffbd30b..63e2c08 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -206,7 +206,7 @@
     return out;
   }
 
-  private Map<String, ActionInfo> toActionMap(
+  private ImmutableMap<String, ActionInfo> toActionMap(
       RevisionResource rsrc,
       List<ActionVisitor> visitors,
       ChangeInfo changeInfo,
@@ -226,6 +226,6 @@
       }
       out.put(d.getId(), actionInfo);
     }
-    return out;
+    return ImmutableMap.copyOf(out);
   }
 }
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 7423900..62b2f77 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -41,6 +41,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -809,17 +810,17 @@
     out.submitter = accountLoader.get(s.get().accountId());
   }
 
-  private Collection<ChangeMessageInfo> messages(ChangeData cd) {
+  private ImmutableList<ChangeMessageInfo> messages(ChangeData cd) {
     List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
     if (messages.isEmpty()) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size());
     for (ChangeMessage message : messages) {
       result.add(createChangeMessageInfo(message, accountLoader));
     }
-    return result;
+    return ImmutableList.copyOf(result);
   }
 
   private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
diff --git a/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
index 547452e..c993c42 100644
--- a/java/com/google/gerrit/server/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -196,7 +196,7 @@
       List<PatchSetData> start)
       throws PermissionBackendException {
     if (start.isEmpty()) {
-      return ImmutableList.of();
+      return new ArrayList<>();
     }
     Map<Change.Id, PatchSet.Id> maxPatchSetIds = new HashMap<>();
     Set<PatchSetData> seen = new HashSet<>();
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index 816a904..44a3d16 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -112,8 +112,8 @@
     return Iterables.concat(sortedByProject);
   }
 
-  private List<PatchSetData> sortProject(Project.NameKey project, Collection<ChangeData> in)
-      throws IOException {
+  private ImmutableList<PatchSetData> sortProject(
+      Project.NameKey project, Collection<ChangeData> in) throws IOException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       rw.setRetainBody(retainBody);
@@ -181,7 +181,7 @@
           }
         }
       }
-      return result;
+      return ImmutableList.copyOf(result);
     }
   }
 
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 5d50d22..a63feeb 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -174,7 +174,7 @@
     String nameLC = name.toLowerCase(Locale.US);
     SortedMap<String, GroupReference> matches = namesToGroups.tailMap(nameLC);
     if (matches.isEmpty()) {
-      return Collections.emptyList();
+      return new ArrayList<>();
     }
 
     List<GroupReference> r = new ArrayList<>(matches.size());
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index 24bcaf0..582956d 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -231,7 +231,7 @@
 
   public static void ensureConsistentWithGroupNameNotes(
       Repository allUsersRepo, InternalGroup group) throws IOException {
-    List<ConsistencyCheckInfo.ConsistencyProblemInfo> problems =
+    ImmutableList<ConsistencyCheckInfo.ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
             allUsersRepo, group.getNameKey(), group.getGroupUUID());
     problems.forEach(GroupsNoteDbConsistencyChecker::logConsistencyProblem);
@@ -246,7 +246,7 @@
    * @return a list of {@code ConsistencyProblemInfo} containing the problem details.
    */
   @VisibleForTesting
-  static List<ConsistencyProblemInfo> checkWithGroupNameNotes(
+  static ImmutableList<ConsistencyProblemInfo> checkWithGroupNameNotes(
       Repository allUsersRepo, AccountGroup.NameKey groupName, AccountGroup.UUID groupUUID)
       throws IOException {
     try {
@@ -273,7 +273,7 @@
         problems.add(
             warning("group note of name '%s' claims to represent name of '%s'", name, actualName));
       }
-      return problems;
+      return ImmutableList.copyOf(problems);
     } 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/mail/send/CommentFormatter.java b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
index 2590505..f04ce9d 100644
--- a/java/com/google/gerrit/server/mail/send/CommentFormatter.java
+++ b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
@@ -17,9 +17,9 @@
 import static com.google.common.base.Strings.isNullOrEmpty;
 
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 public class CommentFormatter {
@@ -47,9 +47,9 @@
    * @param source The raw, unescaped comment in the Gerrit wiki-like format.
    * @return List of block objects, each with unescaped comment content.
    */
-  public static List<Block> parse(@Nullable String source) {
+  public static ImmutableList<Block> parse(@Nullable String source) {
     if (isNullOrEmpty(source)) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     List<Block> result = new ArrayList<>();
@@ -64,7 +64,7 @@
         result.add(makeParagraph(p));
       }
     }
-    return result;
+    return ImmutableList.copyOf(result);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index 012a91c..de3adec 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -49,6 +49,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.lib.Config;
@@ -404,14 +405,17 @@
         df.setDetectRenames(false);
         diffEntries = df.scan(oldCommit.equals(ObjectId.zeroId()) ? null : oldCommit, newCommit);
       }
-      return diffEntries.stream()
-          .map(
-              entry ->
-                  ModifiedFile.builder()
-                      .changeType(toChangeType(entry.getChangeType()))
-                      .oldPath(getGitPath(entry.getOldPath()))
-                      .newPath(getGitPath(entry.getNewPath()))
-                      .build())
+      List<ModifiedFile> modifiedFiles =
+          diffEntries.stream()
+              .map(
+                  entry ->
+                      ModifiedFile.builder()
+                          .changeType(toChangeType(entry.getChangeType()))
+                          .oldPath(getGitPath(entry.getOldPath()))
+                          .newPath(getGitPath(entry.getNewPath()))
+                          .build())
+              .collect(Collectors.toList());
+      return DiffUtil.mergeRewrittenModifiedFiles(modifiedFiles).stream()
           .collect(ImmutableMap.toImmutableMap(ModifiedFile::getDefaultPath, Function.identity()));
     } catch (IOException e) {
       throw new DiffNotAvailableException(
diff --git a/java/com/google/gerrit/server/patch/DiffUtil.java b/java/com/google/gerrit/server/patch/DiffUtil.java
index 1e88f9f..e75d50c 100644
--- a/java/com/google/gerrit/server/patch/DiffUtil.java
+++ b/java/com/google/gerrit/server/patch/DiffUtil.java
@@ -15,9 +15,16 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.common.collect.ArrayListMultimap;
+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;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -29,6 +36,40 @@
 public class DiffUtil {
 
   /**
+   * Return the {@code modifiedFiles} input list while merging rewritten entries.
+   *
+   * <p>Background: In some cases, JGit returns two diff entries (ADDED/DELETED, RENAMED/DELETED,
+   * etc...) for the same file path. This happens e.g. when a file's mode is changed between
+   * 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<>();
+    ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
+    modifiedFiles.stream()
+        .forEach(
+            f -> {
+              if (f.changeType() == ChangeType.DELETED) {
+                byPath.get(f.oldPath().get()).add(f);
+              } else {
+                byPath.get(f.newPath().get()).add(f);
+              }
+            });
+    for (String path : byPath.keySet()) {
+      List<ModifiedFile> entries = byPath.get(path);
+      if (entries.size() == 1) {
+        result.add(entries.get(0));
+      } else {
+        // More than one. Return a single REWRITE entry.
+        // Convert the first entry (prioritized according to change type enum order) to REWRITE
+        entries.sort(Comparator.comparingInt(o -> o.changeType().ordinal()));
+        result.add(entries.get(0).toBuilder().changeType(ChangeType.REWRITE).build());
+      }
+    }
+    return result;
+  }
+
+  /**
    * Returns the Git tree object ID pointed to by the commitId parameter.
    *
    * @param rw a {@link RevWalk} of an opened repository that is used to walk the commit graph.
diff --git a/java/com/google/gerrit/server/patch/MergeListBuilder.java b/java/com/google/gerrit/server/patch/MergeListBuilder.java
index 433fcad..337d940 100644
--- a/java/com/google/gerrit/server/patch/MergeListBuilder.java
+++ b/java/com/google/gerrit/server/patch/MergeListBuilder.java
@@ -22,7 +22,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class MergeListBuilder {
-  public static List<RevCommit> build(RevWalk rw, RevCommit merge, int uninterestingParent)
+  public static ImmutableList<RevCommit> build(RevWalk rw, RevCommit merge, int uninterestingParent)
       throws IOException {
     rw.reset();
     rw.parseBody(merge);
@@ -45,6 +45,6 @@
     while ((c = rw.next()) != null) {
       result.add(c);
     }
-    return result;
+    return ImmutableList.copyOf(result);
   }
 }
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
index 460c2e2..9c4d601 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -18,14 +18,11 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.DiffNotAvailableException;
@@ -40,7 +37,6 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Stream;
@@ -143,7 +139,7 @@
               .bTree(bTree)
               .renameScore(key.renameScore())
               .build();
-      List<ModifiedFile> modifiedFiles = mergeRewrittenEntries(gitCache.get(gitKey));
+      List<ModifiedFile> modifiedFiles = DiffUtil.mergeRewrittenModifiedFiles(gitCache.get(gitKey));
       if (key.aCommit().equals(ObjectId.zeroId())) {
         return ImmutableList.copyOf(modifiedFiles);
       }
@@ -206,37 +202,5 @@
       // value as the set of file paths shouldn't contain it.
       return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
     }
-
-    /**
-     * Return the {@code modifiedFiles} input list while merging rewritten entries.
-     *
-     * <p>Background: In some cases, JGit returns two diff entries (ADDED/DELETED, RENAMED/DELETED,
-     * etc...) for the same file path. This happens e.g. when a file's mode is changed between
-     * 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}.
-     */
-    private static List<ModifiedFile> mergeRewrittenEntries(List<ModifiedFile> modifiedFiles) {
-      List<ModifiedFile> result = new ArrayList<>();
-      ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
-      modifiedFiles.stream()
-          .forEach(
-              f -> {
-                if (f.changeType() == ChangeType.DELETED) {
-                  byPath.get(f.oldPath().get()).add(f);
-                } else {
-                  byPath.get(f.newPath().get()).add(f);
-                }
-              });
-      for (String path : byPath.keySet()) {
-        List<ModifiedFile> entries = byPath.get(path);
-        if (entries.size() == 1) {
-          result.add(entries.get(0));
-        } else {
-          // More than one. Return a single REWRITE entry.
-          result.add(entries.get(0).toBuilder().changeType(ChangeType.REWRITE).build());
-        }
-      }
-      return result;
-    }
   }
 }
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index c8a6f60..8c54742 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -112,7 +112,7 @@
   }
 
   /** Filters given refs and tags by visibility. */
-  Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
+  ImmutableList<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
       throws PermissionBackendException {
     visibleChangesCache = visibleChangesCacheFactory.create(projectControl, repo);
     logger.atFinest().log(
@@ -138,7 +138,7 @@
         boolean isChangeRefVisisble = canSeeSingleChangeRef(refName);
         if (isChangeRefVisisble) {
           logger.atFinest().log("Change ref %s is visible", refName);
-          return refs;
+          return ImmutableList.copyOf(refs);
         }
         logger.atFinest().log("Filter out non-visible change ref %s", refName);
         return ImmutableList.of();
@@ -178,7 +178,7 @@
     }
 
     logger.atFinest().log("visible refs = %s", visibleRefs);
-    return visibleRefs;
+    return ImmutableList.copyOf(visibleRefs);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/java/com/google/gerrit/server/plugins/PluginScannerThread.java
index 705e3c0..1c2c836 100644
--- a/java/com/google/gerrit/server/plugins/PluginScannerThread.java
+++ b/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.common.util.concurrent.Uninterruptibles;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -45,10 +46,6 @@
 
   void end() {
     done.countDown();
-    try {
-      join();
-    } catch (InterruptedException e) {
-      // Ignored
-    }
+    Uninterruptibles.joinUninterruptibly(this);
   }
 }
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index fca1b36..39d9aec7 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -247,7 +247,7 @@
     return r;
   }
 
-  private List<ChangeInfo> executeQueryAndAutoCloseChanges(
+  private ImmutableList<ChangeInfo> executeQueryAndAutoCloseChanges(
       Predicate<ChangeData> basePredicate,
       Set<Change.Id> seenChanges,
       List<Predicate<ChangeData>> predicates,
@@ -306,7 +306,7 @@
         }
       }
 
-      return autoCloseableChangesByBranch;
+      return ImmutableList.copyOf(autoCloseableChangesByBranch);
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
       throw new StorageException(e);
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 76ebd81..39ffab6 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -22,6 +22,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
@@ -293,7 +294,7 @@
     return and(project(project), or(groupPredicates));
   }
 
-  public static List<ChangeData> byProjectGroups(
+  public static ImmutableList<ChangeData> byProjectGroups(
       Provider<InternalChangeQuery> queryProvider,
       IndexConfig indexConfig,
       Project.NameKey project,
@@ -322,6 +323,6 @@
         }
       }
     }
-    return result;
+    return ImmutableList.copyOf(result);
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 4a54c03..4f181a4 100644
--- a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ProjectInfo;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 
@@ -39,11 +39,11 @@
     this.value = value;
   }
 
-  protected static List<Predicate<ChangeData>> predicates(
+  protected static ImmutableList<Predicate<ChangeData>> predicates(
       ProjectCache projectCache, ChildProjects childProjects, String value) {
     Optional<ProjectState> projectState = projectCache.get(Project.nameKey(value));
     if (!projectState.isPresent()) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     List<Predicate<ChangeData>> r = new ArrayList<>();
@@ -55,7 +55,7 @@
     } catch (PermissionBackendException e) {
       logger.atWarning().withCause(e).log("cannot check permissions to expand child projects");
     }
-    return r;
+    return ImmutableList.copyOf(r);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java
index 0eef468..7a1808b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRelated.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -37,7 +38,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -63,7 +63,7 @@
     return Response.ok(relatedChangesInfo);
   }
 
-  public List<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
+  public ImmutableList<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
       throws IOException, PermissionBackendException {
     boolean isEdit = rsrc.getEdit().isPresent();
     PatchSet basePs = isEdit ? rsrc.getEdit().get().getBasePatchSet() : rsrc.getPatchSet();
@@ -92,10 +92,10 @@
     if (result.size() == 1) {
       RelatedChangeAndCommitInfo r = result.get(0);
       if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().commitId().name())) {
-        return Collections.emptyList();
+        return ImmutableList.of();
       }
     }
-    return result;
+    return ImmutableList.copyOf(result);
   }
 
   static RelatedChangeAndCommitInfo newChangeAndCommit(
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 24b0799..d88efa6 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -1309,17 +1309,17 @@
       return robotComment;
     }
 
-    private List<FixSuggestion> createFixSuggestionsFromInput(
+    private ImmutableList<FixSuggestion> createFixSuggestionsFromInput(
         List<FixSuggestionInfo> fixSuggestionInfos) {
       if (fixSuggestionInfos == null) {
-        return Collections.emptyList();
+        return ImmutableList.of();
       }
 
       List<FixSuggestion> fixSuggestions = new ArrayList<>(fixSuggestionInfos.size());
       for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
         fixSuggestions.add(createFixSuggestionFromInput(fixSuggestionInfo));
       }
-      return fixSuggestions;
+      return ImmutableList.copyOf(fixSuggestions);
     }
 
     private FixSuggestion createFixSuggestionFromInput(FixSuggestionInfo fixSuggestionInfo) {
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 7f7c1ad..cc81aac 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -18,7 +18,6 @@
 
 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.common.Nullable;
 import com.google.gerrit.entities.Account;
@@ -44,6 +43,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -227,7 +227,7 @@
     } catch (QueryParseException e) {
       // Unhandled, because owner:self will never provoke a QueryParseException
       logger.atSevere().withCause(e).log("Exception while suggesting reviewers");
-      return ImmutableMap.of();
+      return new HashMap<>();
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index 69b82ba..e11ab75 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.PatchSet;
@@ -40,7 +41,6 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
@@ -121,7 +121,7 @@
     }
   }
 
-  private List<RevisionResource> find(ChangeResource change, String id)
+  private ImmutableList<RevisionResource> find(ChangeResource change, String id)
       throws IOException, AuthException {
     if (id.equals("0") || id.equals("edit")) {
       return loadEdit(change, null);
@@ -131,7 +131,7 @@
     } else if (id.length() < 4 || id.length() > ObjectIds.STR_LEN) {
       // Require a minimum of 4 digits.
       // Impossibly long identifier will never match.
-      return Collections.emptyList();
+      return ImmutableList.of();
     } else {
       List<RevisionResource> out = new ArrayList<>();
       for (PatchSet ps : psUtil.byChange(change.getNotes())) {
@@ -143,20 +143,20 @@
       if (out.isEmpty() && ObjectId.isId(id)) {
         return loadEdit(change, ObjectId.fromString(id));
       }
-      return out;
+      return ImmutableList.copyOf(out);
     }
   }
 
-  private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id) {
+  private ImmutableList<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id) {
     PatchSet ps = psUtil.get(change.getNotes(), PatchSet.id(change.getId(), Integer.parseInt(id)));
     if (ps != null) {
-      return Collections.singletonList(new RevisionResource(change, ps));
+      return ImmutableList.of(new RevisionResource(change, ps));
     }
-    return Collections.emptyList();
+    return ImmutableList.of();
   }
 
-  private List<RevisionResource> loadEdit(ChangeResource change, @Nullable ObjectId commitId)
-      throws AuthException, IOException {
+  private ImmutableList<RevisionResource> loadEdit(
+      ChangeResource change, @Nullable ObjectId commitId) throws AuthException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(change.getNotes(), change.getUser());
     if (edit.isPresent()) {
       RevCommit editCommit = edit.get().getEditCommit();
@@ -168,9 +168,9 @@
               .createdOn(new Timestamp(editCommit.getCommitterIdent().getWhen().getTime()))
               .build();
       if (commitId == null || editCommit.equals(commitId)) {
-        return Collections.singletonList(new RevisionResource(change, ps, edit));
+        return ImmutableList.of(new RevisionResource(change, ps, edit));
       }
     }
-    return Collections.emptyList();
+    return ImmutableList.of();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index c18e7c2..74ddae1 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.TOPIC_CLOSURE;
 import static java.util.Collections.reverseOrder;
-import static java.util.stream.Collectors.toList;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
@@ -157,11 +158,11 @@
     }
   }
 
-  private List<ChangeData> sort(List<ChangeData> cds, int hidden) throws IOException {
+  private ImmutableList<ChangeData> sort(List<ChangeData> cds, int hidden) throws IOException {
     if (cds.size() <= 1 && hidden == 0) {
       // Skip sorting for singleton lists, to avoid WalkSorter opening the
       // repo just to fill out the commit field in PatchSetData.
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     long numProjectsDistinct = cds.stream().map(ChangeData::project).distinct().count();
@@ -171,7 +172,7 @@
       // We either have only a single change per project which means that WalkSorter won't make a
       // difference compared to our index-backed sort, or we are looking at more than 5 projects
       // which would make WalkSorter too expensive for this call.
-      return cds.stream().sorted(COMPARATOR).collect(toList());
+      return cds.stream().sorted(COMPARATOR).collect(toImmutableList());
     }
 
     // Perform more expensive walk-sort.
@@ -179,7 +180,7 @@
     for (PatchSetData psd : sorter.get().sort(cds)) {
       sorted.add(psd.data());
     }
-    return sorted;
+    return ImmutableList.copyOf(sorted);
   }
 
   private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 92038b0..75dd014 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.Change;
@@ -49,7 +50,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -113,8 +113,8 @@
         .checkStatePermitsWrite();
 
     MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
-    List<AccessSection> removals = setAccess.getAccessSections(input.remove);
-    List<AccessSection> additions = setAccess.getAccessSections(input.add);
+    ImmutableList<AccessSection> removals = setAccess.getAccessSections(input.remove);
+    ImmutableList<AccessSection> additions = setAccess.getAccessSections(input.add);
 
     Project.NameKey newParentProjectName =
         input.parent == null ? null : Project.nameKey(input.parent);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index f3b2bad..8203346 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.RefNames;
@@ -57,7 +58,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.locks.Lock;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -194,7 +194,8 @@
     }
   }
 
-  private List<String> normalizeBranchNames(List<String> branches) throws BadRequestException {
+  private ImmutableList<String> normalizeBranchNames(List<String> branches)
+      throws BadRequestException {
     if (branches == null || branches.isEmpty()) {
       // Use host-level default for HEAD or fall back to 'master' if nothing else was specified in
       // the input.
@@ -203,7 +204,7 @@
           defaultBranch != null
               ? normalizeAndValidateBranch(defaultBranch)
               : Constants.R_HEADS + Constants.MASTER;
-      return Collections.singletonList(defaultBranch);
+      return ImmutableList.of(defaultBranch);
     }
     List<String> normalizedBranches = new ArrayList<>();
     for (String branch : branches) {
@@ -212,7 +213,7 @@
         normalizedBranches.add(branch);
       }
     }
-    return normalizedBranches;
+    return ImmutableList.copyOf(normalizedBranches);
   }
 
   private String normalizeAndValidateBranch(String branch) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 4406719..9029e11 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -100,7 +100,7 @@
     return tree.values();
   }
 
-  private List<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
+  private ImmutableList<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
       throws ResourceNotFoundException, IOException, PermissionBackendException {
     if (!state.statePermitsRead()) {
       return ImmutableList.of();
@@ -118,13 +118,13 @@
           // Do nothing.
         }
       }
-      return all;
+      return ImmutableList.copyOf(all);
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException(project, e);
     }
   }
 
-  private List<DashboardInfo> scanDashboards(
+  private ImmutableList<DashboardInfo> scanDashboards(
       Project definingProject,
       Repository git,
       RevWalk rw,
@@ -155,6 +155,6 @@
         }
       }
     }
-    return list;
+    return ImmutableList.copyOf(list);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index 65cc5a2..0a9503f 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccessSection;
@@ -42,7 +43,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -66,10 +66,10 @@
     this.pluginPermissionsUtil = pluginPermissionsUtil;
   }
 
-  List<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
+  ImmutableList<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
       throws UnprocessableEntityException {
     if (sectionInfos == null) {
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
 
     List<AccessSection> sections = new ArrayList<>(sectionInfos.size());
@@ -120,7 +120,7 @@
       }
       sections.add(accessSection.build());
     }
-    return sections;
+    return ImmutableList.copyOf(sections);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index a09ba63..71e248f 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -45,7 +45,7 @@
   }
 
   @Override
-  public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+  public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
     boolean first = true;
@@ -62,7 +62,7 @@
       }
       first = false;
     }
-    return ops;
+    return ImmutableList.copyOf(ops);
   }
 
   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 8a30898..ad01d31 100644
--- a/java/com/google/gerrit/server/submit/FastForwardOnly.java
+++ b/java/com/google/gerrit/server/submit/FastForwardOnly.java
@@ -30,7 +30,7 @@
   }
 
   @Override
-  public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+  public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
 
     Map<BranchNameKey, CodeReviewCommit> branchToCommit = new HashMap<>();
@@ -57,7 +57,7 @@
         ops.add(new NotFastForwardOp(c));
       }
     }
-    return ops;
+    return ImmutableList.copyOf(ops);
   }
 
   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 c3f186a..7258448 100644
--- a/java/com/google/gerrit/server/submit/MergeAlways.java
+++ b/java/com/google/gerrit/server/submit/MergeAlways.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -25,7 +26,7 @@
   }
 
   @Override
-  public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+  public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
     if (args.mergeTip.getInitialTip() == null && !sorted.isEmpty()) {
@@ -38,7 +39,7 @@
       CodeReviewCommit n = sorted.remove(0);
       ops.add(new MergeOneOp(args, n));
     }
-    return ops;
+    return ImmutableList.copyOf(ops);
   }
 
   static boolean dryRun(
diff --git a/java/com/google/gerrit/server/submit/MergeIfNecessary.java b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
index 30f1661..29fc240 100644
--- a/java/com/google/gerrit/server/submit/MergeIfNecessary.java
+++ b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.submit;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -25,7 +26,7 @@
   }
 
   @Override
-  public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+  public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
 
@@ -43,7 +44,7 @@
       CodeReviewCommit n = sorted.remove(0);
       ops.add(new MergeOneOp(args, n));
     }
-    return ops;
+    return ImmutableList.copyOf(ops);
   }
 
   static boolean dryRun(
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 355d25f..8aef3c7 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.submit;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
 import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
-import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.BooleanProjectConfig;
@@ -56,7 +56,7 @@
   }
 
   @Override
-  public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+  public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
     List<CodeReviewCommit> sorted;
     try {
       sorted = args.rebaseSorter.sort(toMerge);
@@ -92,7 +92,7 @@
         // found a merge commit that depends on a normal change, this means we are required to merge
         // the whole series at once
         sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
-        return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toList());
+        return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toImmutableList());
       }
       foundNonMerge = true;
     }
@@ -114,7 +114,7 @@
       }
       first = false;
     }
-    return ops;
+    return ImmutableList.copyOf(ops);
   }
 
   private class RebaseRootOp extends SubmitStrategyOp {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index 6291e6c..83c6634 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.entities.BranchNameKey;
@@ -293,5 +294,5 @@
     }
   }
 
-  protected abstract List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge);
+  protected abstract ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge);
 }
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 917e967..6d13854 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -615,7 +615,7 @@
       BatchUpdate.this.executed = manager.isExecuted();
     }
 
-    List<ListenableFuture<ChangeData>> startIndexFutures() {
+    ImmutableList<ListenableFuture<ChangeData>> startIndexFutures() {
       if (dryrun) {
         return ImmutableList.of();
       }
@@ -636,7 +636,7 @@
             throw new IllegalStateException("unexpected result: " + e.getValue());
         }
       }
-      return indexFutures;
+      return ImmutableList.copyOf(indexFutures);
     }
   }
 
diff --git a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
index 9025691..6745b1d 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.warning;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
@@ -95,7 +96,7 @@
   @Test
   public void groupNameNoteFailToParse() throws Exception {
     updateGroupNamesRef("g-1", "[invalid");
-    List<ConsistencyProblemInfo> problems =
+    ImmutableList<ConsistencyProblemInfo> problems =
         GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
             allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
     assertThat(problems)
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index 8b62628..5a8b266 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -17,12 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.DiffOperationsTest.FileEntity.FileType;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -35,6 +36,7 @@
 import java.util.Optional;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -68,12 +70,15 @@
 
   @Test
   public void diffModifiedFileAgainstParent() throws Exception {
-    ImmutableMap<String, String> oldFiles =
-        ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2);
+    ImmutableList<FileEntity> oldFiles =
+        ImmutableList.of(
+            new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
     ObjectId oldCommitId = createCommit(repo, null, oldFiles);
 
-    ImmutableMap<String, String> newFiles =
-        ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2 + "\nnew line here");
+    ImmutableList<FileEntity> newFiles =
+        ImmutableList.of(
+            new FileEntity(fileName1, fileContent1),
+            new FileEntity(fileName2, fileContent2 + "\nnew line here"));
     ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
 
     FileDiffOutput diffOutput =
@@ -88,19 +93,18 @@
 
   @Test
   public void diffAgainstAutoMergePersistsAutoMergeInRepo() throws Exception {
-    ObjectId parent1 = createCommit(repo, null, ImmutableMap.of("file_1.txt", "file 1 content"));
-    ObjectId parent2 = createCommit(repo, null, ImmutableMap.of("file_2.txt", "file 2 content"));
+    ObjectId parent1 =
+        createCommit(repo, null, ImmutableList.of(new FileEntity("file_1.txt", "file 1 content")));
+    ObjectId parent2 =
+        createCommit(repo, null, ImmutableList.of(new FileEntity("file_2.txt", "file 2 content")));
 
     ObjectId merge =
         createMergeCommit(
             repo,
-            ImmutableMap.of(
-                "file_1.txt",
-                "file 1 content",
-                "file_2.txt",
-                "file 2 content",
-                "file_3.txt",
-                "file 3 content"),
+            ImmutableList.of(
+                new FileEntity("file_1.txt", "file 1 content"),
+                new FileEntity("file_2.txt", "file 2 content"),
+                new FileEntity("file_3.txt", "file 3 content")),
             parent1,
             parent2);
 
@@ -118,12 +122,15 @@
 
   @Test
   public void loadModifiedFiles() throws Exception {
-    ImmutableMap<String, String> oldFiles =
-        ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2);
+    ImmutableList<FileEntity> oldFiles =
+        ImmutableList.of(
+            new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
     ObjectId oldCommitId = createCommit(repo, null, oldFiles);
 
-    ImmutableMap<String, String> newFiles =
-        ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2 + "\nnew line here");
+    ImmutableList<FileEntity> newFiles =
+        ImmutableList.of(
+            new FileEntity(fileName1, fileContent1),
+            new FileEntity(fileName2, fileContent2 + "\nnew line here"));
     ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
 
     Repository repository = repoManager.openRepository(testProjectName);
@@ -147,13 +154,49 @@
   }
 
   @Test
-  public void loadModifiedFilesAgainstParent() throws Exception {
-    ImmutableMap<String, String> oldFiles =
-        ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2);
+  public void loadModifiedFiles_withSymlinkConvertedToRegularFile() throws Exception {
+    // Commit 1: Create a regular fileName1 with fileContent1
+    ImmutableList<FileEntity> oldFiles = ImmutableList.of(new FileEntity(fileName1, fileContent1));
     ObjectId oldCommitId = createCommit(repo, null, oldFiles);
 
-    ImmutableMap<String, String> newFiles =
-        ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2 + "\nnew line here");
+    // Commit 2: Create a symlink with name FileName1 pointing to target file "target"
+    ImmutableList<FileEntity> newFiles =
+        ImmutableList.of(new FileEntity(fileName1, "target", FileType.SYMLINK));
+    ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+    Repository repository = repoManager.openRepository(testProjectName);
+    ObjectReader objectReader = repository.newObjectReader();
+
+    Map<String, ModifiedFile> modifiedFiles =
+        diffOperations.loadModifiedFiles(
+            testProjectName,
+            newCommitId,
+            oldCommitId,
+            DiffOptions.DEFAULTS,
+            new RevWalk(objectReader),
+            repository.getConfig());
+
+    assertThat(modifiedFiles)
+        .containsExactly(
+            fileName1,
+            ModifiedFile.builder()
+                .changeType(ChangeType.REWRITE)
+                .oldPath(Optional.empty())
+                .newPath(Optional.of(fileName1))
+                .build());
+  }
+
+  @Test
+  public void loadModifiedFilesAgainstParent() throws Exception {
+    ImmutableList<FileEntity> oldFiles =
+        ImmutableList.of(
+            new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
+    ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+    ImmutableList<FileEntity> newFiles =
+        ImmutableList.of(
+            new FileEntity(fileName1, fileContent1),
+            new FileEntity(fileName2, fileContent2 + "\nnew line here"));
     ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
 
     Repository repository = repoManager.openRepository(testProjectName);
@@ -176,22 +219,38 @@
                 .build());
   }
 
+  static class FileEntity {
+    String name;
+    String content;
+    FileType type;
+
+    enum FileType {
+      REGULAR,
+      SYMLINK
+    };
+
+    FileEntity(String name, String content) {
+      this(name, content, FileType.REGULAR);
+    }
+
+    FileEntity(String name, String content, FileType type) {
+      this.name = name;
+      this.content = content;
+      this.type = type;
+    }
+  }
+
   private ObjectId createMergeCommit(
-      Repository repo,
-      ImmutableMap<String, String> fileNameToContent,
-      ObjectId parent1,
-      ObjectId parent2)
+      Repository repo, ImmutableList<FileEntity> fileEntities, ObjectId parent1, ObjectId parent2)
       throws IOException {
-    ObjectId treeId = createTree(repo, fileNameToContent);
+    ObjectId treeId = createTree(repo, fileEntities);
     return createCommitInRepo(repo, treeId, parent1, parent2);
   }
 
   private ObjectId createCommit(
-      Repository repo,
-      @Nullable ObjectId parentCommit,
-      ImmutableMap<String, String> fileNameToContent)
+      Repository repo, @Nullable ObjectId parentCommit, ImmutableList<FileEntity> fileEntities)
       throws IOException {
-    ObjectId treeId = createTree(repo, fileNameToContent);
+    ObjectId treeId = createTree(repo, fileEntities);
     return parentCommit == null
         ? createCommitInRepo(repo, treeId)
         : createCommitInRepo(repo, treeId, parentCommit);
@@ -217,17 +276,21 @@
     }
   }
 
-  private static ObjectId createTree(
-      Repository repo, ImmutableMap<String, String> fileNameToContent) throws IOException {
+  private static ObjectId createTree(Repository repo, ImmutableList<FileEntity> fileEntities)
+      throws IOException {
     try (ObjectInserter oi = repo.newObjectInserter();
         ObjectReader reader = repo.newObjectReader();
         RevWalk rw = new RevWalk(reader); ) {
       TreeFormatter formatter = new TreeFormatter();
-      for (Map.Entry<String, String> entry : fileNameToContent.entrySet()) {
-        String fileName = entry.getKey();
-        String fileContent = entry.getValue();
+      for (FileEntity fileEntity : fileEntities) {
+        String fileName = fileEntity.name;
+        String fileContent = fileEntity.content;
         ObjectId fileObjId = createBlob(repo, fileContent);
-        formatter.append(fileName, rw.lookupBlob(fileObjId));
+        if (fileEntity.type.equals(FileType.REGULAR)) {
+          formatter.append(fileName, rw.lookupBlob(fileObjId));
+        } else {
+          formatter.append(fileName, FileMode.SYMLINK, fileObjId);
+        }
       }
       ObjectId treeId = oi.insert(formatter);
       oi.flush();
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 95508f9..848e714 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -66,12 +66,16 @@
   private finalizables: Finalizable[] = [];
 
   override connectedCallback() {
-    super.connectedCallback();
+    // NOTE: Polymer renders its template synchronously as part of
+    // super.connectedCallback. This means that we need to provide the
+    // dependencies before.  After migration to lit, super.connectedCallback
+    // can come first again.
     const dependencies = createAppDependencies(appContext);
     for (const [token, service] of dependencies) {
       this.finalizables.push(service);
       provide(this, token, () => service);
     }
+    super.connectedCallback();
   }
 
   override disconnectedCallback() {
diff --git a/polygerrit-ui/app/services/dependency.ts b/polygerrit-ui/app/services/dependency.ts
index 56b5fe6..d49f2ef 100644
--- a/polygerrit-ui/app/services/dependency.ts
+++ b/polygerrit-ui/app/services/dependency.ts
@@ -170,10 +170,10 @@
   private readonly ___controllers: ReactiveController[] = [];
 
   override connectedCallback() {
-    super.connectedCallback();
     for (const c of this.___controllers) {
       c.hostConnected?.();
     }
+    super.connectedCallback();
   }
 
   override disconnectedCallback() {
diff --git a/polygerrit-ui/app/services/dependency_test.ts b/polygerrit-ui/app/services/dependency_test.ts
index 969fa5c..fa7cc29 100644
--- a/polygerrit-ui/app/services/dependency_test.ts
+++ b/polygerrit-ui/app/services/dependency_test.ts
@@ -45,8 +45,8 @@
   }
 }
 
-@customElement('foo-provider')
-export class FooProviderElement extends LitElement {
+@customElement('lit-foo-provider')
+export class LitFooProviderElement extends LitElement {
   @query('bar-provider')
   bar?: BarProviderElement;
 
@@ -67,6 +67,22 @@
   }
 }
 
+@polyCustomElement('polymer-foo-provider')
+export class PolymerFooProviderElement extends DIPolymerElement {
+  bar() {
+    return this.$.bar as BarProviderElement;
+  }
+
+  override connectedCallback() {
+    provide(this, fooToken, () => new FooImpl('foo'));
+    super.connectedCallback();
+  }
+
+  static get template() {
+    return polyHtml`<bar-provider id="bar"></bar-provider>`;
+  }
+}
+
 @customElement('bar-provider')
 export class BarProviderElement extends LitElement {
   @query('leaf-lit-element')
@@ -128,14 +144,21 @@
 
 suite('Dependency', () => {
   test('It instantiates', async () => {
-    const fixture = fixtureFromElement('foo-provider');
+    const fixture = fixtureFromElement('lit-foo-provider');
     const element = fixture.instantiate();
     await element.updateComplete;
     assert.isDefined(element.bar?.litChild?.barRef());
   });
 
+  test('It instantiates in polymer', async () => {
+    const fixture = fixtureFromElement('polymer-foo-provider');
+    const element = fixture.instantiate();
+    await element.bar().updateComplete;
+    assert.isDefined(element.bar().litChild?.barRef());
+  });
+
   test('It works by connecting and reconnecting', async () => {
-    const fixture = fixtureFromElement('foo-provider');
+    const fixture = fixtureFromElement('lit-foo-provider');
     const element = fixture.instantiate();
     await element.updateComplete;
     assert.isDefined(element.bar?.litChild?.barRef());
@@ -150,7 +173,7 @@
   });
 
   test('It works by connecting and reconnecting Polymer', async () => {
-    const fixture = fixtureFromElement('foo-provider');
+    const fixture = fixtureFromElement('lit-foo-provider');
     const element = fixture.instantiate();
     await element.updateComplete;
 
@@ -167,7 +190,8 @@
 
 declare global {
   interface HTMLElementTagNameMap {
-    'foo-provider': FooProviderElement;
+    'lit-foo-provider': LitFooProviderElement;
+    'polymer-foo-provider': PolymerFooProviderElement;
     'bar-provider': BarProviderElement;
     'leaf-lit-element': LeafLitElement;
     'leaf-polymer-element': LeafPolymerElement;
diff --git a/tools/BUILD b/tools/BUILD
index e279786..6252312 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -311,7 +311,7 @@
         "-Xep:MisusedDayOfYear:ERROR",
         "-Xep:MisusedWeekYear:ERROR",
         "-Xep:MixedDescriptors:ERROR",
-        # "-Xep:MixedMutabilityReturnType:WARN",
+        "-Xep:MixedMutabilityReturnType:ERROR",
         "-Xep:MockitoUsage:ERROR",
         "-Xep:ModifiedButNotUsed:ERROR",
         "-Xep:ModifyCollectionInEnhancedForLoop:ERROR",
@@ -409,7 +409,7 @@
         "-Xep:TemporalAccessorGetChronoField:ERROR",
         "-Xep:TestParametersNotInitialized:ERROR",
         "-Xep:TheoryButNoTheories:ERROR",
-        # "-Xep:ThreadJoinLoop:WARN",
+        "-Xep:ThreadJoinLoop:ERROR",
         "-Xep:ThreadLocalUsage:ERROR",
         # "-Xep:ThreadPriorityCheck:WARN",
         "-Xep:ThreeLetterTimeZoneID:ERROR",