Merge "Revert "Add Start Review button to Change View""
diff --git a/Documentation/dev-cla.txt b/Documentation/dev-cla.txt
index a5a9932..2aa459c 100644
--- a/Documentation/dev-cla.txt
+++ b/Documentation/dev-cla.txt
@@ -1,7 +1,7 @@
 :linkattrs:
 = Gerrit Code Review - Contributor License Agreement
 
-In order to link::dev-community.html#how-to-contribute[contribute] to
+In order to link:dev-community.html#how-to-contribute[contribute] to
 Gerrit a Contributor License Agreement must be completed before
 contributions are accepted. To view and accept the agreements do the
 following:
diff --git a/Documentation/dev-community.txt b/Documentation/dev-community.txt
index 5892253..53829c9 100644
--- a/Documentation/dev-community.txt
+++ b/Documentation/dev-community.txt
@@ -18,10 +18,7 @@
 * link:dev-design.html[System Design]
 * Processes
 ** link:dev-processes.html#project-governance[Project Governance / Engineering Steering Committee]
-** link:dev-contributing.html#contribution-processes[Contribution Processes]
-*** link:dev-contributing.html#lightweight-contribution-process[Lightweight Contribution Process]
-*** link:dev-contributing.html#design-driven-contribution-process[Design-Driven Contribution Process]
-*** link:dev-contributing.html#mentorship[Mentorship]
+** link:#how-to-contribute[Contribution Processes]
 ** link:dev-design-docs.html#review[Design doc reviews]
 ** link:dev-processes.html#dev-in-stable-branches[Development in stable branches]
 ** link:dev-processes.html#backporting[Backporting to stable branches]
@@ -38,7 +35,7 @@
 ** link:dev-roles.html#release-manager[Release Manager]
 
 [[how-to-contribute]]
-== How to contribute?
+== How to Contribute
 * link:dev-cla.html[Contributor License Agreement]
 * link:dev-contributing.html#contribution-processes[Contribution Processes]
 ** link:dev-contributing.html#lightweight-contribution-process[Lightweight Contribution Process]
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 30da8c5..470faf4 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -107,16 +107,16 @@
 file contains the commands and repository used during the e2e 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.
+`com.google.gerrit.scenarios` package. The uppercase keywords are discussed further below.
 
 ----
 [
   {
-    "url": "ssh://admin@localhost:29418/loadtest-repo",
+    "url": "ssh://admin@HOSTNAME:SSH_PORT/loadtest-repo",
     "cmd": "clone"
   },
   {
-    "url": "http://localhost:8080/loadtest-repo",
+    "url": "http://HOSTNAME:HTTP_PORT/loadtest-repo",
     "cmd": "clone"
   }
 ]
@@ -143,6 +143,21 @@
 Executing the `CloneUsingBothProtocols` scenario, as is, does require setting the http credentials.
 That is because of the aforementioned create/delete project (http) scenarios composed within it.
 
+=== Environment properties
+
+The `JAVA_OPTS` environment variable
+link:https://gatling.io/docs/current/cookbook/passing_parameters[can optionally be used] to define
+non-default values for keys found in scenario `json` data files. That variable can currently be set
+with either one or many of these supported properties, from the core framework:
+
+* `-Dcom.google.gerrit.scenarios.hostname=localhost`
+* `-Dcom.google.gerrit.scenarios.ssh_port=29418`
+* `-Dcom.google.gerrit.scenarios.http_port=8080`
+
+Above, the properties can be set with values matching specific deployment topologies under test.
+The example values shown above are the currently coded default ones. The framework could support
+differing or more properties over time. Plugin (non-core) scenarios may do so just as well.
+
 == How to run tests
 
 Run all tests:
diff --git a/Documentation/index.txt b/Documentation/index.txt
index d270f97..4fb977a 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -10,11 +10,13 @@
 . link:intro-quick.html[Product Overview]
 . link:intro-how-gerrit-works.html[How Gerrit Works]
 . link:intro-gerrit-walkthrough.html[Basic Gerrit Walkthrough]
-.. link:intro-gerrit-walkthrough-github.html[Basic Gerrit Walkthrough -- For GitHub Users]
-. link:dev-community.html[Gerrit Community]
-.. link:dev-contributing.html[Contributor Guide]
+. link:intro-gerrit-walkthrough-github.html[Basic Gerrit Walkthrough -- For GitHub Users]
 
-== Guides
+== Contributor Guides
+. link:dev-community.html[Gerrit Community]
+. link:dev-community.html#how-to-contribute[How to Contribute]
+
+== User Guides
 . link:intro-user.html[User Guide]
 . link:intro-project-owner.html[Project Owner Guide]
 . link:https://source.android.com/source/developing[Default Android Workflow,role=external,window=_blank] (external)
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
index 0335b2f..1125687 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
@@ -1,10 +1,10 @@
 [
   {
-    "url": "ssh://admin@localhost:29418/loadtest-repo",
+    "url": "ssh://admin@HOSTNAME:SSH_PORT/loadtest-repo",
     "cmd": "clone"
   },
   {
-    "url": "http://localhost:8080/loadtest-repo",
+    "url": "http://HOSTNAME:HTTP_PORT/loadtest-repo",
     "cmd": "clone"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
index 2e54de5..f1a38ae 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://localhost:8080/a/projects/loadtest-repo"
+    "url": "http://HOSTNAME:HTTP_PORT/a/projects/loadtest-repo"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
index 9312fb4..e5167b5 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://localhost:8080/a/projects/loadtest-repo/delete-project~delete"
+    "url": "http://HOSTNAME:HTTP_PORT/a/projects/loadtest-repo/delete-project~delete"
   }
 ]
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
index 19fbf1b..182ac48 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
@@ -21,7 +21,7 @@
 import scala.concurrent.duration._
 
 class CloneUsingBothProtocols extends GitSimulation {
-  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
+  private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).queue
 
   private val test: ScenarioBuilder = scenario(name)
       .feed(data)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
index 58c8994..13d3519 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateProject.scala
@@ -19,7 +19,7 @@
 import io.gatling.core.structure.ScenarioBuilder
 
 class CreateProject extends GerritSimulation {
-  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
+  private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).queue
 
   val test: ScenarioBuilder = scenario(name)
       .feed(data)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
index 4b723cb..70b901d 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
@@ -19,7 +19,7 @@
 import io.gatling.core.structure.ScenarioBuilder
 
 class DeleteProject extends GerritSimulation {
-  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
+  private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).queue
 
   val test: ScenarioBuilder = scenario(name)
       .feed(data)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
index b628bc7..a159977 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
@@ -23,7 +23,8 @@
 class GerritSimulation extends Simulation {
   implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
 
-  private val path: String = this.getClass.getPackage.getName.replaceAllLiterally(".", "/")
+  private val pack: String = this.getClass.getPackage.getName
+  private val path: String = pack.replaceAllLiterally(".", "/")
   protected val name: String = this.getClass.getSimpleName
   protected val resource: String = s"data/$path/$name.json"
 
@@ -31,4 +32,27 @@
   protected val httpProtocol: HttpProtocolBuilder = http.basicAuth(
     conf.httpConfiguration.userName,
     conf.httpConfiguration.password)
+
+  protected val url: PartialFunction[(String, Any), Any] = {
+    case ("url", url) =>
+      var in = replaceProperty("hostname", "localhost", url.toString)
+      in = replaceProperty("http_port", 8080, in)
+      replaceProperty("ssh_port", 29418, in)
+  }
+
+  private def replaceProperty(term: String, default: Any, in: String): String = {
+    val key: String = term.toUpperCase
+    val property = pack + "." + term
+    var value = default
+    default match {
+      case _: String =>
+        val propertyValue = Option(System.getProperty(property))
+        if (propertyValue.nonEmpty) {
+          value = propertyValue.get
+        }
+      case _: Integer =>
+        value = Integer.getInteger(property, default.asInstanceOf[Integer])
+    }
+    in.replaceAllLiterally(key, value.toString)
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/RestSession.java b/java/com/google/gerrit/acceptance/RestSession.java
index 0e7ad4b..7ee1b26 100644
--- a/java/com/google/gerrit/acceptance/RestSession.java
+++ b/java/com/google/gerrit/acceptance/RestSession.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.net.HttpHeaders.ACCEPT;
+import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+import static com.google.gerrit.json.OutputFormat.JSON_COMPACT;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.net.HttpHeaders;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RawInput;
-import com.google.gerrit.json.OutputFormat;
 import java.io.IOException;
 import org.apache.http.Header;
 import org.apache.http.client.fluent.Request;
@@ -40,7 +41,7 @@
   }
 
   public RestResponse getJsonAccept(String endPoint) throws IOException {
-    return getWithHeader(endPoint, new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
+    return getWithHeader(endPoint, new BasicHeader(ACCEPT, "application/json"));
   }
 
   public RestResponse getWithHeader(String endPoint, Header header) throws IOException {
@@ -74,8 +75,7 @@
       put.addHeader(header);
     }
     if (content != null) {
-      put.addHeader(new BasicHeader("Content-Type", "application/json"));
-      put.body(new StringEntity(OutputFormat.JSON_COMPACT.newGson().toJson(content), UTF_8));
+      addContentToRequest(put, content);
     }
     return execute(put);
   }
@@ -83,7 +83,7 @@
   public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
     requireNonNull(stream);
     Request put = Request.Put(getUrl(endPoint));
-    put.addHeader(new BasicHeader("Content-Type", stream.getContentType()));
+    put.addHeader(new BasicHeader(CONTENT_TYPE, stream.getContentType()));
     put.body(
         new BufferedHttpEntity(
             new InputStreamEntity(stream.getInputStream(), stream.getContentLength())));
@@ -105,12 +105,16 @@
       post.addHeader(header);
     }
     if (content != null) {
-      post.addHeader(new BasicHeader("Content-Type", "application/json"));
-      post.body(new StringEntity(OutputFormat.JSON_COMPACT.newGson().toJson(content), UTF_8));
+      addContentToRequest(post, content);
     }
     return execute(post);
   }
 
+  private static void addContentToRequest(Request request, Object content) {
+    request.addHeader(new BasicHeader(CONTENT_TYPE, "application/json"));
+    request.body(new StringEntity(JSON_COMPACT.newGson().toJson(content), UTF_8));
+  }
+
   public RestResponse delete(String endPoint) throws IOException {
     return execute(Request.Delete(getUrl(endPoint)));
   }
diff --git a/java/com/google/gerrit/common/data/PatchScript.java b/java/com/google/gerrit/common/data/PatchScript.java
index d8ec6a9..e3c0ba6 100644
--- a/java/com/google/gerrit/common/data/PatchScript.java
+++ b/java/com/google/gerrit/common/data/PatchScript.java
@@ -16,8 +16,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
@@ -63,13 +61,11 @@
     }
   }
 
-  private final Change.Key changeId;
   private final ChangeType changeType;
   private final ImmutableList<String> header;
   private final DiffPreferencesInfo diffPrefs;
   private final ImmutableList<Edit> edits;
   private final ImmutableSet<Edit> editsDueToRebase;
-  private final ImmutableList<Patch> history;
   private final boolean intralineFailure;
   private final boolean intralineTimeout;
   private final boolean binary;
@@ -77,7 +73,6 @@
   private final PatchScriptFileInfo fileInfoB;
 
   public PatchScript(
-      Change.Key ck,
       ChangeType ct,
       String on,
       String nn,
@@ -93,19 +88,16 @@
       DisplayMethod mb,
       String mta,
       String mtb,
-      ImmutableList<Patch> hist,
       boolean idf,
       boolean idt,
       boolean bin,
       String cma,
       String cmb) {
-    changeId = ck;
     changeType = ct;
     header = h;
     diffPrefs = dp;
     edits = e;
     this.editsDueToRebase = editsDueToRebase;
-    history = hist;
     intralineFailure = idf;
     intralineTimeout = idt;
     binary = bin;
@@ -114,10 +106,6 @@
     fileInfoB = new PatchScriptFileInfo(nn, nm, cb, mb, mtb, cmb);
   }
 
-  public Change.Key getChangeId() {
-    return changeId;
-  }
-
   public List<String> getPatchHeader() {
     return header;
   }
@@ -134,10 +122,6 @@
     return fileInfoB.name;
   }
 
-  public List<Patch> getHistory() {
-    return history;
-  }
-
   public DiffPreferencesInfo getDiffPrefs() {
     return diffPrefs;
   }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index d3fa8da..706e796 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -409,7 +409,7 @@
     if (fields.contains(ChangeField.ATTENTION_SET_FULL.getName())) {
       ChangeField.parseAttentionSet(
           FluentIterable.from(source.getAsJsonArray(ChangeField.ATTENTION_SET_FULL.getName()))
-              .transform(JsonElement::getAsString)
+              .transform(ElasticChangeIndex::decodeBase64JsonElement)
               .toSet(),
           cd);
     }
@@ -432,12 +432,16 @@
     }
     ChangeField.parseSubmitRecords(
         FluentIterable.from(records)
-            .transform(i -> new String(decodeBase64(i.getAsString()), UTF_8))
+            .transform(ElasticChangeIndex::decodeBase64JsonElement)
             .toList(),
         opts,
         out);
   }
 
+  private static String decodeBase64JsonElement(JsonElement input) {
+    return new String(decodeBase64(input.getAsString()), UTF_8);
+  }
+
   private void decodeUnresolvedCommentCount(JsonObject doc, String fieldName, ChangeData out) {
     JsonElement count = doc.get(fieldName);
     if (count == null) {
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index cc38bda..d4589d4 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -168,12 +168,6 @@
   /** What type of patch is this; see {@link PatchType}. */
   protected char patchType;
 
-  /** Number of published comments on this patch. */
-  protected int nbrComments;
-
-  /** Number of drafts by the current user; not persisted in the datastore. */
-  protected int nbrDrafts;
-
   /** Number of lines added to the file. */
   protected int insertions;
 
@@ -198,22 +192,6 @@
     return key;
   }
 
-  public int getCommentCount() {
-    return nbrComments;
-  }
-
-  public void setCommentCount(int n) {
-    nbrComments = n;
-  }
-
-  public int getDraftCount() {
-    return nbrDrafts;
-  }
-
-  public void setDraftCount(int n) {
-    nbrDrafts = n;
-  }
-
   public int getInsertions() {
     return insertions;
   }
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 5087d0a..75a17a5 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.template.soy.data.ordainers.GsonOrdainer.serializeObject;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
@@ -37,8 +38,9 @@
 import com.google.template.soy.data.SanitizedContent;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -160,6 +162,18 @@
     return data.build();
   }
 
