Merge "Shortcut for moving cursor to visible code"
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 9dd4d0f..86221eb 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -18,9 +18,14 @@
 to run tests at Git protocol level.
 
 Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios
-implementation easy even without any Scala knowledge.
+implementation easy even without any Scala knowledge. The
+link:https://gitenterprise.me/2019/12/20/stress-your-gerrit-with-gatling/[Stress your Gerrit with Gatling]
+blog post has more introductory information.
 
-Examples of scenarios can be found in the `e2e-tests` directory.
+Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory
+should be formatted using the mainstream
+link:https://plugins.jetbrains.com/plugin/1347-scala[Scala plugin for IntelliJ,role=external,window=_blank].
+The latter is not mandatory but preferred for `sbt` and Scala IDE purposes in this project.
 
 == How to build the tests
 
@@ -36,9 +41,7 @@
 
 The following warning, if present when executing `sbt` commands, can be removed by creating the
 link:https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html#step+3%3A+Credentials[related credentials file,role=external,window=_blank]
-locally. Dummy values for `user` and `password` in that file can be used initially. Notice the
-plural form for the file name, expected by the warning (below), compared to the one from that linked
-example which is singular.
+locally. Dummy values for `user` and `password` in that file can be used initially.
 
 ----
 [warn] Credentials file ~/.sbt/sonatype_credentials does not exist
@@ -67,26 +70,36 @@
 link:https://stackoverflow.com/questions/53134212/invalid-privatekey-when-using-jsch[otherwise,role=external,window=_blank]):
 
 ----
+mkdir /tmp/ssh-keys
 ssh-keygen -m PEM -t rsa -C "test@mail.com" -f /tmp/ssh-keys/id_rsa
 ----
 
-*NOTE*: Don't forget to add the public keys for the testing user(s) to your git server.
+The public key in `/tmp/ssh-keys/id_rsa.pub` has to be added to the test user(s) `SSH Keys` in
+Gerrit. Now, the host from which the latter runs may need public key scanning to become known.
+This applies to the local user that runs the forthcoming `sbt` testing commands. An example
+assuming `localhost` follows:
+
+----
+ssh-keyscan -t rsa -p 29418 localhost > ~/.ssh/known_hosts
+----
 
 === Input file
 
-The `ReplayRecordsFromFeederScenario` is fed with the data coming from the
-`src/test/resources/data/requests.json` file. Such a file contains the commands and repo used
-during the load test. Example below:
+The `CloneUsingBothProtocols` scenario is fed with the data coming from the
+`src/test/resources/data/CloneUsingBothProtocols.json` file. Such a file contains the commands and
+repository used during the load test. That file currently looks like below. This scenario serves
+as a simple example with no actual load in it. It can be used to test or validate the local setup.
+More complex scenarios can be further developed, under the `com.google.gerrit.scenarios` package.
 
 ----
 [
   {
-    "url": "ssh://admin@localhost:29418/loadtest-repo.git",
+    "url": "ssh://admin@localhost:29418/loadtest-repo",
     "cmd": "clone"
   },
   {
-    "url": "http://localhost:8080/loadtest-repo.git",
-    "cmd": "fetch"
+    "url": "http://localhost:8080/loadtest-repo",
+    "cmd": "clone"
   }
 ]
 ----
@@ -98,6 +111,8 @@
 * `push`
 * `clone`
 
+The example above assumes that the `loadtest-repo` project exists in the Gerrit under test.
+
 == How to run tests
 
 Run all tests:
@@ -107,7 +122,7 @@
 
 Run a single test:
 ----
-sbt "gatling:testOnly com.google.gerrit.scenarios.ReplayRecordsFromFeederScenario"
+sbt "gatling:testOnly com.google.gerrit.scenarios.CloneUsingBothProtocols"
 ----
 
 Generate the last report:
@@ -122,7 +137,7 @@
 === How to run using Docker
 
 ----
-docker run -it e2e-tests -s com.google.gerrit.scenarios.ReplayRecordsFromFeederScenario
+docker run -it e2e-tests -s com.google.gerrit.scenarios.CloneUsingBothProtocols
 ----
 
 GERRIT
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 9714e18..3266cb1 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -476,9 +476,9 @@
 ----
 
 Plugins which define new Events should register them via the
-`com.google.gerrit.server.events.EventTypes.registerClass()`
-method. This will make the EventType known to the system.
-Deserializing events with the
+`com.google.gerrit.server.events.EventTypes.register()` method.
+This will make the EventType known to the system. Deserializing
+events with the
 `com.google.gerrit.server.events.EventDeserializer` class requires
 that the event be registered in EventTypes.
 
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 00e5b97..41c241f 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -65,6 +65,18 @@
 
 === HTTP
 
+==== Jetty
+
+* `http/server/jetty/threadpool/active_threads`: Active threads
+* `http/server/jetty/threadpool/idle_threads`: Idle threads
+* `http/server/jetty/threadpool/reserved_threads`: Reserved threads
+* `http/server/jetty/threadpool/max_pool_size`: Maximum thread pool size
+* `http/server/jetty/threadpool/min_pool_size`: Minimum thread pool size
+* `http/server/jetty/threadpool/pool_size`: Current thread pool size
+* `http/server/jetty/threadpool/queue_size`: Queued requests waiting for a thread
+
+==== REST API
+
 * `http/server/error_count`: Rate of REST API error responses.
 * `http/server/success_count`: Rate of REST API success responses.
 * `http/server/rest_api/count`: Rate of REST API calls by view.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 1379de7..284eb27 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7221,9 +7221,12 @@
 |`reviewer`      ||
 The link:rest-api-accounts.html#account-id[ID] of one account that
 should be added as reviewer or the link:rest-api-groups.html#group-id[
-ID] of one group for which all members should be added as reviewers. +
+ID] of one internal group for which all members should be added as reviewers. +
 If an ID identifies both an account and a group, only the account is
 added as reviewer to the change.
+External groups, such as LDAP groups, will be silently omitted from a
+link:#set-review[set-review] or
+link:rest-api-changes.html#add-reviewer[add-reviewer] call.
 |`state`         |optional|
 Add reviewer in this state. Possible reviewer states are `REVIEWER`
 and `CC`. If not given, defaults to `REVIEWER`.
diff --git a/WORKSPACE b/WORKSPACE
index 1619c45..7c4ee1b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -57,10 +57,10 @@
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "f04d2373bcaf8aa09bccb08a98a57e721306c8f6043a2a0ee610fd6853dcde3d",
+    sha256 = "b34cbe1a7514f5f5487c3bfee7340a4496713ddf4f119f7a225583d6cafd793a",
     urls = [
-        "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/0.18.6/rules_go-0.18.6.tar.gz",
-        "https://github.com/bazelbuild/rules_go/releases/download/0.18.6/rules_go-0.18.6.tar.gz",
+        "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
     ],
 )
 
diff --git a/e2e-tests/load-tests/.gitignore b/e2e-tests/load-tests/.gitignore
index 052f424..097d9fa 100644
--- a/e2e-tests/load-tests/.gitignore
+++ b/e2e-tests/load-tests/.gitignore
@@ -1,16 +1,13 @@
-.idea/
+/.idea/
+
+# mpeltonen/sbt-idea plugin
+/.idea_modules/
 
 # File-based project format
 *.iws
 
 # IntelliJ
-out/
+/out/
 
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-### Scala ###
-*.class
-*.log
-target
-project/target
+# Scala sbt
+target/
diff --git a/e2e-tests/load-tests/build.sbt b/e2e-tests/load-tests/build.sbt
index 46a3202..685db37 100644
--- a/e2e-tests/load-tests/build.sbt
+++ b/e2e-tests/load-tests/build.sbt
@@ -4,15 +4,15 @@
 
 lazy val gatlingGitExtension = RootProject(uri("git://github.com/GerritForge/gatling-git.git"))
 lazy val root = (project in file("."))
-  .settings(
-    inThisBuild(List(
-      organization := "com.google.gerrit",
-      scalaVersion := "2.12.8",
-      version := "0.1.0-SNAPSHOT"
-    )),
-    name := "gerrit",
-    libraryDependencies ++=
-      gatling ++
-        Seq("io.gatling" % "gatling-core" % "3.1.1" ) ++
-        Seq("io.gatling" % "gatling-app" % "3.1.1" )
-  ) dependsOn(gatlingGitExtension)
+    .settings(
+      inThisBuild(List(
+        organization := "com.google.gerrit",
+        scalaVersion := "2.12.8",
+        version := "0.1.0-SNAPSHOT"
+      )),
+      name := "gerrit",
+      libraryDependencies ++=
+          gatling ++
+              Seq("io.gatling" % "gatling-core" % "3.1.1") ++
+              Seq("io.gatling" % "gatling-app" % "3.1.1")
+    ) dependsOn gatlingGitExtension
diff --git a/e2e-tests/load-tests/project/build.properties b/e2e-tests/load-tests/project/build.properties
index 0cd8b07..a82bb05 100644
--- a/e2e-tests/load-tests/project/build.properties
+++ b/e2e-tests/load-tests/project/build.properties
@@ -1 +1 @@
-sbt.version=1.2.3
+sbt.version=1.3.7
diff --git a/e2e-tests/load-tests/src/test/resources/data/CloneUsingBothProtocols.json b/e2e-tests/load-tests/src/test/resources/data/CloneUsingBothProtocols.json
new file mode 100644
index 0000000..0335b2f
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/resources/data/CloneUsingBothProtocols.json
@@ -0,0 +1,10 @@
+[
+  {
+    "url": "ssh://admin@localhost:29418/loadtest-repo",
+    "cmd": "clone"
+  },
+  {
+    "url": "http://localhost:8080/loadtest-repo",
+    "cmd": "clone"
+  }
+]
diff --git a/e2e-tests/load-tests/src/test/resources/data/requests.json b/e2e-tests/load-tests/src/test/resources/data/ReplayRecordsFromFeeder.json
similarity index 100%
rename from e2e-tests/load-tests/src/test/resources/data/requests.json
rename to e2e-tests/load-tests/src/test/resources/data/ReplayRecordsFromFeeder.json
diff --git a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
new file mode 100644
index 0000000..9e2aca0
--- /dev/null
+++ b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
@@ -0,0 +1,61 @@
+// 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.scenarios
+
+import java.io._
+
+import com.github.barbasa.gatling.git.protocol.GitProtocol
+import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
+import com.github.barbasa.gatling.git.{GatlingGitConfiguration, GitRequestSession}
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FileBasedFeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import org.apache.commons.io.FileUtils
+import org.eclipse.jgit.hooks._
+
+import scala.concurrent.duration._
+
+class CloneUsingBothProtocols extends Simulation {
+
+  implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
+  implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
+
+  private val name: String = this.getClass.getSimpleName
+  private val file = s"data/$name.json"
+  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(file).circular
+  private val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
+  private val protocol: GitProtocol = GitProtocol()
+
+  private val test: ScenarioBuilder = scenario(name)
+      .feed(data)
+      .exec(request)
+
+  setUp(
+    test.inject(
+      constantUsersPerSec(1) during (2 seconds)
+    )).protocols(protocol)
+
+  after {
+    Thread.sleep(5000)
+    val path = conf.tmpBasePath
+    try {
+      FileUtils.deleteDirectory(new File(path))
+    } catch {
+      case e: IOException =>
+        System.err.println("Unable to delete temporary directory " + path)
+        e.printStackTrace()
+    }
+  }
+}
diff --git a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
index c0eab39..5a3bb99 100644
--- a/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
+++ b/e2e-tests/load-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
@@ -14,59 +14,55 @@
 
 package com.google.gerrit.scenarios
 
-import com.github.barbasa.gatling.git.protocol.GitProtocol
-import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
-import io.gatling.core.Predef._
-import io.gatling.core.structure.ScenarioBuilder
 import java.io._
 
-import com.github.barbasa.gatling.git.{
-  GatlingGitConfiguration,
-  GitRequestSession
-}
+import com.github.barbasa.gatling.git.protocol.GitProtocol
+import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
+import com.github.barbasa.gatling.git.{GatlingGitConfiguration, GitRequestSession}
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FileBasedFeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
 import org.apache.commons.io.FileUtils
-
-import scala.concurrent.duration._
 import org.eclipse.jgit.hooks._
 
-class ReplayRecordsFromFeederScenario extends Simulation {
+import scala.concurrent.duration._
 
-  val gitProtocol = GitProtocol()
-  implicit val conf = GatlingGitConfiguration()
-  implicit val postMessageHook: Option[String] = Some(
-    s"hooks/${CommitMsgHook.NAME}")
+class ReplayRecordsFromFeeder extends Simulation {
 
-  val feeder = jsonFile("data/requests.json").circular
+  implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
+  implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
 
-  val replayCallsScenario: ScenarioBuilder =
-    scenario("Git commands")
+  private val name: String = this.getClass.getSimpleName
+  private val file = s"data/$name.json"
+  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(file).circular
+  private val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
+  private val protocol: GitProtocol = GitProtocol()
+
+  private val test: ScenarioBuilder = scenario(name)
       .repeat(10000) {
-        feed(feeder)
-          .exec(new GitRequestBuilder(GitRequestSession("${cmd}", "${url}")))
+        feed(data)
+            .exec(request)
       }
 
   setUp(
-    replayCallsScenario.inject(
+    test.inject(
       nothingFor(4 seconds),
       atOnceUsers(10),
       rampUsers(10) during (5 seconds),
       constantUsersPerSec(20) during (15 seconds),
       constantUsersPerSec(20) during (15 seconds) randomized
-    ))
-    .protocols(gitProtocol)
-    .maxDuration(60 seconds)
+    )).protocols(protocol)
+      .maxDuration(60 seconds)
 
   after {
+    Thread.sleep(5000)
+    val path = conf.tmpBasePath
     try {
-      //After is often called too early. Some retries should be implemented.
-      Thread.sleep(5000)
-      FileUtils.deleteDirectory(new File(conf.tmpBasePath))
+      FileUtils.deleteDirectory(new File(path))
     } catch {
-      case e: IOException => {
-        System.err.println(
-          "Unable to delete temporary directory: " + conf.tmpBasePath)
-        e.printStackTrace
-      }
+      case e: IOException =>
+        System.err.println("Unable to delete temporary directory " + path)
+        e.printStackTrace()
     }
   }
 }
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index c768094..b36b5f9 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -16,20 +16,14 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.Optional;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
 
 /**
  * A change proposed to be merged into a branch.
@@ -101,15 +95,6 @@
  * notice of a replacement patch set is sent, or when notice of the change submission occurs.
  */
 public final class Change {
-  private static final SecureRandom rng;
-
-  static {
-    try {
-      rng = SecureRandom.getInstance("SHA1PRNG");
-    } catch (NoSuchAlgorithmException e) {
-      throw new RuntimeException("Cannot create RNG for Change-Id generator", e);
-    }
-  }
 
   public static Id id(int id) {
     return new AutoValue_Change_Id(id);
@@ -282,20 +267,6 @@
     }
   }
 
-  public static ObjectId generateChangeId() {
-    byte[] rand = new byte[Constants.OBJECT_ID_STRING_LENGTH];
-    rng.nextBytes(rand);
-    String randomString = new String(rand, UTF_8);
-
-    try (ObjectInserter f = new ObjectInserter.Formatter()) {
-      return f.idFor(Constants.OBJ_COMMIT, Constants.encode(randomString));
-    }
-  }
-
-  public static Key generateKey() {
-    return key("I" + generateChangeId().name());
-  }
-
   public static Key key(String key) {
     return new AutoValue_Change_Key(key);
   }
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 7d4f555..6c6389e5 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
-import com.google.gerrit.extensions.common.CherryPickChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.DiffInfo;
@@ -68,7 +68,7 @@
 
   ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
 
-  CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
+  ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
 
   default ChangeApi rebase() throws RestApiException {
     RebaseInput in = new RebaseInput();
@@ -204,7 +204,7 @@
     }
 
     @Override
-    public CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
+    public ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/java/com/google/gerrit/extensions/client/ListOption.java b/java/com/google/gerrit/extensions/client/ListOption.java
index e694c0e..4dea42f 100644
--- a/java/com/google/gerrit/extensions/client/ListOption.java
+++ b/java/com/google/gerrit/extensions/client/ListOption.java
@@ -16,6 +16,7 @@
 
 import java.lang.reflect.InvocationTargetException;
 import java.util.EnumSet;
+import java.util.Set;
 
 /** Enum that can be expressed as a bitset in query parameters. */
 public interface ListOption {
@@ -46,4 +47,13 @@
     }
     return r;
   }
+
+  static String toHex(Set<ListChangesOption> options) {
+    int v = 0;
+    for (ListChangesOption option : options) {
+      v |= 1 << option.getValue();
+    }
+
+    return Integer.toHexString(v);
+  }
 }
diff --git a/java/com/google/gerrit/extensions/common/CherryPickChangeInfo.java b/java/com/google/gerrit/extensions/common/CherryPickChangeInfo.java
deleted file mode 100644
index 8b02e23..0000000
--- a/java/com/google/gerrit/extensions/common/CherryPickChangeInfo.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2018 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.common;
-
-/**
- * {@link ChangeInfo} that is returned when a change was cherry-picked.
- *
- * <p>This class used to define additional fields to {@link ChangeInfo}, but those were pulled up
- * into {@link ChangeInfo}. Now this class is no longer needed, but we need to keep it for backwards
- * compatibility of the Java API.
- */
-public class CherryPickChangeInfo extends ChangeInfo {}
diff --git a/java/com/google/gerrit/extensions/systemstatus/MessageOfTheDay.java b/java/com/google/gerrit/extensions/systemstatus/MessageOfTheDay.java
deleted file mode 100644
index 180a0e6..0000000
--- a/java/com/google/gerrit/extensions/systemstatus/MessageOfTheDay.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2014 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.systemstatus;
-
-import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.TimeZone;
-
-/**
- * Supplies a message of the day when the page is first loaded.
- *
- * <pre>
- * DynamicSet.bind(binder(), MessageOfTheDay.class).to(MyMessage.class);
- * </pre>
- */
-@ExtensionPoint
-public abstract class MessageOfTheDay {
-  /**
-   * Retrieve the message of the day as an HTML fragment.
-   *
-   * @return message as an HTML fragment; null if no message is available.
-   */
-  public abstract String getHtmlMessage();
-
-  /**
-   * Unique identifier for this message.
-   *
-   * <p>Messages with the same identifier will be hidden from the user until redisplay has occurred.
-   *
-   * @return unique message identifier. This identifier should be unique within the server.
-   */
-  public abstract String getMessageId();
-
-  /**
-   * When should the message be displayed?
-   *
-   * <p>Default implementation returns {@code tomorrow at 00:00:00 GMT}.
-   *
-   * @return a future date after which the message should be redisplayed.
-   */
-  public Date getRedisplay() {
-    Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
-    cal.set(Calendar.HOUR_OF_DAY, 0);
-    cal.set(Calendar.MINUTE, 0);
-    cal.set(Calendar.SECOND, 0);
-    cal.set(Calendar.MILLISECOND, 0);
-    cal.add(Calendar.DAY_OF_MONTH, 1);
-    return cal.getTime();
-  }
-}
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index c7b65d0..ea0c148 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -237,7 +237,7 @@
     try {
       return SecureRandom.getInstance("SHA1PRNG");
     } catch (NoSuchAlgorithmException e) {
-      throw new IllegalArgumentException("No SecureRandom available for GitHub authentication", e);
+      throw new IllegalStateException("No SecureRandom available for GitHub authentication", e);
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index f9e6286..b987c68 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -222,7 +222,7 @@
     try {
       return SecureRandom.getInstance("SHA1PRNG");
     } catch (NoSuchAlgorithmException e) {
-      throw new IllegalArgumentException("No SecureRandom available for GitHub authentication", e);
+      throw new IllegalStateException("No SecureRandom available for GitHub authentication", e);
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index b1d4ac6..6a66ba3 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -18,15 +18,20 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.UsedAt.Project;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.json.OutputFormat;
 import com.google.gson.Gson;
 import com.google.template.soy.data.SanitizedContent;
@@ -34,11 +39,46 @@
 import java.net.URISyntaxException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /** Helper for generating parts of {@code index.html}. */
 public class IndexHtmlUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  static final String changeCanonicalUrl = ".*/c/(?<project>.+)/\\+/(?<changeNum>\\d+)";
+  static final String basePatchNumUrlPart = "(/(-?\\d+|edit)(\\.\\.(\\d+|edit))?)";
+  static final Pattern changeUrlPattern =
+      Pattern.compile(changeCanonicalUrl + basePatchNumUrlPart + "?" + "/?$");
+  static final Pattern diffUrlPattern =
+      Pattern.compile(changeCanonicalUrl + basePatchNumUrlPart + "(/(.+))" + "/?$");
+
+  public static String getDefaultChangeDetailHex() {
+    Set<ListChangesOption> options =
+        ImmutableSet.of(
+            ListChangesOption.ALL_COMMITS,
+            ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.CHANGE_ACTIONS,
+            ListChangesOption.DETAILED_LABELS,
+            ListChangesOption.DOWNLOAD_COMMANDS,
+            ListChangesOption.MESSAGES,
+            ListChangesOption.SUBMITTABLE,
+            ListChangesOption.WEB_LINKS,
+            ListChangesOption.SKIP_DIFFSTAT);
+
+    return ListOption.toHex(options);
+  }
+
+  public static String getDefaultDiffDetailHex() {
+    Set<ListChangesOption> options =
+        ImmutableSet.of(
+            ListChangesOption.ALL_COMMITS,
+            ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.SKIP_DIFFSTAT);
+
+    return ListOption.toHex(options);
+  }
 
   /**
    * Returns both static and dynamic parameters of {@code index.html}. The result is to be used when
@@ -50,12 +90,18 @@
       String cdnPath,
       String faviconPath,
       Map<String, String[]> urlParameterMap,
-      Function<String, SanitizedContent> urlInScriptTagOrdainer)
+      Function<String, SanitizedContent> urlInScriptTagOrdainer,
+      String requestedURL)
       throws URISyntaxException, RestApiException {
     return ImmutableMap.<String, Object>builder()
         .putAll(
             staticTemplateData(
-                canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
+                canonicalURL,
+                cdnPath,
+                faviconPath,
+                urlParameterMap,
+                urlInScriptTagOrdainer,
+                requestedURL))
         .putAll(dynamicTemplateData(gerritApi))
         .build();
   }
@@ -98,7 +144,8 @@
       String cdnPath,
       String faviconPath,
       Map<String, String[]> urlParameterMap,
-      Function<String, SanitizedContent> urlInScriptTagOrdainer)
+      Function<String, SanitizedContent> urlInScriptTagOrdainer,
+      String requestedURL)
       throws URISyntaxException {
     String canonicalPath = computeCanonicalPath(canonicalURL);
 
@@ -133,9 +180,39 @@
     if (urlParameterMap.containsKey("gf")) {
       data.put("useGoogleFonts", "true");
     }
+
+    if (urlParameterMap.containsKey("pl") && requestedURL != null) {
+      data.put("defaultChangeDetailHex", getDefaultChangeDetailHex());
+      data.put("defaultDiffDetailHex", getDefaultDiffDetailHex());
+
+      String changeRequestsPath = computeChangeRequestsPath(requestedURL, changeUrlPattern);
+      if (changeRequestsPath != null) {
+        data.put("preloadChangePage", "true");
+      } else {
+        changeRequestsPath = computeChangeRequestsPath(requestedURL, diffUrlPattern);
+        data.put("preloadDiffPage", "true");
+      }
+
+      if (changeRequestsPath != null) {
+        data.put("changeRequestsPath", changeRequestsPath);
+      }
+    }
+
     return data.build();
   }
 
+  static String computeChangeRequestsPath(String requestedURL, Pattern pattern) {
+    Matcher matcher = pattern.matcher(requestedURL);
+    if (matcher.matches()) {
+      Integer changeId = Ints.tryParse(matcher.group("changeNum"));
+      if (changeId != null) {
+        return "changes/" + Url.encode(matcher.group("project")) + "~" + changeId;
+      }
+    }
+
+    return null;
+  }
+
   private static String computeCanonicalPath(@Nullable String canonicalURL)
       throws URISyntaxException {
     if (Strings.isNullOrEmpty(canonicalURL)) {
diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
index a0b41b21..97d2270 100644
--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -70,10 +70,11 @@
     SoySauce.Renderer renderer;
     try {
       Map<String, String[]> parameterMap = req.getParameterMap();
+      String requestUrl = req.getRequestURL() == null ? null : req.getRequestURL().toString();
       // TODO(hiesel): Remove URL ordainer as parameter once Soy is consistent
       ImmutableMap<String, Object> templateData =
           IndexHtmlUtil.templateData(
-              gerritApi, canonicalUrl, cdnPath, faviconPath, parameterMap, urlOrdainer);
+              gerritApi, canonicalUrl, cdnPath, faviconPath, parameterMap, urlOrdainer, requestUrl);
       renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
     } catch (URISyntaxException | RestApiException e) {
       throw new IOException(e);
diff --git a/java/com/google/gerrit/metrics/dropwizard/BUILD b/java/com/google/gerrit/metrics/dropwizard/BUILD
index 4b3859f..3079809 100644
--- a/java/com/google/gerrit/metrics/dropwizard/BUILD
+++ b/java/com/google/gerrit/metrics/dropwizard/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/pgm/http/jetty",
         "//java/com/google/gerrit/server",
         "//lib:args4j",
         "//lib:guava",
diff --git a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
index 438f70e..3819786 100644
--- a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
@@ -28,12 +28,12 @@
         new Description("Bytes of memory retained in JGit block cache.")
             .setGauge()
             .setUnit(Units.BYTES),
-        WindowCacheStats::getOpenBytes);
+        () -> WindowCacheStats.getStats().getOpenByteCount());
 
     metrics.newCallbackMetric(
         "jgit/block_cache/open_files",
-        Integer.class,
+        Long.class,
         new Description("File handles held open by JGit block cache.").setGauge().setUnit("fds"),
-        WindowCacheStats::getOpenFiles);
+        () -> WindowCacheStats.getStats().getOpenFileCount());
   }
 }
diff --git a/java/com/google/gerrit/pgm/http/jetty/BUILD b/java/com/google/gerrit/pgm/http/jetty/BUILD
index 7b1c3eb..32247fb 100644
--- a/java/com/google/gerrit/pgm/http/jetty/BUILD
+++ b/java/com/google/gerrit/pgm/http/jetty/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/sshd",
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyMetrics.java b/java/com/google/gerrit/pgm/http/jetty/JettyMetrics.java
new file mode 100644
index 0000000..b6a2d38
--- /dev/null
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyMetrics.java
@@ -0,0 +1,91 @@
+// 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.pgm.http.jetty;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class JettyMetrics {
+
+  @Inject
+  JettyMetrics(JettyServer jetty, MetricMaker metrics) {
+    CallbackMetric0<Integer> minPoolSize =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/min_pool_size",
+            Integer.class,
+            new Description("Minimum thread pool size").setGauge());
+    CallbackMetric0<Integer> maxPoolSize =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/max_pool_size",
+            Integer.class,
+            new Description("Maximum thread pool size").setGauge());
+    CallbackMetric0<Integer> poolSize =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/pool_size",
+            Integer.class,
+            new Description("Current thread pool size").setGauge());
+    CallbackMetric0<Integer> idleThreads =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/idle_threads",
+            Integer.class,
+            new Description("Idle threads").setGauge().setUnit("threads"));
+    CallbackMetric0<Integer> busyThreads =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/active_threads",
+            Integer.class,
+            new Description("Active threads").setGauge().setUnit("threads"));
+    CallbackMetric0<Integer> reservedThreads =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/reserved_threads",
+            Integer.class,
+            new Description("Reserved threads").setGauge().setUnit("threads"));
+    CallbackMetric0<Integer> queueSize =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/queue_size",
+            Integer.class,
+            new Description("Queued requests waiting for a thread").setGauge().setUnit("requests"));
+    CallbackMetric0<Boolean> lowOnThreads =
+        metrics.newCallbackMetric(
+            "http/server/jetty/threadpool/is_low_on_threads",
+            Boolean.class,
+            new Description("Whether thread pool is low on threads").setGauge());
+    JettyServer.Metrics jettyMetrics = jetty.getMetrics();
+    metrics.newTrigger(
+        ImmutableSet.<CallbackMetric<?>>of(
+            idleThreads,
+            busyThreads,
+            reservedThreads,
+            minPoolSize,
+            maxPoolSize,
+            poolSize,
+            queueSize,
+            lowOnThreads),
+        () -> {
+          minPoolSize.set(jettyMetrics.getMinThreads());
+          maxPoolSize.set(jettyMetrics.getMaxThreads());
+          poolSize.set(jettyMetrics.getThreads());
+          idleThreads.set(jettyMetrics.getIdleThreads());
+          busyThreads.set(jettyMetrics.getBusyThreads());
+          reservedThreads.set(jettyMetrics.getReservedThreads());
+          queueSize.set(jettyMetrics.getQueueSize());
+          lowOnThreads.set(jettyMetrics.isLowOnThreads());
+        });
+  }
+}
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyModule.java b/java/com/google/gerrit/pgm/http/jetty/JettyModule.java
index c818276..32a8b6d 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyModule.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyModule.java
@@ -31,5 +31,6 @@
     bind(JettyServer.class);
     listener().to(JettyServer.Lifecycle.class);
     install(new FactoryModuleBuilder().build(HttpLogFactory.class));
+    bind(JettyMetrics.class);
   }
 }
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 096e4a1..5851fd0 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -68,7 +68,6 @@
 import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
-import org.eclipse.jetty.util.thread.ThreadPool;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -117,9 +116,49 @@
     }
   }
 
+  static class Metrics {
+    private final QueuedThreadPool threadPool;
+
+    Metrics(QueuedThreadPool threadPool) {
+      this.threadPool = threadPool;
+    }
+
+    public int getIdleThreads() {
+      return threadPool.getIdleThreads();
+    }
+
+    public int getBusyThreads() {
+      return threadPool.getBusyThreads();
+    }
+
+    public int getReservedThreads() {
+      return threadPool.getReservedThreads();
+    }
+
+    public int getMinThreads() {
+      return threadPool.getMinThreads();
+    }
+
+    public int getMaxThreads() {
+      return threadPool.getMaxThreads();
+    }
+
+    public int getThreads() {
+      return threadPool.getThreads();
+    }
+
+    public int getQueueSize() {
+      return threadPool.getQueueSize();
+    }
+
+    public boolean isLowOnThreads() {
+      return threadPool.isLowOnThreads();
+    }
+  }
+
   private final SitePaths site;
   private final Server httpd;
-
+  private final Metrics metrics;
   private boolean reverseProxy;
 
   @Inject
@@ -131,8 +170,10 @@
       HttpLogFactory httpLogFactory) {
     this.site = site;
 
-    httpd = new Server(threadPool(cfg, threadSettingsConfig));
+    QueuedThreadPool pool = threadPool(cfg, threadSettingsConfig);
+    httpd = new Server(pool);
     httpd.setConnectors(listen(httpd, cfg));
+    metrics = new Metrics(pool);
 
     Handler app = makeContext(env, cfg);
     if (cfg.getBoolean("httpd", "requestLog", !reverseProxy)) {
@@ -161,6 +202,10 @@
     httpd.setStopAtShutdown(false);
   }
 
+  Metrics getMetrics() {
+    return metrics;
+  }
+
   private Connector[] listen(Server server, Config cfg) {
     // OpenID and certain web-based single-sign-on products can cause
     // some very long headers, especially in the Referer header. We
@@ -336,7 +381,7 @@
     return site.resolve(path);
   }
 
-  private ThreadPool threadPool(Config cfg, ThreadSettingsConfig threadSettingsConfig) {
+  private QueuedThreadPool threadPool(Config cfg, ThreadSettingsConfig threadSettingsConfig) {
     int maxThreads = threadSettingsConfig.getHttpdMaxThreads();
     int minThreads = cfg.getInt("httpd", null, "minthreads", 5);
     int maxQueued = cfg.getInt("httpd", null, "maxqueued", 200);
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 1a4cbb8..48a8689 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -40,7 +40,7 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
-import com.google.gerrit.extensions.common.CherryPickChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.DescriptionInput;
@@ -294,7 +294,7 @@
   }
 
   @Override
-  public CherryPickChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
+  public ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
     try {
       return cherryPick.apply(revision, in).value();
     } catch (Exception e) {
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index d07ef0c..c53ba83 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -337,7 +337,7 @@
   @Override
   public boolean accountBelongsToRealm(Collection<ExternalId> externalIds) {
     for (ExternalId id : externalIds) {
-      if (id.toString().contains(SCHEME_GERRIT)) {
+      if (id.isScheme(SCHEME_GERRIT)) {
         return true;
       }
     }
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
index 1a50014..944bd44 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -122,7 +122,7 @@
   @Override
   public boolean accountBelongsToRealm(Collection<ExternalId> externalIds) {
     for (ExternalId id : externalIds) {
-      if (id.toString().contains(SCHEME_EXTERNAL)) {
+      if (id.isScheme(SCHEME_EXTERNAL)) {
         return true;
       }
     }
diff --git a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
index 8da2a90..1b9008d 100644
--- a/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/change/AccountPatchReviewStore.java
@@ -96,8 +96,8 @@
    *
    * @param psId patch set ID
    * @param accountId account ID of the user
-   * @return optionally, all files the have been reviewed by the given user that belong to the patch
-   *     set that is smaller or equals to the given patch set
+   * @return optionally, all files that have been reviewed by the given user that belong to the
+   *     patch set that is smaller or equals to the given patch set
    */
   Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId);
 }
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 770f36c..f0a3024 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -68,6 +68,7 @@
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.InsertChangeOp;
 import com.google.gerrit.server.update.RepoContext;
+import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -207,7 +208,7 @@
     }
     // A Change-Id is generated for the review, but not appended to the commit message.
     // This can happen if requireChangeId is false.
-    return Change.generateKey();
+    return CommitMessageUtil.generateKey();
   }
 
   public PatchSet.Id getPatchSetId() {
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 70e7967..65ca741 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -110,7 +110,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.Supplier;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -276,17 +275,13 @@
     return format(changeDataFactory.create(change));
   }
 
-  public ChangeInfo format(Project.NameKey project, Change.Id id) {
-    return format(project, id, ChangeInfo::new);
-  }
-
   public ChangeInfo format(ChangeData cd) {
-    return format(cd, Optional.empty(), true, ChangeInfo::new);
+    return format(cd, Optional.empty(), true);
   }
 
   public ChangeInfo format(RevisionResource rsrc) {
     ChangeData cd = changeDataFactory.create(rsrc.getNotes());
-    return format(cd, Optional.of(rsrc.getPatchSet().id()), true, ChangeInfo::new);
+    return format(cd, Optional.of(rsrc.getPatchSet().id()), true);
   }
 
   public List<List<ChangeInfo>> format(List<QueryResult<ChangeData>> in)
@@ -312,14 +307,13 @@
     ensureLoaded(in);
     List<ChangeInfo> out = new ArrayList<>(in.size());
     for (ChangeData cd : in) {
-      out.add(format(cd, Optional.empty(), false, ChangeInfo::new));
+      out.add(format(cd, Optional.empty(), false));
     }
     accountLoader.fill();
     return out;
   }
 
-  public <I extends ChangeInfo> I format(
-      Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier) {
+  public ChangeInfo format(Project.NameKey project, Change.Id id) {
     ChangeNotes notes;
     try {
       notes = notesFactory.createChecked(project, id);
@@ -327,9 +321,9 @@
       if (!has(CHECK)) {
         throw e;
       }
-      return checkOnly(changeDataFactory.create(project, id), changeInfoSupplier);
+      return checkOnly(changeDataFactory.create(project, id));
     }
-    return format(changeDataFactory.create(notes), Optional.empty(), true, changeInfoSupplier);
+    return format(changeDataFactory.create(notes), Optional.empty(), true);
   }
 
   private static Collection<SubmitRequirementInfo> requirementsFor(ChangeData cd) {
@@ -360,19 +354,16 @@
     return !Sets.intersection(toFind, set).isEmpty();
   }
 
-  private <I extends ChangeInfo> I format(
-      ChangeData cd,
-      Optional<PatchSet.Id> limitToPsId,
-      boolean fillAccountLoader,
-      Supplier<I> changeInfoSupplier) {
+  private ChangeInfo format(
+      ChangeData cd, Optional<PatchSet.Id> limitToPsId, boolean fillAccountLoader) {
     try {
       if (fillAccountLoader) {
         accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-        I res = toChangeInfo(cd, limitToPsId, changeInfoSupplier);
+        ChangeInfo res = toChangeInfo(cd, limitToPsId);
         accountLoader.fill();
         return res;
       }
-      return toChangeInfo(cd, limitToPsId, changeInfoSupplier);
+      return toChangeInfo(cd, limitToPsId);
     } catch (PatchListNotAvailableException
         | GpgException
         | IOException
@@ -382,7 +373,7 @@
         Throwables.throwIfInstanceOf(e, StorageException.class);
         throw new StorageException(e);
       }
-      return checkOnly(cd, changeInfoSupplier);
+      return checkOnly(cd);
     }
   }
 
@@ -431,7 +422,7 @@
         // Compute and cache if possible
         try {
           ensureLoaded(Collections.singleton(cd));
-          info = format(cd, Optional.empty(), false, ChangeInfo::new);
+          info = format(cd, Optional.empty(), false);
           changeInfos.add(info);
           if (isCacheable) {
             cache.put(Change.id(info._number), info);
@@ -445,14 +436,14 @@
     }
   }
 
-  private <I extends ChangeInfo> I checkOnly(ChangeData cd, Supplier<I> changeInfoSupplier) {
+  private ChangeInfo checkOnly(ChangeData cd) {
     ChangeNotes notes;
     try {
       notes = cd.notes();
     } catch (StorageException e) {
       String msg = "Error loading change";
       logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
-      I info = changeInfoSupplier.get();
+      ChangeInfo info = new ChangeInfo();
       info._number = cd.getId().get();
       ProblemInfo p = new ProblemInfo();
       p.message = msg;
@@ -461,7 +452,7 @@
     }
 
     ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix);
-    I info = changeInfoSupplier.get();
+    ChangeInfo info = new ChangeInfo();
     Change c = result.change();
     if (c != null) {
       info.project = c.getProject().get();
@@ -486,18 +477,16 @@
     return info;
   }
 
-  private <I extends ChangeInfo> I toChangeInfo(
-      ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
+  private ChangeInfo toChangeInfo(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
       throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
     try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
-      return toChangeInfoImpl(cd, limitToPsId, changeInfoSupplier);
+      return toChangeInfoImpl(cd, limitToPsId);
     }
   }
 
-  private <I extends ChangeInfo> I toChangeInfoImpl(
-      ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
+  private ChangeInfo toChangeInfoImpl(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
       throws PatchListNotAvailableException, GpgException, PermissionBackendException, IOException {
-    I out = changeInfoSupplier.get();
+    ChangeInfo out = new ChangeInfo();
     CurrentUser user = userProvider.get();
 
     if (has(CHECK)) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 012e4cd..3914205 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -61,7 +61,6 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
@@ -365,7 +364,6 @@
     DynamicItem.itemOf(binder(), AvatarProvider.class);
     DynamicSet.setOf(binder(), LifecycleListener.class);
     DynamicSet.setOf(binder(), TopMenu.class);
-    DynamicSet.setOf(binder(), MessageOfTheDay.class);
     DynamicMap.mapOf(binder(), DownloadScheme.class);
     DynamicMap.mapOf(binder(), DownloadCommand.class);
     DynamicMap.mapOf(binder(), CloneCommand.class);
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 818212c..df53133 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -151,7 +152,7 @@
         ObjectInserter oi = git.newObjectInserter();
         ObjectReader reader = oi.newReader();
         RevWalk revWalk = new RevWalk(reader)) {
-      ObjectId generatedChangeId = Change.generateChangeId();
+      ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
       ObjectId revCommit =
           createRevertCommit(message, notes, user, timestamp, oi, revWalk, generatedChangeId);
       return createRevertChangeFromCommit(
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index c52bbbc..2ed755a 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.git.GitUpdateFailureException;
 import com.google.gerrit.git.LockFailureException;
@@ -26,6 +25,7 @@
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.util.CommitMessageUtil;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
@@ -330,7 +330,8 @@
         }
 
         if (update.insertChangeId()) {
-          commit.setMessage(ChangeIdUtil.insertId(commit.getMessage(), Change.generateChangeId()));
+          commit.setMessage(
+              ChangeIdUtil.insertId(commit.getMessage(), CommitMessageUtil.generateChangeId()));
         }
 
         src = rw.parseCommit(inserter.insert(commit));
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
index 6a34786..55f2352 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -234,7 +234,7 @@
       byte[] bytes = hash.digest(data.getBytes(UTF_8));
       return BaseEncoding.base64Url().encode(bytes);
     } catch (NoSuchAlgorithmException e) {
-      throw new RuntimeException("No MD5 available", e);
+      throw new IllegalStateException("No MD5 available", e);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index cf16073..98c9873 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -17,8 +17,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.Comment;
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -31,44 +29,44 @@
  * @param <T> the RevisionNote for the comment type.
  */
 class RevisionNoteMap<T extends RevisionNote<? extends Comment>> {
-  // CommitID => blob ID
+  /** CommitID => blob ID */
   final NoteMap noteMap;
 
-  // CommitID => parsed data, immutable map.
+  /** CommitID => parsed data */
   final ImmutableMap<ObjectId, T> revisionNotes;
 
+  private RevisionNoteMap(NoteMap noteMap, ImmutableMap<ObjectId, T> revisionNotes) {
+    this.noteMap = noteMap;
+    this.revisionNotes = revisionNotes;
+  }
+
   static RevisionNoteMap<ChangeRevisionNote> parse(
       ChangeNoteJson noteJson, ObjectReader reader, NoteMap noteMap, Comment.Status status)
       throws ConfigInvalidException, IOException {
-    Map<ObjectId, ChangeRevisionNote> result = new HashMap<>();
+    ImmutableMap.Builder<ObjectId, ChangeRevisionNote> result = ImmutableMap.builder();
     for (Note note : noteMap) {
       ChangeRevisionNote rn = new ChangeRevisionNote(noteJson, reader, note.getData(), status);
       rn.parse();
 
       result.put(note.copy(), rn);
     }
-    return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
+    return new RevisionNoteMap<>(noteMap, result.build());
   }
 
   static RevisionNoteMap<RobotCommentsRevisionNote> parseRobotComments(
       ChangeNoteJson changeNoteJson, ObjectReader reader, NoteMap noteMap)
       throws ConfigInvalidException, IOException {
-    Map<ObjectId, RobotCommentsRevisionNote> result = new HashMap<>();
+    ImmutableMap.Builder<ObjectId, RobotCommentsRevisionNote> result = ImmutableMap.builder();
     for (Note note : noteMap) {
       RobotCommentsRevisionNote rn =
           new RobotCommentsRevisionNote(changeNoteJson, reader, note.getData());
       rn.parse();
       result.put(note.copy(), rn);
     }
-    return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
+    return new RevisionNoteMap<>(noteMap, result.build());
   }
 
   static <T extends RevisionNote<? extends Comment>> RevisionNoteMap<T> emptyMap() {
     return new RevisionNoteMap<>(NoteMap.newEmptyMap(), ImmutableMap.of());
   }
-
-  private RevisionNoteMap(NoteMap noteMap, ImmutableMap<ObjectId, T> revisionNotes) {
-    this.noteMap = noteMap;
-    this.revisionNotes = revisionNotes;
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
index 7a03005..f690acb 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toSet;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -79,12 +80,14 @@
   public Response<?> apply(IdentifiedUser user, String email)
       throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException,
           IOException, ConfigInvalidException {
-    if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
+    Account.Id accountId = user.getAccountId();
+    if (realm.accountBelongsToRealm(externalIds.byAccount(accountId))
+        && !realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow deleting emails");
     }
 
     Set<ExternalId> extIds =
-        externalIds.byAccount(user.getAccountId()).stream()
+        externalIds.byAccount(accountId).stream()
             .filter(e -> email.equals(e.email()))
             .collect(toSet());
     if (extIds.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 11bcf74..2427def 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -68,7 +68,7 @@
     try {
       rng = SecureRandom.getInstance("SHA1PRNG");
     } catch (NoSuchAlgorithmException e) {
-      throw new RuntimeException("Cannot create RNG for password generator", e);
+      throw new IllegalStateException("Cannot create RNG for password generator", e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index c7496b9..78430c3 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.common.NameInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -29,6 +30,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -50,6 +52,7 @@
   private final Provider<CurrentUser> self;
   private final Realm realm;
   private final PermissionBackend permissionBackend;
+  private final ExternalIds externalIds;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
 
   @Inject
@@ -57,10 +60,12 @@
       Provider<CurrentUser> self,
       Realm realm,
       PermissionBackend permissionBackend,
+      ExternalIds externalIds,
       @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
     this.self = self;
     this.realm = realm;
     this.permissionBackend = permissionBackend;
+    this.externalIds = externalIds;
     this.accountsUpdateProvider = accountsUpdateProvider;
   }
 
@@ -81,7 +86,9 @@
       input = new NameInput();
     }
 
-    if (!realm.allowsEdit(AccountFieldName.FULL_NAME)) {
+    Account.Id accountId = user.getAccountId();
+    if (realm.accountBelongsToRealm(externalIds.byAccount(accountId))
+        && !realm.allowsEdit(AccountFieldName.FULL_NAME)) {
       throw new MethodNotAllowedException("realm does not allow editing name");
     }
 
@@ -89,7 +96,7 @@
     AccountState accountState =
         accountsUpdateProvider
             .get()
-            .update("Set Full Name via API", user.getAccountId(), u -> u.setFullName(newName))
+            .update("Set Full Name via API", accountId, u -> u.setFullName(newName))
             .orElseThrow(() -> new ResourceNotFoundException("account not found"));
     return Strings.isNullOrEmpty(accountState.account().fullName())
         ? Response.none()
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index dc841b8..05bf1fd 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -89,15 +89,16 @@
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
-    if (!realm.allowsEdit(AccountFieldName.USER_NAME)) {
-      throw new MethodNotAllowedException("realm does not allow editing username");
-    }
-
     Account.Id accountId = rsrc.getUser().getAccountId();
     if (!externalIds.byAccount(accountId, SCHEME_USERNAME).isEmpty()) {
       throw new MethodNotAllowedException("Username cannot be changed.");
     }
 
+    if (realm.accountBelongsToRealm(externalIds.byAccount(accountId))
+        && !realm.allowsEdit(AccountFieldName.USER_NAME)) {
+      throw new MethodNotAllowedException("realm does not allow editing username");
+    }
+
     if (input == null || Strings.isNullOrEmpty(input.username)) {
       throw new BadRequestException("input required");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index 2902eca..4886a4f 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
-import com.google.gerrit.extensions.common.CherryPickChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -71,7 +71,7 @@
   }
 
   @Override
-  public Response<CherryPickChangeInfo> apply(RevisionResource rsrc, CherryPickInput input)
+  public Response<ChangeInfo> apply(RevisionResource rsrc, CherryPickInput input)
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
     input.parent = input.parent == null ? 1 : input.parent;
@@ -96,9 +96,8 @@
               rsrc.getPatchSet(),
               input,
               BranchNameKey.create(rsrc.getProject(), refName));
-      CherryPickChangeInfo changeInfo =
-          json.noOptions()
-              .format(rsrc.getProject(), cherryPickResult.changeId(), CherryPickChangeInfo::new);
+      ChangeInfo changeInfo =
+          json.noOptions().format(rsrc.getProject(), cherryPickResult.changeId());
       changeInfo.containsGitConflicts =
           !cherryPickResult.filesWithGitConflicts().isEmpty() ? true : null;
       return Response.ok(changeInfo);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index ac81b45..9354727 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -60,6 +60,7 @@
 import com.google.gerrit.server.submit.MergeIdenticalTreeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -304,7 +305,9 @@
       PersonIdent committerIdent = identifiedUser.newCommitterIdent(timestamp, serverTimeZone);
 
       final ObjectId generatedChangeId =
-          changeIdForNewChange != null ? changeIdForNewChange : Change.generateChangeId();
+          changeIdForNewChange != null
+              ? changeIdForNewChange
+              : CommitMessageUtil.generateChangeId();
       String commitMessage = ChangeIdUtil.insertId(message, generatedChangeId).trim() + '\n';
 
       CodeReviewCommit cherryPickCommit;
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index 8b5b07c..2773e29 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
-import com.google.gerrit.extensions.common.CherryPickChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -65,7 +65,7 @@
   }
 
   @Override
-  public Response<CherryPickChangeInfo> apply(CommitResource rsrc, CherryPickInput input)
+  public Response<ChangeInfo> apply(CommitResource rsrc, CherryPickInput input)
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
     String destination = Strings.nullToEmpty(input.destination).trim();
@@ -93,9 +93,7 @@
               rsrc.getCommit(),
               input,
               BranchNameKey.create(rsrc.getProjectState().getNameKey(), refName));
-      CherryPickChangeInfo changeInfo =
-          json.noOptions()
-              .format(projectName, cherryPickResult.changeId(), CherryPickChangeInfo::new);
+      ChangeInfo changeInfo = json.noOptions().format(projectName, cherryPickResult.changeId());
       changeInfo.containsGitConflicts =
           !cherryPickResult.filesWithGitConflicts().isEmpty() ? true : null;
       return Response.ok(changeInfo);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 0d3aaac..60631d2 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -76,6 +76,7 @@
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -461,7 +462,7 @@
     // Add a Change-Id line if there isn't already one
     String commitMessage = subject;
     if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
-      ObjectId id = Change.generateChangeId();
+      ObjectId id = CommitMessageUtil.generateChangeId();
       commitMessage = ChangeIdUtil.insertId(commitMessage, id);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index d67fc6a..032f08e 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -68,6 +68,7 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -302,7 +303,7 @@
     // TODO (paiking): As a future change, the revert should just be done directly on the
     // target rather than just creating a commit and then cherry-picking it.
     cherryPickInput.message = revertInput.message;
-    ObjectId generatedChangeId = Change.generateChangeId();
+    ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
     Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
     // TODO (paiking): In the the future, the timestamp should be the same for all the revert
     // changes.
@@ -526,14 +527,6 @@
             potentialCommitToReturn.getName(), changeNotes.getChange().getChangeId()));
   }
 
-  /**
-   * @param submissionId the submission id of the change.
-   * @return True if the submission has more than one change, false otherwise.
-   */
-  private Boolean isChangePartOfSubmission(String submissionId) {
-    return (queryProvider.get().setLimit(2).bySubmissionId(submissionId).size() > 1);
-  }
-
   private class CreateCherryPickOp implements BatchUpdateOp {
     private final ObjectId revCommitId;
     private final String topic;
@@ -582,9 +575,7 @@
               .getCurrentPatchSet()
               .commitId()
               .getName();
-      results.add(
-          json.noOptions()
-              .format(change.getProject(), cherryPickResult.changeId(), ChangeInfo::new));
+      results.add(json.noOptions().format(change.getProject(), cherryPickResult.changeId()));
       return true;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index 1df485f..d0a1498 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -126,8 +126,8 @@
     long mTotal = r.totalMemory();
     long mInuse = mTotal - mFree;
 
-    int jgitOpen = WindowCacheStats.getOpenFiles();
-    long jgitBytes = WindowCacheStats.getOpenBytes();
+    long jgitOpen = WindowCacheStats.getStats().getOpenFileCount();
+    long jgitBytes = WindowCacheStats.getStats().getOpenByteCount();
 
     MemSummaryInfo memSummaryInfo = new MemSummaryInfo();
     memSummaryInfo.total = bytes(mTotal);
@@ -135,7 +135,7 @@
     memSummaryInfo.free = bytes(mFree);
     memSummaryInfo.buffers = bytes(jgitBytes);
     memSummaryInfo.max = bytes(mMax);
-    memSummaryInfo.openFiles = toInteger(jgitOpen);
+    memSummaryInfo.openFiles = Long.valueOf(jgitOpen);
     return memSummaryInfo;
   }
 
@@ -258,7 +258,7 @@
     public String free;
     public String buffers;
     public String max;
-    public Integer openFiles;
+    public Long openFiles;
   }
 
   public static class ThreadSummaryInfo {
diff --git a/java/com/google/gerrit/server/util/CommitMessageUtil.java b/java/com/google/gerrit/server/util/CommitMessageUtil.java
index e984f46..1c8ce0c 100644
--- a/java/com/google/gerrit/server/util/CommitMessageUtil.java
+++ b/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -14,12 +14,29 @@
 
 package com.google.gerrit.server.util;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 
 /** Utility functions to manipulate commit messages. */
 public class CommitMessageUtil {
+  private static final SecureRandom rng;
+
+  static {
+    try {
+      rng = SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalStateException("Cannot create RNG for Change-Id generator", e);
+    }
+  }
 
   private CommitMessageUtil() {}
 
@@ -42,4 +59,18 @@
     trimmed = trimmed + "\n";
     return trimmed;
   }
+
+  public static ObjectId generateChangeId() {
+    byte[] rand = new byte[Constants.OBJECT_ID_STRING_LENGTH];
+    rng.nextBytes(rand);
+    String randomString = new String(rand, UTF_8);
+
+    try (ObjectInserter f = new ObjectInserter.Formatter()) {
+      return f.idFor(Constants.OBJ_COMMIT, Constants.encode(randomString));
+    }
+  }
+
+  public static Change.Key generateKey() {
+    return Change.key("I" + generateChangeId().name());
+  }
 }
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 7e0439f..1d756de 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -297,6 +297,10 @@
     return i != null ? i : 0;
   }
 
+  private static long nullToZero(Long i) {
+    return i != null ? i : 0;
+  }
+
   private void sshSummary() {
     IoAcceptor acceptor = daemon.getIoAcceptor();
     if (acceptor == null) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index ae5066b..0eefe02 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4637,7 +4637,6 @@
             ListChangesOption.ALL_COMMITS,
             ListChangesOption.ALL_REVISIONS,
             ListChangesOption.CHANGE_ACTIONS,
-            ListChangesOption.CURRENT_ACTIONS,
             ListChangesOption.DETAILED_LABELS,
             ListChangesOption.DOWNLOAD_COMMANDS,
             ListChangesOption.MESSAGES,
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 36b7265..70fcfc4 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -73,7 +73,6 @@
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.common.CherryPickChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -324,7 +323,7 @@
     ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
 
     assertThat(orig.get().messages).hasSize(1);
-    CherryPickChangeInfo changeInfo = orig.revision(r.getCommit().name()).cherryPickAsInfo(in);
+    ChangeInfo changeInfo = orig.revision(r.getCommit().name()).cherryPickAsInfo(in);
     assertThat(changeInfo.containsGitConflicts).isNull();
     assertThat(changeInfo.workInProgress).isNull();
     ChangeApi cherry = gApi.changes().id(changeInfo._number);
@@ -576,8 +575,7 @@
 
     // Cherry-pick with auto merge should succeed.
     in.allowConflicts = true;
-    CherryPickChangeInfo cherryPickChange =
-        changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in);
+    ChangeInfo cherryPickChange = changeApi.revision(r.getCommit().name()).cherryPickAsInfo(in);
     assertThat(cherryPickChange.containsGitConflicts).isTrue();
     assertThat(cherryPickChange.workInProgress).isTrue();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index cdffa3c..9624ec2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -57,7 +57,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -645,14 +644,7 @@
     //  |
     // C0 -- Master
     //
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .getProject()
-          .setBooleanConfig(
-              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
-              InheritableBoolean.TRUE);
-      u.save();
-    }
+    enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push1 =
         pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index d9438f0..cad4796 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -15,6 +15,9 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.raw.IndexHtmlUtil.changeUrlPattern;
+import static com.google.gerrit.httpd.raw.IndexHtmlUtil.computeChangeRequestsPath;
+import static com.google.gerrit.httpd.raw.IndexHtmlUtil.diffUrlPattern;
 import static com.google.gerrit.httpd.raw.IndexHtmlUtil.staticTemplateData;
 
 import com.google.template.soy.data.SanitizedContent;
@@ -29,7 +32,12 @@
   public void noPathAndNoCDN() throws Exception {
     assertThat(
             staticTemplateData(
-                "http://example.com/", null, null, new HashMap<>(), IndexHtmlUtilTest::ordain))
+                "http://example.com/",
+                null,
+                null,
+                new HashMap<>(),
+                IndexHtmlUtilTest::ordain,
+                null))
         .containsExactly("canonicalPath", "", "staticResourcePath", ordain(""));
   }
 
@@ -41,7 +49,8 @@
                 null,
                 null,
                 new HashMap<>(),
-                IndexHtmlUtilTest::ordain))
+                IndexHtmlUtilTest::ordain,
+                null))
         .containsExactly("canonicalPath", "/gerrit", "staticResourcePath", ordain("/gerrit"));
   }
 
@@ -53,7 +62,8 @@
                 "http://my-cdn.com/foo/bar/",
                 null,
                 new HashMap<>(),
-                IndexHtmlUtilTest::ordain))
+                IndexHtmlUtilTest::ordain,
+                null))
         .containsExactly(
             "canonicalPath", "", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
   }
@@ -66,7 +76,8 @@
                 "http://my-cdn.com/foo/bar/",
                 null,
                 new HashMap<>(),
-                IndexHtmlUtilTest::ordain))
+                IndexHtmlUtilTest::ordain,
+                null))
         .containsExactly(
             "canonicalPath", "/gerrit", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
   }
@@ -77,11 +88,51 @@
     urlParms.put("gf", new String[0]);
     assertThat(
             staticTemplateData(
-                "http://example.com/", null, null, urlParms, IndexHtmlUtilTest::ordain))
+                "http://example.com/", null, null, urlParms, IndexHtmlUtilTest::ordain, null))
         .containsExactly(
             "canonicalPath", "", "staticResourcePath", ordain(""), "useGoogleFonts", "true");
   }
 
+  @Test
+  public void usePreloadRest() throws Exception {
+    Map<String, String[]> urlParms = new HashMap<>();
+    urlParms.put("pl", new String[0]);
+    assertThat(
+            staticTemplateData(
+                "http://example.com/",
+                null,
+                null,
+                urlParms,
+                IndexHtmlUtilTest::ordain,
+                "/c/project/+/123"))
+        .containsExactly(
+            "canonicalPath", "",
+            "staticResourcePath", ordain(""),
+            "defaultChangeDetailHex", "916314",
+            "defaultDiffDetailHex", "800014",
+            "preloadChangePage", "true",
+            "changeRequestsPath", "changes/project~123");
+  }
+
+  @Test
+  public void computeChangePath() throws Exception {
+    assertThat(computeChangeRequestsPath("/c/project/+/123", changeUrlPattern))
+        .isEqualTo("changes/project~123");
+
+    assertThat(computeChangeRequestsPath("/c/project/+/124/2", changeUrlPattern))
+        .isEqualTo("changes/project~124");
+
+    assertThat(computeChangeRequestsPath("/c/project/src/+/23", changeUrlPattern))
+        .isEqualTo("changes/project%2Fsrc~23");
+
+    assertThat(computeChangeRequestsPath("/q/project/src/+/23", changeUrlPattern)).isEqualTo(null);
+
+    assertThat(computeChangeRequestsPath("/c/Scripts/+/232/1//COMMIT_MSG", changeUrlPattern))
+        .isEqualTo(null);
+    assertThat(computeChangeRequestsPath("/c/Scripts/+/232/1//COMMIT_MSG", diffUrlPattern))
+        .isEqualTo("changes/Scripts~232");
+  }
+
   private static SanitizedContent ordain(String s) {
     return UnsafeSanitizedContentOrdainer.ordainAsSafe(
         s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
diff --git a/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java b/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
new file mode 100644
index 0000000..ba40d8c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
@@ -0,0 +1,94 @@
+// 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.auth.ldap;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_EXIST_CACHE;
+import static com.google.gerrit.server.auth.ldap.LdapModule.PARENT_GROUPS_CACHE;
+import static com.google.gerrit.server.auth.ldap.LdapModule.USERNAME_CACHE;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.TypeLiteral;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class LdapRealmTest {
+  @Inject private LdapRealm ldapRealm = null;
+
+  @Before
+  public void setUpInjector() throws Exception {
+    Injector injector =
+        Guice.createInjector(
+            new InMemoryModule(),
+            new CacheModule() {
+              @Override
+              protected void configure() {
+                cache(GROUP_CACHE, String.class, new TypeLiteral<Set<AccountGroup.UUID>>() {})
+                    .loader(LdapRealm.MemberLoader.class);
+                cache(USERNAME_CACHE, String.class, new TypeLiteral<Optional<Account.Id>>() {})
+                    .loader(LdapRealm.UserLoader.class);
+                cache(GROUP_EXIST_CACHE, String.class, new TypeLiteral<Boolean>() {})
+                    .loader(LdapRealm.ExistenceLoader.class);
+                cache(
+                    PARENT_GROUPS_CACHE, String.class, new TypeLiteral<ImmutableSet<String>>() {});
+              }
+            });
+    injector.injectMembers(this);
+  }
+
+  private ExternalId id(String scheme, String id) {
+    return ExternalId.create(scheme, id, Account.id(1000));
+  }
+
+  private boolean accountBelongsToRealm(ExternalId... ids) {
+    return ldapRealm.accountBelongsToRealm(Arrays.asList(ids));
+  }
+
+  private boolean accountBelongsToRealm(String scheme, String id) {
+    return accountBelongsToRealm(id(scheme, id));
+  }
+
+  @Test
+  public void accountBelongsToRealm() throws Exception {
+    assertThat(accountBelongsToRealm(SCHEME_GERRIT, "test")).isTrue();
+    assertThat(accountBelongsToRealm(id(SCHEME_USERNAME, "test"), id(SCHEME_GERRIT, "test")))
+        .isTrue();
+    assertThat(accountBelongsToRealm(id(SCHEME_GERRIT, "test"), id(SCHEME_USERNAME, "test")))
+        .isTrue();
+
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "foo@bar.com")).isFalse();
+
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "gerrit")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "xxgerritxx")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "gerrit.foo@bar.com")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "bar.gerrit@bar.com")).isFalse();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
new file mode 100644
index 0000000..dc62a61
--- /dev/null
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
@@ -0,0 +1,69 @@
+// 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.auth.oauth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class OAuthRealmTest {
+  @Inject private OAuthRealm oauthRealm = null;
+
+  @Before
+  public void setUpInjector() throws Exception {
+    Injector injector = Guice.createInjector(new InMemoryModule());
+    injector.injectMembers(this);
+  }
+
+  private ExternalId id(String scheme, String id) {
+    return ExternalId.create(scheme, id, Account.id(1000));
+  }
+
+  private boolean accountBelongsToRealm(ExternalId... ids) {
+    return oauthRealm.accountBelongsToRealm(Arrays.asList(ids));
+  }
+
+  private boolean accountBelongsToRealm(String scheme, String id) {
+    return accountBelongsToRealm(id(scheme, id));
+  }
+
+  @Test
+  public void accountBelongsToRealm() throws Exception {
+    assertThat(accountBelongsToRealm(SCHEME_EXTERNAL, "test")).isTrue();
+    assertThat(accountBelongsToRealm(id(SCHEME_USERNAME, "test"), id(SCHEME_EXTERNAL, "test")))
+        .isTrue();
+    assertThat(accountBelongsToRealm(id(SCHEME_EXTERNAL, "test"), id(SCHEME_USERNAME, "test")))
+        .isTrue();
+
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "foo@bar.com")).isFalse();
+
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "external")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "xxexternalxx")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "external.foo@bar.com")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "bar.external@bar.com")).isFalse();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index bfcb9e4..3a8d7e4 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -176,13 +176,14 @@
 
   private static Ref createRefWithNonEmptyTreeCommit(Repository usersRepo, int changeId, int userId)
       throws IOException {
-    RevWalk rw = new RevWalk(usersRepo);
-    ObjectId fileObj = createBlob(usersRepo, String.format("file %d content", changeId));
-    ObjectId treeObj =
-        createTree(usersRepo, rw.lookupBlob(fileObj), String.format("file%d.txt", changeId));
-    ObjectId commitObj = createCommit(usersRepo, treeObj, null);
-    Ref refObj = createRef(usersRepo, commitObj, getRefName(changeId, userId));
-    return refObj;
+    try (RevWalk rw = new RevWalk(usersRepo)) {
+      ObjectId fileObj = createBlob(usersRepo, String.format("file %d content", changeId));
+      ObjectId treeObj =
+          createTree(usersRepo, rw.lookupBlob(fileObj), String.format("file%d.txt", changeId));
+      ObjectId commitObj = createCommit(usersRepo, treeObj, null);
+      Ref refObj = createRef(usersRepo, commitObj, getRefName(changeId, userId));
+      return refObj;
+    }
   }
 
   private static Ref createRefWithEmptyTreeCommit(Repository usersRepo, int changeId, int userId)
diff --git a/modules/jgit b/modules/jgit
index a7e454b..730b7a5 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit a7e454bc51d359c2d46b19fd559f770cad8fd7d4
+Subproject commit 730b7a5ebf149e8df085a19ce4d4eddcea5958dd
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
index 5a9213b..d1980a5 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.html
@@ -38,7 +38,10 @@
       input {
         width: 20em;
       }
-      .hideItem {
+      /* Add css selector with #id to increase priority
+      (otherwise ".gr-form-styles section" rule wins) */
+      .hideItem,
+      #itemAnnotationSection.hideItem {
         display: none;
       }
     </style>
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 e3a3b31..5035c92 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
@@ -18,7 +18,7 @@
   'use strict';
 
   const LookupQueryPatterns = {
-    CHANGE_ID: /^\s*i?[0-9a-f]{8,40}\s*$/i,
+    CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i,
     CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
   };
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 9073342..0ab165a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -207,22 +207,35 @@
               max-reviewers-displayed="3"></gr-reviewer-list>
         </span>
       </section>
-      <section>
-        <span class="title">Repo</span>
-        <span class="value">
-          <a href$="[[_computeProjectURL(change.project)]]">
-            <gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text>
-          </a>
-        </span>
-      </section>
-      <section>
-        <span class="title">Branch</span>
-        <span class="value">
-          <a href$="[[_computeBranchURL(change.project, change.branch)]]">
-            <gr-limited-text limit="40" text="[[change.branch]]"></gr-limited-text>
-          </a>
-        </span>
-      </section>
+      <template is="dom-if"
+                if="[[_computeShowRepoBranchTogether(change.project, change.branch)]]">
+        <section>
+          <span class="title">Repo Branch</span>
+          <span class="value">
+            <a href$="[[_computeProjectUrl(change.project)]]">[[change.project]]</a>
+            <a href$="[[_computeBranchUrl(change.project, change.branch)]]">[[change.branch]]</a>
+          </span>
+        </section>
+      </template>
+      <template is="dom-if"
+                if="[[!_computeShowRepoBranchTogether(change.project, change.branch)]]">
+        <section>
+          <span class="title">Repo</span>
+          <span class="value">
+            <a href$="[[_computeProjectUrl(change.project)]]">
+              <gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text>
+            </a>
+          </span>
+        </section>
+        <section>
+          <span class="title">Branch</span>
+          <span class="value">
+            <a href$="[[_computeBranchUrl(change.project, change.branch)]]">
+              <gr-limited-text limit="40" text="[[change.branch]]"></gr-limited-text>
+            </a>
+          </span>
+        </section>
+      </template>
       <section>
         <span class="title">[[_computeParentsLabel(_currentParents)]]</span>
         <span class="value">
@@ -252,7 +265,7 @@
             <gr-linked-chip
                 text="[[change.topic]]"
                 limit="40"
-                href="[[_computeTopicURL(change.topic)]]"
+                href="[[_computeTopicUrl(change.topic)]]"
                 removable="[[!_topicReadOnly]]"
                 on-remove="_handleTopicRemoved"></gr-linked-chip>
           </template>
@@ -274,7 +287,7 @@
         <section>
           <span class="title">Cherry pick of</span>
           <span class="value">
-            <a href$="[[_computeCherryPickOfURL(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]">
+            <a href$="[[_computeCherryPickOfUrl(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]">
               <gr-limited-text
                   text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]"
                   limit="40">
@@ -294,7 +307,7 @@
             <gr-linked-chip
                 class="hashtagChip"
                 text="[[item]]"
-                href="[[_computeHashtagURL(item)]]"
+                href="[[_computeHashtagUrl(item)]]"
                 removable="[[!_hashtagReadOnly]]"
                 on-remove="_handleHashtagRemoved">
             </gr-linked-chip>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 3237d72..0f31f91 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -351,26 +351,30 @@
       return [msg + ':'].concat(key.problems).join('\n');
     }
 
-    _computeProjectURL(project) {
+    _computeShowRepoBranchTogether(repo, branch) {
+      return !!repo && !!branch && repo.length + branch.length < 40;
+    }
+
+    _computeProjectUrl(project) {
       return Gerrit.Nav.getUrlForProjectChanges(project);
     }
 
-    _computeBranchURL(project, branch) {
+    _computeBranchUrl(project, branch) {
       if (!this.change || !this.change.status) return '';
       return Gerrit.Nav.getUrlForBranch(branch, project,
           this.change.status == this.ChangeStatus.NEW ? 'open' :
             this.change.status.toLowerCase());
     }
 
-    _computeCherryPickOfURL(change, patchset, project) {
+    _computeCherryPickOfUrl(change, patchset, project) {
       return Gerrit.Nav.getUrlForChangeById(change, project, patchset);
     }
 
-    _computeTopicURL(topic) {
+    _computeTopicUrl(topic) {
       return Gerrit.Nav.getUrlForTopic(topic);
     }
 
-    _computeHashtagURL(hashtag) {
+    _computeHashtagUrl(hashtag) {
       return Gerrit.Nav.getUrlForHashtag(hashtag);
     }
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 0e86746..11e2217 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -353,6 +353,9 @@
           z-index: var(--reply-overlay-z-index);
         }
       }
+      .patch-set-dropdown {
+        margin: var(--spacing-m) 0 0 var(--spacing-m);
+      }
     </style>
     <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
     <div
@@ -640,6 +643,12 @@
       </template>
       <template is="dom-if" if="[[_isSelectedView(_currentView,
         _commentTabs.ROBOT_COMMENTS)]]">
+        <gr-dropdown-list
+          class="patch-set-dropdown"
+          items="[[_robotCommentsPatchSetDropdownItems]]"
+          on-value-change="_handleRobotCommentPatchSetChanged"
+          value="[[_currentRobotCommentsPatchSet]]">
+        </gr-dropdown-list>
         <gr-thread-list
             threads="[[_robotCommentThreads]]"
             change="[[_change]]"
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 71f8d6f..d2f8c87 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
@@ -141,7 +141,7 @@
         _robotCommentThreads: {
           type: Array,
           computed: '_computeRobotCommentThreads(_commentThreads,'
-            + ' _selectedRevision)',
+            + ' _currentRobotCommentsPatchSet)',
         },
         /** @type {?} */
         _serverConfig: {
@@ -181,6 +181,7 @@
           type: Object,
           computed: '_computeCurrentRevision(_change.current_revision, ' +
             '_change.revisions)',
+          observer: '_handleCurrentRevisionUpdate',
         },
         _files: Object,
         _changeNum: String,
@@ -318,6 +319,14 @@
         _selectedFilesTabPluginEndpoint: {
           type: String,
         },
+        _robotCommentsPatchSetDropdownItems: {
+          type: Array,
+          value() { return []; },
+          computed: '_computeRobotCommentsPatchSetDropdownItems(_change)',
+        },
+        _currentRobotCommentsPatchSet: {
+          type: Number,
+        },
       };
     }
 
@@ -476,6 +485,10 @@
 
     _handleCommentTabChange() {
       this._currentView = this.$.commentTabs.selected;
+      const type = Object.keys(CommentTabs).find(key => CommentTabs[key] ===
+          this._currentView);
+      this.$.reporting.reportInteraction('comment-tab-changed', {tabName:
+          type});
     }
 
     _isSelectedView(currentView, view) {
@@ -580,12 +593,35 @@
       return false;
     }
 
-    _computeRobotCommentThreads(commentThreads, selectedRevision) {
-      if (!commentThreads || !selectedRevision) return [];
+    _computeRobotCommentsPatchSetDropdownItems(change) {
+      if (!change.revisions) return [];
+      return Object.values(change.revisions)
+          .filter(patch => patch._number !== 'edit')
+          .map(patch => {
+            return {
+              text: 'Patchset ' + patch._number,
+              value: patch._number,
+            };
+          })
+          .sort((a, b) => b.value - a.value);
+    }
+
+    _handleCurrentRevisionUpdate(currentRevision) {
+      this._currentRobotCommentsPatchSet = currentRevision._number;
+    }
+
+    _handleRobotCommentPatchSetChanged(e) {
+      const patchSet = parseInt(e.detail.value);
+      if (patchSet === this._currentRobotCommentsPatchSet) return;
+      this._currentRobotCommentsPatchSet = patchSet;
+    }
+
+    _computeRobotCommentThreads(commentThreads, currentRobotCommentsPatchSet) {
+      if (!commentThreads || !currentRobotCommentsPatchSet) return [];
       return commentThreads.filter(thread => {
         const comments = thread.comments || [];
         return comments.length && comments[0].robot_id && (comments[0].patch_set
-          === selectedRevision._number);
+          === currentRobotCommentsPatchSet);
       });
     }
 
@@ -795,12 +831,9 @@
     }
 
     _paramsChanged(value) {
-      // Change the content of the comment tabs back to messages list, but
-      // do not yet change the tab itself. The animation of tab switching will
-      // get messed up if changed here, because it requires the tabs to be on
-      // the streen, and they are hidden shortly after this. The tab switching
-      // animation will happen in post render tasks.
+      // TODO(dhruvsri): Fix underlining of comment tab when page loads
       this._currentView = CommentTabs.CHANGE_LOG;
+      this._setPrimaryTab();
       if (value.view !== Gerrit.Nav.View.CHANGE) {
         this._initialLoadComplete = false;
         return;
@@ -835,7 +868,6 @@
         }
         this._reloadPatchNumDependentResources().then(() => {
           this._sendShowChangeEvent();
-          this._setPrimaryTab();
         });
         return;
       }
@@ -870,8 +902,6 @@
 
       this._sendShowChangeEvent();
 
-      this._setPrimaryTab();
-
       this.async(() => {
         if (this.viewState.scrollTop) {
           document.documentElement.scrollTop =
@@ -1262,7 +1292,8 @@
      * @param {string=} opt_section
      */
     _openReplyDialog(opt_section) {
-      this.$.replyOverlay.open().then(() => {
+      this.$.replyOverlay.open().finally(() => {
+        // the following code should be executed no matter open succeed or not
         this._resetReplyOverlayFocusStops();
         this.$.replyDialog.open(opt_section);
         Polymer.dom.flush();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index fe02876..f0ab218 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -645,12 +645,9 @@
           assert.equal(element.$.commentTabs.selected, 1);
           assert.equal(element._currentView, CommentTabs.COMMENT_THREADS);
 
-          // When the change is partially reloaded (ex: Shift+R), the content
-          // is swapped out before the tab, so messages list will display even
-          // though the tab for comment threads is still temporarily selected.
           element._paramsChanged(element.params);
           assert.equal(element.$.commentTabs.selected,
-              CommentTabs.COMMENT_THREADS);
+              CommentTabs.CHANGE_LOG);
           assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
           flush(() => {
             // Correct tab is selected after the patchset is changed
@@ -665,8 +662,18 @@
 
     suite('Findings comment tab', () => {
       setup(done => {
+        element._change = {
+          change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+          revisions: {
+            rev2: {_number: 2, commit: {parents: []}},
+            rev1: {_number: 1, commit: {parents: []}},
+            rev13: {_number: 13, commit: {parents: []}},
+            rev3: {_number: 3, commit: {parents: []}},
+            rev4: {_number: 4, commit: {parents: []}},
+          },
+          current_revision: 'rev4',
+        };
         element._commentThreads = THREADS;
-        element._selectedRevision = {_number: 4};
         flush(() => {
           done();
         });
@@ -679,7 +686,7 @@
             'rc2');
       });
       test('changing patchsets resets robot comments', done => {
-        element._selectedRevision = {_number: 3};
+        element.set('_change.current_revision', 'rev3');
         flush(() => {
           assert.equal(element._robotCommentThreads.length, 0);
           done();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
index 1a1276d..a845ed4 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.html
@@ -16,12 +16,15 @@
 -->
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
 
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
 <link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
+
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-confirm-submit-dialog">
@@ -33,6 +36,11 @@
       p {
         margin-bottom: var(--spacing-l);
       }
+      .warningBeforeSubmit {
+        color: var(--error-text-color);
+        vertical-align: top;
+        margin-right: var(--spacing-s);
+      }
       @media screen and (max-width: 50em) {
         #dialog {
           min-width: inherit;
@@ -53,7 +61,17 @@
         <gr-endpoint-decorator name="confirm-submit-change">
           <p>Ready to submit &ldquo;<strong>[[change.subject]]</strong>&rdquo;?</p>
           <template is="dom-if" if="[[change.is_private]]">
-            <p><strong>Heads Up!</strong> Submitting this private change will also make it public.</p>
+            <p>
+              <iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon>
+              <strong>Heads Up!</strong>
+              Submitting this private change will also make it public.
+            </p>
+          </template>
+          <template is="dom-if" if="[[change.unresolved_comment_count]]">
+            <p>
+              <iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon>
+              [[_computeUnresolvedCommentsWarning(change)]]
+            </p>
           </template>
           <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
           <gr-endpoint-param name="action" value="[[action]]"></gr-endpoint-param>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index ea8bdb5..aa26681 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -57,6 +57,12 @@
       this.$.dialog.resetFocus();
     }
 
+    _computeUnresolvedCommentsWarning(change) {
+      const unresolvedCount = change.unresolved_comment_count;
+      const plural = unresolvedCount > 1 ? 's' : '';
+      return `Heads Up! ${unresolvedCount} unresolved comment${plural}.`;
+    }
+
     _handleConfirmTap(e) {
       e.preventDefault();
       e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
index 515147f..2ae58af 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
@@ -61,5 +61,15 @@
       assert.notEqual(message.textContent.length, 0);
       assert.notEqual(message.textContent.indexOf('my-subject'), -1);
     });
+
+    test('_computeUnresolvedCommentsWarning', () => {
+      const change = {unresolved_comment_count: 1};
+      assert.equal(element._computeUnresolvedCommentsWarning(change),
+          'Heads Up! 1 unresolved comment.');
+
+      const change2 = {unresolved_comment_count: 2};
+      assert.equal(element._computeUnresolvedCommentsWarning(change2),
+          'Heads Up! 2 unresolved comments.');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index 46fd227..134dd5c 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -27,42 +27,37 @@
       /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
     </style>
     <style include="shared-styles">
-      .labelContainer {
-        align-items: center;
-        display: flex;
-        margin-bottom: var(--spacing-m);
+      .labelNameCell,
+      .buttonsCell,
+      .selectedValueCell {
+        padding: var(--spacing-s) var(--spacing-m);
+        display: table-cell;
       }
-      .labelName {
-        display: inline-block;
-        flex: 0 0 auto;
-        margin-right: var(--spacing-m);
-        min-width: 7em;
-        text-align: left;
-        width: 20%;
+      /* We want the :hover highlight to extend to the border of the dialog. */
+      .labelNameCell {
+        padding-left: var(--spacing-xl);
+      }
+      .selectedValueCell {
+        padding-right: var(--spacing-xl);
+      }
+      /* This is a trick to let the selectedValueCell take the remaining width. */
+      .labelNameCell,
+      .buttonsCell {
+        white-space: nowrap;
+      }
+      .selectedValueCell {
+        width: 75%;
       }
       .labelMessage {
         color: var(--deemphasized-text-color);
       }
-      .placeholder::before {
-        content: ' ';
-      }
-      .selectedValueText {
-        color: var(--deemphasized-text-color);
-        font-style: italic;
-        margin: 0 var(--spacing-m);
-      }
-      .selectedValueText.hidden {
-        display: none;
-      }
-      .buttonWrapper {
-        flex: none;
-      }
       gr-button {
-        min-width: 40px;
+        min-width: 42px;
+        box-sizing: border-box;
         --gr-button: {
           background-color: var(--button-background-color, var(--table-header-background-color));
           color: var(--primary-text-color);
-          padding: var(--spacing-xs) var(--spacing-m);
+          padding: 0 var(--spacing-m);
           @apply --vote-chip-styles;
         }
       }
@@ -83,63 +78,62 @@
       }
       .placeholder {
         display: inline-block;
-        width: 40px;
+        width: 42px;
+        height: 1px;
+      }
+      .placeholder::before {
+        content: ' ';
+      }
+      .selectedValueCell {
+        color: var(--deemphasized-text-color);
+        font-style: italic;
+      }
+      .selectedValueCell.hidden {
+        display: none;
       }
       @media only screen and (max-width: 50em) {
-        .selectedValueText {
+        .selectedValueCell {
           display: none;
         }
       }
-      @media only screen and (max-width: 25em) {
-        .labelName {
-          margin: 0;
-          text-align: center;
-          width: 100%;
-        }
-        .labelContainer {
-          display: block;
-        }
-      }
     </style>
-    <div class="labelContainer">
-      <span class="labelName">[[label.name]]</span>
-      <div class="buttonWrapper">
+    <span class="labelNameCell">[[label.name]]</span>
+    <div class="buttonsCell">
+      <template is="dom-repeat"
+          items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]"
+          as="value">
+        <span class="placeholder" data-label$="[[label.name]]"></span>
+      </template>
+      <iron-selector
+          id="labelSelector"
+          attr-for-selected="data-value"
+          selected="[[_computeLabelValue(labels, permittedLabels, label)]]"
+          hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"
+          on-selected-item-changed="_setSelectedValueText">
         <template is="dom-repeat"
-            items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]"
+            items="[[_items]]"
             as="value">
-          <span class="placeholder" data-label$="[[label.name]]"></span>
+          <gr-button
+              class$="[[_computeButtonClass(value, index, _items.length)]]"
+              has-tooltip
+              data-name$="[[label.name]]"
+              data-value$="[[value]]"
+              title$="[[_computeLabelValueTitle(labels, label.name, value)]]">
+            [[value]]</gr-button>
         </template>
-        <iron-selector
-            id="labelSelector"
-            attr-for-selected="data-value"
-            selected="[[_computeLabelValue(labels, permittedLabels, label)]]"
-            hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"
-            on-selected-item-changed="_setSelectedValueText">
-          <template is="dom-repeat"
-              items="[[_items]]"
-              as="value">
-            <gr-button
-                class$="[[_computeButtonClass(value, index, _items.length)]]"
-                has-tooltip
-                data-name$="[[label.name]]"
-                data-value$="[[value]]"
-                title$="[[_computeLabelValueTitle(labels, label.name, value)]]">
-              [[value]]</gr-button>
-          </template>
-        </iron-selector>
-        <template is="dom-repeat"
-            items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]"
-            as="value">
-          <span class="placeholder" data-label$="[[label.name]]"></span>
-        </template>
-        <span class="labelMessage"
-            hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
-          You don't have permission to edit this label.
-        </span>
-      </div>
-      <div class$="selectedValueText [[_computeHiddenClass(permittedLabels, label.name)]]">
-        <span id="selectedValueLabel">[[_selectedValueText]]</span>
-      </div>
+      </iron-selector>
+      <template is="dom-repeat"
+          items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]"
+          as="value">
+        <span class="placeholder" data-label$="[[label.name]]"></span>
+      </template>
+      <span class="labelMessage"
+          hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
+        You don't have permission to edit this label.
+      </span>
+    </div>
+    <div class$="selectedValueCell [[_computeHiddenClass(permittedLabels, label.name)]]">
+      <span id="selectedValueLabel">[[_selectedValueText]]</span>
     </div>
   </template>
   <script src="gr-label-score-row.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
index c607a9f..29b83a3 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
@@ -23,18 +23,23 @@
 <dom-module id="gr-label-scores">
   <template>
     <style include="shared-styles">
+      :host {
+        display: table;
+        width: 100%;
+      }
       .mergedMessage {
         font-style: italic;
         text-align: center;
         width: 100%;
       }
-      gr-label-score-row.no-access {
-        display: var(--label-no-access-display, initial);
+      gr-label-score-row:hover {
+        background-color: var(--hover-background-color);
       }
-      @media only screen and (max-width: 25em) {
-        :host {
-          text-align: center;
-        }
+      gr-label-score-row {
+        display: table-row;
+      }
+      gr-label-score-row.no-access {
+        display: var(--label-no-access-display, table-row);
       }
     </style>
     <template is="dom-repeat" items="[[_labels]]" as="label">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index e77bf57..2ea1134 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -156,6 +156,9 @@
       .commentsIcon {
         vertical-align: top;
       }
+      .score.removed {
+        background-color: var(--vote-color-neutral);
+      }
       .score.negative {
         background-color: var(--vote-color-disliked);
       }
@@ -195,7 +198,7 @@
             on behalf of
           </span>
           <gr-account-label account="[[author]]" class="authorLabel"></gr-account-label>
-          <template is="dom-repeat" items="[[_getScores(message)]]" as="score">
+          <template is="dom-repeat" items="[[_getScores(message, labelExtremes)]]" as="score">
             <span class$="score [[_computeScoreClass(score, labelExtremes)]]">
               [[score.label]] [[score.value]]
             </span>
@@ -215,7 +218,7 @@
                 class="message hideOnCollapsed"
                 content="[[_messageContentExpanded]]"
                 config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
-            <template is="dom-if" if="[[!_isMessageContentEmpty(message.message)]]">
+            <template is="dom-if" if="[[!_isMessageContentEmpty()]]">
               <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
                 <gr-button link small on-click="_handleReplyTap">Reply</gr-button>
               </div>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 95109bb..d5505a4 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -17,8 +17,8 @@
 (function() {
   'use strict';
 
-  const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+: /;
-  const LABEL_TITLE_SCORE_PATTERN = /^([A-Za-z0-9-]+)([+-]\d+)$/;
+  const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
+  const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
 
   /**
    * @appliesMixin Gerrit.FireMixin
@@ -99,11 +99,13 @@
         },
         _messageContentExpanded: {
           type: String,
-          computed: '_computeMessageContentExpanded(message.message)',
+          computed:
+              '_computeMessageContentExpanded(message.message, message.tag)',
         },
         _messageContentCollapsed: {
           type: String,
-          computed: '_computeMessageContentCollapsed(message.message)',
+          computed:
+              '_computeMessageContentCollapsed(message.message, message.tag)',
         },
         _commentCountText: {
           type: Number,
@@ -166,29 +168,54 @@
       }
     }
 
-    _computeMessageContentExpanded(content) {
-      return this._computeMessageContent(content, true);
+    _computeMessageContentExpanded(content, tag) {
+      return this._computeMessageContent(content, tag, true);
     }
 
-    _computeMessageContentCollapsed(content) {
-      return this._computeMessageContent(content, false);
+    _computeMessageContentCollapsed(content, tag) {
+      return this._computeMessageContent(content, tag, false);
     }
 
-    _computeMessageContent(content, isExpanded) {
-      if (!content) return '';
+    _computeMessageContent(content, tag, isExpanded) {
+      content = content || '';
+      tag = tag || '';
+      const isNewPatchSet = tag.endsWith(':newPatchSet') ||
+          tag.endsWith(':newWipPatchSet');
       const lines = content.split('\n');
       const filteredLines = lines.filter(line => {
-        if (!isExpanded && line.startsWith('>')) return false;
-        if (line.startsWith('Patch Set ')) return false;
-        if (line.startsWith('(') && line.endsWith(' comment)')) return false;
-        if (line.startsWith('(') && line.endsWith(' comments)')) return false;
+        if (!isExpanded && line.startsWith('>')) {
+          return false;
+        }
+        if (line.startsWith('(') && line.endsWith(' comment)')) {
+          return false;
+        }
+        if (line.startsWith('(') && line.endsWith(' comments)')) {
+          return false;
+        }
+        if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
+          return false;
+        }
         return true;
       });
-      return filteredLines.join('\n').trim();
+      const mappedLines = filteredLines.map(line => {
+        // The change message formatting is not very consistent, so
+        // unfortunately we have to do a bit of tweaking here:
+        //   Labels should be stripped from lines like this:
+        //     Patch Set 29: Verified+1
+        //   Rebase messages (which have a ':newPatchSet' tag) should be kept on
+        //   lines like this:
+        //     Patch Set 27: Patch Set 26 was rebased
+        if (isNewPatchSet) {
+          line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
+        }
+        return line;
+      });
+      return mappedLines.join('\n').trim();
     }
 
-    _isMessageContentEmpty(content) {
-      return this._computeMessageContent(content).trim().length === 0;
+    _isMessageContentEmpty() {
+      return !this._messageContentExpanded
+          || this._messageContentExpanded.length === 0;
     }
 
     _computeAuthor(message) {
@@ -247,17 +274,28 @@
       return event.type === 'REVIEWER_UPDATE';
     }
 
-    _getScores(message) {
-      if (!message.message) { return []; }
+    _getScores(message, labelExtremes) {
+      if (!message || !message.message || !labelExtremes) {
+        return [];
+      }
       const line = message.message.split('\n', 1)[0];
       const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
-      if (!line.match(patchSetPrefix)) { return []; }
+      if (!line.match(patchSetPrefix)) {
+        return [];
+      }
       const scoresRaw = line.split(patchSetPrefix)[1];
-      if (!scoresRaw) { return []; }
+      if (!scoresRaw) {
+        return [];
+      }
       return scoresRaw.split(' ')
           .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
-          .filter(ms => ms && ms.length === 3)
-          .map(ms => { return {label: ms[1], value: ms[2]}; });
+          .filter(ms =>
+            ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2]))
+          .map(ms => {
+            const label = ms[2];
+            const value = ms[1] === '-' ? 'removed' : ms[3];
+            return {label, value};
+          });
     }
 
     _computeScoreClass(score, labelExtremes) {
@@ -265,6 +303,9 @@
       if ([score, labelExtremes].some(arg => arg === undefined)) {
         return '';
       }
+      if (score.value === 'removed') {
+        return 'removed';
+      }
       const classes = [];
       if (score.value > 0) {
         classes.push('positive');
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 01bc691..20d87d2 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -184,16 +184,72 @@
       assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
     });
 
+    suite('compute messages', () => {
+      test('empty', () => {
+        assert.equal(element._computeMessageContent('', '', true), '');
+        assert.equal(element._computeMessageContent('', '', false), '');
+      });
+
+      test('new patchset', () => {
+        const original = 'Uploaded patch set 1.';
+        const tag = 'autogenerated:gerrit:newPatchSet';
+        let actual = element._computeMessageContent(original, tag, true);
+        assert.equal(actual, original);
+        actual = element._computeMessageContent(original, tag, false);
+        assert.equal(actual, original);
+      });
+
+      test('new patchset rebased', () => {
+        const original = 'Patch Set 27: Patch Set 26 was rebased';
+        const tag = 'autogenerated:gerrit:newPatchSet';
+        const expected = 'Patch Set 26 was rebased';
+        let actual = element._computeMessageContent(original, tag, true);
+        assert.equal(actual, expected);
+        actual = element._computeMessageContent(original, tag, false);
+        assert.equal(actual, expected);
+      });
+
+      test('ready for review', () => {
+        const original = 'Patch Set 1:\n\nThis change is ready for review.';
+        const tag = undefined;
+        const expected = 'This change is ready for review.';
+        let actual = element._computeMessageContent(original, tag, true);
+        assert.equal(actual, expected);
+        actual = element._computeMessageContent(original, tag, false);
+        assert.equal(actual, expected);
+      });
+
+      test('vote', () => {
+        const original = 'Patch Set 1: Code-Style+1';
+        const tag = undefined;
+        const expected = '';
+        let actual = element._computeMessageContent(original, tag, true);
+        assert.equal(actual, expected);
+        actual = element._computeMessageContent(original, tag, false);
+        assert.equal(actual, expected);
+      });
+
+      test('comments', () => {
+        const original = 'Patch Set 1:\n\n(3 comments)';
+        const tag = undefined;
+        const expected = '';
+        let actual = element._computeMessageContent(original, tag, true);
+        assert.equal(actual, expected);
+        actual = element._computeMessageContent(original, tag, false);
+        assert.equal(actual, expected);
+      });
+    });
+
     test('votes', () => {
       element.message = {
         author: {},
         expanded: false,
-        message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Ready+1',
+        message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
       };
       element.labelExtremes = {
         'Verified': {max: 1, min: -1},
         'Code-Review': {max: 2, min: -2},
-        'Trybot-Ready': {max: 3, min: 0},
+        'Trybot-Label3': {max: 3, min: 0},
       };
       flushAsynchronousOperations();
       const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
@@ -209,6 +265,25 @@
       assert.isFalse(scoreChips[2].classList.contains('min'));
     });
 
+    test('removed votes', () => {
+      element.message = {
+        author: {},
+        expanded: false,
+        message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
+      };
+      element.labelExtremes = {
+        'Verified': {max: 1, min: -1},
+        'Code-Review': {max: 2, min: -2},
+        'Commit-Queue': {max: 3, min: 0},
+      };
+      flushAsynchronousOperations();
+      const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
+      assert.equal(scoreChips.length, 3);
+
+      assert.isTrue(scoreChips[1].classList.contains('removed'));
+      assert.isTrue(scoreChips[2].classList.contains('removed'));
+    });
+
     test('false negative vote', () => {
       element.message = {
         author: {},
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index cd759d3..20680b5 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -367,8 +367,7 @@
           acc[val] = (acc[val] || 0) + 1;
           return acc;
         }, {all: messages.length});
-        this.$.reporting.reportInteraction('messages-count',
-            JSON.stringify(tagsCounted));
+        this.$.reporting.reportInteraction('messages-count', tagsCounted);
       }
     }
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index b0747f4..9c3f53b 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -553,6 +553,23 @@
       assert.isFalse(element._hasAutomatedMessages(messages));
     });
 
+    test('initially show only 20 messages', () => {
+      sandbox.stub(element.$.reporting, 'reportInteraction',
+          (eventName, details) => {
+            assert.equal(typeof(eventName), 'string');
+            if (details) {
+              assert.equal(typeof(details), 'object');
+            }
+          });
+      const messages = Array.from(Array(23).keys())
+          .map(() => {
+            return {};
+          });
+      element._processedMessagesChanged(messages);
+
+      assert.equal(element._visibleMessages.length, 20);
+    });
+
     test('_computeLabelExtremes', () => {
       const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index d54669f..7674099 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -64,6 +64,10 @@
         padding: var(--spacing-m) var(--spacing-xl);
         width: 100%;
       }
+      section.labelsContainer {
+        /* We want the :hover highlight to extend to the border of the dialog. */
+        padding: var(--spacing-m) 0;
+      }
       .actions {
         background-color: var(--dialog-background-color);
         bottom: 0;
@@ -295,11 +299,10 @@
                 class="action save"
                 has-tooltip
                 title="[[_saveTooltip]]"
-                on-click="_saveClickHandler">Send</gr-button>
+                on-click="_saveClickHandler">Save</gr-button>
           </template>
           <gr-button
               id="sendButton"
-              link
               primary
               disabled="[[_sendDisabled]]"
               class="action send"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 90f8540..fac631b 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -43,7 +43,7 @@
   };
 
   const ButtonTooltips = {
-    SAVE: 'Send but do not send notification or change review state',
+    SAVE: 'Save but do not send notification or change review state',
     START_REVIEW: 'Mark as ready for review and send reply',
     SEND: 'Send reply',
   };
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
index 1569094..06edb9b 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html
@@ -67,10 +67,10 @@
             </div>
             <div class="diffContainer">
               <gr-diff
-              prefs="[[overridePartialPrefs(prefs)]]"
-              change-num="[[changeNum]]"
-              path="[[item.filepath]]"
-              diff="[[item.preview]]"></gr-diff>
+                prefs="[[overridePartialPrefs(prefs)]]"
+                change-num="[[changeNum]]"
+                path="[[item.filepath]]"
+                diff="[[item.preview]]"></gr-diff>
             </div>
           </template>
         </div>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
index 7561f1b..45e8c07 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -75,6 +75,18 @@
           });
     },
 
+    attached() {
+      this.refitOverlay = () => {
+        // re-center the dialog as content changed
+        this.$.applyFixOverlay.fire('iron-resize');
+      };
+      this.addEventListener('diff-context-expanded', this.refitOverlay);
+    },
+
+    detached() {
+      this.removeEventListener('diff-context-expanded', this.refitOverlay);
+    },
+
     _showSelectedFixSuggestion(fixSuggestion) {
       this._currentFix = fixSuggestion;
       return this._fetchFixPreview(fixSuggestion.fix_id);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 40fbe3c..021d962 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html">
 <link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
+<link rel="import" href="../../../elements/shared/gr-hovercard/gr-hovercard.html">
 <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
 
 <dom-module id="gr-diff-builder">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 33cd4cb..05bff9f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -574,14 +574,30 @@
     const date = (new Date(commit.time * 1000)).toLocaleDateString();
     const blameNode = this._createElement('span',
         isStartOfRange ? 'startOfRange' : '');
-    const shaNode = this._createElement('span', 'sha');
-    shaNode.innerText = commit.id.substr(0, 7);
-    shaNode.onclick = function() {
-      location.href = '/q/' + shaNode.innerText;
-    };
 
+    const shaNode = this._createElement('span', 'blameDate');
+    shaNode.innerText = `${date}`;
+    shaNode.onclick = function() {
+      location.href = '/q/' + commit.id;
+    };
     blameNode.appendChild(shaNode);
-    blameNode.append(` on ${date} by ${commit.author}`);
+
+    const shortName = commit.author.split(' ')[0];
+    const authorNode = this._createElement('span', 'blameAuthor');
+    authorNode.innerText = ` ${shortName}`;
+    blameNode.appendChild(authorNode);
+
+    const hoverCardFragment = this._createElement('span', 'blameHoverCard');
+    hoverCardFragment.innerText =
+      `Commit ${commit.id}
+Author: ${commit.author}
+Date: ${date}
+
+${commit.commit_msg}`;
+    const hovercard = this._createElement('gr-hovercard');
+    hovercard.appendChild(hoverCardFragment);
+    blameNode.appendChild(hovercard);
+
     return blameNode;
   };
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 867a067..589d7c2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -1146,6 +1146,30 @@
         assert.equal(result.getAttribute('data-line-number'), '3');
         assert.equal(result.firstChild, mocbBlameCell);
       });
+
+      test('_getBlameForBaseLine', () => {
+        const mockCommit = {
+          time: 1576105200,
+          id: 1234567890,
+          author: 'Clark Kent',
+          commit_msg: 'Testing Commit',
+          ranges: [1],
+        };
+        const blameNode = builder._getBlameForBaseLine(1, mockCommit);
+
+        const authors = blameNode.getElementsByClassName('blameAuthor');
+        assert.equal(authors.length, 1);
+        assert.equal(authors[0].innerText, ' Clark');
+
+        const date = (new Date(mockCommit.time * 1000)).toLocaleDateString();
+        Polymer.dom.flush();
+        const cards = blameNode.getElementsByClassName('blameHoverCard');
+        assert.equal(cards.length, 1);
+        assert.equal(cards[0].innerHTML,
+            `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}`
+          + '<br><br>Testing Commit'
+        );
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index a15ca37..da22f6b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -640,17 +640,20 @@
       }
     }
 
+    _sortComments(comments) {
+      return comments.slice(0).sort((a, b) => {
+        if (b.__draft && !a.__draft ) { return -1; }
+        if (a.__draft && !b.__draft ) { return 1; }
+        return util.parseDate(a.updated) - util.parseDate(b.updated);
+      });
+    }
+
     /**
      * @param {!Array<!Object>} comments
      * @return {!Array<!Object>} Threads for the given comments.
      */
     _createThreads(comments) {
-      const sortedComments = comments.slice(0).sort((a, b) => {
-        if (b.__draft && !a.__draft ) { return 0; }
-        if (a.__draft && !b.__draft ) { return 1; }
-        return util.parseDate(a.updated) - util.parseDate(b.updated);
-      });
-
+      const sortedComments = this._sortComments(comments);
       const threads = [];
       for (const comment of sortedComments) {
         // If the comment is in reply to another comment, find that comment's
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index d28f2a3..5aee282 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -1102,6 +1102,36 @@
       });
     });
 
+    test('comments sorting', () => {
+      const comments = [
+        {
+          id: 'new_draft',
+          message: 'i do not like either of you',
+          __commentSide: 'left',
+          __draft: true,
+          updated: '2015-12-20 15:01:20.396000000',
+        },
+        {
+          id: 'sallys_confession',
+          message: 'i like you, jack',
+          updated: '2015-12-23 15:00:20.396000000',
+          line: 1,
+          __commentSide: 'left',
+        }, {
+          id: 'jacks_reply',
+          message: 'i like you, too',
+          updated: '2015-12-24 15:01:20.396000000',
+          __commentSide: 'left',
+          line: 1,
+          in_reply_to: 'sallys_confession',
+        },
+      ];
+      const sortedComments = element._sortComments(comments);
+      assert.equal(sortedComments[0], comments[1]);
+      assert.equal(sortedComments[1], comments[2]);
+      assert.equal(sortedComments[2], comments[0]);
+    });
+
     test('_createThreads', () => {
       const comments = [
         {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index d7eaabf..cb89c0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -304,6 +304,12 @@
       return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray(
           changeNum, patchRange).then(files => {
         this._fileList = files;
+
+        // in case current file is not in changed files
+        // (file has no change but has comments)
+        if (this._path && !this._fileList.includes(this._path)) {
+          this._fileList.push(this._path);
+        }
       });
     }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 29cc950..bd172a5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -433,6 +433,23 @@
       });
     });
 
+    suite('unchanged file', () => {
+      test('unchanged file should be included in _fileList', done => {
+        element._path = 'aaa.txt';
+        // trigger reload on files
+        element._changeNum = '123';
+        element._patchRange = {
+          basePatchNum: PARENT,
+          patchNum: '1',
+        };
+        assert.isFalse(element._fileList.includes('aaa.txt'));
+        flush(() => {
+          assert.isTrue(element._fileList.includes('aaa.txt'));
+          done();
+        });
+      });
+    });
+
     suite('url params', () => {
       setup(() => {
         sandbox.stub(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index dea9789..172a151 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -82,6 +82,7 @@
       }
       .diff-row {
         outline: none;
+        user-select: none;
       }
       .diff-row.target-row.target-side-left .lineNum.left,
       .diff-row.target-row.target-side-right .lineNum.right,
@@ -176,7 +177,7 @@
       .ignoredWhitespaceOnly .content.remove .contentText .intraline,
       .delta.total.ignoredWhitespaceOnly .content.remove .contentText,
       .ignoredWhitespaceOnly .content.remove .contentText {
-        background: none;
+        background-color: var(--view-background-color);
       }
 
       .content .contentText:empty:after {
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 5d32e9b..ef22188 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -815,9 +815,10 @@
         // for each line from the start.
         let lastEl;
         for (const threadEl of addedThreadEls) {
+          const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
           const commentSide = threadEl.getAttribute('comment-side');
-          const lineEl = this._getLineElement(threadEl,
-              commentSide);
+          const lineEl = this.$.diffBuilder.getLineElByNumber(
+              lineNumString, commentSide);
           const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
           const contentEl = contentText.parentElement;
           const threadGroupEl = this._getOrCreateThreadGroup(
@@ -843,18 +844,6 @@
       });
     }
 
-    _getLineElement(threadEl, commentSide) {
-      const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
-      const lineEl = this.$.diffBuilder.getLineElByNumber(
-          lineNumString, commentSide);
-      if (lineEl) {
-        return lineEl;
-      }
-      // It is possible to add comment to non-existing line via API
-      threadEl.invalidLineNumber = true;
-      return this.$.diffBuilder.getLineElByNumber('FILE', commentSide);
-    }
-
     _unobserveIncrementalNodes() {
       if (this._incrementalNodeObserver) {
         Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver);
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
index 26ece30..a8bb06b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
@@ -16,7 +16,6 @@
 -->
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-plugin-popup.html">
 
 <dom-module id="gr-popup-interface">
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
index 55b7ac5..20245e0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -25,6 +25,7 @@
 <script src="/bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-popup-interface.html"/>
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <script>void(0);</script>
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
index 8e6c053..b3f6aec 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
@@ -16,7 +16,6 @@
 -->
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-plugin-repo-command.html">
 
 <dom-module id="gr-repo-api">
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
index 20cc71b..999ecfa 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
@@ -19,8 +19,6 @@
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../settings/gr-settings-view/gr-settings-item.html">
 <link rel="import" href="../../settings/gr-settings-view/gr-settings-menu-item.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
 <dom-module id="gr-settings-api">
   <script src="gr-settings-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
index d6e67fe..ef1c9d4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
@@ -16,7 +16,6 @@
 -->
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="gr-custom-plugin-header.html">
 
 <dom-module id="gr-theme-api">
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
index 7bc93b5..d615a7f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.html
@@ -16,8 +16,6 @@
 -->
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
 <link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -47,7 +45,7 @@
         background-color: var(--comment-background-color);
         color: var(--comment-text-color);
         display: block;
-        margin: 0 4px 4px 4px;
+        margin: 0 var(--spacing-s) var(--spacing-s);
         white-space: normal;
         box-shadow: var(--elevation-level-2);
         border-radius: var(--border-radius);
@@ -75,23 +73,13 @@
       .pathInfo {
         display: flex;
         align-items: baseline;
+        justify-content: space-between;
+        padding: 0 var(--spacing-s) var(--spacing-s);
       }
       .descriptionText {
         margin-left: var(--spacing-m);
         font-style: italic;
       }
-      .invalidLineNumber {
-        padding: var(--spacing-m);
-      }
-      .invalidLineNumberText {
-        color: var(--error-text-color);
-      }
-      .invalidLineNumberIcon {
-        color: var(--error-text-color);
-        vertical-align: top;
-        margin-right: var(--spacing-s);
-      }
-
     </style>
     <template is="dom-if" if="[[showFilePath]]">
       <div class="pathInfo">
@@ -100,11 +88,6 @@
       </div>
     </template>
     <div id="container" class$="[[_computeHostClass(unresolved, isRobotComment)]]">
-      <template is="dom-if" if="[[invalidLineNumber]]">
-        <div class="invalidLineNumber">
-          <span class="invalidLineNumberText"><iron-icon icon="gr-icons:error" class="invalidLineNumberIcon"></iron-icon>This comment thread is attached to non-existing line [[lineNum]].</span>
-        </div>
-      </template>
       <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
           as="comment">
         <gr-comment
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index 00ff03a..d8a56f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -124,11 +124,6 @@
           type: Boolean,
           value: false,
         },
-        /** It is possible to add comment to non-existing line via API */
-        invalidLineNumber: {
-          type: Number,
-          reflectToAttribute: true,
-        },
         /** Necessary only if showFilePath is true or when used with gr-diff */
         lineNum: {
           type: Number,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
index afd0e59..333ca9d 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.html
@@ -264,6 +264,7 @@
             id="deleteBtn"
             link
             class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
+            hidden$="[[isRobotComment]]"
             on-click="_handleCommentDelete">
           <iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
         </gr-button>
@@ -352,8 +353,7 @@
                 secondary
                 class="action show-fix"
                 hidden$="[[_hasNoFix(comment)]]"
-                on-click="_handleShowFix"
-                disabled="[[robotButtonDisabled]]">
+                on-click="_handleShowFix">
               Show Fix
             </gr-button>
             <gr-endpoint-decorator name="robot-comment-controls">
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 ae8f3f6..e3391a0 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
@@ -273,7 +273,7 @@
       flush(() => {
         element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
           const dialog =
-              this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
+              window.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
           dialog.message = 'removal reason';
           element._handleConfirmDeleteComment();
           assert.isTrue(element.$.restAPI.deleteComment.calledWith(
@@ -440,6 +440,9 @@
       assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
       assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
 
+      // Delete button is not hidden by default
+      assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);
+
       element.isRobotComment = true;
       element.draft = true;
       assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
@@ -463,6 +466,9 @@
       flushAsynchronousOperations();
       assert.notEqual(getComputedStyle(element.$$('.robotRun.link')).display,
           'none');
+
+      // Delete button is hidden for robot comments
+      assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
     });
 
     test('collapsible drafts', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index f3ea177..ae5a945 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -31,7 +31,7 @@
       }
     </style>
     <span>
-      [[_computeDateStr(dateStr, _timeFormat, _relative, showDateAndTime)]]
+      [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative, showDateAndTime)]]
     </span>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 8c247e3..7be041b 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -27,8 +27,29 @@
     TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM
     TIME_24: 'HH:mm', // 14:14
     TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00
-    MONTH_DAY: 'MMM DD', // Aug 29
-    MONTH_DAY_YEAR: 'MMM DD, YYYY', // Aug 29, 1997
+  };
+
+  const DateFormats = {
+    STD: {
+      short: 'MMM DD', // Aug 29
+      full: 'MMM DD, YYYY', // Aug 29, 1997
+    },
+    US: {
+      short: 'MM/DD', // 08/29
+      full: 'MM/DD/YY', // 08/29/97
+    },
+    ISO: {
+      short: 'MM-DD', // 08-29
+      full: 'YYYY-MM-DD', // 1997-08-29
+    },
+    EURO: {
+      short: 'DD. MMM', // 29. Aug
+      full: 'DD.MM.YYYY', // 29.08.1997
+    },
+    UK: {
+      short: 'DD/MM', // 29/08
+      full: 'DD/MM/YYYY', // 29/08/1997
+    },
   };
 
   /**
@@ -66,9 +87,11 @@
         title: {
           type: String,
           reflectToAttribute: true,
-          computed: '_computeFullDateStr(dateStr, _timeFormat)',
+          computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
         },
 
+        /** @type {?{short: string, full: string}} */
+        _dateFormat: Object,
         _timeFormat: String, // No default value to prevent flickering.
         _relative: Boolean, // No default value to prevent flickering.
       };
@@ -88,6 +111,7 @@
       return this._getLoggedIn().then(loggedIn => {
         if (!loggedIn) {
           this._timeFormat = TimeFormats.TIME_24;
+          this._dateFormat = DateFormats.STD;
           this._relative = false;
           return;
         }
@@ -101,19 +125,47 @@
     _loadTimeFormat() {
       return this._getPreferences().then(preferences => {
         const timeFormat = preferences && preferences.time_format;
-        switch (timeFormat) {
-          case 'HHMM_12':
-            this._timeFormat = TimeFormats.TIME_12;
-            break;
-          case 'HHMM_24':
-            this._timeFormat = TimeFormats.TIME_24;
-            break;
-          default:
-            throw Error('Invalid time format: ' + timeFormat);
-        }
+        const dateFormat = preferences && preferences.date_format;
+        this._decideTimeFormat(timeFormat);
+        this._decideDateFormat(dateFormat);
       });
     }
 
+    _decideTimeFormat(timeFormat) {
+      switch (timeFormat) {
+        case 'HHMM_12':
+          this._timeFormat = TimeFormats.TIME_12;
+          break;
+        case 'HHMM_24':
+          this._timeFormat = TimeFormats.TIME_24;
+          break;
+        default:
+          throw Error('Invalid time format: ' + timeFormat);
+      }
+    }
+
+    _decideDateFormat(dateFormat) {
+      switch (dateFormat) {
+        case 'STD':
+          this._dateFormat = DateFormats.STD;
+          break;
+        case 'US':
+          this._dateFormat = DateFormats.US;
+          break;
+        case 'ISO':
+          this._dateFormat = DateFormats.ISO;
+          break;
+        case 'EURO':
+          this._dateFormat = DateFormats.EURO;
+          break;
+        case 'UK':
+          this._dateFormat = DateFormats.UK;
+          break;
+        default:
+          throw Error('Invalid date format: ' + dateFormat);
+      }
+    }
+
     _loadRelative() {
       return this._getPreferences().then(prefs => {
         // prefs.relative_date_in_change_table is not set when false.
@@ -143,11 +195,13 @@
     _isWithinHalfYear(now, date) {
       const diff = -date.diff(now);
       return (date.day() !== now.getDay() || diff >= Duration.DAY) &&
-          diff < 180 * Duration.DAY;
+        diff < 180 * Duration.DAY;
     }
 
-    _computeDateStr(dateStr, timeFormat, relative, showDateAndTime) {
-      if (!dateStr) { return ''; }
+    _computeDateStr(
+        dateStr, timeFormat, dateFormat, relative, showDateAndTime
+    ) {
+      if (!dateStr || !timeFormat || !dateFormat) { return ''; }
       const date = moment(util.parseDate(dateStr));
       if (!date.isValid()) { return ''; }
       if (relative) {
@@ -159,12 +213,12 @@
         }
       }
       const now = new Date();
-      let format = TimeFormats.MONTH_DAY_YEAR;
+      let format = dateFormat.full;
       if (this._isWithinDay(now, date)) {
         format = timeFormat;
       } else {
         if (this._isWithinHalfYear(now, date)) {
-          format = TimeFormats.MONTH_DAY;
+          format = dateFormat.short;
         }
         if (this.showDateAndTime) {
           format = `${format} ${timeFormat}`;
@@ -179,11 +233,12 @@
         TimeFormats.TIME_24_WITH_SEC;
     }
 
-    _computeFullDateStr(dateStr, timeFormat) {
+    _computeFullDateStr(dateStr, timeFormat, dateFormat) {
       // Polymer 2: check for undefined
       if ([
         dateStr,
         timeFormat,
+        dateFormat,
       ].some(arg => arg === undefined)) {
         return undefined;
       }
@@ -191,7 +246,7 @@
       if (!dateStr) { return ''; }
       const date = moment(util.parseDate(dateStr));
       if (!date.isValid()) { return ''; }
-      let format = TimeFormats.MONTH_DAY_YEAR + ', ';
+      let format = dateFormat.full + ', ';
       format += this._timeToSecondsFormat(timeFormat);
       return date.format(format) + this._getUtcOffsetString();
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index fe2f110..d2f7489 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -88,10 +88,12 @@
       return Promise.all([loggedInPromise, preferencesPromise]);
     }
 
-    suite('24 hours time format preference', () => {
-      setup(() => stubRestAPI(
-          {time_format: 'HHMM_24', relative_date_in_change_table: false}
-      ).then(() => {
+    suite('STD + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'STD',
+        relative_date_in_change_table: false,
+      }).then(() => {
         element = fixture('basic');
         sandbox.stub(element, '_getUtcOffsetString').returns('');
         return element._loadPreferences();
@@ -135,11 +137,155 @@
       });
     });
 
-    suite('12 hours time format preference', () => {
+    suite('US + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'US',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '07/29/15, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '07/28',
+            '07/28 20:25',
+            '07/28/15, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '06/15',
+            '06/15 03:25',
+            '06/15/15, 03:25:14', done);
+      });
+    });
+
+    suite('ISO + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'ISO',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '2015-07-29, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '07-28',
+            '07-28 20:25',
+            '2015-07-28, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '06-15',
+            '06-15 03:25',
+            '2015-06-15, 03:25:14', done);
+      });
+    });
+
+    suite('EURO + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'EURO',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '29.07.2015, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '28. Jul',
+            '28. Jul 20:25',
+            '28.07.2015, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '15. Jun',
+            '15. Jun 03:25',
+            '15.06.2015, 03:25:14', done);
+      });
+    });
+
+    suite('UK + 24 hours time format preference', () => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_24',
+        date_format: 'UK',
+        relative_date_in_change_table: false,
+      }).then(() => {
+        element = fixture('basic');
+        sandbox.stub(element, '_getUtcOffsetString').returns('');
+        return element._loadPreferences();
+      }));
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '15:34',
+            '15:34',
+            '29/07/2015, 15:34:14', done);
+      });
+
+      test('Within 24 hours on different days', done => {
+        testDates('2015-07-29 03:34:14.985000000',
+            '2015-07-28 20:25:14.985000000',
+            '28/07',
+            '28/07 20:25',
+            '28/07/2015, 20:25:14', done);
+      });
+
+      test('More than 24 hours but less than six months', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-06-15 03:25:14.985000000',
+            '15/06',
+            '15/06 03:25',
+            '15/06/2015, 03:25:14', done);
+      });
+    });
+
+    suite('STD + 12 hours time format preference', () => {
       setup(() =>
-      // relative_date_in_change_table is not set when false.
+        // relative_date_in_change_table is not set when false.
         stubRestAPI(
-            {time_format: 'HHMM_12'}
+            {time_format: 'HHMM_12', date_format: 'STD'}
         ).then(() => {
           element = fixture('basic');
           sandbox.stub(element, '_getUtcOffsetString').returns('');
@@ -156,10 +302,96 @@
       });
     });
 
+    suite('US + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'US'}
+        ).then(() => {
+          element = fixture('basic');
+          sandbox.stub(element, '_getUtcOffsetString').returns('');
+          return element._loadPreferences();
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '07/29/15, 3:34:14 PM', done);
+      });
+    });
+
+    suite('ISO + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'ISO'}
+        ).then(() => {
+          element = fixture('basic');
+          sandbox.stub(element, '_getUtcOffsetString').returns('');
+          return element._loadPreferences();
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '2015-07-29, 3:34:14 PM', done);
+      });
+    });
+
+    suite('EURO + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'EURO'}
+        ).then(() => {
+          element = fixture('basic');
+          sandbox.stub(element, '_getUtcOffsetString').returns('');
+          return element._loadPreferences();
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '29.07.2015, 3:34:14 PM', done);
+      });
+    });
+
+    suite('UK + 12 hours time format preference', () => {
+      setup(() =>
+        // relative_date_in_change_table is not set when false.
+        stubRestAPI(
+            {time_format: 'HHMM_12', date_format: 'UK'}
+        ).then(() => {
+          element = fixture('basic');
+          sandbox.stub(element, '_getUtcOffsetString').returns('');
+          return element._loadPreferences();
+        })
+      );
+
+      test('Within 24 hours on same day', done => {
+        testDates('2015-07-29 20:34:14.985000000',
+            '2015-07-29 15:34:14.985000000',
+            '3:34 PM',
+            '3:34 PM',
+            '29/07/2015, 3:34:14 PM', done);
+      });
+    });
+
     suite('relative date preference', () => {
-      setup(() => stubRestAPI(
-          {time_format: 'HHMM_12', relative_date_in_change_table: true}
-      ).then(() => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_12',
+        date_format: 'STD',
+        relative_date_in_change_table: true,
+      }).then(() => {
         element = fixture('basic');
         sandbox.stub(element, '_getUtcOffsetString').returns('');
         return element._loadPreferences();
@@ -183,15 +415,19 @@
     });
 
     suite('logged in', () => {
-      setup(() => stubRestAPI(
-          {time_format: 'HHMM_12', relative_date_in_change_table: true}
-      ).then(() => {
+      setup(() => stubRestAPI({
+        time_format: 'HHMM_12',
+        date_format: 'US',
+        relative_date_in_change_table: true,
+      }).then(() => {
         element = fixture('basic');
         return element._loadPreferences();
       }));
 
       test('Preferences are respected', () => {
         assert.equal(element._timeFormat, 'h:mm A');
+        assert.equal(element._dateFormat.short, 'MM/DD');
+        assert.equal(element._dateFormat.full, 'MM/DD/YY');
         assert.isTrue(element._relative);
       });
     });
@@ -204,6 +440,8 @@
 
       test('Default preferences are respected', () => {
         assert.equal(element._timeFormat, 'HH:mm');
+        assert.equal(element._dateFormat.short, 'MMM DD');
+        assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
         assert.isFalse(element._relative);
       });
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
index 77418df..475f6e2 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.html
@@ -27,6 +27,7 @@
         color: var(--primary-text-color);
         display: block;
         max-height: 90vh;
+        overflow: auto;
       }
       .container {
         display: flex;
@@ -42,6 +43,13 @@
         display: flex;
         flex-shrink: 1;
         width: 100%;
+        flex: 1;
+        /* IMPORTANT: required for firefox */
+        min-height: 0px;
+      }
+      main .overflow-container {
+        flex: 1;
+        overflow: auto;
       }
       footer {
         display: flex;
@@ -58,7 +66,11 @@
     </style>
     <div class="container" on-keydown="_handleKeydown">
       <header class="font-h3"><slot name="header"></slot></header>
-      <main><slot name="main"></slot></main>
+      <main>
+        <div class="overflow-container">
+          <slot name="main"></slot>
+        </div>
+      </main>
       <footer>
         <slot name="footer"></slot>
         <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-click="_handleCancelTap">
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
index f7349eb..7586876 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -59,8 +59,9 @@
         cursor: pointer;
         flex-direction: column;
         font-size: inherit;
-        // This variable was introduced in Dec 2019. We keep both min-height
-        // rules around, because --paper-item-min-height is not yet upstreamed.
+        /* This variable was introduced in Dec 2019. We keep both min-height
+         * rules around, because --paper-item-min-height is not yet upstreamed.
+         */
         --paper-item-min-height: 0;
         --paper-item: {
           min-height: 0;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
index 7b03308..a316fd5 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
@@ -41,7 +41,7 @@
     let sendStub;
 
     setup(() => {
-      this.clock = sinon.useFakeTimers();
+      window.clock = sinon.useFakeTimers();
       sandbox = sinon.sandbox.create();
       sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
       stub('gr-rest-api-interface', {
@@ -56,7 +56,7 @@
     });
 
     teardown(() => {
-      this.clock.restore();
+      window.clock.restore();
       sandbox.restore();
       element._removeEventCallbacks();
       Gerrit._testOnly_resetPlugins();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 917c0a9..323a730 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -49,7 +49,7 @@
     };
 
     setup(() => {
-      this.clock = sinon.useFakeTimers();
+      window.clock = sinon.useFakeTimers();
       sandbox = sinon.sandbox.create();
       getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
       sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
@@ -70,7 +70,7 @@
     });
 
     teardown(() => {
-      this.clock.restore();
+      window.clock.restore();
       sandbox.restore();
       element._removeEventCallbacks();
       plugin = null;
@@ -226,7 +226,7 @@
       assert.isFalse(spy.called);
 
       // Timeout on loading plugins
-      this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+      window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
 
       flush(() => {
         assert.isTrue(spy.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
index 7ddb666..85c23dd 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -43,7 +43,7 @@
     let sendStub;
 
     setup(() => {
-      this.clock = sinon.useFakeTimers();
+      window.clock = sinon.useFakeTimers();
       sandbox = sinon.sandbox.create();
       sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
       stub('gr-rest-api-interface', {
@@ -61,7 +61,7 @@
 
     teardown(() => {
       sandbox.restore();
-      this.clock.restore();
+      window.clock.restore();
       Gerrit._testOnly_resetPlugins();
     });
 
@@ -110,7 +110,7 @@
       Gerrit._loadPlugins(plugins);
       assert.isFalse(Gerrit._arePluginsLoaded());
       // Timeout on loading plugins
-      this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+      window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
 
       flush(() => {
         assert.isTrue(Gerrit._arePluginsLoaded());
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index e9477bf..b0186f0 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -50,7 +50,11 @@
       element.config = {
         ph: {
           match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
-          link: 'https://code.google.com/p/gerrit/issues/detail?id=$2',
+          link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
+        },
+        prefixsameinlinkandpattern: {
+          match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)',
+          link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
         },
         changeid: {
           match: '(I[0-9a-f]{8,40})',
@@ -91,7 +95,7 @@
 
     test('URL pattern was parsed and linked.', () => {
       // Regular inline link.
-      const url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+      const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
       element.content = url;
       const linkEl = element.$.output.childNodes[0];
       assert.equal(linkEl.target, '_blank');
@@ -105,7 +109,7 @@
       element.content = 'Issue 3650';
 
       let linkEl = element.$.output.childNodes[0];
-      const url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+      const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
       assert.equal(linkEl.target, '_blank');
       assert.equal(linkEl.href, url);
       assert.equal(linkEl.textContent, 'Issue 3650');
@@ -118,6 +122,18 @@
       assert.equal(linkEl.textContent, 'Bug 3650');
     });
 
+    test('Pattern with same prefix as link was correctly parsed', () => {
+      // Pattern starts with the same prefix (`http`) as the url.
+      element.content = 'httpexample 3650';
+
+      assert.equal(element.$.output.childNodes.length, 1);
+      const linkEl = element.$.output.childNodes[0];
+      const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+      assert.equal(linkEl.target, '_blank');
+      assert.equal(linkEl.href, url);
+      assert.equal(linkEl.textContent, 'httpexample 3650');
+    });
+
     test('Change-Id pattern was parsed and linked', () => {
       // "Change-Id:" pattern.
       const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
@@ -159,12 +175,12 @@
 
       assert.equal(linkEl1.target, '_blank');
       assert.equal(linkEl1.href,
-          'https://code.google.com/p/gerrit/issues/detail?id=3650');
+          'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650');
       assert.equal(linkEl1.textContent, 'Issue 3650');
 
       assert.equal(linkEl2.target, '_blank');
       assert.equal(linkEl2.href,
-          'https://code.google.com/p/gerrit/issues/detail?id=3450');
+          'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450');
       assert.equal(linkEl2.textContent, 'Issue 3450');
     });
 
@@ -177,7 +193,7 @@
       const bug = 'Issue 3650';
 
       const changeUrl = '/q/' + changeID;
-      const bugUrl = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+      const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
 
       element.content = prefix + changeID + bug;
 
@@ -262,16 +278,58 @@
       let links = element.$.output.querySelectorAll('a');
       assert.equal(links.length, 1);
       assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+      assert.equal(links[0].innerHTML, 'mailto:test@google.com');
 
       element.content = 'xx http://google.com yy';
       links = element.$.output.querySelectorAll('a');
       assert.equal(links.length, 1);
       assert.equal(links[0].getAttribute('href'), 'http://google.com');
+      assert.equal(links[0].innerHTML, 'http://google.com');
 
       element.content = 'xx https://google.com yy';
       links = element.$.output.querySelectorAll('a');
       assert.equal(links.length, 1);
       assert.equal(links[0].getAttribute('href'), 'https://google.com');
+      assert.equal(links[0].innerHTML, 'https://google.com');
+
+      element.content = 'xx ssh://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 0);
+
+      element.content = 'xx ftp://google.com yy';
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 0);
+    });
+
+    test('links without leading whitespace are linkified', () => {
+      element.content = 'xx abcmailto:test@google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
+      let links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+      assert.equal(links[0].innerHTML, 'mailto:test@google.com');
+
+      element.content = 'xx defhttp://google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'http://google.com');
+      assert.equal(links[0].innerHTML, 'http://google.com');
+
+      element.content = 'xx qwehttps://google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'https://google.com');
+      assert.equal(links[0].innerHTML, 'https://google.com');
+
+      // Non-latin character
+      element.content = 'xx абвhttps://google.com yy';
+      assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
+      links = element.$.output.querySelectorAll('a');
+      assert.equal(links.length, 1);
+      assert.equal(links[0].getAttribute('href'), 'https://google.com');
+      assert.equal(links[0].innerHTML, 'https://google.com');
 
       element.content = 'xx ssh://google.com yy';
       links = element.$.output.querySelectorAll('a');
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index eb2b0d0..aac6d76 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -22,7 +22,7 @@
    *
    * @type {RegExp}
    */
-  const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/;
+  const URL_PROTOCOL_PATTERN = /^(.*)(https?:\/\/|mailto:)/;
 
   /**
    * Construct a parser for linkifying text. Will linkify plain URLs that appear
@@ -257,13 +257,29 @@
     // the source text does not include a protocol, the protocol will be added
     // by ba-linkify. Create the link if the href is provided and its protocol
     // matches the expected pattern.
-    if (href && URL_PROTOCOL_PATTERN.test(href)) {
-      this.addText(text, href);
-    } else {
-      // For the sections of text that lie between the links found by
-      // ba-linkify, we search for the project-config-specified link patterns.
-      this.parseLinks(text, this.linkConfig);
+    if (href) {
+      const result = URL_PROTOCOL_PATTERN.exec(href);
+      if (result) {
+        const prefixText = result[1];
+        if (prefixText.length > 0) {
+          // Fix for simple cases from
+          // https://bugs.chromium.org/p/gerrit/issues/detail?id=11697
+          // When leading whitespace is missed before link,
+          // linkify add this text before link as a schema name to href.
+          // We suppose, that prefixText just a single word
+          // before link and add this word as is, without processing
+          // any patterns in it.
+          this.parseLinks(prefixText, []);
+          text = text.substring(prefixText.length);
+          href = href.substring(prefixText.length);
+        }
+        this.addText(text, href);
+        return;
+      }
     }
+    // For the sections of text that lie between the links found by
+    // ba-linkify, we search for the project-config-specified link patterns.
+    this.parseLinks(text, this.linkConfig);
   };
 
   /**
@@ -304,14 +320,15 @@
         let result = match[0].replace(pattern,
             patterns[p].html || patterns[p].link);
 
-        let i;
-        // Skip portion of replacement string that is equal to original.
-        for (i = 0; i < result.length; i++) {
-          if (result[i] !== match[0][i]) { break; }
-        }
-        result = result.slice(i);
-
         if (patterns[p].html) {
+          let i;
+          // Skip portion of replacement string that is equal to original to
+          // allow overlapping patterns.
+          for (i = 0; i < result.length; i++) {
+            if (result[i] !== match[0][i]) { break; }
+          }
+          result = result.slice(i);
+
           this.addHTML(
               result,
               susbtrIndex + match.index + i,
@@ -321,8 +338,8 @@
           this.addLink(
               match[0],
               result,
-              susbtrIndex + match.index + i,
-              match[0].length - i,
+              susbtrIndex + match.index,
+              match[0].length,
               outputArray);
         } else {
           throw Error('linkconfig entry ' + p +
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index 5dee2fe..3a957ef 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -63,13 +63,13 @@
     }
 
     open(...args) {
-      return new Promise(resolve => {
+      return new Promise((resolve, reject) => {
         Polymer.IronOverlayBehaviorImpl.open.apply(this, args);
         if (this._isMobile()) {
           this.fire('fullscreen-overlay-opened');
           this._fullScreenOpen = true;
         }
-        this._awaitOpen(resolve);
+        this._awaitOpen(resolve, reject);
       });
     }
 
@@ -96,7 +96,7 @@
      * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
      * opening. Eventually replace with a direct way to listen to the overlay.
      */
-    _awaitOpen(fn) {
+    _awaitOpen(fn, reject) {
       let iters = 0;
       const step = () => {
         this.async(() => {
@@ -105,11 +105,7 @@
           } else if (iters++ < AWAIT_MAX_ITERS) {
             step.call(this);
           } else {
-            // TODO(crbug.com/gerrit/10774): Once this is confirmed as the root
-            // cause of the bug, fix it by either making sure to resolve the fn
-            // function or find a better way to listen on the overlay being
-            // shown.
-            console.warn('gr-overlay _awaitOpen failed to resolve');
+            reject(new Error('gr-overlay _awaitOpen failed to resolve'));
           }
         }, AWAIT_STEP);
       };
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 4aefe46..ce26bf6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -990,6 +990,20 @@
      * @param {function()=} opt_cancelCondition
      */
     getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
+      return this.getConfig(false).then(config => {
+        const optionsHex = this._getChangeOptionsHex(config);
+        return this._getChangeDetail(
+            changeNum, optionsHex, opt_errFn, opt_cancelCondition)
+            .then(GrReviewerUpdatesParser.parse);
+      });
+    }
+
+    _getChangeOptionsHex(config) {
+      if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage
+          && !(config.receive && config.receive.enable_signed_push)) {
+        return window.DEFAULT_DETAIL_HEXES.changePage;
+      }
+
       // This list MUST be kept in sync with
       // ChangeIT#changeDetailsDoesNotRequireIndex
       const options = [
@@ -1003,15 +1017,10 @@
         this.ListChangesOption.WEB_LINKS,
         this.ListChangesOption.SKIP_DIFFSTAT,
       ];
-      return this.getConfig(false).then(config => {
-        if (config.receive && config.receive.enable_signed_push) {
-          options.push(this.ListChangesOption.PUSH_CERTIFICATES);
-        }
-        const optionsHex = this.listChangesOptionsToHex(...options);
-        return this._getChangeDetail(
-            changeNum, optionsHex, opt_errFn, opt_cancelCondition)
-            .then(GrReviewerUpdatesParser.parse);
-      });
+      if (config.receive && config.receive.enable_signed_push) {
+        options.push(this.ListChangesOption.PUSH_CERTIFICATES);
+      }
+      return this.listChangesOptionsToHex(...options);
     }
 
     /**
@@ -1020,11 +1029,16 @@
      * @param {function()=} opt_cancelCondition
      */
     getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
-      const optionsHex = this.listChangesOptionsToHex(
-          this.ListChangesOption.ALL_COMMITS,
-          this.ListChangesOption.ALL_REVISIONS,
-          this.ListChangesOption.SKIP_DIFFSTAT
-      );
+      let optionsHex = '';
+      if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
+        optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
+      } else {
+        optionsHex = this.listChangesOptionsToHex(
+            this.ListChangesOption.ALL_COMMITS,
+            this.ListChangesOption.ALL_REVISIONS,
+            this.ListChangesOption.SKIP_DIFFSTAT
+        );
+      }
       return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
           opt_cancelCondition);
     }
@@ -1040,7 +1054,7 @@
         const urlWithParams = this._restApiHelper
             .urlWithParams(url, optionsHex);
         const params = {O: optionsHex};
-        let req = {
+        const req = {
           url,
           errFn: opt_errFn,
           cancelCondition: opt_cancelCondition,
@@ -1048,7 +1062,6 @@
           fetchOptions: this._etags.getOptions(urlWithParams),
           anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
         };
-        req = this._restApiHelper.addAcceptJsonHeader(req);
         return this._restApiHelper.fetchRawJSON(req).then(response => {
           if (response && response.status === 304) {
             return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
@@ -2039,12 +2052,15 @@
        * @param {string|number=} opt_patchNum
        * @return {!Promise<!Object>} Diff comments response.
        */
+      // We don't want to add accept header, since preloading of comments is
+      // working only without accept header.
+      const noAcceptHeader = true;
       const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
         changeNum,
         endpoint,
         patchNum: opt_patchNum,
         reportEndpointAsIs: true,
-      });
+      }, noAcceptHeader);
 
       if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
         return fetchComments();
@@ -2609,7 +2625,7 @@
      * @param {Gerrit.ChangeFetchRequest} req
      * @return {!Promise<!Object>}
      */
-    _getChangeURLAndFetch(req) {
+    _getChangeURLAndFetch(req, noAcceptHeader) {
       const anonymizedEndpoint = req.reportEndpointAsIs ?
         req.endpoint : req.anonymizedEndpoint;
       const anonymizedBaseUrl = req.patchNum ?
@@ -2622,7 +2638,7 @@
             fetchOptions: req.fetchOptions,
             anonymizedUrl: anonymizedEndpoint ?
               (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
-          }));
+          }, noAcceptHeader));
     }
 
     /**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index ca4b246..ed7d29f 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -1036,16 +1036,6 @@
         });
       });
 
-      test('_getChangeDetail accepts only json', () => {
-        const authFetchStub = sandbox.stub(element._auth, 'fetch')
-            .returns(Promise.resolve());
-        const errFn = sinon.stub();
-        element._getChangeDetail(123, '516714', errFn);
-        assert.isTrue(authFetchStub.called);
-        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
-            'application/json');
-      });
-
       test('_getChangeDetail populates _projectLookup', () => {
         sandbox.stub(element, 'getChangeActionURL')
             .returns(Promise.resolve(''));
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
index 91fef29..2c326df 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
@@ -204,9 +204,12 @@
      * Same as {@link fetchRawJSON}, plus error handling.
      *
      * @param {Gerrit.FetchJSONRequest} req
+     * @param {boolean} noAcceptHeader - don't add default accept json header
      */
-    fetchJSON(req) {
-      req = this.addAcceptJsonHeader(req);
+    fetchJSON(req, noAcceptHeader) {
+      if (!noAcceptHeader) {
+        req = this.addAcceptJsonHeader(req);
+      }
       return this.fetchRawJSON(req).then(response => {
         if (!response) {
           return;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
index b4fefe1..ec56912 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
@@ -16,17 +16,20 @@
 -->
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-icons/gr-icons.html">
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 
 <dom-module id="gr-tooltip-content">
   <template>
     <style>
-      .arrow {
-        color: var(--arrow-color);
+      iron-icon {
+        width: var(--line-height-normal);
+        height: var(--line-height-normal);
+        vertical-align: top;
       }
     </style>
     <slot></slot><!--
- --><span class="arrow" hidden$="[[!showIcon]]">&#9432;</span>
+ --><iron-icon icon="gr-icons:info" hidden$="[[!showIcon]]"></iron-icon>
   </template>
   <script src="gr-tooltip-content.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
index 4276195..7098d7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -43,7 +43,7 @@
 
     test('icon is not visible by default', () => {
       assert.equal(Polymer.dom(element.root)
-          .querySelector('.arrow').hidden, true);
+          .querySelector('iron-icon').hidden, true);
     });
 
     test('position-below attribute is reflected', () => {
@@ -55,7 +55,7 @@
     test('icon is visible with showIcon property', () => {
       element.showIcon = true;
       assert.equal(Polymer.dom(element.root)
-          .querySelector('.arrow').hidden, false);
+          .querySelector('iron-icon').hidden, false);
     });
   });
 </script>
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 5046a2a..b182309 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -28,6 +28,11 @@
   {@param? polyfillSD: ?}
   {@param? polyfillSC: ?}
   {@param? useGoogleFonts: ?}
+  {@param? changeRequestsPath: ?}
+  {@param? defaultChangeDetailHex: ?}
+  {@param? defaultDiffDetailHex: ?}
+  {@param? preloadChangePage: ?}
+  {@param? preloadDiffPage: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}
@@ -43,6 +48,14 @@
     // Disable extra font load from paper-styles
     window.polymerSkipLoadingFontRoboto = true;
     window.CLOSURE_NO_DEPS = true;
+    window.DEFAULT_DETAIL_HEXES = {lb}
+      {if $defaultChangeDetailHex}
+        changePage: '{$defaultChangeDetailHex}',
+      {/if}
+      {if $defaultDiffDetailHex}
+        diffPage: '{$defaultDiffDetailHex}',
+      {/if}
+    {rb};
     {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
     {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
     {if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
@@ -68,6 +81,16 @@
   {else}
     <link rel="icon" type="image/x-icon" href="{$canonicalPath}/favicon.ico">{\n}
   {/if}
+  {if $changeRequestsPath}
+    {if $preloadChangePage and $defaultChangeDetailHex}
+      <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/detail?O={$defaultChangeDetailHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+    {/if}
+    {if $preloadDiffPage and $defaultDiffDetailHex}
+      <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/detail?O={$defaultDiffDetailHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+    {/if}
+    <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/comments" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+    <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/robotcomments" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+  {/if}
 
   // RobotoMono fonts are used in styles/fonts.css
   {if $useGoogleFonts}
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 7c94f32..84eef77 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -13,6 +13,7 @@
 bzl = text/x-python
 BUCK = text/x-python
 BUILD = text/x-python
+BUILD.bazel = text/x-python
 c = text/x-csrc
 cfg = text/x-ttcn-cfg
 cl = text/x-common-lisp
@@ -247,6 +248,7 @@
 vtl = text/velocity
 webidl = text/x-webidl
 wsdl = application/xml
+WORKSPACE = text/x-python
 xaml = application/xml
 xhtml = text/html
 xml = application/xml
diff --git a/tools/dev-hooks/pre-commit b/tools/dev-hooks/pre-commit
index b77f382..af87b7e 100755
--- a/tools/dev-hooks/pre-commit
+++ b/tools/dev-hooks/pre-commit
@@ -31,7 +31,7 @@
 eslint=${gitroot}/node_modules/eslint/bin/eslint.js
 
 # Run eslint over changed frontend code
-CHANGED_UI_FILES=$(git diff --cached --name-only -- '*.js' '*.html' | grep 'polygerrit-ui') && true
+CHANGED_UI_FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.js' '*.html' | grep 'polygerrit-ui') && true
 if [ "${CHANGED_UI_FILES}" ]; then
   if $eslint --fix ${CHANGED_UI_FILES}; then
     # Add again in case lint fix modified some files
diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
index c9a5df6..c1d7d00 100755
--- a/tools/js/download_bower.py
+++ b/tools/js/download_bower.py
@@ -46,7 +46,8 @@
         raise
     out, err = p.communicate()
     if p.returncode:
-        sys.stderr.write(err)
+        # For python3 support we wrap str around err.
+        sys.stderr.write(str(err))
         raise OSError('Command failed: %s' % ' '.join(cmd))
 
     try: