Merge "A11y - Improvements on dashboard page"
diff --git a/.bazelversion b/.bazelversion
index fd2a018..47b322c 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-3.1.0
+3.4.1
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index c9e2ed5..8bb5d54 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -869,6 +869,14 @@
 private changes (even without having the `View Private Changes` access
 right assigned).
 
+[[category_toggle_work_in_progress_state]]
+=== Toggle Work In Progress state
+
+This category controls who is able to flip the Work In Progress bit.
+
+Change owner, server administrators and project owners can always flip
+the Work In Progress bit of the change (even without having the
+`Toggle Work In Progress state` access right assigned).
 
 [[category_delete_own_changes]]
 === Delete Own Changes
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt
index 9bc7f0b..e1b2147 100644
--- a/Documentation/dev-crafting-changes.txt
+++ b/Documentation/dev-crafting-changes.txt
@@ -10,6 +10,37 @@
 beyond simple spacing issues.  Blame it on our short attention
 spans, we really do want your code.
 
+
+[[branch]]
+== Branch
+
+Gerrit provides support for more than one version, which naturally
+raises the question of which branch you should start your contribution
+on. There are no hard and fast rules, but below we try to outline some
+guidelines:
+
+* Genuinely new and/or disruptive features, should generally start on
+  `master`. Also consider submitting a
+  link:dev-design-docs.html[design doc] beforehand to allow discussion
+  by the ESC and the community.
+* Improvements of existing features should also generally go into
+  `master`. But we understand that if you cannot run `master`, it
+  might take a while until you could benefit from it. In that case,
+  start on the newest `stable-*` branch that you can run.
+* Bug-fixes should generally at least cover the oldest affected and
+  still supported version. If you're affected and run an even older
+  version, you're welcome to upload to that older version, even if
+  it is no longer officially supported, bearing in mind that
+  verification and release may happen only once merged upstream.
+
+Regardless of the above, changes might get moved to a different branch
+before being submitted or might get cherry-picked/re-merged to a
+different branch even after they've landed.
+
+For each of the above items, you'll find ad-hoc exceptions. The point
+is: We'd much rather see your code and fixes than not see them.
+
+
 [[commit-message]]
 == Commit Message
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index c8c2dff..c3df396 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2092,6 +2092,14 @@
 No Guice bindings or modules are required. Gerrit will automatically
 discover and bind the implementation.
 
+[[gerrit-replica]]
+== Gerrit Replica
+
+Gerrit can be run as a read-only replica. Some plugins may need to know
+whether Gerrit is run as a primary- or a replica instance. For that purpose
+Gerrit exposes the `@GerritIsReplica` annotation. A boolean annotated with
+this annotation will indicate whether Gerrit is run as a replica.
+
 [[accountcreation]]
 == Account Creation
 
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 2483ba3..eb2025c 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -590,8 +590,9 @@
 reply-modal.
 
 Change owners, project owners, site administrators and members of a group that
-was granted "Toggle Work In Progress state" permission can mark changes as
-`work-in-progress` and `ready`.
+was granted link:access-control.html#category_toggle_work_in_progress_state[
+Toggle Work In Progress state] permission can mark changes as `work-in-progress`
+and `ready`.
 
 [[private-changes]]
 == Private Changes
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
index 77b180e..ce26280 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -48,6 +48,7 @@
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
       "filename": "delete-project.jar",
+      "api_version": "2.9.3-SNAPSHOT",
       "version": "2.9-SNAPSHOT"
     }
   }
@@ -455,12 +456,13 @@
 
 [options="header",cols="1,^2,4"]
 |=======================
-|Field Name ||Description
-|`id`       ||The ID of the plugin.
-|`version`  ||The version of the plugin.
-|`index_url`|optional|URL of the plugin's default page.
-|`filename` |optional|The plugin's filename.
-|`disabled` |not set if `false`|Whether the plugin is disabled.
+|Field Name   ||Description
+|`id`         ||The ID of the plugin.
+|`version`    ||The version of the plugin.
+|`api_version`|optional|The version of the Gerrit Api used by the plugin.
+|`index_url`  |optional|URL of the plugin's default page.
+|`filename`   |optional|The plugin's filename.
+|`disabled`   |not set if `false`|Whether the plugin is disabled.
 |=======================
 
 [[plugin-input]]
diff --git a/WORKSPACE b/WORKSPACE
index 41d6ef8..b8d08dc 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -626,18 +626,18 @@
     sha1 = "a3ae34e57fa8a4040e28247291d0cc3d6b8c7bcf",
 )
 
-AUTO_VALUE_VERSION = "1.7.3"
+AUTO_VALUE_VERSION = "1.7.4"
 
 maven_jar(
     name = "auto-value",
     artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
-    sha1 = "cbd30873f839545c7c9264bed61d500bf85bd33e",
+    sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
 )
 
 maven_jar(
     name = "auto-value-annotations",
     artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
-    sha1 = "59ce5ee6aea918f674229f1147da95fdf7f31ce6",
+    sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
 )
 
 declare_nongoogle_deps()
diff --git a/java/com/google/gerrit/extensions/common/PluginInfo.java b/java/com/google/gerrit/extensions/common/PluginInfo.java
index 0df6235..47f9b6a 100644
--- a/java/com/google/gerrit/extensions/common/PluginInfo.java
+++ b/java/com/google/gerrit/extensions/common/PluginInfo.java
@@ -17,13 +17,21 @@
 public class PluginInfo {
   public final String id;
   public final String version;
+  public final String apiVersion;
   public final String indexUrl;
   public final String filename;
   public final Boolean disabled;
 
-  public PluginInfo(String id, String version, String indexUrl, String filename, Boolean disabled) {
+  public PluginInfo(
+      String id,
+      String version,
+      String apiVersion,
+      String indexUrl,
+      String filename,
+      Boolean disabled) {
     this.id = id;
     this.version = version;
+    this.apiVersion = apiVersion;
     this.indexUrl = indexUrl;
     this.filename = filename;
     this.disabled = disabled;
diff --git a/java/com/google/gerrit/server/config/GerritIsReplica.java b/java/com/google/gerrit/server/config/GerritIsReplica.java
new file mode 100644
index 0000000..154fdcd
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritIsReplica.java
@@ -0,0 +1,25 @@
+// 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+/* Marker on {@link Boolean} indicating whether Gerrit is run as a read-only replica. */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface GerritIsReplica {}
diff --git a/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
new file mode 100644
index 0000000..bd07f7d
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
@@ -0,0 +1,46 @@
+// 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.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Provides {@link Boolean} annotated with {@link GerritIsReplica}.
+ *
+ * <p>The returned boolean indicates whether Gerrit is run as a read-only replica.
+ */
+@Singleton
+public final class GerritIsReplicaProvider implements Provider<Boolean> {
+  public static final String CONFIG_SECTION = "container";
+  public static final String REPLICA_KEY = "replica";
+  public static final String DEPRECATED_REPLICA_KEY = "slave";
+
+  public final boolean isReplica;
+
+  @Inject
+  public GerritIsReplicaProvider(@GerritServerConfig Config config) {
+    this.isReplica =
+        config.getBoolean(CONFIG_SECTION, REPLICA_KEY, false)
+            || config.getBoolean(CONFIG_SECTION, DEPRECATED_REPLICA_KEY, false);
+  }
+
+  @Override
+  public Boolean get() {
+    return isReplica;
+  }
+}
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
index 25ee759..3777a55 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigModule.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -78,5 +78,8 @@
         .annotatedWith(GerritServerConfig.class)
         .toProvider(GerritServerConfigProvider.class);
     bind(SecureStore.class).toProvider(SecureStoreProvider.class).in(SINGLETON);
+    bind(Boolean.class)
+        .annotatedWith(GerritIsReplica.class)
+        .toProvider(GerritIsReplicaProvider.class);
   }
 }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 3dc8961..5d36e70 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -1005,7 +1005,15 @@
   private String buildError(String error, List<String> branches) {
     StringBuilder sb = new StringBuilder();
     if (branches.size() == 1) {
-      sb.append("branch ").append(branches.get(0)).append(":\n");
+      String branch = branches.get(0);
+      sb.append("branch ").append(branch).append(":\n");
+      // As of 2020, there are still many git-review <1.27 installations in the wild.
+      // These users will see failures as their old git-review assumes that
+      // `refs/publish/...` is still magic, which it isn't. As Gerrit's default error messages are
+      // misleading for these users, we hint them at upgrading their git-review.
+      if (branch.startsWith("refs/publish/")) {
+        sb.append("If you are using git-review, update to at least git-review 1.27. Otherwise:\n");
+      }
       sb.append(error);
       return sb.toString();
     }
diff --git a/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
index 090d257..9b74341 100644
--- a/java/com/google/gerrit/server/plugins/CopyConfigModule.java
+++ b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -17,6 +17,8 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritIsReplica;
+import com.google.gerrit.server.config.GerritIsReplicaProvider;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
@@ -101,6 +103,14 @@
     return secureStore;
   }
 
+  @Inject private GerritIsReplicaProvider isReplicaProvider;
+
+  @Provides
+  @GerritIsReplica
+  boolean getIsReplica() {
+    return isReplicaProvider.get();
+  }
+
   @Inject
   CopyConfigModule() {}
 
diff --git a/java/com/google/gerrit/server/plugins/ListPlugins.java b/java/com/google/gerrit/server/plugins/ListPlugins.java
index 465d041..0408efc 100644
--- a/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -146,12 +146,14 @@
   public static PluginInfo toPluginInfo(Plugin p) {
     String id;
     String version;
+    String apiVersion;
     String indexUrl;
     String filename;
     Boolean disabled;
 
     id = Url.encode(p.getName());
     version = p.getVersion();
+    apiVersion = p.getApiVersion();
     disabled = p.isDisabled() ? true : null;
     if (p.getSrcFile() != null) {
       indexUrl = String.format("plugins/%s/", p.getName());
@@ -161,6 +163,6 @@
       filename = null;
     }
 
-    return new PluginInfo(id, version, indexUrl, filename, disabled);
+    return new PluginInfo(id, version, apiVersion, indexUrl, filename, disabled);
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/Plugin.java b/java/com/google/gerrit/server/plugins/Plugin.java
index 5759705..238066b 100644
--- a/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/java/com/google/gerrit/server/plugins/Plugin.java
@@ -116,6 +116,11 @@
     return apiType;
   }
 
+  @Nullable
+  public String getApiVersion() {
+    return null;
+  }
+
   public Plugin.CacheKey getCacheKey() {
     return cacheKey;
   }
diff --git a/java/com/google/gerrit/server/plugins/PluginUtil.java b/java/com/google/gerrit/server/plugins/PluginUtil.java
index 932a01d..4f00cd0 100644
--- a/java/com/google/gerrit/server/plugins/PluginUtil.java
+++ b/java/com/google/gerrit/server/plugins/PluginUtil.java
@@ -53,7 +53,9 @@
   }
 
   static Path asTemp(InputStream in, String prefix, String suffix, Path dir) throws IOException {
-    Files.createDirectories(dir);
+    if (!Files.exists(dir)) {
+      Files.createDirectories(dir);
+    }
     Path tmp = Files.createTempFile(dir, prefix, suffix);
     boolean keep = false;
     try (OutputStream out = Files.newOutputStream(tmp)) {
diff --git a/java/com/google/gerrit/server/plugins/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index f236202..320b618 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -154,6 +154,13 @@
   }
 
   @Override
+  @Nullable
+  public String getApiVersion() {
+    Attributes main = manifest.getMainAttributes();
+    return main.getValue("Gerrit-ApiVersion");
+  }
+
+  @Override
   protected boolean canReload() {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ReloadMode");
diff --git a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index e5dad7e..3a952f0 100644
--- a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -49,15 +49,17 @@
           .toJson(output, new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
       stdout.print('\n');
     } else {
-      stdout.format("%-30s %-10s %-8s %s\n", "Name", "Version", "Status", "File");
+      String template = "%-30s %-10s %-16s %-8s %s\n";
+      stdout.format(template, "Name", "Version", "Api-Version", "Status", "File");
       stdout.print(
           "-------------------------------------------------------------------------------\n");
       for (Map.Entry<String, PluginInfo> p : output.entrySet()) {
         PluginInfo info = p.getValue();
         stdout.format(
-            "%-30s %-10s %-8s %s\n",
+            template,
             p.getKey(),
             Strings.nullToEmpty(info.version),
+            Strings.nullToEmpty(info.apiVersion),
             status(info.disabled),
             Strings.nullToEmpty(info.filename));
       }
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index ff873dd..6838f8d 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -37,7 +37,12 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.plugins.MandatoryPluginsCollection;
 import com.google.inject.Inject;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
 import org.junit.Test;
 
 @NoHttpd
@@ -51,7 +56,14 @@
 
   private static final ImmutableList<String> PLUGINS =
       ImmutableList.of(
-          "plugin-a.js", "plugin-b.html", "plugin-c.js", "plugin-d.html", "plugin_e.js");
+          "plugin-a.js",
+          "plugin-b.html",
+          "plugin-c.js",
+          "plugin-d.html",
+          "plugin-normal.jar",
+          "plugin-empty.jar",
+          "plugin-unset.jar",
+          "plugin_e.js");
 
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private MandatoryPluginsCollection mandatoryPluginsCollection;
@@ -67,13 +79,14 @@
     // Install all the plugins
     InstallPluginInput input = new InstallPluginInput();
     for (String plugin : PLUGINS) {
-      input.raw = plugin.endsWith(".js") ? JS_PLUGIN_CONTENT : HTML_PLUGIN_CONTENT;
+      input.raw = pluginContent(plugin);
       api = gApi.plugins().install(plugin, input);
       assertThat(api).isNotNull();
       PluginInfo info = api.get();
       String name = pluginName(plugin);
       assertThat(info.id).isEqualTo(name);
       assertThat(info.version).isEqualTo(pluginVersion(plugin));
+      assertThat(info.apiVersion).isEqualTo(pluginApiVersion(plugin));
       assertThat(info.indexUrl).isEqualTo(String.format("plugins/%s/", name));
       assertThat(info.filename).isEqualTo(plugin);
       assertThat(info.disabled).isNull();
@@ -168,12 +181,52 @@
     return plugin.substring(0, dot);
   }
 
+  private RawInput pluginJarContent(String plugin) throws IOException {
+    ByteArrayOutputStream arrayStream = new ByteArrayOutputStream();
+    Manifest manifest = new Manifest();
+    Attributes attributes = manifest.getMainAttributes();
+    attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+    if (!plugin.endsWith("-unset.jar")) {
+      attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, pluginVersion(plugin));
+      attributes.put(new Attributes.Name("Gerrit-ApiVersion"), pluginApiVersion(plugin));
+    }
+    try (JarOutputStream jarStream = new JarOutputStream(arrayStream, manifest)) {}
+    return RawInputUtil.create(arrayStream.toByteArray());
+  }
+
+  private RawInput pluginContent(String plugin) throws IOException {
+    if (plugin.endsWith(".js")) {
+      return JS_PLUGIN_CONTENT;
+    }
+    if (plugin.endsWith(".html")) {
+      return HTML_PLUGIN_CONTENT;
+    }
+    assertThat(plugin).endsWith(".jar");
+    return pluginJarContent(plugin);
+  }
+
   private String pluginVersion(String plugin) {
     String name = pluginName(plugin);
+    if (name.endsWith("empty")) {
+      return "";
+    }
+    if (name.endsWith("unset")) {
+      return null;
+    }
     int dash = name.lastIndexOf("-");
     return dash > 0 ? name.substring(dash + 1) : "";
   }
 
+  private String pluginApiVersion(String plugin) {
+    if (plugin.endsWith("normal.jar")) {
+      return "2.16.19-SNAPSHOT";
+    }
+    if (plugin.endsWith("empty.jar")) {
+      return "";
+    }
+    return null;
+  }
+
   private void assertBadRequest(ListRequest req) throws Exception {
     assertThrows(BadRequestException.class, () -> req.get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/config/BUILD b/javatests/com/google/gerrit/acceptance/server/config/BUILD
new file mode 100644
index 0000000..17802bd
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/config/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_config",
+    labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
new file mode 100644
index 0000000..d01a81d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
@@ -0,0 +1,46 @@
+// 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.server.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.server.config.GerritIsReplicaProvider;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class GerritIsReplicaIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return new Config();
+  }
+
+  @Inject GerritIsReplicaProvider isReplicaProvider;
+
+  @Test
+  public void isNotReplica() {
+    assertThat(isReplicaProvider.get()).isFalse();
+  }
+
+  @Test
+  @Sandboxed
+  public void isReplica() throws Exception {
+    restartAsSlave();
+    assertThat(isReplicaProvider.get()).isTrue();
+  }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
index 042364f..bd2bea3 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
@@ -21,7 +21,9 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <style include="gr-table-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+    .placeholder {
+      color: var(--deemphasized-text-color);
+    }
   </style>
   <gr-list-view
     filter="[[_filter]]"
@@ -36,6 +38,7 @@
         <tr class="headerRow">
           <th class="name topHeader">Plugin Name</th>
           <th class="version topHeader">Version</th>
+          <th class="apiVersion topHeader">API Version</th>
           <th class="status topHeader">Status</th>
         </tr>
         <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
@@ -53,7 +56,22 @@
                 [[item.id]]
               </template>
             </td>
-            <td class="version">[[item.version]]</td>
+            <td class="version">
+              <template is="dom-if" if="[[item.version]]">
+                [[item.version]]
+              </template>
+              <template is="dom-if" if="[[!item.version]]">
+                <span class="placeholder">--</span>
+              </template>
+            </td>
+            <td class="apiVersion">
+              <template is="dom-if" if="[[item.api_version]]">
+                [[item.api_version]]
+              </template>
+              <template is="dom-if" if="[[!item.api_version]]">
+                <span class="placeholder">--</span>
+              </template>
+            </td>
             <td class="status">[[_status(item)]]</td>
           </tr>
         </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
index a73c7cf..d60483e 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
@@ -25,13 +25,18 @@
 const pluginGenerator = () => {
   const plugin = {
     id: `test${++counter}`,
-    version: '3.0-SNAPSHOT',
     disabled: false,
   };
 
   if (counter !== 2) {
     plugin.index_url = `plugins/test${counter}/`;
   }
+  if (counter !== 3) {
+    plugin.version = `version-${counter}`;
+  }
+  if (counter !== 4) {
+    plugin.api_version = `api-version-${counter}`;
+  }
   return plugin;
 };
 
@@ -61,10 +66,11 @@
 
     test('plugin in the list is formatted correctly', done => {
       flush(() => {
-        assert.equal(element._plugins[2].id, 'test3');
-        assert.equal(element._plugins[2].index_url, 'plugins/test3/');
-        assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
-        assert.equal(element._plugins[2].disabled, false);
+        assert.equal(element._plugins[4].id, 'test5');
+        assert.equal(element._plugins[4].index_url, 'plugins/test5/');
+        assert.equal(element._plugins[4].version, 'version-5');
+        assert.equal(element._plugins[4].api_version, 'api-version-5');
+        assert.equal(element._plugins[4].disabled, false);
         done();
       });
     });
@@ -80,6 +86,25 @@
       });
     });
 
+    test('versions', done => {
+      flush(() => {
+        const versions = element.root.querySelectorAll('.version');
+        assert.equal(versions[2].innerText, 'version-2');
+        assert.equal(versions[3].innerText, '--');
+        done();
+      });
+    });
+
+    test('api versions', done => {
+      flush(() => {
+        const apiVersions = element.root.querySelectorAll(
+            '.apiVersion');
+        assert.equal(apiVersions[3].innerText, 'api-version-3');
+        assert.equal(apiVersions[4].innerText, '--');
+        done();
+      });
+    });
+
     test('_shownPlugins', () => {
       assert.equal(element._shownPlugins.length, 25);
     });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 1e002e8..953c917 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -186,7 +186,9 @@
             for (const query in LookupQueryPatterns) {
               if (LookupQueryPatterns.hasOwnProperty(query) &&
               this._query.match(LookupQueryPatterns[query])) {
-                GerritNav.navigateToChange(changes[0]);
+                // "Back"/"Forward" buttons work correctly only with
+                // opt_redirect options
+                GerritNav.navigateToChange(changes[0], null, null, null, true);
                 return;
               }
             }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
index f945476..db622d5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
@@ -192,10 +192,12 @@
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
-        assert.equal(url, change);
-        done();
-      });
+      sinon.stub(GerritNav, 'navigateToChange').callsFake(
+          (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
+            assert.equal(url, change);
+            assert.isTrue(opt_redirect);
+            done();
+          });
 
       element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
     });
@@ -204,10 +206,12 @@
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
-        assert.equal(url, change);
-        done();
-      });
+      sinon.stub(GerritNav, 'navigateToChange').callsFake(
+          (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
+            assert.equal(url, change);
+            assert.isTrue(opt_redirect);
+            done();
+          });
 
       element.params = {view: GerritNav.View.SEARCH, query: '1'};
     });
@@ -216,10 +220,12 @@
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
-        assert.equal(url, change);
-        done();
-      });
+      sinon.stub(GerritNav, 'navigateToChange').callsFake(
+          (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
+            assert.equal(url, change);
+            assert.isTrue(opt_redirect);
+            done();
+          });
 
       element.params = {view: GerritNav.View.SEARCH, query: COMMIT_HASH};
     });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index ddaf82d..1cd9f3f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -476,6 +476,9 @@
 
     this.addEventListener('diff-comments-modified',
         () => this._handleReloadCommentThreads());
+
+    this.addEventListener('open-reply-dialog',
+        e => this._openReplyDialog());
   }
 
   /** @override */
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 6bb15c5..ff8cbac 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -1726,6 +1726,20 @@
           })));
     });
 
+    test('show reply dialog on open-reply-dialog event', done => {
+      sinon.stub(element, '_openReplyDialog');
+      element.dispatchEvent(
+          new CustomEvent('open-reply-dialog', {
+            composed: true,
+            bubbles: true,
+            detail: {},
+          }));
+      flush(() => {
+        assert.isTrue(element._openReplyDialog.calledOnce);
+        done();
+      });
+    });
+
     test('reply from comment adds quote text', () => {
       const e = {detail: {message: {message: 'quote text'}}};
       element._handleMessageReply(e);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
index 2f32706..1142d8e 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -228,8 +228,10 @@
   /**
    * Setup router implementation.
    *
-   * @param {function(!string)} navigate the router-abstracted equivalent of
-   *     `window.location.href = ...`. Takes a string.
+   * @param {function(!string, boolean=)} navigate the router-abstracted equivalent of
+   *     `window.location.href = ...` or window.location.replace(...). The
+   *     string is a new location and boolean defines is it redirect or not
+   *     (true means redirect, i.e. equivalent of window.location.replace).
    * @param {function(!Object): string} generateUrl generates a URL given
    *     navigation parameters, detailed in the file header.
    * @param {function(!Object): string} generateWeblinks weblinks generator
@@ -417,10 +419,15 @@
    * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
    *     used for none.
    * @param {boolean=} opt_isEdit
+   * @param {boolean=} opt_redirect redirect to a change - if true, the current
+   *     location (i.e. page which makes redirect) is not added to a history.
+   *     I.e. back/forward buttons skip current location
+   *
    */
-  navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
+  navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
+      opt_redirect) {
     this._navigate(this.getUrlForChange(change, opt_patchNum,
-        opt_basePatchNum, opt_isEdit));
+        opt_basePatchNum, opt_isEdit), opt_redirect);
   },
 
   /**
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 72413a1..792a751 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -724,7 +724,13 @@
     }
 
     GerritNav.setup(
-        url => { page.show(url); },
+        (url, opt_redirect) => {
+          if (opt_redirect) {
+            page.redirect(url);
+          } else {
+            page.show(url);
+          }
+        },
         this._generateUrl.bind(this),
         params => this._generateWeblinks(params),
         x => x
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 7bce192..8e45b62 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -292,10 +292,11 @@
     this._unobserveNodes();
   }
 
-  showNoChangeMessage(loading, prefs, diffLength) {
+  showNoChangeMessage(loading, prefs, diffLength, diff) {
     return !loading &&
-      prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
-      diffLength === 0;
+        diff && !diff.binary &&
+        prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
+        diffLength === 0;
   }
 
   _enableSelectionObserver(loggedIn, isAttached) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
index 279f968..94a3701 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
@@ -432,7 +432,7 @@
 
           <template
             is="dom-if"
-            if="[[showNoChangeMessage(loading, prefs, _diffLength)]]"
+            if="[[showNoChangeMessage(loading, prefs, _diffLength, diff)]]"
           >
             <div class="whitespace-change-only-message">
               This file only contains whitespace changes. Modify the whitespace
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index cbe2413..86a2b65 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -979,6 +979,8 @@
   });
   const setupSampleDiff = function(params) {
     const {ignore_whitespace, content} = params;
+    // binary can't be undefined, use false if not set
+    const binary = params.binary || false;
     element = basicFixture.instantiate();
     element.prefs = {
       ignore_whitespace: ignore_whitespace || 'IGNORE_ALL',
@@ -1007,7 +1009,7 @@
         'file differ',
       ],
       content,
-      binary: false,
+      binary,
     };
     element._renderDiffTable();
     flushAsynchronousOperations();
@@ -1081,7 +1083,18 @@
       assert.isTrue(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
+      ));
+    });
+
+    test('do not show the message for binary files', () => {
+      setupSampleDiff({content: [{skip: 100}], binary: true});
+      assert.isFalse(element.showNoChangeMessage(
+          /* loading= */ false,
+          element.prefs,
+          element._diffLength,
+          element.diff
       ));
     });
 
@@ -1090,7 +1103,8 @@
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ true,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
       ));
     });
 
@@ -1109,7 +1123,8 @@
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
       ));
     });
 
@@ -1127,7 +1142,8 @@
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
       ));
     });
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
similarity index 85%
rename from polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
rename to polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
index 49ed980..70ee196 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-ranged-comment-theme">
@@ -38,4 +44,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
similarity index 94%
rename from polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
rename to polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
index 76a01de..ac015e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-syntax-theme">
@@ -118,4 +124,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
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 1d04969..47ac8a6 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
@@ -242,14 +242,15 @@
         <span class="authorName">
           [[_computeAuthorName(comment, _serverConfig)]]
         </span>
-        <span class="draftLabel">DRAFT</span>
         <gr-tooltip-content
           class="draftTooltip"
           has-tooltip=""
           title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key."
           max-width="20em"
           show-icon=""
-        ></gr-tooltip-content>
+        >
+          <span class="draftLabel">DRAFT</span>
+        </gr-tooltip-content>
       </div>
       <div class="headerMiddle">
         <span class="collapsedContent">[[comment.message]]</span>
@@ -277,12 +278,14 @@
         <span class="patchset-text"> Patchset [[patchNum]]</span>
       </template>
       <span class="separator"></span>
-      <span class="date" tabindex="0" on-click="_handleAnchorClick">
-        <gr-date-formatter
-          has-tooltip=""
-          date-str="[[comment.updated]]"
-        ></gr-date-formatter>
-      </span>
+      <template is="dom-if" if="[[comment.updated]]">
+        <span class="date" tabindex="0" on-click="_handleAnchorClick">
+          <gr-date-formatter
+            has-tooltip=""
+            date-str="[[comment.updated]]"
+          ></gr-date-formatter>
+        </span>
+      </template>
       <div class="show-hide" tabindex="0">
         <label
           class="show-hide"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index b227383..eb82daa 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -97,6 +97,7 @@
       element.side = 'PARENT';
       const stub = sinon.stub();
       element.addEventListener('comment-anchor-tap', stub);
+      flushAsynchronousOperations();
       const dateEl = element.shadowRoot
           .querySelector('.date');
       assert.ok(dateEl);
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
similarity index 99%
rename from polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
rename to polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 5ffe028..ccaf40e 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -14,8 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-icon/iron-icon.js';
-import '@polymer/iron-iconset-svg/iron-iconset-svg.js';
+import '@polymer/iron-icon/iron-icon';
+import '@polymer/iron-iconset-svg/iron-iconset-svg';
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<iron-iconset-svg name="gr-icons" size="24">
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.js b/polygerrit-ui/app/styles/dashboard-header-styles.ts
similarity index 88%
rename from polygerrit-ui/app/styles/dashboard-header-styles.js
rename to polygerrit-ui/app/styles/dashboard-header-styles.ts
index 683202e..2354f65 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.js
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="dashboard-header-styles">
@@ -55,4 +61,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.js b/polygerrit-ui/app/styles/gr-change-list-styles.ts
similarity index 95%
rename from polygerrit-ui/app/styles/gr-change-list-styles.js
rename to polygerrit-ui/app/styles/gr-change-list-styles.ts
index a7f231b..25d7f52 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-change-list-styles">
@@ -190,4 +196,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
similarity index 89%
rename from polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
rename to polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
index aabdde5..3d07d2e 100644
--- a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-shared-styles">
@@ -55,4 +61,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
similarity index 91%
rename from polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
rename to polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
index 4bfb742..57c8d78 100644
--- a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-change-view-integration-shared-styles">
@@ -70,4 +76,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-form-styles.js b/polygerrit-ui/app/styles/gr-form-styles.ts
similarity index 94%
rename from polygerrit-ui/app/styles/gr-form-styles.js
rename to polygerrit-ui/app/styles/gr-form-styles.ts
index 91763c5..3284ad5 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.js
+++ b/polygerrit-ui/app/styles/gr-form-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-form-styles">
@@ -124,4 +130,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.js b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
similarity index 91%
rename from polygerrit-ui/app/styles/gr-menu-page-styles.js
rename to polygerrit-ui/app/styles/gr-menu-page-styles.ts
index e52a895..8e8b264 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.js
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-menu-page-styles">
@@ -79,4 +85,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.js b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
similarity index 91%
rename from polygerrit-ui/app/styles/gr-page-nav-styles.js
rename to polygerrit-ui/app/styles/gr-page-nav-styles.ts
index 97f1a03..9010b2d 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.js
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-page-nav-styles">
@@ -72,4 +78,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.js b/polygerrit-ui/app/styles/gr-subpage-styles.ts
similarity index 85%
rename from polygerrit-ui/app/styles/gr-subpage-styles.js
rename to polygerrit-ui/app/styles/gr-subpage-styles.ts
index f94cc9c..640da66 100644
--- a/polygerrit-ui/app/styles/gr-subpage-styles.js
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-subpage-styles">
@@ -42,4 +48,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-table-styles.js b/polygerrit-ui/app/styles/gr-table-styles.ts
similarity index 94%
rename from polygerrit-ui/app/styles/gr-table-styles.js
rename to polygerrit-ui/app/styles/gr-table-styles.ts
index ceac675..52fdc67 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.js
+++ b/polygerrit-ui/app/styles/gr-table-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-table-styles">
@@ -116,4 +122,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.js b/polygerrit-ui/app/styles/gr-voting-styles.ts
similarity index 86%
rename from polygerrit-ui/app/styles/gr-voting-styles.js
rename to polygerrit-ui/app/styles/gr-voting-styles.ts
index 60bf623..d4e6d52 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.js
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-voting-styles">
@@ -40,4 +46,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/shared-styles.js b/polygerrit-ui/app/styles/shared-styles.ts
similarity index 95%
rename from polygerrit-ui/app/styles/shared-styles.js
rename to polygerrit-ui/app/styles/shared-styles.ts
index 3e81761..04dca9c 100644
--- a/polygerrit-ui/app/styles/shared-styles.js
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="shared-styles">
@@ -127,7 +133,7 @@
         --iron-icon-width: 20px;
       }
 
-      /* Stopgap solution until we remove hidden\$ attributes. */
+      /* Stopgap solution until we remove hidden$ attributes. */
 
       [hidden] {
         display: none !important;
@@ -196,4 +202,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/themes/app-theme.js b/polygerrit-ui/app/styles/themes/app-theme.ts
similarity index 97%
rename from polygerrit-ui/app/styles/themes/app-theme.js
rename to polygerrit-ui/app/styles/themes/app-theme.ts
index 1a8296f..f48e43f 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.js
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `
@@ -223,4 +229,4 @@
   }
 </style></custom-style>`;
 
-document.head.appendChild($_documentContainer.content);
\ No newline at end of file
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.js b/polygerrit-ui/app/styles/themes/dark-theme.ts
similarity index 100%
rename from polygerrit-ui/app/styles/themes/dark-theme.js
rename to polygerrit-ui/app/styles/themes/dark-theme.ts
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index c556c37..b2eb3dc 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -167,8 +167,18 @@
 		// with the import error, so we can catch this problem easily.
 		writer.Header().Set("Content-Type", "text/html")
 	} else if isJsFile {
-		moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
-		data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
+	  // The following code updates import statements.
+	  // 1. Keep all imports started with '.' character unchanged (i.e. all relative
+	  // imports like import ... from './a.js' or import ... from '../b/c/d.js'
+	  // 2. For other imports it adds '/node_modules/' prefix. Additionally,
+	  //   if an in imported file has .js or .mjs extension, the code keeps
+	  //   the file extension unchanged. Otherwise, it adds .js extension.
+	  //   Examples:
+	  //   '@polymer/polymer.js' -> '/node_modules/@polymer/polymer.js'
+    //   'page/page.mjs' -> '/node_modules/page.mjs'
+    //   '@polymer/iron-icon' -> '/node_modules/@polymer/iron-icon.js'
+		moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*?)(\\.(m?)js)?';$")
+		data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2.${4}js';"))
 		writer.Header().Set("Content-Type", "application/javascript")
 	} else if strings.HasSuffix(normalizedContentPath, ".css") {
 		writer.Header().Set("Content-Type", "text/css")
diff --git a/resources/com/google/gerrit/pgm/init/gerrit.sh b/resources/com/google/gerrit/pgm/init/gerrit.sh
index eba7e4b..ce858d5 100755
--- a/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -345,7 +345,8 @@
 
 test -z "$GERRIT_USER" && GERRIT_USER=`whoami`
 RUN_ARGS="-jar $GERRIT_WAR daemon -d $GERRIT_SITE"
-if test "`get_config --bool container.slave`" = "true" ; then
+if test "`get_config --bool container.slave`" = "true" || \
+    test "`get_config --bool container.replica`" = "true"; then
   RUN_ARGS="$RUN_ARGS --replica --enable-httpd --headless"
 fi
 DAEMON_OPTS=`get_config --get-all container.daemonOpt`
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index ce5d62d..d445be2 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -68,7 +68,7 @@
             "export TZ",
             "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % dir_name.upper(),
             "cd $$TMP",
-            "unzip -q $$ROOT/$<",
+            "unzip -qo $$ROOT/$<",
             "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
             "find . -exec touch '{}' ';'",
             "zip -Xqr $$ROOT/$@ .",
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 96ea42c..5934512 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -23,8 +23,8 @@
 
     maven_jar(
         name = "dropwizard-core",
-        artifact = "io.dropwizard.metrics:metrics-core:4.1.9",
-        sha1 = "dd76a62b007ffea9e6aba10f64c04173ef65f895",
+        artifact = "io.dropwizard.metrics:metrics-core:4.1.10.1",
+        sha1 = "e55d1e4de0ccec6f404dbf775c62626d8b9f79a4",
     )
 
     SSHD_VERS = "2.4.0"