+  /** Returns experimentData to be used in {@code index.html}. */
+  public static Set<String> experimentData(Map<String, String[]> urlParameterMap) {
+    // Allow enable experiments with url
+    // ?experiment=a&experiment=b should result in:
+    // "experiment" => [a,b]
+    if (urlParameterMap.containsKey("experiment")) {
+      return Arrays.asList(urlParameterMap.get("experiment")).stream().collect(toSet());
+    }
+
+    return Collections.emptySet();
+  }
+
   /** Returns all static parameters of {@code index.html}. */
   static Map<String, Object> staticTemplateData(
       String canonicalURL,
@@ -217,24 +231,6 @@
     return data.build();
   }
 
-  /** Returns experimentData to be used in {@code index.html}. */
-  static Set<String> experimentData(Map<String, String[]> urlParameterMap)
-      throws URISyntaxException {
-    Set<String> enabledExperiments = new HashSet<>();
-
-    // Allow enable experiments with url
-    // ?experiment=a&experiment=b should result in:
-    // "experiment" => [a,b]
-    if (urlParameterMap.containsKey("experiment")) {
-      String[] experiments = urlParameterMap.get("experiment");
-      for (String exp : experiments) {
-        enabledExperiments.add(exp);
-      }
-    }
-
-    return enabledExperiments;
-  }
-
   private static String computeCanonicalPath(@Nullable String canonicalURL)
       throws URISyntaxException {
     if (Strings.isNullOrEmpty(canonicalURL)) {
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 4fe9daa..345da81 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -137,7 +137,9 @@
     try {
       Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
       if (!optionalExtId.isPresent()) {
-        // New account, automatically create and return.
+        logger.atFine().log(
+            "External ID for account %s not found. A new account will be automatically created.",
+            who.getUserName());
         return create(who);
       }
 
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index c12f3a7..60a1e3e 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -148,8 +148,9 @@
   }
 
   /**
-   * @see GroupConfig#loadForGroup(Project.NameKey, Repository, AccountGroup.UUID). This method will
-   *     load the group for a specific revision.
+   * Load the group for a specific revision.
+   *
+   * @see GroupConfig#loadForGroup(Project.NameKey, Repository, AccountGroup.UUID)
    */
   public static GroupConfig loadForGroup(
       Project.NameKey projectName,
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index c6eb398..2be1324 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -31,7 +31,6 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Enums;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -47,7 +46,6 @@
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AttentionSetUpdate;
-import com.google.gerrit.entities.AttentionSetUpdate.Operation;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.entities.PatchSetApproval;
@@ -297,7 +295,7 @@
     final long timestampMillis;
     final int userId;
     final String reason;
-    final Operation operation;
+    final AttentionSetUpdate.Operation operation;
 
     StoredAttentionSetEntry(AttentionSetUpdate attentionSetUpdate) {
       timestampMillis = attentionSetUpdate.timestamp().toEpochMilli();
@@ -423,8 +421,7 @@
         continue;
       }
 
-      com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
-          Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
+      Optional<ReviewerStateInternal> reviewerState = getReviewerState(v.substring(0, i));
       if (!reviewerState.isPresent()) {
         logger.atWarning().log(
             "Failed to parse reviewer state of reviewer field from change %s: %s",
@@ -475,8 +472,7 @@
         continue;
       }
 
-      com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
-          Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
+      Optional<ReviewerStateInternal> reviewerState = getReviewerState(v.substring(0, i));
       if (!reviewerState.isPresent()) {
         logger.atWarning().log(
             "Failed to parse reviewer state of reviewer by email field from change %s: %s",
@@ -506,6 +502,14 @@
     return ReviewerByEmailSet.fromTable(b.build());
   }
 
+  private static Optional<ReviewerStateInternal> getReviewerState(String value) {
+    try {
+      return Optional.of(ReviewerStateInternal.valueOf(value));
+    } catch (IllegalArgumentException | NullPointerException e) {
+      return Optional.empty();
+    }
+  }
+
   private static ImmutableSet<Integer> getAttentionSetUserIds(ChangeData changeData) {
     return additionsOnly(changeData.attentionSet()).stream()
         .map(update -> update.account().get())
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index fee9088..624c5d7 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -138,8 +138,6 @@
    * <p>The returned list items do not populate:
    *
    * <ul>
-   *   <li>{@link Patch#getCommentCount()}
-   *   <li>{@link Patch#getDraftCount()}
    *   <li>{@link Patch#isReviewedByCurrentUser()}
    * </ul>
    *
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index d88b626..9b8409d 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
-import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.FixReplacement;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Patch.ChangeType;
@@ -53,7 +52,6 @@
 
 class PatchScriptBuilder {
 
-  private Change change;
   private DiffPreferencesInfo diffPrefs;
   private final FileTypeRegistry registry;
   private IntraLineDiffCalculator intralineDiffCalculator;
@@ -63,10 +61,6 @@
     registry = ftr;
   }
 
-  void setChange(Change c) {
-    this.change = c;
-  }
-
   void setDiffPrefs(DiffPreferencesInfo dp) {
     diffPrefs = dp;
   }
@@ -76,11 +70,7 @@
   }
 
   PatchScript toPatchScript(
-      Repository git,
-      PatchList list,
-      PatchListEntry content,
-      CommentDetail comments,
-      ImmutableList<Patch> history)
+      Repository git, PatchList list, PatchListEntry content, CommentDetail comments)
       throws IOException {
 
     PatchFileChange change =
@@ -96,7 +86,7 @@
     ResolvedSides sides =
         resolveSides(
             git, sidesResolver, oldName(change), newName(change), list.getOldId(), list.getNewId());
-    return build(sides.a, sides.b, change, comments, history);
+    return build(sides.a, sides.b, change, comments);
   }
 
   private ResolvedSides resolveSides(
@@ -146,7 +136,7 @@
             ChangeType.MODIFIED,
             PatchType.UNIFIED);
 
-    return build(a, b, change, null, null);
+    return build(a, b, change, null);
   }
 
   private PatchSide resolveSideA(
@@ -158,11 +148,7 @@
   }
 
   private PatchScript build(
-      PatchSide a,
-      PatchSide b,
-      PatchFileChange content,
-      CommentDetail comments,
-      ImmutableList<Patch> history) {
+      PatchSide a, PatchSide b, PatchFileChange content, CommentDetail comments) {
 
     ImmutableList<Edit> contentEdits = content.getEdits();
     ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase();
@@ -181,7 +167,6 @@
             new TextSource(a.src), new TextSource(b.src), finalEdits, comments);
 
     return new PatchScript(
-        change.getKey(),
         content.getChangeType(),
         content.getOldName(),
         content.getNewName(),
@@ -197,7 +182,6 @@
         b.displayMethod,
         a.mimeType,
         b.mimeType,
-        history,
         intralineResult.failure,
         intralineResult.timeout,
         content.getPatchType() == Patch.PatchType.BINARY,
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 380fd07..29a89d6 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
-import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
@@ -52,8 +51,6 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
@@ -100,7 +97,6 @@
   private final ProjectCache projectCache;
 
   private final Change.Id changeId;
-  private boolean loadHistory = true;
   private boolean loadComments = true;
 
   private ChangeNotes notes;
@@ -178,10 +174,6 @@
     checkArgument(parentNum >= 0, "parentNum must be >= 0");
   }
 
-  public void setLoadHistory(boolean load) {
-    loadHistory = load;
-  }
-
   public void setLoadComments(boolean load) {
     loadComments = load;
   }
@@ -226,11 +218,9 @@
         final PatchScriptBuilder b = newBuilder();
         final PatchListEntry content = list.get(fileName);
 
-        Optional<ImmutableList<Patch>> history = loadHistory(content, changeEdit);
-        Optional<CommentDetail> comments =
-            loadComments(content, changeEdit, history.orElse(ImmutableList.of()));
+        Optional<CommentDetail> comments = loadComments(content, changeEdit);
 
-        return b.toPatchScript(git, list, content, comments.orElse(null), history.orElse(null));
+        return b.toPatchScript(git, list, content, comments.orElse(null));
       } catch (PatchListNotAvailableException e) {
         throw new NoSuchChangeException(changeId, e);
       } catch (IOException e) {
@@ -248,26 +238,12 @@
     }
   }
 
-  private Optional<CommentDetail> loadComments(
-      PatchListEntry content, boolean changeEdit, ImmutableList<Patch> history) {
+  private Optional<CommentDetail> loadComments(PatchListEntry content, boolean changeEdit) {
     if (!loadComments) {
       return Optional.empty();
     }
     return new CommentsLoader(psa, psb, userProvider, notes, commentsUtil)
-        .load(
-            changeEdit,
-            content.getChangeType(),
-            content.getOldName(),
-            content.getNewName(),
-            history);
-  }
-
-  private Optional<ImmutableList<Patch>> loadHistory(PatchListEntry content, boolean changeEdit) {
-    if (!loadHistory) {
-      return Optional.empty();
-    }
-    HistoryLoader loader = new HistoryLoader(psa, psb, psUtil, notes, fileName);
-    return Optional.of(loader.load(changeEdit, content.getChangeType(), content.getOldName()));
+        .load(changeEdit, content.getChangeType(), content.getOldName(), content.getNewName());
   }
 
   private Optional<ObjectId> getAId() {
@@ -300,7 +276,6 @@
 
   private PatchScriptBuilder newBuilder() {
     final PatchScriptBuilder b = builderFactory.get();
-    b.setChange(notes.getChange());
     b.setDiffPrefs(diffPrefs);
     if (diffPrefs.intralineDifference) {
       b.setIntraLineDiffCalculator(
@@ -325,59 +300,6 @@
     }
   }
 
-  private static class HistoryLoader {
-    private final PatchSet.Id psa;
-    private final PatchSet.Id psb;
-    private final PatchSetUtil psUtil;
-    private final ChangeNotes notes;
-    private final String fileName;
-
-    HistoryLoader(
-        PatchSet.Id psa, PatchSet.Id psb, PatchSetUtil psUtil, ChangeNotes notes, String fileName) {
-      this.psa = psa;
-      this.psb = psb;
-      this.psUtil = psUtil;
-      this.notes = notes;
-      this.fileName = fileName;
-    }
-
-    private ImmutableList<Patch> load(boolean changeEdit, ChangeType changeType, String oldName) {
-      // This seems like a cheap trick. It doesn't properly account for a
-      // file that gets renamed between patch set 1 and patch set 2. We
-      // will wind up packing the wrong Patch object because we didn't do
-      // proper rename detection between the patch sets.
-      //
-      ImmutableList.Builder<Patch> historyBuilder = ImmutableList.builder();
-      for (PatchSet ps : psUtil.byChange(notes)) {
-        String name = fileName;
-        if (psa != null) {
-          switch (changeType) {
-            case COPIED:
-            case RENAMED:
-              if (ps.id().equals(psa)) {
-                name = oldName;
-              }
-              break;
-
-            case MODIFIED:
-            case DELETED:
-            case ADDED:
-            case REWRITE:
-              break;
-          }
-        }
-
-        Patch p = new Patch(Patch.key(ps.id(), name));
-        historyBuilder.add(p);
-      }
-      if (changeEdit) {
-        Patch p = new Patch(Patch.key(PatchSet.id(psb.changeId(), 0), fileName));
-        historyBuilder.add(p);
-      }
-      return historyBuilder.build();
-    }
-  }
-
   private static class CommentsLoader {
     private final PatchSet.Id psa;
     private final PatchSet.Id psb;
@@ -400,36 +322,30 @@
     }
 
     private Optional<CommentDetail> load(
-        boolean changeEdit,
-        ChangeType changeType,
-        String oldName,
-        String newName,
-        ImmutableList<Patch> history) {
+        boolean changeEdit, ChangeType changeType, String oldName, String newName) {
       // TODO: Implement this method with CommentDetailBuilder (this class doesn't exists yet).
       // This is a legacy code which create final object and populate it and then returns it.
       if (changeEdit) {
         return Optional.empty();
       }
-      Map<Patch.Key, Patch> byKey = new HashMap<>();
-      history.forEach(p -> byKey.put(p.getKey(), p));
 
       comments = new CommentDetail(psa, psb);
       switch (changeType) {
         case ADDED:
         case MODIFIED:
-          loadPublished(byKey, newName);
+          loadPublished(newName);
           break;
 
         case DELETED:
-          loadPublished(byKey, newName);
+          loadPublished(newName);
           break;
 
         case COPIED:
         case RENAMED:
           if (psa != null) {
-            loadPublished(byKey, oldName);
+            loadPublished(oldName);
           }
-          loadPublished(byKey, newName);
+          loadPublished(newName);
           break;
 
         case REWRITE:
@@ -442,19 +358,19 @@
         switch (changeType) {
           case ADDED:
           case MODIFIED:
-            loadDrafts(byKey, me, newName);
+            loadDrafts(me, newName);
             break;
 
           case DELETED:
-            loadDrafts(byKey, me, newName);
+            loadDrafts(me, newName);
             break;
 
           case COPIED:
           case RENAMED:
             if (psa != null) {
-              loadDrafts(byKey, me, oldName);
+              loadDrafts(me, oldName);
             }
-            loadDrafts(byKey, me, newName);
+            loadDrafts(me, newName);
             break;
 
           case REWRITE:
@@ -464,27 +380,15 @@
       return Optional.of(comments);
     }
 
-    private void loadPublished(Map<Patch.Key, Patch> byKey, String file) {
+    private void loadPublished(String file) {
       for (Comment c : commentsUtil.publishedByChangeFile(notes, file)) {
         comments.include(notes.getChangeId(), c);
-        PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId);
-        Patch.Key pKey = Patch.key(psId, c.key.filename);
-        Patch p = byKey.get(pKey);
-        if (p != null) {
-          p.setCommentCount(p.getCommentCount() + 1);
-        }
       }
     }
 
-    private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, String file) {
+    private void loadDrafts(Account.Id me, String file) {
       for (Comment c : commentsUtil.draftByChangeFileAuthor(notes, file, me)) {
         comments.include(notes.getChangeId(), c);
-        PatchSet.Id psId = PatchSet.id(notes.getChangeId(), c.key.patchSetId);
-        Patch.Key pKey = Patch.key(psId, c.key.filename);
-        Patch p = byKey.get(pKey);
-        if (p != null) {
-          p.setDraftCount(p.getDraftCount() + 1);
-        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java b/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java
index 86dad71..accd2bd 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java
@@ -132,7 +132,6 @@
 
   private PatchScriptBuilder newBuilder() {
     PatchScriptBuilder b = builderFactory.get();
-    b.setChange(notes.getChange());
     b.setDiffPrefs(diffPrefs);
     return b;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 743f770..68e39e7 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -123,7 +123,7 @@
     }
 
     @Override
-    public Response<?> apply(ChangeResource resource, IdString id, FileContentInput input)
+    public Response<Object> apply(ChangeResource resource, IdString id, FileContentInput input)
         throws AuthException, BadRequestException, ResourceConflictException, IOException,
             PermissionBackendException {
       putEdit.apply(resource, id.get(), input);
@@ -142,7 +142,7 @@
     }
 
     @Override
-    public Response<?> apply(ChangeResource rsrc, IdString id, Input in)
+    public Response<Object> apply(ChangeResource rsrc, IdString id, Input in)
         throws IOException, AuthException, BadRequestException, ResourceConflictException,
             PermissionBackendException {
       return deleteContent.apply(rsrc, id.get());
@@ -245,7 +245,7 @@
     }
 
     @Override
-    public Response<?> apply(ChangeResource resource, Post.Input input)
+    public Response<Object> apply(ChangeResource resource, Post.Input input)
         throws AuthException, BadRequestException, IOException, ResourceConflictException,
             PermissionBackendException {
       Project.NameKey project = resource.getProject();
@@ -292,13 +292,13 @@
     }
 
     @Override
-    public Response<?> apply(ChangeEditResource rsrc, FileContentInput input)
+    public Response<Object> apply(ChangeEditResource rsrc, FileContentInput input)
         throws AuthException, BadRequestException, ResourceConflictException, IOException,
             PermissionBackendException {
       return apply(rsrc.getChangeResource(), rsrc.getPath(), input);
     }
 
-    public Response<?> apply(ChangeResource rsrc, String path, FileContentInput input)
+    public Response<Object> apply(ChangeResource rsrc, String path, FileContentInput input)
         throws AuthException, BadRequestException, ResourceConflictException, IOException,
             PermissionBackendException {
       if (input.content == null) {
@@ -343,13 +343,13 @@
     }
 
     @Override
-    public Response<?> apply(ChangeEditResource rsrc, Input input)
+    public Response<Object> apply(ChangeEditResource rsrc, Input input)
         throws AuthException, BadRequestException, ResourceConflictException, IOException,
             PermissionBackendException {
       return apply(rsrc.getChangeResource(), rsrc.getPath());
     }
 
-    public Response<?> apply(ChangeResource rsrc, String filePath)
+    public Response<Object> apply(ChangeResource rsrc, String filePath)
         throws AuthException, BadRequestException, IOException, ResourceConflictException,
             PermissionBackendException {
       try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index 48b35c2..f4e2ddd 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -143,7 +143,6 @@
     }
 
     try {
-      psf.setLoadHistory(false);
       psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT);
       PatchScript ps = psf.call();
       Project.NameKey projectName = resource.getRevision().getChange().getProject();
diff --git a/java/com/google/gerrit/truth/MapSubject.java b/java/com/google/gerrit/truth/MapSubject.java
new file mode 100644
index 0000000..8217920
--- /dev/null
+++ b/java/com/google/gerrit/truth/MapSubject.java
@@ -0,0 +1,56 @@
+/*
+ * 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.truth;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import java.util.Map;
+
+/**
+ * A Truth subject for maps providing additional methods simplifying tests but missing on Truth's
+ * {@link com.google.common.truth.MapSubject}.
+ */
+public class MapSubject extends com.google.common.truth.MapSubject {
+
+  private final Map<?, ?> map;
+
+  public static MapSubject assertThatMap(Map<?, ?> map) {
+    return assertAbout(mapEntries()).that(map);
+  }
+
+  public static Subject.Factory<MapSubject, Map<?, ?>> mapEntries() {
+    return MapSubject::new;
+  }
+
+  private MapSubject(FailureMetadata failureMetadata, Map<?, ?> map) {
+    super(failureMetadata, map);
+    this.map = map;
+  }
+
+  public IterableSubject keys() {
+    isNotNull();
+    return check("keys()").that(map.keySet());
+  }
+
+  public IterableSubject values() {
+    isNotNull();
+    return check("values()").that(map.values());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index f30926b..dcf2afd 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -28,6 +28,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.stream.Collectors.toList;
@@ -217,11 +218,10 @@
     gApi.groups().id(group2.get()).addGroups("ldap:external_g2");
 
     assertThat(groupIncludeCache.allExternalMembers())
-        .containsExactlyElementsIn(
+        .containsAtLeastElementsIn(
             ImmutableList.of(
                 AccountGroup.UUID.parse("ldap:external_g1"),
-                AccountGroup.UUID.parse("ldap:external_g2"),
-                AccountGroup.UUID.parse("global:Registered-Users")));
+                AccountGroup.UUID.parse("ldap:external_g2")));
 
     assertThat(groupIncludeCache.parentGroupsOf(AccountGroup.UUID.parse("ldap:external_g1")))
         .containsExactly(group1);
@@ -247,18 +247,14 @@
 
     /** GroupIncludeCache should return ldap:external_g2 only */
     assertThat(groupIncludeCache.allExternalMembers())
-        .containsExactlyElementsIn(
-            ImmutableList.of(
-                AccountGroup.UUID.parse("ldap:external_g2"),
-                AccountGroup.UUID.parse("global:Registered-Users")));
+        .contains(AccountGroup.UUID.parse("ldap:external_g2"));
 
     /** Testing groups.getExternalGroups() with the old Snapshot */
     assertThat(groups.getExternalGroups(snapshot.groupsRefs()))
-        .containsExactlyElementsIn(
+        .containsAtLeastElementsIn(
             ImmutableList.of(
                 AccountGroup.UUID.parse("ldap:external_g1"),
-                AccountGroup.UUID.parse("ldap:external_g2"),
-                AccountGroup.UUID.parse("global:Registered-Users")));
+                AccountGroup.UUID.parse("ldap:external_g2")));
   }
 
   private ObjectId getObjectIdFromSnapshot(GroupsSnapshotReader.Snapshot snapshot, String refName) {
@@ -928,7 +924,8 @@
     List<String> expectedGroups =
         groups.getAllGroupReferences().map(GroupReference::getName).sorted().collect(toList());
     assertThat(expectedGroups.size()).isAtLeast(2);
-    assertThat(gApi.groups().list().getAsMap().keySet())
+    assertThatMap(gApi.groups().list().getAsMap())
+        .keys()
         .containsExactlyElementsIn(expectedGroups)
         .inOrder();
   }
@@ -975,20 +972,19 @@
     gApi.groups().create(in);
 
     requestScopeOperations.setApiUser(user.id());
-    assertThat(gApi.groups().list().getAsMap()).doesNotContainKey(newGroupName);
+    assertThatMap(gApi.groups().list().getAsMap()).keys().doesNotContain(newGroupName);
 
     requestScopeOperations.setApiUser(admin.id());
     gApi.groups().id(newGroupName).addMembers(user.username());
 
     requestScopeOperations.setApiUser(user.id());
-    assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
+    assertThatMap(gApi.groups().list().getAsMap()).keys().contains(newGroupName);
   }
 
   @Test
   public void suggestGroup() throws Exception {
     Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap();
-    assertThat(groups).containsKey("Administrators");
-    assertThat(groups).hasSize(1);
+    assertThatMap(groups).keys().containsExactly("Administrators");
     assertBadRequest(gApi.groups().list().withSuggest("adm").withSubstring("foo"));
     assertBadRequest(gApi.groups().list().withSuggest("adm").withRegex("foo.*"));
     assertBadRequest(gApi.groups().list().withSuggest("adm").withUser("user"));
@@ -1005,19 +1001,15 @@
     // Choose a substring which isn't part of any group or test method within this class.
     String substring = "efghijk";
     Map<String, GroupInfo> groups = gApi.groups().list().withSubstring(substring).getAsMap();
-    assertThat(groups).containsKey(group);
-    assertThat(groups).hasSize(1);
+    assertThatMap(groups).keys().containsExactly(group);
 
     groups = gApi.groups().list().withSubstring("abcdefghi").getAsMap();
-    assertThat(groups).containsKey(group);
-    assertThat(groups).hasSize(1);
+    assertThatMap(groups).keys().containsExactly(group);
 
     String otherGroup = name("Abcdefghijklmnop2");
     gApi.groups().create(otherGroup);
     groups = gApi.groups().list().withSubstring(substring).getAsMap();
-    assertThat(groups).hasSize(2);
-    assertThat(groups).containsKey(group);
-    assertThat(groups).containsKey(otherGroup);
+    assertThatMap(groups).keys().containsExactly(group, otherGroup);
 
     groups = gApi.groups().list().withSubstring("non-existing-substring").getAsMap();
     assertThat(groups).isEmpty();
@@ -1026,15 +1018,13 @@
   @Test
   public void withRegex() throws Exception {
     Map<String, GroupInfo> groups = gApi.groups().list().withRegex("Admin.*").getAsMap();
-    assertThat(groups).containsKey("Administrators");
-    assertThat(groups).hasSize(1);
+    assertThatMap(groups).keys().containsExactly("Administrators");
 
     groups = gApi.groups().list().withRegex("admin.*").getAsMap();
     assertThat(groups).isEmpty();
 
     groups = gApi.groups().list().withRegex(".*istrators").getAsMap();
-    assertThat(groups).containsKey("Administrators");
-    assertThat(groups).hasSize(1);
+    assertThatMap(groups).keys().containsExactly("Administrators");
 
     assertBadRequest(gApi.groups().list().withRegex(".*istrators").withSubstring("s"));
   }
@@ -1043,8 +1033,7 @@
   public void allGroupInfoFieldsSetCorrectly() throws Exception {
     InternalGroup adminGroup = adminGroup();
     Map<String, GroupInfo> groups = gApi.groups().list().addGroup(adminGroup.getName()).getAsMap();
-    assertThat(groups).hasSize(1);
-    assertThat(groups).containsKey("Administrators");
+    assertThatMap(groups).keys().containsExactly("Administrators");
     assertGroupInfo(adminGroup, Iterables.getOnlyElement(groups.values()));
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 911a04d..c6a2819 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -89,21 +90,16 @@
     gApi.changes().id(changeId2).current().review(ReviewInput.approve());
     gApi.changes().id(changeId2).current().submit();
     Map<String, ActionInfo> actions1 = getChangeActions(changeId1);
-    assertThat(actions1).containsKey("revert");
-    assertThat(actions1).containsKey("revert_submission");
+    assertThatMap(actions1).keys().containsAtLeast("revert", "revert_submission");
     Map<String, ActionInfo> actions2 = getChangeActions(changeId2);
-    assertThat(actions2).containsKey("revert");
-    assertThat(actions2).containsKey("revert_submission");
+    assertThatMap(actions2).keys().containsAtLeast("revert", "revert_submission");
   }
 
   @Test
   public void revisionActionsOneChangePerTopicUnapproved() throws Exception {
     String changeId = createChangeWithTopic().getChangeId();
     Map<String, ActionInfo> actions = getActions(changeId);
-    assertThat(actions).hasSize(3);
-    assertThat(actions).containsKey("cherrypick");
-    assertThat(actions).containsKey("rebase");
-    assertThat(actions).containsKey("description");
+    assertThatMap(actions).keys().containsExactly("cherrypick", "rebase", "description");
   }
 
   @Test
@@ -507,11 +503,7 @@
   }
 
   private void commonActionsAssertions(Map<String, ActionInfo> actions) {
-    assertThat(actions).hasSize(4);
-    assertThat(actions).containsKey("cherrypick");
-    assertThat(actions).containsKey("submit");
-    assertThat(actions).containsKey("description");
-    assertThat(actions).containsKey("rebase");
+    assertThatMap(actions).keys().containsExactly("cherrypick", "submit", "description", "rebase");
   }
 
   private PushOneCommit.Result createChangeWithTopic() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 2a8cca0..a30c1c7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.ConfigSubject.assertThat;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.ExtensionRegistry;
@@ -430,7 +431,8 @@
 
     accessInput.add.put(REFS_ALL, accessSection);
     ProjectAccessInfo result = pApi().access(accessInput);
-    assertThat(result.groups.keySet())
+    assertThatMap(result.groups)
+        .keys()
         .containsExactly(
             SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
 
@@ -443,7 +445,8 @@
 
     // Get call returns groups too.
     ProjectAccessInfo loggedInResult = pApi().access();
-    assertThat(loggedInResult.groups.keySet())
+    assertThatMap(loggedInResult.groups)
+        .keys()
         .containsExactly(
             SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
 
@@ -456,7 +459,8 @@
     // PROJECT_OWNERS is invisible to anonymous user, but GetAccess disregards visibility.
     requestScopeOperations.setApiUserAnonymous();
     ProjectAccessInfo anonResult = pApi().access();
-    assertThat(anonResult.groups.keySet())
+    assertThatMap(anonResult.groups)
+        .keys()
         .containsExactly(
             SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
   }
@@ -510,12 +514,8 @@
 
     ProjectAccessInfo updatedAccessSectionInfo =
         gApi.projects().name(allProjects.get()).access(accessInput);
-    assertThat(
-            updatedAccessSectionInfo
-                .local
-                .get(AccessSection.GLOBAL_CAPABILITIES)
-                .permissions
-                .keySet())
+    assertThatMap(updatedAccessSectionInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+        .keys()
         .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
   }
 
@@ -544,12 +544,9 @@
 
       ProjectAccessInfo updatedAccessSectionInfo =
           gApi.projects().name(allProjects.get()).access(accessInput);
-      assertThat(
-              updatedAccessSectionInfo
-                  .local
-                  .get(AccessSection.GLOBAL_CAPABILITIES)
-                  .permissions
-                  .keySet())
+      assertThatMap(
+              updatedAccessSectionInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+          .keys()
           .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
     }
   }
@@ -644,12 +641,8 @@
 
     ProjectAccessInfo updatedProjectAccessInfo =
         gApi.projects().name(allProjects.get()).access(accessInput);
-    assertThat(
-            updatedProjectAccessInfo
-                .local
-                .get(AccessSection.GLOBAL_CAPABILITIES)
-                .permissions
-                .keySet())
+    assertThatMap(updatedProjectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+        .keys()
         .containsAtLeastElementsIn(accessSectionInfo.permissions.keySet());
 
     // Remove
@@ -657,12 +650,8 @@
     accessInput.remove.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
 
     updatedProjectAccessInfo = gApi.projects().name(allProjects.get()).access(accessInput);
-    assertThat(
-            updatedProjectAccessInfo
-                .local
-                .get(AccessSection.GLOBAL_CAPABILITIES)
-                .permissions
-                .keySet())
+    assertThatMap(updatedProjectAccessInfo.local.get(AccessSection.GLOBAL_CAPABILITIES).permissions)
+        .keys()
         .containsNoneIn(accessSectionInfo.permissions.keySet());
   }
 
@@ -754,14 +743,12 @@
 
     // Assert that the permission was synced from All-Projects (global) to All-Users (ref)
     Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local;
-    assertThat(local).isNotNull();
-    assertThat(local).containsKey(RefNames.REFS_GROUPS + "*");
+    assertThatMap(local).keys().contains(RefNames.REFS_GROUPS + "*");
     Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions;
-    assertThat(permissions).hasSize(2);
     // READ is the default permission and should be preserved by the syncer
-    assertThat(permissions.keySet()).containsExactly(Permission.READ, Permission.CREATE);
+    assertThatMap(permissions).keys().containsExactly(Permission.READ, Permission.CREATE);
     Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules;
-    assertThat(rules.values()).containsExactly(pri);
+    assertThatMap(rules).values().containsExactly(pri);
 
     // Revoke the permission
     accessInput.add.clear();
@@ -770,12 +757,10 @@
 
     // Assert that the permission was synced from All-Projects (global) to All-Users (ref)
     Map<String, AccessSectionInfo> local2 = gApi.projects().name("All-Users").access().local;
-    assertThat(local2).isNotNull();
-    assertThat(local2).containsKey(RefNames.REFS_GROUPS + "*");
+    assertThatMap(local2).keys().contains(RefNames.REFS_GROUPS + "*");
     Map<String, PermissionInfo> permissions2 = local2.get(RefNames.REFS_GROUPS + "*").permissions;
-    assertThat(permissions2).hasSize(1);
     // READ is the default permission and should be preserved by the syncer
-    assertThat(permissions2.keySet()).containsExactly(Permission.READ);
+    assertThatMap(permissions2).keys().containsExactly(Permission.READ);
   }
 
   @Test
@@ -803,14 +788,13 @@
 
     // Assert that the permissions were synced from All-Projects (global) to All-Users (ref)
     Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local;
-    assertThat(local).isNotNull();
-    assertThat(local).containsKey(RefNames.REFS_GROUPS + "*");
+    assertThatMap(local).keys().contains(RefNames.REFS_GROUPS + "*");
     Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions;
-    assertThat(permissions).hasSize(2);
     // READ is the default permission and should be preserved by the syncer
-    assertThat(permissions.keySet()).containsExactly(Permission.READ, Permission.CREATE);
+    assertThatMap(permissions).keys().containsExactly(Permission.READ, Permission.CREATE);
     Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules;
-    assertThat(rules.keySet())
+    assertThatMap(rules)
+        .keys()
         .containsExactly(SystemGroupBackend.REGISTERED_USERS.get(), adminGroupUuid().get());
     assertThat(rules.get(SystemGroupBackend.REGISTERED_USERS.get())).isEqualTo(pri);
     assertThat(rules.get(adminGroupUuid().get())).isEqualTo(pri);
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 6802873..2ae59b0 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -23,17 +23,12 @@
 import org.eclipse.jgit.lib.Config;
 
 public final class ElasticTestUtils {
-  public static class ElasticNodeInfo {
-    public final int port;
-
-    public ElasticNodeInfo(int port) {
-      this.port = port;
-    }
-  }
-
-  public static void configure(Config config, int port, String prefix, ElasticVersion version) {
+  public static void configure(
+      Config config, ElasticContainer container, String prefix, ElasticVersion version) {
+    String hostname = container.getHttpHost().getHostName();
+    int port = container.getHttpHost().getPort();
     config.setString("index", null, "type", "elasticsearch");
-    config.setString("elasticsearch", null, "server", "http://localhost:" + port);
+    config.setString("elasticsearch", null, "server", "http://" + hostname + ":" + port);
     config.setString("elasticsearch", null, "prefix", prefix);
     config.setInt("index", null, "maxLimit", 10000);
     String password = version == ElasticVersion.V5_6 ? "changeme" : null;
@@ -42,8 +37,8 @@
     }
   }
 
-  public static void configure(Config config, int port, String prefix) {
-    configure(config, port, prefix, null);
+  public static void configure(Config config, ElasticContainer container, String prefix) {
+    configure(config, container, prefix, null);
   }
 
   public static void createAllIndexes(Injector injector) {
@@ -55,12 +50,10 @@
   }
 
   public static Config getConfig(ElasticVersion version) {
-    ElasticNodeInfo elasticNodeInfo;
     ElasticContainer container = ElasticContainer.createAndStart(version);
-    elasticNodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
     String indicesPrefix = UUID.randomUUID().toString();
     Config cfg = new Config();
-    configure(cfg, elasticNodeInfo.port, indicesPrefix, version);
+    configure(cfg, container, indicesPrefix, version);
     return cfg;
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
index e5bd19f..ab2b98d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,8 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(
-        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
index e1aadb8..97f235c 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.GerritTestName;
@@ -33,18 +32,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -67,8 +62,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(
-        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
index fcec859..c490e78 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,8 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(
-        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
index 16f06d5..a6a8605 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,8 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(
-        elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index 7a69546..15d8dd6 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,7 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 1601756..d734f1e 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -16,7 +16,6 @@
 
 import static java.util.concurrent.TimeUnit.MINUTES;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.GerritTestName;
@@ -40,21 +39,17 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
   private static CloseableHttpAsyncClient client;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
+      client = HttpAsyncClients.createDefault();
+      client.start();
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
-    client = HttpAsyncClients.createDefault();
-    client.start();
   }
 
   @AfterClass
@@ -68,12 +63,16 @@
 
   @After
   public void closeIndex() throws Exception {
+    // Close the index after each test to prevent exceeding Elasticsearch's
+    // shard limit (see Issue 10120).
     client
         .execute(
             new HttpPost(
                 String.format(
-                    "http://localhost:%d/%s*/_close",
-                    nodeInfo.port, testName.getSanitizedMethodName())),
+                    "http://%s:%d/%s*/_close",
+                    container.getHttpHost().getHostName(),
+                    container.getHttpHost().getPort(),
+                    testName.getSanitizedMethodName())),
             HttpClientContext.create(),
             null)
         .get(5, MINUTES);
@@ -90,7 +89,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index 76a4f9b..28d798e 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,7 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
index 7813a02..6658d72 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_8);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,7 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index 4bfcb6c..7636e5d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,7 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index d927c2d..cbb8300d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -16,7 +16,6 @@
 
 import static java.util.concurrent.TimeUnit.MINUTES;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.GerritTestName;
@@ -40,21 +39,17 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
   private static CloseableHttpAsyncClient client;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
+      client = HttpAsyncClients.createDefault();
+      client.start();
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
-    client = HttpAsyncClients.createDefault();
-    client.start();
   }
 
   @AfterClass
@@ -68,12 +63,16 @@
 
   @After
   public void closeIndex() throws Exception {
+    // Close the index after each test to prevent exceeding Elasticsearch's
+    // shard limit (see Issue 10120).
     client
         .execute(
             new HttpPost(
                 String.format(
-                    "http://localhost:%d/%s*/_close",
-                    nodeInfo.port, testName.getSanitizedMethodName())),
+                    "http://%s:%d/%s*/_close",
+                    container.getHttpHost().getHostName(),
+                    container.getHttpHost().getPort(),
+                    testName.getSanitizedMethodName())),
             HttpClientContext.create(),
             null)
         .get(5, MINUTES);
@@ -90,7 +89,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index 447d47a..5e3c4fb 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,7 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 5eebf41..803b5da 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
 import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.InMemoryModule;
@@ -31,18 +30,14 @@
     return IndexConfig.createForElasticsearch();
   }
 
-  private static ElasticNodeInfo nodeInfo;
   private static ElasticContainer container;
 
   @BeforeClass
   public static void startIndexService() {
-    if (nodeInfo != null) {
-      // do not start Elasticsearch twice
-      return;
+    if (container == null) {
+      // Only start Elasticsearch once
+      container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
     }
-
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_6);
-    nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
   @AfterClass
@@ -63,7 +58,7 @@
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/package.json b/package.json
index 096fbba..18e76bf 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
     "test": "WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh",
     "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
     "eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
-    "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- --fix $(pwd)/polygerrit-ui/app",
+    "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
     "test-template": "./polygerrit-ui/app/run_template_test.sh",
     "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test"
   },
diff --git a/plugins/download-commands b/plugins/download-commands
index 3f5a024..e26ed31 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 3f5a024fd46f30f4646bfceb285763e44fda15a7
+Subproject commit e26ed31aaf070ff884e96b9a09d39c20437de6cb
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
index 5e6f7c6..1d384bc 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
@@ -14,50 +14,35 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  window.Gerrit = window.Gerrit || {};
+/** @polymerBehavior AsyncForeachBehavior */
+export const AsyncForeachBehavior = {
+  /**
+   * @template T
+   * @param {!Array<T>} array
+   * @param {!Function} fn An iteratee function to be passed each element of
+   *     the array in order. Must return a promise, and the following
+   *     iteration will not begin until resolution of the promise returned by
+   *     the previous iteration.
+   *
+   *     An optional second argument to fn is a callback that will halt the
+   *     loop if called.
+   * @return {!Promise<undefined>}
+   */
+  asyncForeach(array, fn) {
+    if (!array.length) { return Promise.resolve(); }
+    let stop = false;
+    const stopCallback = () => { stop = true; };
+    return fn(array[0], stopCallback).then(exit => {
+      if (stop) { return Promise.resolve(); }
+      return this.asyncForeach(array.slice(1), fn);
+    });
+  },
+};
 
-  /** @polymerBehavior Gerrit.AsyncForeachBehavior */
-  Gerrit.AsyncForeachBehavior = {
-    /**
-     * @template T
-     * @param {!Array<T>} array
-     * @param {!Function} fn An iteratee function to be passed each element of
-     *     the array in order. Must return a promise, and the following
-     *     iteration will not begin until resolution of the promise returned by
-     *     the previous iteration.
-     *
-     *     An optional second argument to fn is a callback that will halt the
-     *     loop if called.
-     * @return {!Promise<undefined>}
-     */
-    asyncForeach(array, fn) {
-      if (!array.length) { return Promise.resolve(); }
-      let stop = false;
-      const stopCallback = () => { stop = true; };
-      return fn(array[0], stopCallback).then(exit => {
-        if (stop) { return Promise.resolve(); }
-        return this.asyncForeach(array.slice(1), fn);
-      });
-    },
-  };
-
-  // eslint-disable-next-line no-unused-vars
-  function defineEmptyMixin() {
-    // This is a temporary function.
-    // Polymer linter doesn't process correctly the following code:
-    // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
-    // To workaround this issue, the mock mixin is declared in this method.
-    // In the following changes, legacy behaviors will be converted to mixins.
-
-    /**
-     * @polymer
-     * @mixinFunction
-     */
-    Gerrit.AsyncForeachMixin = base =>
-      class extends base {
-      };
-  }
-})(window);
+// TODO(dmfilippov) Remove the following lines with assignments
+// Plugins can use the behavior because it was accessible with
+// the global Gerrit... variable. To avoid breaking changes in plugins
+// temporary assign global variables.
+window.Gerrit = window.Gerrit || {};
+window.Gerrit.AsyncForeachBehavior = AsyncForeachBehavior;
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
index 4168801..1adeb7c 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -25,11 +25,11 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script type="module">
 import '../../test/common-test-setup.js';
-import './async-foreach-behavior.js';
+import {AsyncForeachBehavior} from './async-foreach-behavior.js';
 suite('async-foreach-behavior tests', () => {
   test('loops over each item', () => {
     const fn = sinon.stub().returns(Promise.resolve());
-    return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
         .then(() => {
           assert.isTrue(fn.calledThrice);
           assert.equal(fn.getCall(0).args[0], 1);
@@ -45,7 +45,7 @@
       stop();
       return Promise.resolve();
     };
-    return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
         .then(() => {
           assert.isTrue(stub.calledOnce);
           assert.equal(stub.lastCall.args[0], 1);
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
index 9682776..4deb089 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
@@ -14,33 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function(window) {
-  'use strict';
 
-  window.Gerrit = window.Gerrit || {};
+/** @polymerBehavior BaseUrlBehavior */
+export const BaseUrlBehavior = {
+  /** @return {string} */
+  getBaseUrl() {
+    return window.CANONICAL_PATH || '';
+  },
+};
 
-  /** @polymerBehavior Gerrit.BaseUrlBehavior */
-  Gerrit.BaseUrlBehavior = {
-    /** @return {string} */
-    getBaseUrl() {
-      return window.CANONICAL_PATH || '';
-    },
-  };
+// TODO(dmfilippov) Remove the following lines with assignments
+// Plugins can use the behavior because it was accessible with
+// the global Gerrit... variable. To avoid breaking changes in plugins
+// temporary assign global variables.
+window.Gerrit = window.Gerrit || {};
+window.Gerrit.BaseUrlBehavior = BaseUrlBehavior;
 
-  // eslint-disable-next-line no-unused-vars
-  function defineEmptyMixin() {
-    // This is a temporary function.
-    // Polymer linter doesn't process correctly the following code:
-    // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
-    // To workaround this issue, the mock mixin is declared in this method.
-    // In the following changes, legacy behaviors will be converted to mixins.
-
-    /**
-     * @polymer
-     * @mixinFunction
-     */
-    Gerrit.BaseUrlMixin = base =>
-      class extends base {
-      };
-  }
-})(window);
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
index 62b497f..28f9ffd 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -25,7 +25,6 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script type="module">
 import '../../test/common-test-setup.js';
-import './base-url-behavior.js';
 /** @type {string} */
 window.CANONICAL_PATH = '/r';
 </script>
@@ -45,8 +44,8 @@
 
 <script type="module">
 import '../../test/common-test-setup.js';
-import './base-url-behavior.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {BaseUrlBehavior} from './base-url-behavior.js';
 suite('base-url-behavior tests', () => {
   let element;
   // eslint-disable-next-line no-unused-vars
@@ -57,7 +56,7 @@
     Polymer({
       is: 'test-element',
       behaviors: [
-        Gerrit.BaseUrlBehavior,
+        BaseUrlBehavior,
       ],
     });
   });
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
index 01bcc87..add1df4 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
@@ -14,66 +14,50 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../base-url-behavior/base-url-behavior.js';
+import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
 
-(function(window) {
-  'use strict';
+const PROBE_PATH = '/Documentation/index.html';
+const DOCS_BASE_PATH = '/Documentation';
 
-  const PROBE_PATH = '/Documentation/index.html';
-  const DOCS_BASE_PATH = '/Documentation';
+let cachedPromise;
 
-  let cachedPromise;
+/** @polymerBehavior DocsUrlBehavior */
+export const DocsUrlBehavior = [{
 
-  window.Gerrit = window.Gerrit || {};
-
-  /** @polymerBehavior Gerrit.DocsUrlBehavior */
-  Gerrit.DocsUrlBehavior = [{
-
-    /**
-     * Get the docs base URL from either the server config or by probing.
-     *
-     * @param {Object} config The server config.
-     * @param {!Object} restApi A REST API instance
-     * @return {!Promise<string>} A promise that resolves with the docs base
-     *     URL.
-     */
-    getDocsBaseUrl(config, restApi) {
-      if (!cachedPromise) {
-        cachedPromise = new Promise(resolve => {
-          if (config && config.gerrit && config.gerrit.doc_url) {
-            resolve(config.gerrit.doc_url);
-          } else {
-            restApi.probePath(this.getBaseUrl() + PROBE_PATH).then(ok => {
-              resolve(ok ? (this.getBaseUrl() + DOCS_BASE_PATH) : null);
-            });
-          }
-        });
-      }
-      return cachedPromise;
-    },
-
-    /** For testing only. */
-    _clearDocsBaseUrlCache() {
-      cachedPromise = undefined;
-    },
+  /**
+   * Get the docs base URL from either the server config or by probing.
+   *
+   * @param {Object} config The server config.
+   * @param {!Object} restApi A REST API instance
+   * @return {!Promise<string>} A promise that resolves with the docs base
+   *     URL.
+   */
+  getDocsBaseUrl(config, restApi) {
+    if (!cachedPromise) {
+      cachedPromise = new Promise(resolve => {
+        if (config && config.gerrit && config.gerrit.doc_url) {
+          resolve(config.gerrit.doc_url);
+        } else {
+          restApi.probePath(this.getBaseUrl() + PROBE_PATH).then(ok => {
+            resolve(ok ? (this.getBaseUrl() + DOCS_BASE_PATH) : null);
+          });
+        }
+      });
+    }
+    return cachedPromise;
   },
-  Gerrit.BaseUrlBehavior,
-  ];
 
-  // eslint-disable-next-line no-unused-vars
-  function defineEmptyMixin() {
-    // This is a temporary function.
-    // Polymer linter doesn't process correctly the following code:
-    // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
-    // To workaround this issue, the mock mixin is declared in this method.
-    // In the following changes, legacy behaviors will be converted to mixins.
+  /** For testing only. */
+  _clearDocsBaseUrlCache() {
+    cachedPromise = undefined;
+  },
+},
+BaseUrlBehavior,
+];
 
-    /**
-     * @polymer
-     * @mixinFunction
-     */
-    Gerrit.DocsUrlMixin = base =>
-      class extends base {
-      };
-  }
-})(window);
+// TODO(dmfilippov) Remove the following lines with assignments
+// Plugins can use the behavior because it was accessible with
+// the global Gerrit... variable. To avoid breaking changes in plugins
+// temporary assign global variables.
+window.Gerrit = window.Gerrit || {};
+window.Gerrit.DocsUrlBehavior = DocsUrlBehavior;
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
index 36eec89..0efd80f 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -29,8 +29,8 @@
 
 <script type="module">
 import '../../test/common-test-setup.js';
-import './docs-url-behavior.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {DocsUrlBehavior} from './docs-url-behavior.js';
 suite('docs-url-behavior tests', () => {
   let element;
 
@@ -38,7 +38,7 @@
     // Define a Polymer element that uses this behavior.
     Polymer({
       is: 'docs-url-behavior-element',
-      behaviors: [Gerrit.DocsUrlBehavior],
+      behaviors: [DocsUrlBehavior],
     });
   });
 
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
index 6e64a4d..1f06398 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
@@ -14,9 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../base-url-behavior/base-url-behavior.js';
 
 import '../gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
 (function(window) {
   'use strict';
 
@@ -56,7 +56,7 @@
       return 0;
     },
   },
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.URLEncodingBehavior,
   ];
 
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
index d52ffbf..c4c938e 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
@@ -16,7 +16,7 @@
  */
 import '../../scripts/bundled-polymer.js';
 
-import '../base-url-behavior/base-url-behavior.js';
+import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
 (function(window) {
   'use strict';
 
@@ -175,7 +175,7 @@
       return this.changeStatuses(change).join(', ');
     },
   },
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   ];
 
   // eslint-disable-next-line no-unused-vars
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index aa24ffc..ecd07bd 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -25,7 +25,6 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script type="module">
 import '../../test/common-test-setup.js';
-import '../base-url-behavior/base-url-behavior.js';
 import './rest-client-behavior.js';
 /** @type {string} */
 window.CANONICAL_PATH = '/r';
@@ -47,9 +46,9 @@
 
 <script type="module">
 import '../../test/common-test-setup.js';
-import '../base-url-behavior/base-url-behavior.js';
 import './rest-client-behavior.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
 suite('rest-client-behavior tests', () => {
   let element;
   // eslint-disable-next-line no-unused-vars
@@ -60,7 +59,7 @@
     Polymer({
       is: 'test-element',
       behaviors: [
-        Gerrit.BaseUrlBehavior,
+        BaseUrlBehavior,
         Gerrit.RESTClientBehavior,
       ],
     });
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index d03df39..3f9d5e0 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import '../../../styles/gr-menu-page-styles.js';
@@ -44,18 +43,18 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-admin-view_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
 
 /**
  * @appliesMixin Gerrit.AdminNavMixin
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrAdminView extends mixinBehaviors( [
   Gerrit.AdminNavBehavior,
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index 94de228..6bea8dd 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -18,7 +18,6 @@
 
 import '@polymer/iron-input/iron-input.js';
 import '../../../scripts/bundled-polymer.js';
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import '../../../styles/gr-form-styles.js';
@@ -33,18 +32,18 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-change-dialog_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const SUGGESTIONS_LIMIT = 15;
 const REF_PREFIX = 'refs/heads/';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrCreateChangeDialog extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   /**
    * Unused in this element, but called by other elements in tests
    * e.g gr-repo-commands_test.
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index 0860fdb..85803fa 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
@@ -27,14 +26,14 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-group-dialog_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrCreateGroupDialog extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index 40ddb66..04c92ed 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
@@ -29,6 +28,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-pointer-dialog_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const DETAIL_TYPES = {
   branches: 'branches',
@@ -36,12 +36,11 @@
 };
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrCreatePointerDialog extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index 7a77874..e1040fb 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
@@ -30,14 +29,14 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-repo-dialog_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrCreateRepoDialog extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index fc9e4a4..e58d543 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -35,6 +34,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-group-members_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const SUGGESTIONS_LIMIT = 15;
 const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
@@ -43,13 +43,12 @@
 const URL_REGEX = '^(?:[a-z]+:)?//';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrGroupMembers extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index 6cfa7ff..a680b39 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -33,6 +32,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-repo-access_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const Defs = {};
 
@@ -86,14 +86,13 @@
 
 /**
  * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrRepoAccess extends mixinBehaviors( [
   Gerrit.AccessBehavior,
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index 2421c46..5342c59018 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -17,7 +17,6 @@
 import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -31,6 +30,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-rule-editor_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
  * Fired when the rule has been modified or removed.
@@ -81,14 +81,13 @@
 
 /**
  * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrRuleEditor extends mixinBehaviors( [
   Gerrit.AccessBehavior,
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   /**
    * Unused in this element, but called by other elements in tests
    * e.g gr-permission_test.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 6e2f11c..3605e15 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
@@ -37,6 +36,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-list-item_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const CHANGE_SIZE = {
   XS: 10,
@@ -46,7 +46,6 @@
 };
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.ChangeTableMixin
  * @appliesMixin Gerrit.PathListMixin
  * @appliesMixin Gerrit.RESTClientMixin
@@ -54,7 +53,7 @@
  * @extends Polymer.Element
  */
 class GrChangeListItem extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.ChangeTableBehavior,
   Gerrit.PathListBehavior,
   Gerrit.RESTClientBehavior,
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 383aafa..c63c1f7 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
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import '../../../scripts/bundled-polymer.js';
@@ -31,6 +30,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-list-view_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const LookupQueryPatterns = {
   CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i,
@@ -45,13 +45,12 @@
 const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrChangeListView extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 33c2ea2..53b3fc4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -35,6 +34,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-list_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const NUMBER_FIXED_COLUMNS = 3;
 const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
@@ -42,7 +42,6 @@
 const MAX_SHORTCUT_CHARS = 5;
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.ChangeTableMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.KeyboardShortcutMixin
@@ -51,7 +50,7 @@
  * @extends Polymer.Element
  */
 class GrChangeList extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.ChangeTableBehavior,
   Gerrit.FireBehavior,
   Gerrit.KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
index b7fdbb7..9da6cf2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -170,6 +170,9 @@
         margin: var(--spacing-l) 0;
         padding: 0 var(--spacing-l);
       }
+      #startReviewBtn {
+        margin-left: var(--spacing-s);
+      }
       .collapseToggleContainer {
         display: flex;
         margin-bottom: 8px;
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 2649733..6e49c58 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -23,7 +23,6 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -36,15 +35,15 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-comment-list_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.PathListMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrCommentList extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.PathListBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 6b8e971..5cc021d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/async-foreach-behavior/async-foreach-behavior.js';
 import '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
@@ -44,6 +43,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-file-list_html.js';
+import {AsyncForeachBehavior} from '../../../behaviors/async-foreach-behavior/async-foreach-behavior.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -70,7 +70,31 @@
 };
 
 /**
- * @appliesMixin Gerrit.AsyncForeachMixin
+ * Type for FileInfo
+ *
+ * This should match with the type returned from `files` API plus
+ * additional info like `__path`.
+ *
+ * @typedef {Object} FileInfo
+ * @property {string} __path
+ * @property {?string} old_path
+ * @property {number} size
+ * @property {number} size_delta - fallback to 0 if not present in api
+ * @property {number} lines_deleted - fallback to 0 if not present in api
+ * @property {number} lines_inserted - fallback to 0 if not present in api
+ */
+
+/**
+ * Type for FileData
+ *
+ * This contains minimal info required about the file to get comments for
+ *
+ * @typedef {Object} FileData
+ * @property {string} path
+ * @property {?string} oldPath
+ */
+
+/**
  * @appliesMixin Gerrit.DomUtilMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.KeyboardShortcutMixin
@@ -79,7 +103,7 @@
  * @extends Polymer.Element
  */
 class GrFileList extends mixinBehaviors( [
-  Gerrit.AsyncForeachBehavior,
+  AsyncForeachBehavior,
   Gerrit.DomUtilBehavior,
   Gerrit.FireBehavior,
   Gerrit.KeyboardShortcutBehavior,
@@ -133,6 +157,8 @@
         notify: true,
       },
       _filesByPath: Object,
+
+      /** @type {!Array<FileInfo>} */
       _files: {
         type: Array,
         observer: '_filesChanged',
@@ -184,7 +210,8 @@
        */
       _reportinShownFilesIncrement: Number,
 
-      _expandedFilePaths: {
+      /** @type {!Array<FileData>} */
+      _expandedFiles: {
         type: Array,
         value() { return []; },
       },
@@ -230,7 +257,7 @@
 
   static get observers() {
     return [
-      '_expandedPathsChanged(_expandedFilePaths.splices)',
+      '_expandedFilesChanged(_expandedFiles.splices)',
       '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
         '_loading)',
     ];
@@ -409,27 +436,27 @@
     return this.$.restAPI.getPreferences();
   }
 
-  _togglePathExpanded(path) {
+  _toggleFileExpanded(file) {
     // Is the path in the list of expanded diffs? IF so remove it, otherwise
     // add it to the list.
-    const pathIndex = this._expandedFilePaths.indexOf(path);
+    const pathIndex = this._expandedFiles.findIndex(f => f.path === file.path);
     if (pathIndex === -1) {
-      this.push('_expandedFilePaths', path);
+      this.push('_expandedFiles', file);
     } else {
-      this.splice('_expandedFilePaths', pathIndex, 1);
+      this.splice('_expandedFiles', pathIndex, 1);
     }
   }
 
-  _togglePathExpandedByIndex(index) {
-    this._togglePathExpanded(this._files[index].__path);
+  _toggleFileExpandedByIndex(index) {
+    this._toggleFileExpanded(this._computeFileData(this._files[index]));
   }
 
   _updateDiffPreferences() {
     if (!this.diffs.length) { return; }
     // Re-render all expanded diffs sequentially.
     this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
-    this._renderInOrder(this._expandedFilePaths, this.diffs,
-        this._expandedFilePaths.length);
+    this._renderInOrder(this._expandedFiles, this.diffs,
+        this._expandedFiles.length);
   }
 
   _forEachDiff(fn) {
@@ -444,23 +471,23 @@
 
     // Find the list of paths that are in the file list, but not in the
     // expanded list.
-    const newPaths = [];
+    const newFiles = [];
     let path;
     for (let i = 0; i < this._shownFiles.length; i++) {
       path = this._shownFiles[i].__path;
-      if (!this._expandedFilePaths.includes(path)) {
-        newPaths.push(path);
+      if (!this._expandedFiles.some(f => f.path === path)) {
+        newFiles.push(this._computeFileData(this._shownFiles[i]));
       }
     }
 
-    this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
+    this.splice(...['_expandedFiles', 0, 0].concat(newFiles));
   }
 
   collapseAllDiffs() {
     this._showInlineDiffs = false;
-    this._expandedFilePaths = [];
+    this._expandedFiles = [];
     this.filesExpanded = this._computeExpandedFiles(
-        this._expandedFilePaths.length, this._files.length);
+        this._expandedFiles.length, this._files.length);
     this.$.diffCursor.handleDiffUpdate();
   }
 
@@ -607,7 +634,7 @@
    * The closure compiler doesn't realize this.specialFilePathCompare is
    * valid.
    *
-   * @suppress {checkTypes}
+   * @returns {!Array<FileInfo>}
    */
   _normalizeChangeFilesResponse(response) {
     if (!response) { return []; }
@@ -618,6 +645,7 @@
       info.__path = paths[i];
       info.lines_inserted = info.lines_inserted || 0;
       info.lines_deleted = info.lines_deleted || 0;
+      info.size_delta = info.size_delta || 0;
       files.push(info);
     }
     return files;
@@ -634,7 +662,13 @@
       row = row.parentElement;
     }
 
-    const path = row.dataset.path;
+    // No action needed for item without a valid file
+    if (!row.dataset.file) {
+      return;
+    }
+
+    const file = JSON.parse(row.dataset.file);
+    const path = file.path;
     // Handle checkbox mark as reviewed.
     if (e.target.classList.contains('markReviewed')) {
       e.preventDefault();
@@ -650,7 +684,23 @@
     if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
 
     e.preventDefault();
-    this._togglePathExpanded(path);
+    this._toggleFileExpanded(file);
+  }
+
+  /**
+   * Generates file data from file info object.
+   *
+   * @param {FileInfo} file
+   * @returns {FileData}
+   */
+  _computeFileData(file) {
+    const fileData = {
+      path: file.__path,
+    };
+    if (file.old_path) {
+      fileData.oldPath = file.old_path;
+    }
+    return fileData;
   }
 
   _handleLeftPane(e) {
@@ -677,7 +727,7 @@
         this.$.fileCursor.index === -1) { return; }
 
     e.preventDefault();
-    this._togglePathExpandedByIndex(this.$.fileCursor.index);
+    this._toggleFileExpandedByIndex(this.$.fileCursor.index);
   }
 
   _handleToggleAllInlineDiffs(e) {
@@ -1051,7 +1101,7 @@
   }
 
   _isFileExpanded(path, expandedFilesRecord) {
-    return expandedFilesRecord.base.includes(path);
+    return expandedFilesRecord.base.some(f => f.path === path);
   }
 
   _onLineSelected(e, detail) {
@@ -1076,20 +1126,20 @@
    *
    * @param {!Array} record The splice record in the expanded paths list.
    */
-  _expandedPathsChanged(record) {
+  _expandedFilesChanged(record) {
     // Clear content for any diffs that are not open so if they get re-opened
     // the stale content does not flash before it is cleared and reloaded.
     const collapsedDiffs = this.diffs.filter(diff =>
-      this._expandedFilePaths.indexOf(diff.path) === -1);
+      this._expandedFiles.findIndex(f => f.path === diff.path) === -1);
     this._clearCollapsedDiffs(collapsedDiffs);
 
     if (!record) { return; } // Happens after "Collapse all" clicked.
 
     this.filesExpanded = this._computeExpandedFiles(
-        this._expandedFilePaths.length, this._files.length);
+        this._expandedFiles.length, this._files.length);
 
     // Find the paths introduced by the new index splices:
-    const newPaths = record.indexSplices
+    const newFiles = record.indexSplices
         .map(splice => splice.object.slice(
             splice.index, splice.index + splice.addedCount))
         .reduce((acc, paths) => acc.concat(paths), []);
@@ -1099,8 +1149,8 @@
 
     this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
 
-    if (newPaths.length) {
-      this._renderInOrder(newPaths, this.diffs, newPaths.length);
+    if (newFiles.length) {
+      this._renderInOrder(newFiles, this.diffs, newFiles.length);
     }
 
     this._updateDiffCursor();
@@ -1119,18 +1169,19 @@
    * for each path in order, awaiting the previous render to complete before
    * continung.
    *
-   * @param  {!Array<string>} paths
+   * @param  {!Array<FileData>} files
    * @param  {!NodeList<!Object>} diffElements (GrDiffHostElement)
    * @param  {number} initialCount The total number of paths in the pass. This
    *   is used to generate log messages.
    * @return {!Promise}
    */
-  _renderInOrder(paths, diffElements, initialCount) {
+  _renderInOrder(files, diffElements, initialCount) {
     let iter = 0;
 
     return (new Promise(resolve => {
       this.fire('reload-drafts', {resolve});
-    })).then(() => this.asyncForeach(paths, (path, cancel) => {
+    })).then(() => this.asyncForeach(files, (file, cancel) => {
+      const path = file.path;
       this._cancelForEachDiff = cancel;
 
       iter++;
@@ -1141,8 +1192,8 @@
         console.warn(`Did not find <gr-diff-host> element for ${path}`);
         return Promise.resolve();
       }
-      diffElem.comments = this.changeComments.getCommentsBySideForPath(
-          path, this.patchRange, this.projectConfig);
+      diffElem.comments = this.changeComments.getCommentsBySideForFile(
+          file, this.patchRange, this.projectConfig);
       const promises = [diffElem.reload()];
       if (this._loggedIn && !this.diffPrefs.manual_review) {
         promises.push(this._reviewFile(path, true));
@@ -1188,7 +1239,7 @@
   reloadCommentsForThreadWithRootId(rootId, path) {
     // Don't bother continuing if we already know that the path that contains
     // the updated comment thread is not expanded.
-    if (!this._expandedFilePaths.includes(path)) { return; }
+    if (!this._expandedFiles.some(f => f.path === path)) { return; }
     const diff = this.diffs.find(d => d.path === path);
 
     const threadEl = diff.getThreadEls().find(t => t.rootId === rootId);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
index 9652156..b20a751 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -296,7 +296,7 @@
       <template is="dom-repeat" items="[[_shownFiles]]" id="files" as="file" initial-count="[[fileListIncrement]]" target-framerate="1">
         [[_reportRenderedRow(index)]]
         <div class="stickyArea">
-          <div class\$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" tabindex="-1">
+          <div class\$="file-row row [[_computePathClass(file.__path, _expandedFiles.*)]]" data-file\$="[[_computeFileData(file)]]" tabindex="-1">
               <div class\$="[[_computeClass('status', file.__path)]]" tabindex="0" title\$="[[_computeFileStatusLabel(file.status)]]" aria-label\$="[[_computeFileStatusLabel(file.status)]]">
               [[_computeFileStatus(file.status)]]
             </div>
@@ -378,14 +378,14 @@
             </div>
             <div class="show-hide">
               <label class="show-hide" data-path\$="[[file.__path]]" data-expand="true">
-                <input type="checkbox" class="show-hide" checked\$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" data-expand="true">
-                  <iron-icon id="icon" icon="[[_computeShowHideIcon(file.__path, _expandedFilePaths.*)]]">
+                <input type="checkbox" class="show-hide" checked\$="[[_isFileExpanded(file.__path, _expandedFiles.*)]]" data-path\$="[[file.__path]]" data-expand="true">
+                  <iron-icon id="icon" icon="[[_computeShowHideIcon(file.__path, _expandedFiles.*)]]">
                   </iron-icon>
               </label>
             </div>
           </div>
-          <template is="dom-if" if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
-            <gr-diff-host no-auto-render="" show-load-failure="" display-line="[[_displayLine]]" hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" change-num="[[changeNum]]" patch-range="[[patchRange]]" path="[[file.__path]]" prefs="[[diffPrefs]]" project-name="[[change.project]]" on-line-selected="_onLineSelected" no-render-on-prefs-change="" view-mode="[[diffViewMode]]"></gr-diff-host>
+          <template is="dom-if" if="[[_isFileExpanded(file.__path, _expandedFiles.*)]]">
+            <gr-diff-host no-auto-render="" show-load-failure="" display-line="[[_displayLine]]" hidden="[[!_isFileExpanded(file.__path, _expandedFiles.*)]]" change-num="[[changeNum]]" patch-range="[[patchRange]]" path="[[file.__path]]" prefs="[[diffPrefs]]" project-name="[[change.project]]" on-line-selected="_onLineSelected" no-render-on-prefs-change="" view-mode="[[diffViewMode]]"></gr-diff-host>
           </template>
         </div>
       </template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 96db120..17f83c5 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -671,47 +671,50 @@
 
       test('i key shows/hides selected inline diff', () => {
         const paths = Object.keys(element._filesByPath);
-        sandbox.stub(element, '_expandedPathsChanged');
+        sandbox.stub(element, '_expandedFilesChanged');
         flushAsynchronousOperations();
         const files = dom(element.root).querySelectorAll('.file-row');
         element.$.fileCursor.stops = files;
         element.$.fileCursor.setCursorAtIndex(0);
         assert.equal(element.diffs.length, 0);
-        assert.equal(element._expandedFilePaths.length, 0);
+        assert.equal(element._expandedFiles.length, 0);
 
         MockInteractions.keyUpOn(element, 73, null, 'i');
         flushAsynchronousOperations();
         assert.equal(element.diffs.length, 1);
         assert.equal(element.diffs[0].path, paths[0]);
-        assert.equal(element._expandedFilePaths.length, 1);
-        assert.equal(element._expandedFilePaths[0], paths[0]);
+        assert.equal(element._expandedFiles.length, 1);
+        assert.equal(element._expandedFiles[0].path, paths[0]);
 
         MockInteractions.keyUpOn(element, 73, null, 'i');
         flushAsynchronousOperations();
         assert.equal(element.diffs.length, 0);
-        assert.equal(element._expandedFilePaths.length, 0);
+        assert.equal(element._expandedFiles.length, 0);
 
         element.$.fileCursor.setCursorAtIndex(1);
         MockInteractions.keyUpOn(element, 73, null, 'i');
         flushAsynchronousOperations();
         assert.equal(element.diffs.length, 1);
         assert.equal(element.diffs[0].path, paths[1]);
-        assert.equal(element._expandedFilePaths.length, 1);
-        assert.equal(element._expandedFilePaths[0], paths[1]);
+        assert.equal(element._expandedFiles.length, 1);
+        assert.equal(element._expandedFiles[0].path, paths[1]);
 
         MockInteractions.keyUpOn(element, 73, 'shift', 'i');
         flushAsynchronousOperations();
         assert.equal(element.diffs.length, paths.length);
-        assert.equal(element._expandedFilePaths.length, paths.length);
+        assert.equal(element._expandedFiles.length, paths.length);
         for (const index in element.diffs) {
           if (!element.diffs.hasOwnProperty(index)) { continue; }
-          assert.include(element._expandedFilePaths, element.diffs[index].path);
+          assert.isTrue(
+              element._expandedFiles
+                  .some(f => f.path === element.diffs[index].path)
+          );
         }
 
         MockInteractions.keyUpOn(element, 73, 'shift', 'i');
         flushAsynchronousOperations();
         assert.equal(element.diffs.length, 0);
-        assert.equal(element._expandedFilePaths.length, 0);
+        assert.equal(element._expandedFiles.length, 0);
       });
 
       test('r key toggles reviewed flag', () => {
@@ -741,7 +744,7 @@
           sandbox.stub(element, 'modifierPressed').returns(false);
           const openCursorStub = sandbox.stub(element, '_openCursorFile');
           const openSelectedStub = sandbox.stub(element, '_openSelectedFile');
-          const expandStub = sandbox.stub(element, '_togglePathExpanded');
+          const expandStub = sandbox.stub(element, '_toggleFileExpanded');
 
           interact = function(opt_payload) {
             openCursorStub.reset();
@@ -880,12 +883,12 @@
 
       const clickSpy = sandbox.spy(element, '_handleFileListClick');
       const reviewStub = sandbox.stub(element, '_reviewFile');
-      const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+      const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
 
       const row = dom(element.root)
-          .querySelector('.row[data-path="f1.txt"]');
+          .querySelector(`.row[data-file='{"path":"f1.txt"}']`);
 
-      // Click on the expand button, resulting in _togglePathExpanded being
+      // Click on the expand button, resulting in _toggleFileExpanded being
       // called and not resulting in a call to _reviewFile.
       row.querySelector('div.show-hide').click();
       assert.isTrue(clickSpy.calledOnce);
@@ -893,7 +896,7 @@
       assert.isFalse(reviewStub.called);
 
       // Click inside the diff. This should result in no additional calls to
-      // _togglePathExpanded or _reviewFile.
+      // _toggleFileExpanded or _reviewFile.
       dom(element.root).querySelector('gr-diff-host')
           .click();
       assert.isTrue(clickSpy.calledTwice);
@@ -901,7 +904,7 @@
       assert.isFalse(reviewStub.called);
 
       // Click the reviewed checkbox, resulting in a call to _reviewFile, but
-      // no additional call to _togglePathExpanded.
+      // no additional call to _toggleFileExpanded.
       row.querySelector('.markReviewed').click();
       assert.isTrue(clickSpy.calledThrice);
       assert.isTrue(toggleExpandSpy.calledOnce);
@@ -922,7 +925,7 @@
       element.editMode = true;
       flushAsynchronousOperations();
       const clickSpy = sandbox.spy(element, '_handleFileListClick');
-      const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+      const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
 
       // Tap the edit controls. Should be ignored by _handleFileListClick.
       MockInteractions.tap(element.shadowRoot
@@ -962,7 +965,7 @@
         patchNum: '2',
       };
       element.$.fileCursor.setCursorAtIndex(0);
-      sandbox.stub(element, '_expandedPathsChanged');
+      sandbox.stub(element, '_expandedFilesChanged');
       flushAsynchronousOperations();
       const fileRows =
           dom(element.root).querySelectorAll('.row:not(.header-row)');
@@ -974,7 +977,9 @@
       assert.isNotOk(showHideCheck.checked);
       MockInteractions.tap(showHideLabel);
       assert.isOk(showHideCheck.checked);
-      assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
+      assert.notEqual(
+          element._expandedFiles.findIndex(f => f.path === 'myfile.txt'),
+          -1);
     });
 
     test('diff mode correctly toggles the diffs', () => {
@@ -1020,14 +1025,14 @@
         basePatchNum: 'PARENT',
         patchNum: '2',
       };
-      sandbox.stub(element, '_expandedPathsChanged');
+      sandbox.stub(element, '_expandedFilesChanged');
       flushAsynchronousOperations();
       const commitMsgFile = dom(element.root)
           .querySelectorAll('.row:not(.header-row) a.pathLink')[0];
 
       // Remove href attribute so the app doesn't route to a diff view
       commitMsgFile.removeAttribute('href');
-      const togglePathSpy = sandbox.spy(element, '_togglePathExpanded');
+      const togglePathSpy = sandbox.spy(element, '_toggleFileExpanded');
 
       MockInteractions.tap(commitMsgFile);
       flushAsynchronousOperations();
@@ -1039,7 +1044,7 @@
       'none');
     });
 
-    test('_togglePathExpanded', () => {
+    test('_toggleFileExpanded', () => {
       const path = 'path/to/my/file.txt';
       element._filesByPath = {[path]: {}};
       const renderSpy = sandbox.spy(element, '_renderInOrder');
@@ -1047,22 +1052,22 @@
 
       assert.equal(element.shadowRoot
           .querySelector('iron-icon').icon, 'gr-icons:expand-more');
-      assert.equal(element._expandedFilePaths.length, 0);
-      element._togglePathExpanded(path);
+      assert.equal(element._expandedFiles.length, 0);
+      element._toggleFileExpanded({path});
       flushAsynchronousOperations();
       assert.equal(collapseStub.lastCall.args[0].length, 0);
       assert.equal(element.shadowRoot
           .querySelector('iron-icon').icon, 'gr-icons:expand-less');
 
       assert.equal(renderSpy.callCount, 1);
-      assert.include(element._expandedFilePaths, path);
-      element._togglePathExpanded(path);
+      assert.isTrue(element._expandedFiles.some(f => f.path === path));
+      element._toggleFileExpanded({path});
       flushAsynchronousOperations();
 
       assert.equal(element.shadowRoot
           .querySelector('iron-icon').icon, 'gr-icons:expand-more');
       assert.equal(renderSpy.callCount, 1);
-      assert.notInclude(element._expandedFilePaths, path);
+      assert.isFalse(element._expandedFiles.some(f => f.path === path));
       assert.equal(collapseStub.lastCall.args[0].length, 1);
     });
 
@@ -1081,13 +1086,13 @@
 
       element.collapseAllDiffs();
       flushAsynchronousOperations();
-      assert.equal(element._expandedFilePaths.length, 0);
+      assert.equal(element._expandedFiles.length, 0);
       assert.isFalse(element._showInlineDiffs);
       assert.isTrue(cursorUpdateStub.calledTwice);
       assert.equal(collapseStub.lastCall.args[0].length, 1);
     });
 
-    test('_expandedPathsChanged', done => {
+    test('_expandedFilesChanged', done => {
       sandbox.stub(element, '_reviewFile');
       const path = 'path/to/my/file.txt';
       const diffs = [{
@@ -1105,7 +1110,7 @@
       sinon.stub(element, 'diffs', {
         get() { return diffs; },
       });
-      element.push('_expandedFilePaths', path);
+      element.push('_expandedFiles', {path});
     });
 
     test('_clearCollapsedDiffs', () => {
@@ -1126,11 +1131,11 @@
       flushAsynchronousOperations();
       assert.equal(element.filesExpanded,
           GrFileListConstants.FilesExpandedState.NONE);
-      element.push('_expandedFilePaths', 'baz.bar');
+      element.push('_expandedFiles', {path: 'baz.bar'});
       flushAsynchronousOperations();
       assert.equal(element.filesExpanded,
           GrFileListConstants.FilesExpandedState.SOME);
-      element.push('_expandedFilePaths', 'foo.bar');
+      element.push('_expandedFiles', {path: 'foo.bar'});
       flushAsynchronousOperations();
       assert.equal(element.filesExpanded,
           GrFileListConstants.FilesExpandedState.ALL);
@@ -1169,7 +1174,9 @@
           return Promise.resolve();
         },
       }];
-      element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
+      element._renderInOrder([
+        {path: 'p2'}, {path: 'p1'}, {path: 'p0'},
+      ], diffs, 3)
           .then(() => {
             assert.isFalse(reviewStub.called);
             assert.isTrue(loadCommentSpy.called);
@@ -1206,7 +1213,9 @@
           return Promise.resolve();
         },
       }];
-      element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
+      element._renderInOrder([
+        {path: 'p2'}, {path: 'p1'}, {path: 'p0'},
+      ], diffs, 3)
           .then(() => {
             assert.equal(reviewStub.callCount, 3);
             done();
@@ -1223,10 +1232,10 @@
         reload() { return Promise.resolve(); },
       }];
 
-      return element._renderInOrder(['p'], diffs, 1).then(() => {
+      return element._renderInOrder([{path: 'p'}], diffs, 1).then(() => {
         assert.isFalse(reviewStub.called);
         delete element.diffPrefs.manual_review;
-        return element._renderInOrder(['p'], diffs, 1).then(() => {
+        return element._renderInOrder([{path: 'p'}], diffs, 1).then(() => {
           assert.isTrue(reviewStub.called);
           assert.isTrue(reviewStub.calledWithExactly('p', true));
         });
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
index 49ced2a..5754fdc 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -80,9 +80,11 @@
       }
       .collapsed .content {
         flex: 1;
-        margin-right: var(--spacing-xs);
+        margin-right: var(--spacing-m);
         min-width: 0;
         overflow: hidden;
+      }
+      .collapsed .content.messageContent {
         text-overflow: ellipsis;
       }
       .collapsed .dateContainer {
@@ -200,7 +202,7 @@
           </div>
         </template>
         <template is="dom-if" if="[[message.message]]">
-          <div class="content">
+          <div class="content messageContent">
             <div class="message hideOnOpen">[[_messageContentCollapsed]]</div>
             <gr-formatted-text no-trailing-margin="" class="message hideOnCollapsed" content="[[_messageContentExpanded]]" config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
             <template is="dom-if" if="[[!_isMessageContentEmpty()]]">
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 305505d..1d3a28d 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
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
@@ -44,6 +43,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-reply-dialog_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -81,7 +81,6 @@
 const SEND_REPLY_TIMING_LABEL = 'SendReply';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.KeyboardShortcutMixin
  * @appliesMixin Gerrit.PatchSetMixin
@@ -89,7 +88,7 @@
  * @extends Polymer.Element
  */
 class GrReplyDialog extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
   Gerrit.KeyboardShortcutBehavior,
   Gerrit.PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index e2284a9..d1c1764 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -21,7 +21,6 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../scripts/bundled-polymer.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
@@ -36,6 +35,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-error-manager_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const HIDE_ALERT_TIMEOUT_MS = 5000;
 const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -46,12 +46,11 @@
 const AUTHENTICATION_REQUIRED = 'Authentication required\n';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @extends Polymer.Element
  */
 class GrErrorManager extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index c8ed50c..6ec8594 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -16,8 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
@@ -32,6 +30,8 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-main-header_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import {DocsUrlBehavior} from '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
 
 const DEFAULT_LINKS = [{
   title: 'Changes',
@@ -87,15 +87,13 @@
 
 /**
  * @appliesMixin Gerrit.AdminNavMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.DocsUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @extends Polymer.Element
  */
 class GrMainHeader extends mixinBehaviors( [
   Gerrit.AdminNavBehavior,
-  Gerrit.BaseUrlBehavior,
-  Gerrit.DocsUrlBehavior,
+  BaseUrlBehavior,
+  DocsUrlBehavior,
   Gerrit.FireBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index e461d1d..f08936f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -30,6 +29,7 @@
 import page from 'page/page.mjs';
 self.page = page;
 import {htmlTemplate} from './gr-router_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const RoutePattern = {
   ROOT: '/',
@@ -218,14 +218,13 @@
 })();
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.PatchSetMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrRouter extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
   Gerrit.PatchSetBehavior,
   Gerrit.URLEncodingBehavior,
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index ac43679..7985e46 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -342,6 +342,7 @@
       comments.left = comments.left.concat(commentsForOldPath.left);
       comments.right = comments.right.concat(commentsForOldPath.right);
     }
+    return 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 cfed401..27b68a4 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
@@ -258,6 +258,8 @@
       '_getProjectConfig(_change.project)',
       '_getFiles(_changeNum, _patchRange.*, _changeComments)',
       '_setReviewedObserver(_loggedIn, params.*, _prefs)',
+      '_recomputeComments(_files.changeFilesByPath,' +
+      '_path, _patchRange, _projectConfig)',
     ];
   }
 
@@ -1112,6 +1114,28 @@
     });
   }
 
+  _recomputeComments(files, path, patchRange, projectConfig) {
+    // Polymer 2: check for undefined
+    if ([
+      files,
+      path,
+      patchRange,
+      projectConfig,
+    ].some(arg => arg === undefined)) {
+      return undefined;
+    }
+
+    const file = files[path];
+    if (file && file.old_path) {
+      this._commentsForDiff = this._changeComments.getCommentsBySideForFile(
+          {path, oldPath: file.old_path},
+          patchRange,
+          projectConfig);
+
+      this.$.diffHost.comments = this._commentsForDiff;
+    }
+  }
+
   _getPaths(patchRange) {
     return this._changeComments.getPaths(patchRange);
   }
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index 0a57883..89234cc 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 70806e2..51cedd2 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -17,7 +17,6 @@
 import '../scripts/util.js';
 
 import '../scripts/bundled-polymer.js';
-import '../behaviors/base-url-behavior/base-url-behavior.js';
 import '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import '../styles/shared-styles.js';
 import '../styles/themes/app-theme.js';
@@ -52,14 +51,14 @@
 import moment from 'moment/src/moment.js';
 self.moment = moment;
 import {htmlTemplate} from './gr-app-element_html.js';
+import {BaseUrlBehavior} from '../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.KeyboardShortcutMixin
  * @extends Polymer.Element
  */
 class GrAppElement extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.KeyboardShortcutBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index 1236f97..935b774 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
index a8bfccdd..c3acf5c 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../settings/gr-settings-view/gr-settings-item.js';
 import '../../settings/gr-settings-view/gr-settings-menu-item.js';
 const $_documentContainer = document.createElement('template');
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 18a0419..390baf6 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
@@ -25,13 +24,13 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-agreements-list_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @extends Polymer.Element
  */
 class GrAgreementsList extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index 373ac63..ef4010f 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '@polymer/iron-input/iron-input.js';
 import '../../../scripts/bundled-polymer.js';
@@ -28,14 +27,14 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-cla-view_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @extends Polymer.Element
  */
 class GrClaView extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index 57f0e1d..74c5eed 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-form-styles.js';
 import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
@@ -28,6 +27,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-identities_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const AUTH = [
   'OPENID',
@@ -35,11 +35,10 @@
 ];
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @extends Polymer.Element
  */
 class GrIdentities extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 733fa56..e86ae99 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -17,7 +17,6 @@
 import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/iron-input/iron-input.js';
-import '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-menu-page-styles.js';
@@ -48,6 +47,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-settings-view_html.js';
+import {DocsUrlBehavior} from '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
 
 const PREFS_SECTION_FIELDS = [
   'changes_per_page',
@@ -78,13 +78,12 @@
 ];
 
 /**
- * @appliesMixin Gerrit.DocsUrlMixin
  * @appliesMixin Gerrit.ChangeTableMixin
  * @appliesMixin Gerrit.FireMixin
  * @extends Polymer.Element
  */
 class GrSettingsView extends mixinBehaviors( [
-  Gerrit.DocsUrlBehavior,
+  DocsUrlBehavior,
   Gerrit.ChangeTableBehavior,
   Gerrit.FireBehavior,
 ], GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index e0d5583..a5738d1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../scripts/bundled-polymer.js';
 import '../../core/gr-navigation/gr-navigation.js';
@@ -25,13 +24,13 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-account-link_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @extends Polymer.Element
  */
 class GrAccountLink extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 857f1b4..f009efd 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../styles/shared-styles.js';
 import '../gr-js-api-interface/gr-js-api-interface.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
@@ -25,13 +24,13 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-avatar_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @extends Polymer.Element
  */
 class GrAvatar extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index f184b6c..37c3d52 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -326,11 +326,15 @@
    * @private
    */
   _getNextindex(delta, opt_condition, opt_clipToTop) {
-    if (!this.stops.length || this.index === -1) {
+    if (!this.stops.length) {
       return -1;
     }
-
     let newIndex = this.index;
+    // If the cursor is not yet set and we are going backwards, start at the
+    // back.
+    if (this.index === -1 && delta < 0) {
+      newIndex = this.stops.length;
+    }
     do {
       newIndex = newIndex + delta;
     } while (newIndex > 0 &&
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
index 2f56ea6..1f5b4c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -127,6 +127,29 @@
     assert.equal(element.index, -1);
   });
 
+  test('next() goes to first element when no cursor is set', () => {
+    element.stops = list.querySelectorAll('li');
+    element.next();
+
+    assert.equal(element.index, 0);
+    assert.equal(element.target, list.children[0]);
+    assert.isTrue(list.children[0].classList.contains('targeted'));
+    assert.isTrue(element.isAtStart());
+    assert.isFalse(element.isAtEnd());
+  });
+
+  test('next() goes to first element when no cursor is set', () => {
+    element.stops = list.querySelectorAll('li');
+    element.previous();
+
+    const lastIndex = list.children.length - 1;
+    assert.equal(element.index, lastIndex);
+    assert.equal(element.target, list.children[lastIndex]);
+    assert.isTrue(list.children[lastIndex].classList.contains('targeted'));
+    assert.isFalse(element.isAtStart());
+    assert.isTrue(element.isAtEnd());
+  });
+
   test('_moveCursor', () => {
     // Initialize the cursor with its stops.
     element.stops = list.querySelectorAll('li');
@@ -143,8 +166,8 @@
     assert.isTrue(getTargetHeight.called);
   });
 
-  test('_moveCursor from -1 does not check height', () => {
-    element.stops = list.querySelectorAll('li');
+  test('_moveCursor from for invalid index does not check height', () => {
+    element.stops = [];
     const getTargetHeight = sinon.stub();
     element._moveCursor(1, () => false, getTargetHeight);
     assert.isFalse(getTargetHeight.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index b4190dc..98a1ca7 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import '../../../scripts/bundled-polymer.js';
@@ -30,17 +29,17 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-dropdown_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const REL_NOOPENER = 'noopener';
 const REL_EXTERNAL = 'external';
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.KeyboardShortcutMixin
  * @extends Polymer.Element
  */
 class GrDropdown extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.KeyboardShortcutBehavior,
 ], GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
index 7828e32..c4ec521 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
@@ -21,6 +21,7 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 
 const HOVER_CLASS = 'hovered';
+const HIDE_CLASS = 'hide';
 
 /**
  * When the hovercard is positioned diagonally (bottom-left, bottom-right,
@@ -256,15 +257,38 @@
 
     // Add it to the DOM and calculate its position
     this.container.appendChild(this);
-    this.updatePosition();
-
-    // Trigger the transition
+    // We temporarily hide the hovercard until we have found the correct
+    // position for it.
+    this.classList.add(HIDE_CLASS);
     this.classList.add(HOVER_CLASS);
+    this.updatePosition();
+    this.classList.remove(HIDE_CLASS);
+  }
+
+  updatePosition() {
+    const positionsToTry = new Set(
+        [this.position, 'right', 'bottom-right', 'top-right',
+          'bottom', 'top', 'bottom-left', 'top-left', 'left']);
+    for (const position of positionsToTry) {
+      this.updatePositionTo(position);
+      if (this._isInsideViewport()) return;
+    }
+    console.warn('Could not find a visible position for the hovercard.');
+  }
+
+  _isInsideViewport() {
+    const thisRect = this.getBoundingClientRect();
+    if (thisRect.top < 0) return false;
+    if (thisRect.left < 0) return false;
+    const docuRect = document.documentElement.getBoundingClientRect();
+    if (thisRect.bottom > docuRect.height) return false;
+    if (thisRect.right > docuRect.width) return false;
+    return true;
   }
 
   /**
-   * Updates the hovercard's position based on the `position` attribute
-   * and the current position of the `target` element.
+   * Updates the hovercard's position based the current position of the `target`
+   * element.
    *
    * The hovercard is supposed to stay open if the user hovers over it.
    * To keep it open when the user moves away from the target, the bounding
@@ -274,25 +298,26 @@
    * update the position of the tooltip while it is already visible (the
    * target element has moved and the tooltip is still open).
    */
-  updatePosition() {
+  updatePositionTo(position) {
     if (!this._target) { return; }
 
-    // Calculate the necessary measurements and positions
-    const parentRect = document.documentElement.getBoundingClientRect();
+    // Make sure that thisRect will not get any paddings and such included
+    // in the width and height of the bounding client rect.
+    this.style.cssText = '';
+
+    const docuRect = document.documentElement.getBoundingClientRect();
     const targetRect = this._target.getBoundingClientRect();
     const thisRect = this.getBoundingClientRect();
 
-    const targetLeft = targetRect.left - parentRect.left;
-    const targetTop = targetRect.top - parentRect.top;
+    const targetLeft = targetRect.left - docuRect.left;
+    const targetTop = targetRect.top - docuRect.top;
 
     let hovercardLeft;
     let hovercardTop;
     const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
     let cssText = '';
 
-    // Find the top and left position values based on the position attribute
-    // of the hovercard.
-    switch (this.position) {
+    switch (position) {
       case 'top':
         hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
         hovercardTop = targetTop - thisRect.height - this.offset;
@@ -312,38 +337,38 @@
             `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
         break;
       case 'right':
-        hovercardLeft = targetRect.right + this.offset;
+        hovercardLeft = targetLeft + targetRect.width + this.offset;
         hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
         cssText +=
             `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
         break;
       case 'bottom-right':
-        hovercardLeft = targetRect.left + targetRect.width + this.offset;
-        hovercardTop = targetRect.top + targetRect.height + this.offset;
+        hovercardLeft = targetLeft + targetRect.width + this.offset;
+        hovercardTop = targetTop + targetRect.height + this.offset;
         cssText += `padding-top:${diagonalPadding}px;`;
         cssText += `padding-left:${diagonalPadding}px;`;
         cssText += `margin-left:-${diagonalPadding}px;`;
         cssText += `margin-top:-${diagonalPadding}px;`;
         break;
       case 'bottom-left':
-        hovercardLeft = targetRect.left - thisRect.width - this.offset;
-        hovercardTop = targetRect.top + targetRect.height + this.offset;
+        hovercardLeft = targetLeft - thisRect.width - this.offset;
+        hovercardTop = targetTop + targetRect.height + this.offset;
         cssText += `padding-top:${diagonalPadding}px;`;
         cssText += `padding-right:${diagonalPadding}px;`;
         cssText += `margin-right:-${diagonalPadding}px;`;
         cssText += `margin-top:-${diagonalPadding}px;`;
         break;
       case 'top-left':
-        hovercardLeft = targetRect.left - thisRect.width - this.offset;
-        hovercardTop = targetRect.top - thisRect.height - this.offset;
+        hovercardLeft = targetLeft - thisRect.width - this.offset;
+        hovercardTop = targetTop - thisRect.height - this.offset;
         cssText += `padding-bottom:${diagonalPadding}px;`;
         cssText += `padding-right:${diagonalPadding}px;`;
         cssText += `margin-bottom:-${diagonalPadding}px;`;
         cssText += `margin-right:-${diagonalPadding}px;`;
         break;
       case 'top-right':
-        hovercardLeft = targetRect.left + targetRect.width + this.offset;
-        hovercardTop = targetRect.top - thisRect.height - this.offset;
+        hovercardLeft = targetLeft + targetRect.width + this.offset;
+        hovercardTop = targetTop - thisRect.height - this.offset;
         cssText += `padding-bottom:${diagonalPadding}px;`;
         cssText += `padding-left:${diagonalPadding}px;`;
         cssText += `margin-bottom:-${diagonalPadding}px;`;
@@ -351,12 +376,6 @@
         break;
     }
 
-    // Prevent hovercard from appearing outside the viewport.
-    // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
-    // right.
-    if (hovercardLeft < 0) { hovercardLeft = 0; }
-    if (hovercardTop < 0) { hovercardTop = 0; }
-    // Set the hovercard's position
     cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
     this.style.cssText = cssText;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
index a392691..5fb1add 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
@@ -21,16 +21,15 @@
   `<template>
     <style include="shared-styles">
       :host {
-        box-sizing: border-box;
-        opacity: 0;
         position: absolute;
-        transition: opacity 200ms;
-        visibility: hidden;
+        display: none;
         z-index: 200;
       }
       :host(.hovered) {
-        visibility: visible;
-        opacity: 1;
+        display: block;
+      }
+      :host(.hide) {
+        visibility: hidden;
       }
       /* You have to use a <div class="container"> in your hovercard in order
          to pick up this consistent styling. */
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
index 7280eb2..81c73ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -45,8 +45,6 @@
 suite('gr-hovercard tests', () => {
   let element;
   let sandbox;
-  // For css animations
-  const TRANSITION_TIME = 500;
 
   setup(() => {
     sandbox = sinon.sandbox.create();
@@ -84,29 +82,22 @@
             (targetTop + targetRect.height + element.offset) + 'px'));
   });
 
-  test('hide', done => {
+  test('hide', () => {
     element.hide({});
-    setTimeout(() => {
-      const style = getComputedStyle(element);
-      assert.isFalse(element._isShowing);
-      assert.isFalse(element.classList.contains('hovered'));
-      assert.equal(style.opacity, '0');
-      assert.equal(style.visibility, 'hidden');
-      assert.notEqual(element.container, dom(element).parentNode);
-      done();
-    }, TRANSITION_TIME);
+    const style = getComputedStyle(element);
+    assert.isFalse(element._isShowing);
+    assert.isFalse(element.classList.contains('hovered'));
+    assert.equal(style.display, 'none');
+    assert.notEqual(element.container, dom(element).parentNode);
   });
 
-  test('show', done => {
+  test('show', () => {
     element.show({});
-    setTimeout(() => {
-      const style = getComputedStyle(element);
-      assert.isTrue(element._isShowing);
-      assert.isTrue(element.classList.contains('hovered'));
-      assert.equal(style.opacity, '1');
-      assert.equal(style.visibility, 'visible');
-      done();
-    }, TRANSITION_TIME);
+    const style = getComputedStyle(element);
+    assert.isTrue(element._isShowing);
+    assert.isTrue(element.classList.contains('hovered'));
+    assert.equal(style.opacity, '1');
+    assert.equal(style.visibility, 'visible');
   });
 
   test('showDelayed does not show immediately', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
index d5e65da..3947f53 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+
 (function(window) {
   'use strict';
 
@@ -30,7 +32,7 @@
   }
 
   function getBaseUrl() {
-    return Gerrit.BaseUrlBehavior.getBaseUrl();
+    return BaseUrlBehavior.getBaseUrl();
   }
 
   /**
@@ -50,7 +52,7 @@
     if (url.protocol === PRELOADED_PROTOCOL) {
       return url.pathname;
     }
-    const base = Gerrit.BaseUrlBehavior.getBaseUrl();
+    const base = BaseUrlBehavior.getBaseUrl();
     let pathname = url.pathname.replace(base, '');
     // Load from ASSETS_PATH
     if (window.ASSETS_PATH && url.href.includes(window.ASSETS_PATH)) {
@@ -114,4 +116,4 @@
     // TEST only methods
     testOnly_resetInternalState,
   };
-})(window);
\ No newline at end of file
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
index 2523d47..0431bdf 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index 6b7b13e..28d386e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 import '../../../scripts/bundled-polymer.js';
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import '../../core/gr-reporting/gr-reporting.js';
 import '../../plugins/gr-admin-api/gr-admin-api.js';
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 e626e33..9a2e688 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
@@ -33,6 +33,8 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-js-api-interface.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+
 suite('gr-js-api-interface tests', () => {
   const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
   let element;
@@ -377,7 +379,7 @@
     let baseUrlPlugin;
 
     setup(() => {
-      sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
+      sandbox.stub(BaseUrlBehavior, 'getBaseUrl').returns('/r');
 
       Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
           'http://test.com/r/plugins/baseurlplugin/static/test.js');
@@ -462,7 +464,7 @@
 
   suite('screen', () => {
     test('screenUrl()', () => {
-      sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base');
+      sandbox.stub(BaseUrlBehavior, 'getBaseUrl').returns('/base');
       assert.equal(
           plugin.screenUrl(),
           `${location.origin}/base/x/testplugin`
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 4440fc8..56801f4 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
@@ -33,6 +33,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-js-api-interface.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 suite('gr-plugin-loader tests', () => {
   const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
   let plugin;
@@ -367,7 +368,7 @@
 
     test('relative path should honor getBaseUrl', () => {
       const testUrl = '/test';
-      sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => testUrl);
+      sandbox.stub(BaseUrlBehavior, 'getBaseUrl', () => testUrl);
 
       Gerrit._loadPlugins([
         'foo/bar.js',
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index a31e5dc..3cf38e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+
 (function(window) {
   'use strict';
 
@@ -136,7 +139,7 @@
   Plugin.prototype.url = function(opt_path) {
     const relPath = '/plugins/' + this._name + (opt_path || '/');
     const sameOriginPath = window.location.origin +
-      `${Gerrit.BaseUrlBehavior.getBaseUrl()}${relPath}`;
+      `${BaseUrlBehavior.getBaseUrl()}${relPath}`;
     if (window.location.origin === this._url.origin) {
       // Plugin loaded from the same origin as gr-app, getBaseUrl in effect.
       return sameOriginPath;
@@ -152,7 +155,7 @@
 
   Plugin.prototype.screenUrl = function(opt_screenName) {
     const origin = location.origin;
-    const base = Gerrit.BaseUrlBehavior.getBaseUrl();
+    const base = BaseUrlBehavior.getBaseUrl();
     const tokenPart = opt_screenName ? '/' + opt_screenName : '';
     return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
   };
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 07ce424..f8a48e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -16,7 +16,6 @@
  */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../styles/shared-styles.js';
 import '../../core/gr-navigation/gr-navigation.js';
 import './link-text-parser.js';
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 aac6d76..185e394 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
@@ -14,9 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+
 (function() {
   'use strict';
-
   /**
    * Pattern describing URLs with supported protocols.
    *
@@ -42,7 +44,7 @@
     this.linkConfig = linkConfig;
     this.callback = callback;
     this.removeZeroWidthSpace = opt_removeZeroWidthSpace;
-    this.baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl();
+    this.baseUrl = BaseUrlBehavior.getBaseUrl();
     Object.preventExtensions(this);
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index d52a912..68a0768 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -18,7 +18,6 @@
 
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/iron-icon/iron-icon.js';
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import '../../../styles/shared-styles.js';
@@ -28,17 +27,17 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-list-view_html.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
 
 /**
- * @appliesMixin Gerrit.BaseUrlMixin
  * @appliesMixin Gerrit.FireMixin
  * @appliesMixin Gerrit.URLEncodingMixin
  * @extends Polymer.Element
  */
 class GrListView extends mixinBehaviors( [
-  Gerrit.BaseUrlBehavior,
+  BaseUrlBehavior,
   Gerrit.FireBehavior,
   Gerrit.URLEncodingBehavior,
 ], GestureEventListeners(
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
index 4f68e4b..ab0b9f4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
@@ -14,6 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
+
 (function(window) {
   'use strict';
   window.Gerrit = window.Gerrit || {};
@@ -41,7 +43,7 @@
     }
 
     get baseUrl() {
-      return Gerrit.BaseUrlBehavior.getBaseUrl();
+      return BaseUrlBehavior.getBaseUrl();
     }
 
     /**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index ab79f64..02b0926 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -25,8 +25,8 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script type="module">
 import '../../../test/common-test-setup.js';
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import './gr-auth.js';
+import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 suite('gr-auth', () => {
   let auth;
   let sandbox;
@@ -273,7 +273,7 @@
 
     test('base url support', done => {
       const baseUrl = 'http://foo';
-      sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
+      sandbox.stub(BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
       auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
         const [url] = fetch.lastCall.args;
         assert.equal(url, 'http://foo/a/url?access_token=zbaz');
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 849be00..b1ef1b3 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
@@ -23,7 +23,6 @@
 */
 import '../../../scripts/bundled-polymer.js';
 
-import '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import '../../../behaviors/fire-behavior/fire-behavior.js';
 import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
diff --git a/tools/js/eslint.bzl b/tools/js/eslint.bzl
index ec2dfa7..bd2bc32 100644
--- a/tools/js/eslint.bzl
+++ b/tools/js/eslint.bzl
@@ -19,25 +19,25 @@
 def eslint(name, plugins, srcs, config, ignore, extensions = [".js"], data = []):
     """ Macro to define eslint rules for files.
 
-        Args:
-            name: name of the rule
-            plugins: list of npm dependencies with plugins, for example "@npm//eslint-config-google"
-            srcs: list of files to be checked (ignored in {name}_bin rule)
-            config: eslint config file
-            ignore: eslint ignore file
-            extensions: list of file extensions to be checked. This is an additional filter for
-                srcs list. Each extension must start with '.' character.
-                Default: [".js"].
-            data: list of additional dependencies. For example if a config file extends an another
-                file, this other file must be added to data.
+    Args:
+        name: name of the rule
+        plugins: list of npm dependencies with plugins, for example "@npm//eslint-config-google"
+        srcs: list of files to be checked (ignored in {name}_bin rule)
+        config: eslint config file
+        ignore: eslint ignore file
+        extensions: list of file extensions to be checked. This is an additional filter for
+            srcs list. Each extension must start with '.' character.
+            Default: [".js"].
+        data: list of additional dependencies. For example if a config file extends an another
+            file, this other file must be added to data.
 
-        Generate: 2 rules:
-            {name}_test rule - runs eslint tests. You can run this rule with
-                'bazel test {name}_test' command. The rule tests all files from srcs with specified
-                extensions inside the package where eslint macro is called.
-            {name}_bin rule - runs eslint with specified settings; ignores srcs. To use this rule
-                you must pass a folder to check, for example:
-                baze run {name}_test -- --fix $(pwd)/polygerrit-ui/app
+    Generate: 2 rules:
+        {name}_test rule - runs eslint tests. You can run this rule with
+            'bazel test {name}_test' command. The rule tests all files from srcs with specified
+            extensions inside the package where eslint macro is called.
+        {name}_bin rule - runs eslint with specified settings; ignores srcs. To use this rule
+            you must pass a folder to check, for example:
+            bazel run {name}_test -- --fix $(pwd)/polygerrit-ui/app
     """
     entry_point = "@npm//:node_modules/eslint/bin/eslint.js"
     bin_data = [