Merge "Add Gerrit.Change to types.js"
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 8887875..30da8c5 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -2,7 +2,7 @@
 = Gerrit Code Review - End to end tests
 
 This document provides descriptions of Gerrit end-to-end (`e2e`) test scenarios implemented using
-link:https://gatling.io/[Gatling,role=external,window=_blank] framework.
+the link:https://gatling.io/[Gatling,role=external,window=_blank] framework.
 
 Similar scenarios have been successfully used to compare performance of different Gerrit versions
 or study the Gerrit response under different load profiles. Although mostly for load, scenarios can
@@ -56,6 +56,14 @@
 [warn] Credentials file ~/.sbt/sonatype_credentials does not exist
 ----
 
+The other warning below can be safely ignored, so far. Running the proposed `sbt evicted` command
+should only list `scala-java8-compat_2.12` as `[warn]`. The other dependency conflicts should show
+as `[info]`. All of the listed conflicts get usually resolved seamlessly or so.
+
+----
+[warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings.
+----
+
 Every `sbt` command can include an optional log level
 link:https://www.scala-sbt.org/1.x/docs/Howto-Logging.html#Change+the+logging+level+globally[argument,role=external,window=_blank].
 Below, `[info]` logs are no longer shown:
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 6f9367d..3fc4ea8d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6177,6 +6177,12 @@
 `contains_git_conflicts` field in the link:#change-info[ChangeInfo]. If
 there are conflicts the cherry-pick change is marked as
 work-in-progress.
+|`topic`            |optional|
+The topic of the created cherry-picked change. If not set, the default depends
+on the source. If the source is a change with a topic, the resulting topic
+of the cherry-picked change will be {source_change_topic}-{destination_branch}.
+Otherwise, if the source change has no topic, or the source is a commit,
+the created change will have no topic.
 |===========================
 
 [[comment-info]]
diff --git a/WORKSPACE b/WORKSPACE
index 7f7f8e0..6a822d7 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -46,11 +46,13 @@
 # otherwise refer to RBE docs.
 rbe_autoconfig(name = "rbe_default")
 
+# TODO(davido): Switch to upstream again, when this PR is merged:
+# https://github.com/bazelbuild/rules_closure/pull/478
 http_archive(
     name = "io_bazel_rules_closure",
-    sha256 = "03c3b16f205085817fd89cfdcb2220a0138647ee7992be9cef291b069dd90301",
-    strip_prefix = "rules_closure-196a45f0ede2faec11dcc6c60fbc5e7471f4bd58",
-    urls = ["https://github.com/bazelbuild/rules_closure/archive/196a45f0ede2faec11dcc6c60fbc5e7471f4bd58.tar.gz"],
+    sha256 = "b9c2bc6ba377aa497eb7c31681d34404febf9d4e3c9c7d98ce0d78238a0af20f",
+    strip_prefix = "rules_closure-0.31",
+    urls = ["https://github.com/davido/rules_closure/archive/V0.31.tar.gz"],
 )
 
 http_archive(
diff --git a/e2e-tests/Dockerfile b/e2e-tests/Dockerfile
index ceae672..17c001b 100644
--- a/e2e-tests/Dockerfile
+++ b/e2e-tests/Dockerfile
@@ -1,6 +1,6 @@
 FROM denvazh/gatling:3.2.1
 
-ARG gatling_git_version=1.0.9
+ARG gatling_git_version=1.0.10
 RUN apk add --no-cache maven
 RUN mvn dependency:get \
         -DgroupId=com.gerritforge \
diff --git a/e2e-tests/README.md b/e2e-tests/README.md
deleted file mode 100644
index 534fde5..0000000
--- a/e2e-tests/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# How to build the Docker image
-
-```$shell
-docker build . -t e2e-tests
-```
-
-# How to run a test
-
-```$shell
-docker run -it e2e-tests -s com.google.gerrit.scenarios.ReplayRecordsFromFeederScenario
-```
diff --git a/e2e-tests/build.sbt b/e2e-tests/build.sbt
index 685db37..a322970 100644
--- a/e2e-tests/build.sbt
+++ b/e2e-tests/build.sbt
@@ -13,6 +13,7 @@
       name := "gerrit",
       libraryDependencies ++=
           gatling ++
-              Seq("io.gatling" % "gatling-core" % "3.1.1") ++
-              Seq("io.gatling" % "gatling-app" % "3.1.1")
+              Seq("io.gatling" % "gatling-core" % GatlingVersion) ++
+              Seq("io.gatling" % "gatling-app" % GatlingVersion),
+      scalacOptions += "-language:postfixOps"
     ) dependsOn gatlingGitExtension
diff --git a/e2e-tests/project/Dependencies.scala b/e2e-tests/project/Dependencies.scala
index 72d2ac2..63328f9 100644
--- a/e2e-tests/project/Dependencies.scala
+++ b/e2e-tests/project/Dependencies.scala
@@ -1,8 +1,10 @@
 import sbt._
 
 object Dependencies {
+  val GatlingVersion = "3.2.0"
+
   lazy val gatling = Seq(
     "io.gatling.highcharts" % "gatling-charts-highcharts",
     "io.gatling" % "gatling-test-framework",
-  ).map(_ % "3.1.1" % Test)
+  ).map(_ % GatlingVersion % Test)
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
index 5ac67e7..4ec6f01 100644
--- a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -29,4 +29,5 @@
 
   public boolean keepReviewers;
   public boolean allowConflicts;
+  public String topic;
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 6c6389e5..7ae570f 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.ArchiveFormat;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -158,6 +159,15 @@
   /** Returns votes on the revision. */
   ListMultimap<String, ApprovalInfo> votes() throws RestApiException;
 
+  /**
+   * Retrieves the revision as an archive.
+   *
+   * @param format the format of the archive
+   * @return the archive as {@link BinaryResult}
+   * @throws RestApiException
+   */
+  BinaryResult getArchive(ArchiveFormat format) throws RestApiException;
+
   abstract class MergeListRequest {
     private boolean addLinks;
     private int uninterestingParent = 1;
@@ -392,5 +402,10 @@
     public String etag() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public BinaryResult getArchive(ArchiveFormat format) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/extensions/client/ArchiveFormat.java b/java/com/google/gerrit/extensions/client/ArchiveFormat.java
new file mode 100644
index 0000000..4ec59cb
--- /dev/null
+++ b/java/com/google/gerrit/extensions/client/ArchiveFormat.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 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.extensions.client;
+
+/**
+ * The {@link com.google.gerrit.server.restapi.change.GetArchive} REST endpoint allows to download
+ * revisions as archive. This enum defines the supported archive formats.
+ */
+public enum ArchiveFormat {
+  TGZ,
+  TAR,
+  TBZ2,
+  TXZ,
+  ZIP;
+}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 48a8689..b515dfe 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.extensions.api.changes.RevisionReviewerApi;
 import com.google.gerrit.extensions.api.changes.RobotCommentApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.client.ArchiveFormat;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -70,6 +71,7 @@
 import com.google.gerrit.server.restapi.change.DraftComments;
 import com.google.gerrit.server.restapi.change.Files;
 import com.google.gerrit.server.restapi.change.Fixes;
+import com.google.gerrit.server.restapi.change.GetArchive;
 import com.google.gerrit.server.restapi.change.GetCommit;
 import com.google.gerrit.server.restapi.change.GetDescription;
 import com.google.gerrit.server.restapi.change.GetFixPreview;
@@ -96,6 +98,7 @@
 import com.google.inject.assistedinject.Assisted;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.lib.Repository;
@@ -146,6 +149,7 @@
   private final GetRelated getRelated;
   private final PutDescription putDescription;
   private final GetDescription getDescription;
+  private final Provider<GetArchive> getArchiveProvider;
   private final ApprovalsUtil approvalsUtil;
   private final AccountLoader.Factory accountLoaderFactory;
 
@@ -190,6 +194,7 @@
       GetRelated getRelated,
       PutDescription putDescription,
       GetDescription getDescription,
+      Provider<GetArchive> getArchiveProvider,
       ApprovalsUtil approvalsUtil,
       AccountLoader.Factory accountLoaderFactory,
       @Assisted RevisionResource r) {
@@ -232,6 +237,7 @@
     this.getRelated = getRelated;
     this.putDescription = putDescription;
     this.getDescription = getDescription;
+    this.getArchiveProvider = getArchiveProvider;
     this.approvalsUtil = approvalsUtil;
     this.accountLoaderFactory = accountLoaderFactory;
     this.revision = r;
@@ -649,4 +655,15 @@
   public String etag() throws RestApiException {
     return revisionActions.getETag(revision);
   }
+
+  @Override
+  public BinaryResult getArchive(ArchiveFormat format) throws RestApiException {
+    GetArchive getArchive = getArchiveProvider.get();
+    getArchive.setFormat(format != null ? format.name().toLowerCase(Locale.US) : null);
+    try {
+      return getArchive.apply(revision).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get archive", e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/change/ArchiveFormat.java b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
similarity index 95%
rename from java/com/google/gerrit/server/change/ArchiveFormat.java
rename to java/com/google/gerrit/server/change/ArchiveFormatInternal.java
index d895a66..f6e9ff9 100644
--- a/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
@@ -28,7 +28,7 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectLoader;
 
-public enum ArchiveFormat {
+public enum ArchiveFormatInternal {
   TGZ("application/x-gzip", new TgzFormat()),
   TAR("application/x-tar", new TarFormat()),
   TBZ2("application/x-bzip2", new Tbz2Format()),
@@ -40,7 +40,7 @@
 
   private final String mimeType;
 
-  ArchiveFormat(String mimeType, ArchiveCommand.Format<?> format) {
+  ArchiveFormatInternal(String mimeType, ArchiveCommand.Format<?> format) {
     this.format = format;
     this.mimeType = mimeType;
     ArchiveCommand.registerFormat(name(), format);
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index 6dea07d..58ce098 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -17,7 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.CoreDownloadSchemes;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.lang.reflect.Field;
@@ -37,7 +37,7 @@
 public class DownloadConfig {
   private final ImmutableSet<String> downloadSchemes;
   private final ImmutableSet<DownloadCommand> downloadCommands;
-  private final ImmutableSet<ArchiveFormat> archiveFormats;
+  private final ImmutableSet<ArchiveFormatInternal> archiveFormats;
 
   @Inject
   DownloadConfig(@GerritServerConfig Config cfg) {
@@ -69,13 +69,13 @@
 
     String v = cfg.getString("download", null, "archive");
     if (v == null) {
-      archiveFormats = ImmutableSet.copyOf(EnumSet.allOf(ArchiveFormat.class));
+      archiveFormats = ImmutableSet.copyOf(EnumSet.allOf(ArchiveFormatInternal.class));
     } else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
       archiveFormats = ImmutableSet.of();
     } else {
       archiveFormats =
           ImmutableSet.copyOf(
-              ConfigUtil.getEnumList(cfg, "download", null, "archive", ArchiveFormat.TGZ));
+              ConfigUtil.getEnumList(cfg, "download", null, "archive", ArchiveFormatInternal.TGZ));
     }
   }
 
@@ -110,7 +110,7 @@
   }
 
   /** Archive formats for downloading. */
-  public ImmutableSet<ArchiveFormat> getArchiveFormats() {
+  public ImmutableSet<ArchiveFormatInternal> getArchiveFormats() {
     return archiveFormats;
   }
 }
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 6c4aacc..1e97a44 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -403,6 +403,17 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
+      // TODO(zieren): Refactor interface to signal the intent of the event instead of hard-coding
+      // it here. Due to interface limitations, this method is called from both receive commits
+      // and from main Gerrit (e.g. when publishing a change edit). This is why we need to gate the
+      // early return on REFS_CHANGES (though pushes to refs/changes are not possible).
+      String refName = receiveEvent.command.getRefName();
+      if (!refName.startsWith("refs/for/") && !refName.startsWith(RefNames.REFS_CHANGES)) {
+        // This is a direct push bypassing review. We don't need to enforce any file-count limits
+        // here.
+        return Collections.emptyList();
+      }
+
       PatchListKey patchListKey =
           PatchListKey.againstBase(
               receiveEvent.commit.getId(), receiveEvent.commit.getParentCount());
diff --git a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
index 2e313a1..ebec3295 100644
--- a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
+++ b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -28,13 +28,13 @@
 
 @Singleton
 public class AllowedFormats {
-  final ImmutableMap<String, ArchiveFormat> extensions;
-  final ImmutableSet<ArchiveFormat> allowed;
+  final ImmutableMap<String, ArchiveFormatInternal> extensions;
+  final ImmutableSet<ArchiveFormatInternal> allowed;
 
   @Inject
   AllowedFormats(DownloadConfig cfg) {
-    Map<String, ArchiveFormat> exts = new HashMap<>();
-    for (ArchiveFormat format : cfg.getArchiveFormats()) {
+    Map<String, ArchiveFormatInternal> exts = new HashMap<>();
+    for (ArchiveFormatInternal format : cfg.getArchiveFormats()) {
       for (String ext : format.getSuffixes()) {
         exts.put(ext, format);
       }
@@ -46,14 +46,14 @@
     // valid JAR file, whose code would have access to cookies on the domain.
     allowed =
         Sets.immutableEnumSet(
-            Iterables.filter(cfg.getArchiveFormats(), f -> f != ArchiveFormat.ZIP));
+            Iterables.filter(cfg.getArchiveFormats(), f -> f != ArchiveFormatInternal.ZIP));
   }
 
-  public Set<ArchiveFormat> getAllowed() {
+  public Set<ArchiveFormatInternal> getAllowed() {
     return allowed;
   }
 
-  public ImmutableMap<String, ArchiveFormat> getExtensions() {
+  public ImmutableMap<String, ArchiveFormatInternal> getExtensions() {
     return extensions;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 82b138d..5960117 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -175,7 +175,6 @@
         null,
         null,
         null,
-        null,
         null);
   }
 
@@ -217,7 +216,6 @@
         null,
         null,
         null,
-        null,
         null);
   }
 
@@ -235,7 +233,6 @@
    * @param ignoreIdenticalTree When false, we throw an error when trying to cherry-pick creates an
    *     empty commit. When true, we allow creation of an empty commit.
    * @param timestamp the current timestamp.
-   * @param topic Topic name for the change created.
    * @param revertedChange The id of the change that is reverted. This is used for the "revertOf"
    *     field to mark the created cherry pick change as "revertOf" the original change that was
    *     reverted.
@@ -263,7 +260,6 @@
       BranchNameKey dest,
       boolean ignoreIdenticalTree,
       Timestamp timestamp,
-      @Nullable String topic,
       @Nullable Change.Id revertedChange,
       @Nullable ObjectId changeIdForNewChange,
       @Nullable Change.Id idForNewChange,
@@ -381,8 +377,11 @@
           } else {
             // Change key not found on destination branch. We can create a new
             // change.
-            String newTopic = topic;
-            if (topic == null
+            String newTopic = null;
+            if (input.topic != null) {
+              newTopic = Strings.emptyToNull(input.topic.trim());
+            }
+            if (newTopic == null
                 && sourceChange != null
                 && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
               newTopic = sourceChange.getTopic() + "-" + newDest.shortName();
diff --git a/java/com/google/gerrit/server/restapi/change/GetArchive.java b/java/com/google/gerrit/server/restapi/change/GetArchive.java
index 4ebcbdd..7ab1432 100644
--- a/java/com/google/gerrit/server/restapi/change/GetArchive.java
+++ b/java/com/google/gerrit/server/restapi/change/GetArchive.java
@@ -17,12 +17,13 @@
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
@@ -38,9 +39,12 @@
 public class GetArchive implements RestReadView<RevisionResource> {
   private final GitRepositoryManager repoManager;
   private final AllowedFormats allowedFormats;
+  @Nullable private String format;
 
   @Option(name = "--format")
-  private String format;
+  public void setFormat(String format) {
+    this.format = format;
+  }
 
   @Inject
   GetArchive(GitRepositoryManager repoManager, AllowedFormats allowedFormats) {
@@ -54,17 +58,17 @@
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
-    final ArchiveFormat f = allowedFormats.extensions.get("." + format);
+    ArchiveFormatInternal f = allowedFormats.extensions.get("." + format);
     if (f == null) {
       throw new BadRequestException("unknown archive format");
     }
-    if (f == ArchiveFormat.ZIP) {
+    if (f == ArchiveFormatInternal.ZIP) {
       throw new MethodNotAllowedException("zip format is disabled");
     }
     boolean close = true;
-    final Repository repo = repoManager.openRepository(rsrc.getProject());
+    Repository repo = repoManager.openRepository(rsrc.getProject());
     try {
-      final RevCommit commit;
+      RevCommit commit;
       String name;
       try (RevWalk rw = new RevWalk(repo)) {
         commit = rw.parseCommit(rsrc.getPatchSet().commitId());
@@ -103,7 +107,7 @@
     }
   }
 
-  private static String name(ArchiveFormat format, RevWalk rw, RevCommit commit)
+  private static String name(ArchiveFormatInternal format, RevWalk rw, RevCommit commit)
       throws IOException {
     return String.format(
         "%s%s", abbreviateName(commit, rw.getObjectReader()), format.getDefaultSuffix());
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
index e6a60d5..ed6c0a5 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
@@ -29,7 +29,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.ioutil.LimitedByteArrayOutputStream;
@@ -87,12 +87,12 @@
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
-    ArchiveFormat f = allowedFormats.extensions.get("." + format);
+    ArchiveFormatInternal f = allowedFormats.extensions.get("." + format);
     if (f == null && format.equals("tgz")) {
       // Always allow tgz, even when the allowedFormats doesn't contain it.
       // Then we allow at least one format even if the list of allowed
       // formats is empty.
-      f = ArchiveFormat.TGZ;
+      f = ArchiveFormatInternal.TGZ;
     }
     if (f == null) {
       throw new BadRequestException("unknown archive format");
@@ -109,7 +109,7 @@
     return Response.ok(getBundles(rsrc, f));
   }
 
-  private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
+  private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormatInternal f)
       throws RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
@@ -138,10 +138,11 @@
   private static class SubmitPreviewResult extends BinaryResult {
 
     private final MergeOp mergeOp;
-    private final ArchiveFormat archiveFormat;
+    private final ArchiveFormatInternal archiveFormat;
     private final int maxBundleSize;
 
-    private SubmitPreviewResult(MergeOp mergeOp, ArchiveFormat archiveFormat, int maxBundleSize) {
+    private SubmitPreviewResult(
+        MergeOp mergeOp, ArchiveFormatInternal archiveFormat, int maxBundleSize) {
       this.mergeOp = mergeOp;
       this.archiveFormat = archiveFormat;
       this.maxBundleSize = maxBundleSize;
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 52cc46f..e7da89a 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
@@ -195,6 +196,9 @@
   }
 
   private String createTopic(String topic, String submissionId) {
+    if (topic != null) {
+      topic = Strings.emptyToNull(topic.trim());
+    }
     if (topic == null) {
       return String.format(
           "revert-%s-%s", submissionId, RandomStringUtils.randomAlphabetic(10).toUpperCase());
@@ -321,12 +325,7 @@
       bu.addOp(
           changeNotes.getChange().getId(),
           new CreateCherryPickOp(
-              revCommitId,
-              revertInput.topic,
-              generatedChangeId,
-              cherryPickRevertChangeId,
-              groupName,
-              timestamp));
+              revCommitId, generatedChangeId, cherryPickRevertChangeId, groupName, timestamp));
       bu.addOp(changeNotes.getChange().getId(), new PostRevertedMessageOp(generatedChangeId));
       bu.addOp(
           cherryPickRevertChangeId,
@@ -356,6 +355,7 @@
     cherryPickInput.notifyDetails = revertInput.notifyDetails;
     cherryPickInput.parent = 1;
     cherryPickInput.keepReviewers = true;
+    cherryPickInput.topic = revertInput.topic;
     return cherryPickInput;
   }
 
@@ -570,7 +570,6 @@
 
   private class CreateCherryPickOp implements BatchUpdateOp {
     private final ObjectId revCommitId;
-    private final String topic;
     private final ObjectId computedChangeId;
     private final Change.Id cherryPickRevertChangeId;
     private final String groupName;
@@ -578,13 +577,11 @@
 
     CreateCherryPickOp(
         ObjectId revCommitId,
-        String topic,
         ObjectId computedChangeId,
         Change.Id cherryPickRevertChangeId,
         String groupName,
         Timestamp timestamp) {
       this.revCommitId = revCommitId;
-      this.topic = topic;
       this.computedChangeId = computedChangeId;
       this.cherryPickRevertChangeId = cherryPickRevertChangeId;
       this.groupName = groupName;
@@ -604,7 +601,6 @@
                   change.getProject(), RefNames.fullName(cherryPickInput.destination)),
               true,
               timestamp,
-              topic,
               change.getId(),
               computedChangeId,
               cherryPickRevertChangeId,
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 4ddc3e8..c83bf42 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -43,7 +43,7 @@
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.avatar.AvatarProvider;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
 import com.google.gerrit.server.change.MergeabilityComputationBehavior;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -254,7 +254,9 @@
           }
         });
     info.archives =
-        archiveFormats.getAllowed().stream().map(ArchiveFormat::getShortName).collect(toList());
+        archiveFormats.getAllowed().stream()
+            .map(ArchiveFormatInternal::getShortName)
+            .collect(toList());
     return info;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index c15fdeb..b901057 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.entities.RefNames.isConfigRef;
 
 import com.google.common.base.Strings;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
@@ -46,7 +45,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -58,8 +56,6 @@
 @Singleton
 public class CreateBranch
     implements RestCollectionCreateView<ProjectResource, BranchResource, BranchInput> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final Provider<IdentifiedUser> identifiedUser;
   private final PermissionBackend permissionBackend;
   private final GitRepositoryManager repoManager;
@@ -114,7 +110,7 @@
               + "\"");
     }
 
-    final BranchNameKey name = BranchNameKey.create(rsrc.getNameKey(), ref);
+    BranchNameKey name = BranchNameKey.create(rsrc.getNameKey(), ref);
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       ObjectId revid = RefUtil.parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
       RevWalk rw = RefUtil.verifyConnected(repo, revid);
@@ -122,80 +118,71 @@
 
       if (ref.startsWith(Constants.R_HEADS)) {
         // Ensure that what we start the branch from is a commit. If we
-        // were given a tag, deference to the commit instead.
+        // were given a tag, dereference to the commit instead.
         //
-        try {
-          object = rw.parseCommit(object);
-        } catch (IncorrectObjectTypeException notCommit) {
-          throw new BadRequestException("\"" + input.revision + "\" not a commit", notCommit);
-        }
+        object = rw.parseCommit(object);
       }
 
       createRefControl.checkCreateRef(identifiedUser, repo, name, object);
 
-      try {
-        final RefUpdate u = repo.updateRef(ref);
-        u.setExpectedOldObjectId(ObjectId.zeroId());
-        u.setNewObjectId(object.copy());
-        u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
-        u.setRefLogMessage("created via REST from " + input.revision, false);
-        refCreationValidator.validateRefOperation(rsrc.getName(), identifiedUser.get(), u);
-        final RefUpdate.Result result = u.update(rw);
-        switch (result) {
-          case FAST_FORWARD:
-          case NEW:
-          case NO_CHANGE:
-            referenceUpdated.fire(
-                name.project(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
-            break;
-          case LOCK_FAILURE:
-            if (repo.getRefDatabase().exactRef(ref) != null) {
-              throw new ResourceConflictException("branch \"" + ref + "\" already exists");
+      RefUpdate u = repo.updateRef(ref);
+      u.setExpectedOldObjectId(ObjectId.zeroId());
+      u.setNewObjectId(object.copy());
+      u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
+      u.setRefLogMessage("created via REST from " + input.revision, false);
+      refCreationValidator.validateRefOperation(rsrc.getName(), identifiedUser.get(), u);
+      RefUpdate.Result result = u.update(rw);
+      switch (result) {
+        case FAST_FORWARD:
+        case NEW:
+        case NO_CHANGE:
+          referenceUpdated.fire(
+              name.project(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
+          break;
+        case LOCK_FAILURE:
+          if (repo.getRefDatabase().exactRef(ref) != null) {
+            throw new ResourceConflictException("branch \"" + ref + "\" already exists");
+          }
+          String refPrefix = RefUtil.getRefPrefix(ref);
+          while (!Constants.R_HEADS.equals(refPrefix)) {
+            if (repo.getRefDatabase().exactRef(refPrefix) != null) {
+              throw new ResourceConflictException(
+                  "Cannot create branch \""
+                      + ref
+                      + "\" since it conflicts with branch \""
+                      + refPrefix
+                      + "\".");
             }
-            String refPrefix = RefUtil.getRefPrefix(ref);
-            while (!Constants.R_HEADS.equals(refPrefix)) {
-              if (repo.getRefDatabase().exactRef(refPrefix) != null) {
-                throw new ResourceConflictException(
-                    "Cannot create branch \""
-                        + ref
-                        + "\" since it conflicts with branch \""
-                        + refPrefix
-                        + "\".");
-              }
-              refPrefix = RefUtil.getRefPrefix(refPrefix);
-            }
-            throw new LockFailureException(String.format("Failed to create %s", ref), u);
-          case FORCED:
-          case IO_FAILURE:
-          case NOT_ATTEMPTED:
-          case REJECTED:
-          case REJECTED_CURRENT_BRANCH:
-          case RENAMED:
-          case REJECTED_MISSING_OBJECT:
-          case REJECTED_OTHER_REASON:
-          default:
-            throw new IOException(String.format("Failed to create %s: %s", ref, result.name()));
-        }
-
-        BranchInfo info = new BranchInfo();
-        info.ref = ref;
-        info.revision = revid.getName();
-
-        if (isConfigRef(name.branch())) {
-          // Never allow to delete the meta config branch.
-          info.canDelete = null;
-        } else {
-          info.canDelete =
-              permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
-                      && rsrc.getProjectState().statePermitsWrite()
-                  ? true
-                  : null;
-        }
-        return Response.created(info);
-      } catch (IOException err) {
-        logger.atSevere().withCause(err).log("Cannot create branch \"%s\"", name);
-        throw err;
+            refPrefix = RefUtil.getRefPrefix(refPrefix);
+          }
+          throw new LockFailureException(String.format("Failed to create %s", ref), u);
+        case FORCED:
+        case IO_FAILURE:
+        case NOT_ATTEMPTED:
+        case REJECTED:
+        case REJECTED_CURRENT_BRANCH:
+        case RENAMED:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
+        default:
+          throw new IOException(String.format("Failed to create %s: %s", ref, result.name()));
       }
+
+      BranchInfo info = new BranchInfo();
+      info.ref = ref;
+      info.revision = revid.getName();
+
+      if (isConfigRef(name.branch())) {
+        // Never allow to delete the meta config branch.
+        info.canDelete = null;
+      } else {
+        info.canDelete =
+            permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
+                    && rsrc.getProjectState().statePermitsWrite()
+                ? true
+                : null;
+      }
+      return Response.created(info);
     } catch (RefUtil.InvalidRevisionException e) {
       throw new BadRequestException("invalid revision \"" + input.revision + "\"", e);
     }
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 8543a1c..67dc5a5 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -20,7 +20,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.ArchiveFormatInternal;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -174,7 +174,7 @@
       // Parse Git arguments
       readArguments();
 
-      ArchiveFormat f = allowedFormats.getExtensions().get("." + options.format);
+      ArchiveFormatInternal f = allowedFormats.getExtensions().get("." + options.format);
       if (f == null) {
         throw new Failure(3, "fatal: upload-archive not permitted for format " + options.format);
       }
@@ -222,8 +222,8 @@
     }
   }
 
-  private Map<String, Object> getFormatOptions(ArchiveFormat f) {
-    if (f == ArchiveFormat.ZIP) {
+  private Map<String, Object> getFormatOptions(ArchiveFormatInternal f) {
+    if (f == ArchiveFormatInternal.ZIP) {
       int value =
           Arrays.asList(
                   options.level0,
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 9399c3b..fd681d8 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3210,7 +3210,8 @@
     mergeInput.source = "dev";
     MergePatchSetInput in = new MergePatchSetInput();
     in.merge = mergeInput;
-    in.subject = "update change by merge ps2";
+    String subject = "update change by merge ps2";
+    in.subject = subject;
 
     TestWorkInProgressStateChangedListener wipStateChangedListener =
         new TestWorkInProgressStateChangedListener();
@@ -3234,6 +3235,36 @@
     List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
     assertThat(messages).hasSize(2);
     assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2.");
+
+    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.message)
+        .contains(subject);
+  }
+
+  @Test
+  public void createMergePatchSet_SubjectCarriesOverByDefault() throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+    createBranch("dev");
+
+    // create a change for master
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String subject = result.getChange().change().getSubject();
+
+    // push a commit into dev branch
+    testRepo.reset(initialHead);
+    PushOneCommit.Result pushResult =
+        pushFactory.create(user.newIdent(), testRepo).to("refs/heads/dev");
+    pushResult.assertOkStatus();
+    MergeInput mergeInput = new MergeInput();
+    mergeInput.source = "dev";
+    MergePatchSetInput in = new MergePatchSetInput();
+    in.merge = mergeInput;
+    in.subject = null;
+
+    // Ensure subject carries over
+    gApi.changes().id(changeId).createMergePatchSet(in);
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    assertThat(changeInfo.subject).isEqualTo(subject);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index 04625c5..e67770c 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -169,6 +169,21 @@
     assertThat(revInfo.commit.message).isEqualTo(input.message + "\n");
   }
 
+  @Test
+  public void cherryPickCommitWithSetTopic() throws Exception {
+    String branch = "foo";
+    RevCommit revCommit = createChange().getCommit();
+    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());
+    CherryPickInput input = new CherryPickInput();
+    input.destination = branch;
+    input.topic = "topic";
+    String changeId =
+        gApi.projects().name(project.get()).commit(revCommit.name()).cherryPick(input).get().id;
+
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    assertThat(changeInfo.topic).isEqualTo(input.topic);
+  }
+
   private IncludedInInfo getIncludedIn(ObjectId id) throws Exception {
     return gApi.projects().name(project.get()).commit(id.name()).includedIn();
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 2c2abfe..2c6d539 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -403,6 +403,19 @@
   }
 
   @Test
+  public void cherryPickWithSetTopic() throws Exception {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "foo";
+    in.topic = "topic";
+    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
+    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
+
+    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
+    assertThat(cherry.get().topic).isEqualTo("topic");
+  }
+
+  @Test
   public void cherryPickWorkInProgressChange() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master%wip");
     CherryPickInput in = new CherryPickInput();
@@ -1079,6 +1092,24 @@
   }
 
   @Test
+  public void setReviewedFlagWithMultiplePatchSets() throws Exception {
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result r1 = push.to("refs/for/master");
+
+    gApi.changes().id(r1.getChangeId()).current().setReviewed(PushOneCommit.FILE_NAME, true);
+
+    /** Amending the change will result in the file being un-reviewed in the latest patchset */
+    PushOneCommit.Result r2 = amendChange(r1.getChangeId());
+
+    assertThat(gApi.changes().id(r2.getChangeId()).current().reviewed()).isEmpty();
+
+    gApi.changes().id(r2.getChangeId()).current().setReviewed(PushOneCommit.FILE_NAME, true);
+
+    assertThat(Iterables.getOnlyElement(gApi.changes().id(r2.getChangeId()).current().reviewed()))
+        .isEqualTo(PushOneCommit.FILE_NAME);
+  }
+
+  @Test
   public void setUnsetReviewedFlagByFileApi() throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
diff --git a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
index 876e342..d7952e4 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -206,4 +207,22 @@
       r4.assertErrorStatus(UPDATE_NONFASTFORWARD.name());
     }
   }
+
+  @Test
+  @GerritConfig(name = "change.maxFiles", value = "0")
+  public void dontEnforceFileCountForDirectPushes() throws Exception {
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "change", "c.txt", "content");
+    PushOneCommit.Result result = push.to("refs/heads/master");
+    result.assertOkStatus();
+  }
+
+  @Test
+  @GerritConfig(name = "change.maxFiles", value = "0")
+  public void enforceFileCountLimitOnPushesForReview() throws Exception {
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "change", "c.txt", "content");
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertErrorStatus("Exceeding maximum number of files per change");
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/GetArchiveIT.java b/javatests/com/google/gerrit/acceptance/rest/change/GetArchiveIT.java
new file mode 100644
index 0000000..15e6360
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/GetArchiveIT.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2020 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.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.ArchiveFormat;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.git.ObjectIds;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.HashMap;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GetArchiveIT extends AbstractDaemonTest {
+  private static final String DIRECTORY_NAME = "foo";
+  private static final String FILE_NAME = DIRECTORY_NAME + "/bar.txt";
+  private static final String FILE_CONTENT = "some content";
+
+  private String changeId;
+  private RevCommit commit;
+
+  @Before
+  public void setUp() throws Exception {
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "My Change", FILE_NAME, FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    changeId = result.getChangeId();
+    commit = result.getCommit();
+  }
+
+  @Test
+  public void formatNotSpecified() throws Exception {
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(changeId).current().getArchive(null));
+    assertThat(ex).hasMessageThat().isEqualTo("format is not specified");
+  }
+
+  @Test
+  public void unknownFormat() throws Exception {
+    // Test this by a REST call, since the Java API doesn't allow to specify an unknown format.
+    RestResponse res =
+        adminRestSession.get(
+            String.format(
+                "/changes/%s/revisions/current/archive?format=%s", changeId, "unknownFormat"));
+    res.assertBadRequest();
+    assertThat(res.getEntityContent()).isEqualTo("unknown archive format");
+  }
+
+  @Test
+  public void zipFormatIsDisabled() throws Exception {
+    MethodNotAllowedException ex =
+        assertThrows(
+            MethodNotAllowedException.class,
+            () -> gApi.changes().id(changeId).current().getArchive(ArchiveFormat.ZIP));
+    assertThat(ex).hasMessageThat().isEqualTo("zip format is disabled");
+  }
+
+  @Test
+  public void getTarArchive() throws Exception {
+    BinaryResult res = gApi.changes().id(changeId).current().getArchive(ArchiveFormat.TAR);
+    assertThat(res.getAttachmentName())
+        .isEqualTo(commit.abbreviate(ObjectIds.ABBREV_STR_LEN).name() + ".tar");
+    assertThat(res.getContentType()).isEqualTo("application/x-tar");
+    assertThat(res.canGzip()).isFalse();
+
+    byte[] archiveBytes = getBinaryContent(res);
+    try (ByteArrayInputStream in = new ByteArrayInputStream(archiveBytes)) {
+      HashMap<String, String> archiveEntries = getTarContent(in);
+      assertThat(archiveEntries)
+          .containsExactly(DIRECTORY_NAME + "/", null, FILE_NAME, FILE_CONTENT);
+    }
+  }
+
+  @Test
+  public void getTgzArchive() throws Exception {
+    BinaryResult res = gApi.changes().id(changeId).current().getArchive(ArchiveFormat.TGZ);
+    assertThat(res.getAttachmentName())
+        .isEqualTo(commit.abbreviate(ObjectIds.ABBREV_STR_LEN).name() + ".tar.gz");
+    assertThat(res.getContentType()).isEqualTo("application/x-gzip");
+    assertThat(res.canGzip()).isFalse();
+
+    byte[] archiveBytes = getBinaryContent(res);
+    try (ByteArrayInputStream in = new ByteArrayInputStream(archiveBytes);
+        GzipCompressorInputStream gzipIn = new GzipCompressorInputStream(in)) {
+      HashMap<String, String> archiveEntries = getTarContent(gzipIn);
+      assertThat(archiveEntries)
+          .containsExactly(DIRECTORY_NAME + "/", null, FILE_NAME, FILE_CONTENT);
+    }
+  }
+
+  private HashMap<String, String> getTarContent(InputStream in) throws Exception {
+    HashMap<String, String> archiveEntries = new HashMap<>();
+    int bufferSize = 100;
+    try (TarArchiveInputStream tarIn = new TarArchiveInputStream(in)) {
+      TarArchiveEntry entry;
+      while ((entry = tarIn.getNextTarEntry()) != null) {
+        if (entry.isDirectory()) {
+          archiveEntries.put(entry.getName(), null);
+        } else {
+          byte data[] = new byte[bufferSize];
+          try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+              BufferedOutputStream bufferedOut = new BufferedOutputStream(out, bufferSize)) {
+            int count;
+            while ((count = tarIn.read(data, 0, bufferSize)) != -1) {
+              bufferedOut.write(data, 0, count);
+            }
+            bufferedOut.flush();
+            archiveEntries.put(entry.getName(), out.toString());
+          }
+        }
+      }
+    }
+    return archiveEntries;
+  }
+
+  private byte[] getBinaryContent(BinaryResult res) throws Exception {
+    try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+      res.writeTo(out);
+      return out.toByteArray();
+    } finally {
+      res.close();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 85d383e..b01a07b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -23,7 +23,9 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -38,8 +40,20 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.events.RefReceivedEvent;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
@@ -47,6 +61,7 @@
 public class CreateBranchIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   private BranchNameKey testBranch;
 
@@ -82,7 +97,63 @@
   @Test
   public void branchAlreadyExists_Conflict() throws Exception {
     assertCreateSucceeds(testBranch);
-    assertCreateFails(testBranch, ResourceConflictException.class);
+    assertCreateFails(
+        testBranch,
+        ResourceConflictException.class,
+        "branch \"" + testBranch.branch() + "\" already exists");
+  }
+
+  @Test
+  public void createBranch_LockFailure() throws Exception {
+    // check that the branch doesn't exist yet
+    assertThrows(ResourceNotFoundException.class, () -> branch(testBranch).get());
+
+    // Register a validation listener that creates the branch to simulate a concurrent request that
+    // creates the same branch.
+    try (ExtensionRegistry.Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(
+                new RefOperationValidationListener() {
+                  @Override
+                  public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+                      throws ValidationException {
+                    try (Repository repo = repoManager.openRepository(project)) {
+                      RefUpdate u = repo.updateRef(testBranch.branch());
+                      u.setExpectedOldObjectId(ObjectId.zeroId());
+                      u.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId());
+                      RefUpdate.Result result = u.update();
+                      if (result != RefUpdate.Result.NEW) {
+                        throw new ValidationException(
+                            "Concurrent creation of branch failed: " + result);
+                      }
+                      return ImmutableList.of();
+                    } catch (IOException e) {
+                      throw new ValidationException("Concurrent creation of branch failed.", e);
+                    }
+                  }
+                })) {
+      // Creating the branch is expected to fail, since it is created by the validation listener
+      // right before the ref update to create the new branch is done.
+      assertCreateFails(
+          testBranch,
+          ResourceConflictException.class,
+          "branch \"" + testBranch.branch() + "\" already exists");
+    }
+  }
+
+  @Test
+  public void conflictingBranchAlreadyExists_Conflict() throws Exception {
+    assertCreateSucceeds(testBranch);
+    BranchNameKey testBranch2 = BranchNameKey.create(project, testBranch.branch() + "/foo/bar");
+    assertCreateFails(
+        testBranch2,
+        ResourceConflictException.class,
+        "Cannot create branch \""
+            + testBranch2.branch()
+            + "\" since it conflicts with branch \""
+            + testBranch.branch()
+            + "\"");
   }
 
   @Test
@@ -119,6 +190,23 @@
   }
 
   @Test
+  public void createMetaConfigBranch() throws Exception {
+    // Since the refs/meta/config branch exists by default, we must delete it before we can test
+    // creating it. Since deleting the refs/meta/config branch is not allowed through the API, we
+    // delete it directly in the remote repository.
+    try (TestRepository<Repository> repo =
+        new TestRepository<>(repoManager.openRepository(project))) {
+      repo.delete(RefNames.REFS_CONFIG);
+    }
+
+    // Create refs/meta/config branch.
+    BranchInfo created =
+        branch(BranchNameKey.create(project, RefNames.REFS_CONFIG)).create(new BranchInput()).get();
+    assertThat(created.ref).isEqualTo(RefNames.REFS_CONFIG);
+    assertThat(created.canDelete).isNull();
+  }
+
+  @Test
   public void createUserBranch_Conflict() throws Exception {
     projectOperations
         .project(allUsers)
@@ -258,6 +346,54 @@
         "invalid revision \"invalid\trevision\"");
   }
 
+  @Test
+  public void cannotCreateBranchInMagicBranchNamespace() throws Exception {
+    assertCreateFails(
+        BranchNameKey.create(project, MagicBranch.NEW_CHANGE + "foo"),
+        BadRequestException.class,
+        "not allowed to create branches under \"" + MagicBranch.NEW_CHANGE + "\"");
+  }
+
+  @Test
+  public void cannotCreateBranchWithInvalidName() throws Exception {
+    assertCreateFails(
+        BranchNameKey.create(project, RefNames.REFS_HEADS),
+        BadRequestException.class,
+        "invalid branch name \"" + RefNames.REFS_HEADS + "\"");
+  }
+
+  @Test
+  public void createBranchLeadingSlashesAreRemoved() throws Exception {
+    BranchNameKey expectedNameKey = BranchNameKey.create(project, "test");
+
+    // check that the branch doesn't exist yet
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.projects().name(project.get()).branch(expectedNameKey.branch()).get());
+
+    // create the branch, but include leading slashes in the branch name,
+    // when creating the branch ensure that the branch name in the URL matches the branch name in
+    // the input (if there is a mismatch the creation request is rejected)
+    BranchInput branchInput = new BranchInput();
+    branchInput.ref = "////" + expectedNameKey.shortName();
+    gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+    // verify that the branch was created without the leading slashes in the name
+    assertThat(gApi.projects().name(project.get()).branch(expectedNameKey.branch()).get().ref)
+        .isEqualTo(expectedNameKey.branch());
+  }
+
+  @Test
+  public void branchNameInInputMustMatchBranchNameInUrl() throws Exception {
+    BranchInput branchInput = new BranchInput();
+    branchInput.ref = "foo";
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.projects().name(project.get()).branch("bar").create(branchInput));
+    assertThat(ex).hasMessageThat().isEqualTo("ref must match URL");
+  }
+
   private void blockCreateReference() throws Exception {
     projectOperations
         .project(project)
@@ -302,9 +438,4 @@
       assertThat(thrown).hasMessageThat().contains(errMsg);
     }
   }
-
-  private void assertCreateFails(BranchNameKey branch, Class<? extends RestApiException> errType)
-      throws Exception {
-    assertCreateFails(branch, errType, null);
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index b60cce5..566308d 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
@@ -85,6 +86,7 @@
   @Inject private Provider<ChangesCollection> changes;
   @Inject private Provider<PostReview> postReview;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private CommentsUtil commentsUtil;
 
   private final Integer[] lines = {0, 1};
 
@@ -446,6 +448,82 @@
   }
 
   @Test
+  public void putDraft_idMismatch() throws Exception {
+    String file = "file";
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraft(file, Side.REVISION, 0, "foo");
+    CommentInfo commentInfo = addDraft(changeId, revId, comment);
+    DraftInput draftInput = newDraft(file, Side.REVISION, 0, "bar");
+    draftInput.id = "anything_but_" + commentInfo.id;
+    BadRequestException e =
+        assertThrows(
+            BadRequestException.class,
+            () -> updateDraft(changeId, revId, draftInput, commentInfo.id));
+    assertThat(e).hasMessageThat().contains("id must match URL");
+  }
+
+  @Test
+  public void putDraft_negativeLine() throws Exception {
+    String file = "file";
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraft(file, Side.REVISION, -666, "foo");
+    BadRequestException e =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+    assertThat(e).hasMessageThat().contains("line must be >= 0");
+  }
+
+  @Test
+  public void putDraft_invalidRange() throws Exception {
+    String file = "file";
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput draftInput = newDraft(file, Side.REVISION, createLineRange(2, 3), "bar");
+    draftInput.line = 666;
+    BadRequestException e =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, draftInput));
+    assertThat(e)
+        .hasMessageThat()
+        .contains("range endLine must be on the same line as the comment");
+  }
+
+  @Test
+  public void putDraft_updatePath() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraft("file_foo", Side.REVISION, 0, "foo");
+    CommentInfo commentInfo = addDraft(changeId, revId, comment);
+    assertThat(getDraftComments(changeId, revId).keySet()).containsExactly("file_foo");
+    DraftInput draftInput = newDraft("file_bar", Side.REVISION, 0, "bar");
+    updateDraft(changeId, revId, draftInput, commentInfo.id);
+    assertThat(getDraftComments(changeId, revId).keySet()).containsExactly("file_bar");
+  }
+
+  @Test
+  public void putDraft_updateInReplyToAndTag() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput draftInput1 = newDraft(FILE_NAME, Side.REVISION, 0, "foo");
+    CommentInfo commentInfo = addDraft(changeId, revId, draftInput1);
+    DraftInput draftInput2 = newDraft(FILE_NAME, Side.REVISION, 0, "bar");
+    String inReplyTo = "in_reply_to";
+    String tag = "täg";
+    draftInput2.inReplyTo = inReplyTo;
+    draftInput2.tag = tag;
+    updateDraft(changeId, revId, draftInput2, commentInfo.id);
+    com.google.gerrit.entities.Comment comment =
+        Iterables.getOnlyElement(commentsUtil.draftByChange(r.getChange().notes()));
+    assertThat(comment.parentUuid).isEqualTo(inReplyTo);
+    assertThat(comment.tag).isEqualTo(tag);
+  }
+
+  @Test
   public void listDrafts() throws Exception {
     String file = "file";
     PushOneCommit.Result r = createChange();
@@ -677,15 +755,15 @@
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
-        newDraft(FILE_NAME, Side.REVISION, createLineRange(1, 4, 10), "Is it that bad?"));
+        newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 10), "Is it that bad?"));
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
-        newDraft(FILE_NAME, Side.PARENT, createLineRange(1, 0, 7), "what happened to this?"));
+        newDraft(FILE_NAME, Side.PARENT, createLineRange(0, 7), "what happened to this?"));
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
-        newDraft(FILE_NAME, Side.REVISION, createLineRange(1, 4, 15), "better now"));
+        newDraft(FILE_NAME, Side.REVISION, createLineRange(4, 15), "better now"));
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
@@ -905,7 +983,7 @@
   @Test
   public void deleteCommentCannotBeAppliedByUser() throws Exception {
     PushOneCommit.Result result = createChange();
-    CommentInput targetComment = addComment(result.getChangeId(), "My password: abc123");
+    CommentInput targetComment = addComment(result.getChangeId());
 
     Map<String, List<CommentInfo>> commentsMap =
         getPublishedComments(result.getChangeId(), result.getCommit().name());
@@ -1083,9 +1161,9 @@
         .collect(toList());
   }
 
-  private CommentInput addComment(String changeId, String message) throws Exception {
+  private CommentInput addComment(String changeId) throws Exception {
     ReviewInput input = new ReviewInput();
-    CommentInput comment = newComment(FILE_NAME, Side.REVISION, 0, message, false);
+    CommentInput comment = newComment(FILE_NAME, Side.REVISION, 0, "a message", false);
     input.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment));
     gApi.changes().id(changeId).current().review(input);
     return comment;
@@ -1240,7 +1318,7 @@
   private static CommentInput newCommentOnParent(
       String path, int parent, int line, String message) {
     CommentInput c = new CommentInput();
-    return populate(c, path, Side.PARENT, Integer.valueOf(parent), line, message, false);
+    return populate(c, path, Side.PARENT, parent, line, message, false);
   }
 
   private DraftInput newDraft(String path, Side side, int line, String message) {
@@ -1255,7 +1333,7 @@
 
   private DraftInput newDraftOnParent(String path, int parent, int line, String message) {
     DraftInput d = new DraftInput();
-    return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, message, false);
+    return populate(d, path, Side.PARENT, parent, line, message, false);
   }
 
   private static <C extends Comment> C populate(
@@ -1284,11 +1362,11 @@
     return populate(c, path, side, parent, line, null, message, unresolved);
   }
 
-  private static Comment.Range createLineRange(int line, int startChar, int endChar) {
+  private static Comment.Range createLineRange(int startChar, int endChar) {
     Comment.Range range = new Comment.Range();
-    range.startLine = line;
+    range.startLine = 1;
     range.startCharacter = startChar;
-    range.endLine = line;
+    range.endLine = 1;
     range.endCharacter = endChar;
     return range;
   }
diff --git a/javatests/com/google/gerrit/server/change/ArchiveFormatInternalTest.java b/javatests/com/google/gerrit/server/change/ArchiveFormatInternalTest.java
new file mode 100644
index 0000000..003225c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/change/ArchiveFormatInternalTest.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2020 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.extensions.client.ArchiveFormat;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+
+public class ArchiveFormatInternalTest {
+  @Test
+  public void internalAndExternalArchiveFormatEnumsMatch() throws Exception {
+    assertThat(getEnumNames(ArchiveFormatInternal.class))
+        .containsExactlyElementsIn(getEnumNames(ArchiveFormat.class));
+  }
+
+  private static List<String> getEnumNames(Class<? extends Enum<?>> e) {
+    return Arrays.stream(e.getEnumConstants()).map(Enum::name).collect(toList());
+  }
+}
diff --git a/plugins/download-commands b/plugins/download-commands
index 5c50f9b..3f5a024 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 5c50f9b17e616fd84d2c561822161fff46bbf902
+Subproject commit 3f5a024fd46f30f4646bfceb285763e44fda15a7
diff --git a/plugins/replication b/plugins/replication
index e3cc87b..864c077 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit e3cc87b88c6a64966a411b8a0a4caad3ebad539b
+Subproject commit 864c077e5e13ebbae5e7d0a3abc95fc8ae3fdc8b
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index c5dd657..de25d79 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -2,7 +2,13 @@
 
 Follow the
 [setup instructions for Gerrit backend developers](https://gerrit-review.googlesource.com/Documentation/dev-readme.html)
-where applicable.
+where applicable, the most important command is:
+
+```
+git clone --recurse-submodules https://gerrit.googlesource.com/gerrit
+```
+
+The --recurse-submodules option is needed on git clone to ensure that the core plugins, which are included as git submodules, are also cloned.
 
 ## Installing [Bazel](https://bazel.build/)
 
@@ -46,17 +52,16 @@
 
 or use [nvm - Node Version Manager](https://github.com/nvm-sh/nvm).
 
-Various steps below require installing additional npm packages. To start developing, it is enough
-to install only top-level packages with the following command: 
+### Additional packages
+
+We have several bazel commands to install packages we may need for FE development.
+
+For first time users to get the local server up, `npm start` should be enough and will take care of all of them for you.
 
 ```sh
 # Install packages from root-level packages.json
 bazel fetch @npm//:node_modules
-```
 
-All other packages are installed by bazel when needed. If you want to install them manually, run the
-following commands:
-```sh
 # Install packages from polygerrit-ui/app/packages.json
 bazel fetch @ui_npm//:node_modules
 
@@ -64,7 +69,7 @@
 bazel fetch @ui_dev_npm//:node_modules
 
 # Install packages from tools/node_tools/packages.json
-bazel fetch @ui_dev_npm//:node_modules
+bazel fetch @tools_npm//:node_modules
 ```
 
 More information for installing and using nodejs rules can be found here https://bazelbuild.github.io/rules_nodejs/install.html
@@ -92,6 +97,8 @@
 ./polygerrit-ui/run-server.sh --plugins=plugins/my_plugin/static/my_plugin.js,plugins/my_plugin/static/my_plugin.html
 ```
 
+If any issues occured, please refer to the Troubleshooting section at the bottom or contact the team!
+
 ## Running locally against production data
 
 ### Local website
@@ -201,7 +208,7 @@
 * To run the linter on all of your local changes:
 
 ```sh
-git diff --name-only master | xargs node_modules/eslint/bin/eslint.js --ext .html,.js
+git diff --name-only HEAD | xargs node_modules/eslint/bin/eslint.js --ext .html,.js
 ```
 
 We also use the `polylint` tool to lint use of Polymer. To install polylint,
@@ -274,4 +281,22 @@
 
 If you are willing to join the queue and help the community review changes,
 you can create an issue through Monorail and request to join the queue!
-We will review your request and start from there.
\ No newline at end of file
+We will review your request and start from there.
+
+## Troubleshotting & Frequently asked questions
+
+1. Local host is blank page and console shows missing files from `polymer-bridges`
+
+Its likely you missed the `polymer-bridges` submodule when you clone the `gerrit` repo.
+
+To fix that, run:
+```
+// fetch the submodule
+git submodule update --init --recursive
+
+// reset the workspace (please save your local changes before running this command)
+npm run clean
+
+// install all dependencies and start the server
+npm start
+```
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index 850bfb4..e89be0f5 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -116,6 +116,12 @@
   _updateSortedThreads(threads) {
     this._sortedThreads =
         threads.map(this._getThreadWithSortInfo).sort((c1, c2) => {
+          // threads will be sorted by:
+          // - unresolved first
+          // - with drafts
+          // - file path
+          // - line
+          // - updated time
           const c1Date = c1.__date || util.parseDate(c1.updated);
           const c2Date = c2.__date || util.parseDate(c2.updated);
           const dateCompare = c2Date - c1Date;
@@ -123,11 +129,28 @@
             if (!c1.unresolved) { return 1; }
             if (!c2.unresolved) { return -1; }
           }
+
           if (c2.hasDraft || c1.hasDraft) {
             if (!c1.hasDraft) { return 1; }
             if (!c2.hasDraft) { return -1; }
           }
 
+          // TODO: Update here once we introduce patchset level comments
+          // they may not have or have a special line or path attribute
+
+          if (c1.thread.path !== c2.thread.path) {
+            return c1.thread.path.localeCompare(c2.thread.path);
+          }
+
+          // File level comments (no `line` property)
+          // should always show before any lines
+          if ([c1, c2].some(c => c.thread.line === undefined)) {
+            if (!c1.thread.line) { return -1; }
+            if (!c2.thread.line) { return 1; }
+          } else if (c1.thread.line !== c2.thread.line) {
+            return c1.thread.line - c2.thread.line;
+          }
+
           if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
             return 0;
           }
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index cb6ba28..3e87077 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -114,7 +114,6 @@
             },
             patch_set: 2,
             id: '8caddf38_44770ec1',
-            line: 4,
             updated: '2018-02-13 22:48:40.000000000',
             message: 'Another unresolved comment',
             unresolved: true,
@@ -122,7 +121,6 @@
         ],
         patchNum: 2,
         path: '/COMMIT_MSG',
-        line: 4,
         rootId: '8caddf38_44770ec1',
         start_datetime: '2018-02-13 22:48:40.000000000',
       },
@@ -205,7 +203,7 @@
             },
             patch_set: 4,
             id: 'rc2',
-            line: 5,
+            line: 7,
             updated: '2019-03-08 18:49:18.000000000',
             message: 'test',
             unresolved: true,
@@ -228,7 +226,7 @@
         ],
         patchNum: 4,
         path: '/COMMIT_MSG',
-        line: 5,
+        line: 7,
         rootId: 'rc2',
         start_datetime: '2019-03-08 18:49:18.000000000',
       },
@@ -259,25 +257,26 @@
 
   test('_computeSortedThreads', () => {
     assert.equal(element._sortedThreads.length, 7);
-    // Draft and unresolved
+    // Draft and unresolved for commit-msg at line 5
     assert.equal(element._sortedThreads[0].thread.rootId,
         'ecf0b9fa_fe1a5f62');
-    // Unresolved robot comment
+    // /COMMIT_MSG
+    // unresolved no draft and file level
     assert.equal(element._sortedThreads[1].thread.rootId,
-        'rc2');
-    // Unresolved robot comment
-    assert.equal(element._sortedThreads[2].thread.rootId,
-        'rc1');
-    // unresolved
-    assert.equal(element._sortedThreads[3].thread.rootId,
-        'scaddf38_44770ec1');
-    // unresolved
-    assert.equal(element._sortedThreads[4].thread.rootId,
         '8caddf38_44770ec1');
-    // resolved and draft
+    // unresolved no draft at line 4
+    assert.equal(element._sortedThreads[2].thread.rootId,
+        'scaddf38_44770ec1');
+    // unresolved no draft at line 5
+    assert.equal(element._sortedThreads[3].thread.rootId,
+        'rc1');
+    // Unresolved no draft at line 7
+    assert.equal(element._sortedThreads[4].thread.rootId,
+        'rc2');
+    // resolved and draft on COMMIT_MSG
     assert.equal(element._sortedThreads[5].thread.rootId,
         'zcf0b9fa_fe1a5f62');
-    // resolved
+    // resolved and on file test.txt
     assert.equal(element._sortedThreads[6].thread.rootId,
         '09a9fb0a_1484e6cf');
   });
@@ -298,19 +297,20 @@
     assert.equal(element._sortedThreads.length, 6);
     assert.equal(element._sortedThreads[0].thread.rootId,
         'ecf0b9fa_fe1a5f62');
-    // Unresolved robot comment
+    // /COMMIT_MSG
+    // unresolved no draft and file level
     assert.equal(element._sortedThreads[1].thread.rootId,
-        'rc1');
-    // unresolved
+        '8caddf38_44770ec1');
+    // unresolved no draft at line 4
     assert.equal(element._sortedThreads[2].thread.rootId,
         'scaddf38_44770ec1');
-    // unresolved
+    // unresolved no draft at line 5
     assert.equal(element._sortedThreads[3].thread.rootId,
-        '8caddf38_44770ec1');
+        'rc1');
     // resolved and draft
     assert.equal(element._sortedThreads[4].thread.rootId,
         'zcf0b9fa_fe1a5f62');
-    // resolved
+    // resolved and on file test.txt
     assert.equal(element._sortedThreads[5].thread.rootId,
         '09a9fb0a_1484e6cf');
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 5bca7be..a3076f0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -737,14 +737,18 @@
     this._prefsChanged(this.prefs);
   }
 
+  _cleanup() {
+    this.cancel();
+    this._blame = null;
+    this._safetyBypass = null;
+    this._showWarning = false;
+    this.clearDiffContent();
+  }
+
   /** @param {boolean} newValue */
   _loadingChanged(newValue) {
     if (newValue) {
-      this.cancel();
-      this._blame = null;
-      this._safetyBypass = null;
-      this._showWarning = false;
-      this.clearDiffContent();
+      this._cleanup();
     }
   }
 
@@ -785,6 +789,7 @@
 
   _diffChanged(newValue) {
     if (newValue) {
+      this._cleanup();
       this._diffLength = this.getDiffLength(newValue);
       this._debounceRenderDiffTable();
     }
@@ -806,7 +811,6 @@
   }
 
   _renderDiffTable() {
-    this._unobserveIncrementalNodes();
     if (!this.prefs) {
       this.dispatchEvent(
           new CustomEvent('render', {bubbles: true, composed: true}));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 18b4e09..08576a2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -952,47 +952,68 @@
       });
     });
   });
+  const setupSampleDiff = function(params) {
+    const {ignore_whitespace, content} = params;
+    element = fixture('basic');
+    element.prefs = {
+      ignore_whitespace: ignore_whitespace || 'IGNORE_ALL',
+      auto_hide_diff_table_header: true,
+      context: 10,
+      cursor_blink_rate: 0,
+      font_size: 12,
+      intraline_difference: true,
+      line_length: 100,
+      line_wrapping: false,
+      show_line_endings: true,
+      show_tabs: true,
+      show_whitespace_errors: true,
+      syntax_highlighting: true,
+      tab_size: 8,
+      theme: 'DEFAULT',
+    };
+    element.diff = {
+      intraline_status: 'OK',
+      change_type: 'MODIFIED',
+      diff_header: [
+        'diff --git a/carrot.js b/carrot.js',
+        'index 2adc47d..f9c2f2c 100644',
+        '--- a/carrot.js',
+        '+++ b/carrot.jjs',
+        'file differ',
+      ],
+      content,
+      binary: false,
+    };
+    element._renderDiffTable();
+    flushAsynchronousOperations();
+  };
+
+  test('clear diff table content as soon as diff changes', () => {
+    const content = [{
+      a: ['all work and no play make andybons a dull boy'],
+    }, {
+      b: [
+        'Non eram nescius, Brute, cum, quae summis ingeniis ',
+      ],
+    }];
+    function assertDiffTableWithContent() {
+      assert.isTrue(element.$.diffTable.innerText.includes(content[0].a));
+    }
+    setupSampleDiff({content});
+    assertDiffTableWithContent();
+    const diffCopy = Object.assign({}, element.diff);
+    element.diff = diffCopy;
+    // immediatelly cleaned up
+    assert.equal(element.$.diffTable.innerHTML, '');
+    element._renderDiffTable();
+    flushAsynchronousOperations();
+    // rendered again
+    assertDiffTableWithContent();
+  });
 
   suite('whitespace changes only message', () => {
-    const setupDiff = function(ignore_whitespace, diffContent) {
-      element = fixture('basic');
-      element.prefs = {
-        ignore_whitespace,
-        auto_hide_diff_table_header: true,
-        context: 10,
-        cursor_blink_rate: 0,
-        font_size: 12,
-        intraline_difference: true,
-        line_length: 100,
-        line_wrapping: false,
-        show_line_endings: true,
-        show_tabs: true,
-        show_whitespace_errors: true,
-        syntax_highlighting: true,
-        tab_size: 8,
-        theme: 'DEFAULT',
-      };
-
-      element.diff = {
-        intraline_status: 'OK',
-        change_type: 'MODIFIED',
-        diff_header: [
-          'diff --git a/carrot.js b/carrot.js',
-          'index 2adc47d..f9c2f2c 100644',
-          '--- a/carrot.js',
-          '+++ b/carrot.jjs',
-          'file differ',
-        ],
-        content: diffContent,
-        binary: true,
-      };
-
-      element._renderDiffTable();
-      flushAsynchronousOperations();
-    };
-
     test('show the message if ignore_whitespace is criteria matches', () => {
-      setupDiff('IGNORE_ALL', [{skip: 100}]);
+      setupSampleDiff({content: [{skip: 100}]});
       assert.isTrue(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
@@ -1001,7 +1022,7 @@
     });
 
     test('do not show the message if still loading', () => {
-      setupDiff('IGNORE_ALL', [{skip: 100}]);
+      setupSampleDiff({content: [{skip: 100}]});
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ true,
           element.prefs,
@@ -1019,7 +1040,7 @@
           'exquisitaque doctrina philosophi Graeco sermone tractavissent',
         ],
       }];
-      setupDiff('IGNORE_ALL', content);
+      setupSampleDiff({content});
       assert.equal(element._diffLength, 3);
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ false,
@@ -1038,7 +1059,7 @@
           'exquisitaque doctrina philosophi Graeco sermone tractavissent',
         ],
       }];
-      setupDiff('IGNORE_NONE', content);
+      setupSampleDiff({ignore_whitespace: 'IGNORE_NONE', content});
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 6f1eaa8..252f409 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -60,11 +60,12 @@
  * All candidates tips to show, will pick randomly.
  */
 const RESPECTFUL_REVIEW_TIPS= [
-  'DO: Assume competence.',
-  'DO: Provide rationale or context.',
-  'DO: Consider how comments may be interpreted.',
-  'DON’T: Criticize the person.',
-  'DON’T: Use harsh language.',
+  'Assume competence.',
+  'Provide rationale or context.',
+  'Consider how comments may be interpreted.',
+  'Avoid harsh language.',
+  'Make your comments specific and actionable.',
+  'When disagreeing, explain the advantage of your approach.',
 ];
 
 /**
@@ -294,8 +295,8 @@
         'respectful-tip-dismissed',
         {tip: this._respectfulReviewTip}
     );
-    // add a 3 day delay to the tip cache
-    this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3);
+    // add a 14-day delay to the tip cache
+    this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
   }
 
   _onRespectfulReadMoreClick() {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
index 4a0f388..6c9ebee 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -282,7 +282,7 @@
                 <a tabindex="-1" on-click="_onRespectfulReadMoreClick" href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html" target="_blank">
                   Read more
                 </a>
-                <iron-icon class="close pointer" on-click="_dismissRespectfulTip" icon="gr-icons:close"></iron-icon>
+                <a tabindex="-1" class="close pointer" on-click="_dismissRespectfulTip">Not helpful</a>
               </div>
             </div>
           </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 756e6fd..33f19c5 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -1161,7 +1161,7 @@
       });
     });
 
-    test('add 3 day delays once dismissed', done => {
+    test('add 14-day delays once dismissed', done => {
       // fake stub for storage
       const respectfulGetStub = sinon.stub();
       const respectfulSetStub = sinon.stub();
@@ -1185,7 +1185,7 @@
         MockInteractions.tap(element.shadowRoot
             .querySelector('.respectfulReviewTip .close'));
         flushAsynchronousOperations();
-        assert.isTrue(respectfulSetStub.lastCall.args[0] === 3);
+        assert.isTrue(respectfulSetStub.lastCall.args[0] === 14);
         done();
       });
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
index 7a5f4c6..be0f2b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
@@ -24,20 +24,8 @@
 
 <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 <script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module" src="../../../test/test-pre-setup.js"></script>
-<script type="module" src="../../../test/common-test-setup.js"></script>
 <script src="../../../node_modules/iron-test-helpers/mock-interactions.js" type="module"></script>
 
-<script type="module" src="./gr-hovercard-account.js"></script>
-
-<script type="module">
-  import '../../../test/test-pre-setup.js';
-  import '../../../test/common-test-setup.js';
-  import './gr-hovercard-account.js';
-
-  void (0);
-</script>
-
 <test-fixture id="basic">
   <template>
     <gr-hovercard-account class="hovered"></gr-hovercard-account>
@@ -46,7 +34,6 @@
 
 
 <script type="module">
-  import '../../../test/test-pre-setup.js';
   import '../../../test/common-test-setup.js';
   import './gr-hovercard-account.js';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
index a77f5f7..1ba3c23 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
@@ -30,6 +30,12 @@
 const DIAGONAL_OVERFLOW = 15;
 
 /**
+ * How long should be wait before showing the hovercard when the user hovers
+ * over the element?
+ */
+const SHOW_DELAY_MS = 500;
+
+/**
  * The mixin for gr-hovercard-behavior.
  *
  * @example
@@ -118,8 +124,8 @@
   attached() {
     super.attached();
     if (!this._target) { this._target = this.target; }
-    this.listen(this._target, 'mouseenter', 'show');
-    this.listen(this._target, 'focus', 'show');
+    this.listen(this._target, 'mouseenter', 'showDelayed');
+    this.listen(this._target, 'focus', 'showDelayed');
     this.listen(this._target, 'mouseleave', 'hide');
     this.listen(this._target, 'blur', 'hide');
     this.listen(this._target, 'click', 'hide');
@@ -184,6 +190,10 @@
    * @param {Event} e DOM Event (e.g. `mouseleave` event)
    */
   hide(e) {
+    this._isScheduledToShow = false;
+    if (!this._isShowing) {
+      return;
+    }
     const targetRect = this._target.getBoundingClientRect();
     const x = e.clientX;
     const y = e.clientY;
@@ -195,10 +205,10 @@
       return;
     }
 
-    // If the hovercard is already hidden or the user is now hovering over the
-    //  hovercard or the user is returning from the hovercard but now hovering
-    //  over the target (to stop an annoying flicker effect), just return.
-    if (!this._isShowing || e.toElement === this ||
+    // If the user is now hovering over the hovercard or the user is returning
+    // from the hovercard but now hovering over the target (to stop an annoying
+    // flicker effect), just return.
+    if (e.toElement === this ||
         (e.fromElement === this && e.toElement === this._target)) {
       return;
     }
@@ -221,12 +231,31 @@
   }
 
   /**
+   * Shows/opens the hovercard with a fixed delay.
+   */
+  showDelayed() {
+    this.showDelayedBy(SHOW_DELAY_MS);
+  }
+
+  /**
+   * Shows/opens the hovercard with the given delay.
+   */
+  showDelayedBy(delayMs) {
+    if (this._isShowing || this._isScheduledToShow) return;
+    this._isScheduledToShow = true;
+    setTimeout(() => {
+      // This happens when the mouse leaves the target before the delay is over.
+      if (!this._isScheduledToShow) return;
+      this._isScheduledToShow = false;
+      this.show();
+    }, delayMs);
+  }
+
+  /**
    * Shows/opens the hovercard. This occurs when the user triggers the
    * `mousenter` event on the hovercard's `target` element.
-   *
-   * @param {Event} e DOM Event (e.g., `mouseenter` event)
    */
-  show(e) {
+  show() {
     if (this._isShowing) {
       return;
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
index 1ffed0a..99791e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -109,14 +109,31 @@
     }, TRANSITION_TIME);
   });
 
-  test('card shows on enter and hides on leave', done => {
+  test('showDelayed does not show immediately', done => {
+    element.showDelayedBy(100);
+    setTimeout(() => {
+      assert.isFalse(element._isShowing);
+      done();
+    }, 0);
+  });
+
+  test('showDelayed shows after delay', done => {
+    element.showDelayedBy(1);
+    setTimeout(() => {
+      assert.isTrue(element._isShowing);
+      done();
+    }, 10);
+  });
+
+  test('card is scheduled to show on enter and hides on leave', done => {
     const button = dom(document).querySelector('button');
     assert.isFalse(element._isShowing);
     button.addEventListener('mouseenter', event => {
-      assert.isTrue(element._isShowing);
+      assert.isTrue(element._isScheduledToShow);
       button.dispatchEvent(new CustomEvent('mouseleave'));
     });
     button.addEventListener('mouseleave', event => {
+      assert.isFalse(element._isScheduledToShow);
       assert.isFalse(element._isShowing);
       done();
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
index df3b9a5..015d71e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
@@ -16,7 +16,6 @@
 -->
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
-<script src="../../../test/test-pre-setup.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <dom-module id="mock-diff-response">
   <template></template>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
index e29d300..e7bc662 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../test/test-pre-setup.js';
 import '../../../test/common-test-setup.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 1597439..8f5c486 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -26,8 +26,8 @@
 const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY;
 
 const CLEANUP_PREFIXES_MAX_AGE_MAP = {
-  // respectfultip has a 3 day expiration
-  'respectfultip:': 3 * DURATION_DAY,
+  // respectfultip has a 14-day expiration
+  'respectfultip:': 14 * DURATION_DAY,
   'draft:': DURATION_DAY,
   'editablecontent:': DURATION_DAY,
 };
diff --git a/polygerrit-ui/app/wct.conf.js b/polygerrit-ui/app/wct.conf.js
index 0e53adb..1a9300e 100644
--- a/polygerrit-ui/app/wct.conf.js
+++ b/polygerrit-ui/app/wct.conf.js
@@ -27,7 +27,7 @@
 */
 
 const headless = 'WCT_HEADLESS_MODE' in process.env ?
-  process.env['WCT_HEADLESS_MODE'] !== '0' : false;
+  process.env['WCT_HEADLESS_MODE'] === '1' : false;
 
 const headlessBrowserOptions = {
   chrome: ['start-maximized', 'headless', 'disable-gpu', 'no-sandbox'],
diff --git a/tools/node_tools/BUILD b/tools/node_tools/BUILD
index 018674c..4019542 100644
--- a/tools/node_tools/BUILD
+++ b/tools/node_tools/BUILD
@@ -8,8 +8,12 @@
 # Usage: rollup_bundle(rollup_bin = "//tools/node_tools:rollup-bin, ...)
 nodejs_binary(
     name = "rollup-bin",
+    # Define only minimal required dependencies.
+    # Otherwise remote build execution fails with the too many
+    # files error when it builds :release target.
     data = [
-        "@tools_npm//:node_modules",
+        "@tools_npm//rollup",
+        "@tools_npm//rollup-plugin-terser",
     ],
     # The entry point must be "@tools_npm:node_modules/rollup/dist/bin/rollup",
     # But bazel doesn't run it correctly with the following command line: