Merge "Add allowEmpty to CherryPickInput"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 23ab39b..88edde2 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1311,7 +1311,9 @@
 
 Allow group creation.  Groups are used to grant users access to different
 actions in projects.  This capability allows the granted group members to
-either link:cmd-create-group.html[create new groups via ssh] or via the web UI.
+either link:cmd-create-group.html[create new groups via ssh] or via the web UI
+by navigating at the top of the page to BROWSE -> Groups, and then pushing the
+"CREATE NEW" button.
 
 
 [[capability_createProject]]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 11980a0..29d3a02 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3123,11 +3123,9 @@
 for production use. For compatibility information, please refer to the
 link:https://www.gerritcodereview.com/elasticsearch.html[project homepage,role=external,window=_blank].
 
-When using Elasticsearch version 5.6, the open and closed changes are
-indexed in a single index, separated into types `open_changes` and `closed_changes`
-respectively. When using version 6.2 or later, the open and closed changes are
-merged into the default `_doc` type. The latter is also used for the accounts and
-groups indices starting with Elasticsearch 6.2.
+In Elasticsearch version 6.2 or later, the open and closed changes are merged
+into the default `_doc` type. The latter is also used for the accounts and groups
+indices starting with Elasticsearch 6.2.
 
 Note that when Gerrit is configured to use Elasticsearch, the Elasticsearch
 server(s) must be reachable during the site initialization.
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 70fbd60..1a9f74c 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -245,6 +245,79 @@
 get reverted so that there is no unused or half-finished code in the
 code base.
 
+[[esc-dd-evaluation]]
+== How the ESC evaluates design documents
+This section describes how the ESC evaluates design documents. It’s
+meant as a guideline rather than being prescriptive for both ESC
+members and contributors.
+
+=== General Process
+As part of the design process, the ESC makes a final decision if a
+design gets to be implemented. If there are multiple alternative
+solutions, the ESC will decide which solution can be implemented.
+
+The ESC should wait until all contributors had the chance to
+voice their opinion in review comments or by proposing alternative
+solutions. Due to the infrequent ESC meetings (every 2-4 weeks)
+the ESC might discuss documents in cases where the discussion is
+already advanced far enough, but not make a decision yet. In this
+case, contributors can still voice concerns or discuss alternatives.
+The decision can be at the next meeting or via email in between
+meetings.
+
+=== Evaluation
+Product/Vision fit
+Q: `Do we believe this feature belongs to Gerrit Code Review use-cases?`
+* Yes: Customizable dashboards
+* No: UI for managing an LDAP server
+
+Q: `Is the proposed solution aligned with Gerrit’s vision?`
+* Yes: Showing comments of older patch sets in newer patch sets to
+  keep track (core code review)
+* No: Implement a bug tracker in Gerrit (not core code review).
+
+=== Impact
+Q: `Will the new feature have a measurable, positive impact?`
+* Yes: Increased productivity, faster/smoother workflow, etc.
+* Yes: Better latency, more reliable system.
+* No: Unclear impact or lacking metrics to measure.
+
+=== Complexity
+Q: `Can we support/maintain this feature once it is in Gerrit?`
+* Yes: Code will fit into codebase, be well tested, easy to
+  understand.
+* No: Will add code or a workflow that is hard to understand
+  and easy to misinterpret.
+Q: `Is the proposed feature or rework adding unnecessary complexity?`
+* Yes: Adding a dependency on a well-supported library.
+* No: Adding a dependency on a library that is not widely used
+  or not actively maintained.
+
+=== Core vs. Plugin decision
+Q: `Would this fit better in a plugin?`
+* Yes:The proposed feature or rework is an implementation (e.g. Lucene
+  is an index implementation) of a generic concept that others
+  might want to implement differently.
+* Yes: The proposed feature or rework is very specific to a custom setup.
+* No: The proposed feature or rework is applicable to a wider user
+  base.
+* No: The proposed feature or rework is a `core code review feature`.
+
+=== Commitment
+Q: `Is someone willing to implement it?` (this question is
+especially important when reviewers propose alternative designs
+to the author’s own solution).
+* Yes: The author or someone else commits to implementing the
+  proposed solution.
+* Yes: If a mentorship is required, a mentor is willing to help.
+* No: Unclear ownership, mentorship or implementation plan.
+
+=== Community
+Q: `If in doubt, is there a substantial benefit to a long-standing
+community member with many users?`
+* The community shapes the future of Gerrit as a product. In
+  cases of doubt, the ESC can be more generous with long-standing community members compared to `drive-by` contributions.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-core-plugins.txt b/Documentation/dev-core-plugins.txt
index 12726b7..04e2420 100644
--- a/Documentation/dev-core-plugins.txt
+++ b/Documentation/dev-core-plugins.txt
@@ -27,6 +27,11 @@
 Non-Gerrit maintainers cannot have link:access-control.html#category_owner[
 Owner] permissions for core plugins.
 
+[[list]]
+== Which core plugins exist?
+
+See link:config-plugins.html#core-plugins[here].
+
 [[criteria]]
 === Criteria for Core Plugins
 
@@ -82,10 +87,73 @@
 their ownership. If the current plugin owners disagree, forking the plugin is
 possible, but this should happen only in exceptional cases.
 
-[[list]]
-== Which core plugins exist?
+[[process-to-make-a-plugin-a-core-plugin]]
+== Process to make a plugin a core plugin
 
-See link:config-plugins.html#core-plugins[here].
+Anyone can propose to make a plugin a core plugin, but to be accepted as core
+plugin the plugin must fulfill certain link:#criteria[criteria].
+
+1. Propose a plugin as core plugin:
++
+File a link:https://bugs.chromium.org/p/gerrit/issues/entry?template=Core+Plugin+Request[
+Core Plugin Request] in the issue tracker and provide the information that is
+being asked for in the request template.
+
+2. Community Feedback:
++
+Anyone can comment on the issue to raise concerns or to support the request.
+The issue should stay open for at least 10 calendar days so that everyone in
+the community has a chance to comment.
+
+3. ESC Decision:
++
+The ESC should discuss the request and reject/approve it or ask for further
+information that the reporter or commenters should provide.
++
+Any decision must be based on the link:#criteria[criteria] that core plugins
+are expected to fulfill and should take the feedback from the community into
+account.
++
+Accepting the request is only possible if the issue was open for at least 10
+calendar days (see 2., this gives the community time to comment).
++
+When the ESC approves/rejects the request a summary of the reasons for the
+decision should be added to the issue.
++
+If a request is rejected, it's possible to ask for a revalidation if the
+concerns that led to the rejection have been addressed (e.g. the plugin was
+rejected due to missing tests, but tests have been added afterwards).
+
+4. Add plugin as core plugin:
++
+If the request was accepted, a Gerrit maintainer should add the plugin as
+core plugin:
++
+--
+** Host the plugin repo on link:https://gerrit-review.googlesource.com/[
+   gerrit-review].
+** Ensure that the plugin repo inherits from the
+   link:https://gerrit-review.googlesource.com/admin/repos/Public-Plugins[
+   Public-Plugins] repo.
+** Remove all permissions on the plugin repo (the inherited permissions from
+   `Public-Plugins` should be enough). Especially make sure that there are no
+   link:access-control.html#category_owner[Owner],
+   link:access-control.html#category_push_direct[Direct Push],
+   link:access-control.html#category_submit[Submit] or
+   link:access-control.html#category_review_labels[Code-Review+2]
+   permissions for non-Gerrit maintainers.
+** Create a component for the plugin in
+   link:https://bugs.chromium.org/p/gerrit/adminComponents[Monorail] and assign
+   all issues that already exist for the plugin to this component.
+** Add the plugin as
+   link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/.gitmodules[Git
+   submodule].
+** Register the plugin as core plugin in
+   link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/tools/bzl/plugins.bzl[
+   plugins.bzl].
+** Announce the new core plugin in the
+   link:https://www.gerritcodereview.com/news.html[project news].
+--
 
 GERRIT
 ------
diff --git a/Documentation/dev-roles.txt b/Documentation/dev-roles.txt
index c16037e..2ca7f22 100644
--- a/Documentation/dev-roles.txt
+++ b/Documentation/dev-roles.txt
@@ -336,7 +336,10 @@
 steering committee members] at the same time so that they can represent
 the community without conflict of interest.
 
-Nomination process, election process and election period for the
+Anybody from the Gerrit community can candidate as community manager.
+This means, in contrast to candidating for the ESC, candidating as
+community manager is not limited to Gerrit maintainers. Otherwise the
+nomination process, election process and election period for the
 non-Google community manager are the same as for
 link:dev-processes.html#steering-committee-election[steering committee
 members].
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index dd55517..efc9de8 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2728,6 +2728,20 @@
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
 ----
 
+To upload a file as binary data in the request body:
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "binary_content": "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="
+  }
+----
+
+Note that it must be base-64 encoded data uri.
+
 When change edit doesn't exist for this change yet it is created. When file
 content isn't provided, it is wiped out for that file. As response
 "`204 No Content`" is returned.
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 730af90..a21f9f5 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -138,8 +138,7 @@
       SitePaths sitePaths,
       Schema<V> schema,
       ElasticRestClientProvider client,
-      String indexName,
-      String indexType) {
+      String indexName) {
     this.config = config;
     this.sitePaths = sitePaths;
     this.schema = schema;
@@ -148,16 +147,7 @@
     this.indexName = config.getIndexName(indexName, schema.getVersion());
     this.indexNameRaw = indexName;
     this.client = client;
-    this.type = client.adapter().getType(indexType);
-  }
-
-  AbstractElasticIndex(
-      ElasticConfiguration cfg,
-      SitePaths sitePaths,
-      Schema<V> schema,
-      ElasticRestClientProvider client,
-      String indexName) {
-    this(cfg, sitePaths, schema, client, indexName, indexName);
+    this.type = client.adapter().getType();
   }
 
   @Override
@@ -223,8 +213,8 @@
 
   protected abstract String getId(V v);
 
-  protected String getMappingsForSingleType(String candidateType, MappingProperties properties) {
-    return getMappingsFor(client.adapter().getType(candidateType), properties);
+  protected String getMappingsForSingleType(MappingProperties properties) {
+    return getMappingsFor(client.adapter().getType(), properties);
   }
 
   protected String getMappingsFor(String type, MappingProperties properties) {
@@ -240,8 +230,8 @@
     return gson.toJson(mappings);
   }
 
-  protected String delete(String type, K id) {
-    return new DeleteRequest(id.toString(), indexName, type, client.adapter()).toString();
+  protected String getDeleteRequest(K id) {
+    return new DeleteRequest(id.toString(), indexName).toString();
   }
 
   protected abstract V fromDocument(JsonObject doc, Set<String> fields);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index c3e3264..33217bd 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -74,7 +74,7 @@
   @Override
   public void replace(AccountState as) {
     BulkRequest bulk =
-        new IndexRequest(getId(as), indexName, type, client.adapter())
+        new IndexRequest(getId(as), indexName)
             .add(new UpdateRequest<>(schema, as, ImmutableSet.of()));
 
     String uri = getURI(type, BULK);
@@ -105,12 +105,12 @@
 
   @Override
   protected String getDeleteActions(Account.Id a) {
-    return delete(type, a);
+    return getDeleteRequest(a);
   }
 
   @Override
   protected String getMappings() {
-    return getMappingsForSingleType(ACCOUNTS, mapping.accounts);
+    return getMappingsForSingleType(mapping.accounts);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 7fc4d0e..fe55eab 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -21,7 +21,6 @@
 
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
@@ -30,7 +29,6 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
-import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
 import com.google.gerrit.entities.Account;
@@ -119,24 +117,8 @@
 
   @Override
   public void replace(ChangeData cd) {
-    String deleteIndex;
-    String insertIndex;
-
-    if (cd.change().isNew()) {
-      insertIndex = OPEN_CHANGES;
-      deleteIndex = CLOSED_CHANGES;
-    } else {
-      insertIndex = CLOSED_CHANGES;
-      deleteIndex = OPEN_CHANGES;
-    }
-
-    ElasticQueryAdapter adapter = client.adapter();
     BulkRequest bulk =
-        new IndexRequest(getId(cd), indexName, adapter.getType(insertIndex), adapter)
-            .add(new UpdateRequest<>(schema, cd, skipFields));
-    if (adapter.deleteToReplace()) {
-      bulk.add(new DeleteRequest(cd.getId().toString(), indexName, deleteIndex, adapter));
-    }
+        new IndexRequest(getId(cd), indexName).add(new UpdateRequest<>(schema, cd, skipFields));
 
     String uri = getURI(type, BULK);
     Response response = postRequest(uri, bulk, getRefreshParam());
@@ -190,18 +172,12 @@
 
   @Override
   protected String getDeleteActions(Change.Id c) {
-    if (!client.adapter().useV5Type()) {
-      return delete(client.adapter().getType(), c);
-    }
-    return delete(OPEN_CHANGES, c) + delete(CLOSED_CHANGES, c);
+    return getDeleteRequest(c);
   }
 
   @Override
   protected String getMappings() {
-    if (!client.adapter().useV5Type()) {
-      return getMappingsFor(client.adapter().getType(), mapping.changes);
-    }
-    return gson.toJson(ImmutableMap.of(MAPPINGS, mapping));
+    return getMappingsFor(client.adapter().getType(), mapping.changes);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index ce2025f..a16ec7f 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -74,7 +74,7 @@
   @Override
   public void replace(InternalGroup group) {
     BulkRequest bulk =
-        new IndexRequest(getId(group), indexName, type, client.adapter())
+        new IndexRequest(getId(group), indexName)
             .add(new UpdateRequest<>(schema, group, ImmutableSet.of()));
 
     String uri = getURI(type, BULK);
@@ -97,12 +97,12 @@
 
   @Override
   protected String getDeleteActions(AccountGroup.UUID g) {
-    return delete(type, g);
+    return getDeleteRequest(g);
   }
 
   @Override
   protected String getMappings() {
-    return getMappingsForSingleType(GROUPS, mapping.groups);
+    return getMappingsForSingleType(mapping.groups);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index 392c776..ed15e34 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -76,7 +76,7 @@
   @Override
   public void replace(ProjectData projectState) {
     BulkRequest bulk =
-        new IndexRequest(projectState.getProject().getName(), indexName, type, client.adapter())
+        new IndexRequest(projectState.getProject().getName(), indexName)
             .add(new UpdateRequest<>(schema, projectState, ImmutableSet.of()));
 
     String uri = getURI(type, BULK);
@@ -99,12 +99,12 @@
 
   @Override
   protected String getDeleteActions(Project.NameKey nameKey) {
-    return delete(type, nameKey);
+    return getDeleteRequest(nameKey);
   }
 
   @Override
   protected String getMappings() {
-    return getMappingsForSingleType(PROJECTS, mapping.projects);
+    return getMappingsForSingleType(mapping.projects);
   }
 
   @Override
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
index 72c52b0..7233682 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -16,15 +16,12 @@
 
 import static com.google.gerrit.elasticsearch.ElasticVersion.V6_7;
 
-import com.google.gson.JsonObject;
-
 public class ElasticQueryAdapter {
   static final String V6_TYPE = "_doc";
 
   private static final String INCLUDE_TYPE = "include_type_name=true";
   private static final String INDICES = "?allow_no_indices=false";
 
-  private final boolean useV5Type;
   private final boolean useV6Type;
   private final boolean omitType;
   private final int defaultNumberOfShards;
@@ -39,7 +36,6 @@
   private final String includeTypeNameParam;
 
   ElasticQueryAdapter(ElasticVersion version) {
-    this.useV5Type = !version.isV6OrLater();
     this.useV6Type = version.isV6();
     this.omitType = version.isV7OrLater();
     this.defaultNumberOfShards = version.isV7OrLater() ? 1 : 5;
@@ -54,12 +50,6 @@
     this.includeTypeNameParam = version.isAtLeastMinorVersion(V6_7) ? "?" + INCLUDE_TYPE : "";
   }
 
-  public void setType(JsonObject properties, String type) {
-    if (useV5Type) {
-      properties.addProperty("_type", type);
-    }
-  }
-
   public String searchFilteringName() {
     return searchFilteringName;
   }
@@ -84,14 +74,6 @@
     return rawFieldsKey;
   }
 
-  boolean deleteToReplace() {
-    return useV5Type;
-  }
-
-  boolean useV5Type() {
-    return useV5Type;
-  }
-
   boolean useV6Type() {
     return useV6Type;
   }
@@ -105,14 +87,7 @@
   }
 
   String getType() {
-    return getType("");
-  }
-
-  String getType(String type) {
-    if (useV6Type()) {
-      return V6_TYPE;
-    }
-    return useV5Type() ? type : "";
+    return useV6Type() ? V6_TYPE : "";
   }
 
   String getVersionDiscoveryUrl(String name) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 6f4ade2..2d023cf 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -18,7 +18,6 @@
 import java.util.regex.Pattern;
 
 public enum ElasticVersion {
-  V5_6("5.6.*"),
   V6_2("6.2.*"),
   V6_3("6.3.*"),
   V6_4("6.4.*"),
diff --git a/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.java b/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.java
index 7392d09..16b821c 100644
--- a/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.java
+++ b/java/com/google/gerrit/elasticsearch/bulk/ActionRequest.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.elasticsearch.bulk;
 
-import com.google.gerrit.elasticsearch.ElasticQueryAdapter;
 import com.google.gson.JsonObject;
 
 abstract class ActionRequest extends BulkRequest {
@@ -22,16 +21,11 @@
   private final String action;
   private final String id;
   private final String index;
-  private final String type;
-  private final ElasticQueryAdapter adapter;
 
-  protected ActionRequest(
-      String action, String id, String index, String type, ElasticQueryAdapter adapter) {
+  protected ActionRequest(String action, String id, String index) {
     this.action = action;
     this.id = id;
     this.index = index;
-    this.type = type;
-    this.adapter = adapter;
   }
 
   @Override
@@ -39,7 +33,6 @@
     JsonObject properties = new JsonObject();
     properties.addProperty("_id", id);
     properties.addProperty("_index", index);
-    adapter.setType(properties, type);
 
     JsonObject jsonAction = new JsonObject();
     jsonAction.add(action, properties);
diff --git a/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java b/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java
index 570d5a0..6451b0f 100644
--- a/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java
+++ b/java/com/google/gerrit/elasticsearch/bulk/DeleteRequest.java
@@ -14,11 +14,9 @@
 
 package com.google.gerrit.elasticsearch.bulk;
 
-import com.google.gerrit.elasticsearch.ElasticQueryAdapter;
-
 public class DeleteRequest extends ActionRequest {
 
-  public DeleteRequest(String id, String index, String type, ElasticQueryAdapter adapter) {
-    super("delete", id, index, type, adapter);
+  public DeleteRequest(String id, String index) {
+    super("delete", id, index);
   }
 }
diff --git a/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java b/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java
index c571a0e..d90b80f 100644
--- a/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java
+++ b/java/com/google/gerrit/elasticsearch/bulk/IndexRequest.java
@@ -14,11 +14,9 @@
 
 package com.google.gerrit.elasticsearch.bulk;
 
-import com.google.gerrit.elasticsearch.ElasticQueryAdapter;
-
 public class IndexRequest extends ActionRequest {
 
-  public IndexRequest(String id, String index, String type, ElasticQueryAdapter adapter) {
-    super("index", id, index, type, adapter);
+  public IndexRequest(String id, String index) {
+    super("index", id, index);
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
index 93c253d..0cfe908 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
@@ -20,4 +20,5 @@
 /** Content to be added to a file (new or existing) via change edit. */
 public class FileContentInput {
   @DefaultInput public RawInput content;
+  public String binary_content;
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 68e39e7..9a25f52 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
@@ -35,6 +36,7 @@
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -66,9 +68,12 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.Base64;
 import org.kohsuke.args4j.Option;
 
 @Singleton
@@ -277,6 +282,10 @@
   /** Put handler that is activated when PUT request is called on collection element. */
   @Singleton
   public static class Put implements RestModifyView<ChangeEditResource, FileContentInput> {
+    private static final Pattern BINARY_DATA_PATTERN =
+        Pattern.compile("data:([\\w/.-]*);([\\w]+),(.*)");
+    private static final String BASE64 = "base64";
+
     private final ChangeEditModifier editModifier;
     private final GitRepositoryManager repositoryManager;
     private final EditMessage editMessage;
@@ -301,22 +310,36 @@
     public Response<Object> apply(ChangeResource rsrc, String path, FileContentInput input)
         throws AuthException, BadRequestException, ResourceConflictException, IOException,
             PermissionBackendException {
-      if (input.content == null) {
-        throw new BadRequestException("new content required");
+
+      if (input.content == null && input.binary_content == null) {
+        throw new BadRequestException("either content or binary_content is required");
       }
 
-      if (Patch.COMMIT_MSG.equals(path)) {
+      RawInput newContent;
+      if (input.binary_content != null) {
+        Matcher m = BINARY_DATA_PATTERN.matcher(input.binary_content);
+        if (m.matches() && BASE64.equals(m.group(2))) {
+          newContent = RawInputUtil.create(Base64.decode(m.group(3)));
+        } else {
+          throw new BadRequestException("binary_content must be encoded as base64 data uri");
+        }
+      } else {
+        newContent = input.content;
+      }
+
+      if (Patch.COMMIT_MSG.equals(path) && input.binary_content == null) {
         EditMessage.Input editCommitMessageInput = new EditMessage.Input();
         editCommitMessageInput.message =
-            new String(ByteStreams.toByteArray(input.content.getInputStream()), UTF_8);
+            new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8);
         return editMessage.apply(rsrc, editCommitMessageInput);
       }
+
       if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
         throw new ResourceConflictException("Invalid path: " + path);
       }
 
       try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
-        editModifier.modifyFile(repository, rsrc.getNotes(), path, input.content);
+        editModifier.modifyFile(repository, rsrc.getNotes(), path, newContent);
       } catch (InvalidChangeOperationException e) {
         throw new ResourceConflictException(e.getMessage());
       }
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index b10dd41..266931a 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -61,12 +61,10 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
-import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.FixReplacementInfo;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -330,23 +328,9 @@
       for (ReviewerAddition reviewerResult : reviewerResults) {
         reviewerResult.op.suppressEmail(); // Send a single batch email below.
         bu.addOp(revision.getChange().getId(), reviewerResult.op);
-        if (!ccOrReviewer && reviewerResult.result.reviewers != null) {
-          for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) {
-            if (Objects.equals(id.get(), reviewerInfo._accountId)) {
-              logger.atFine().log("calling user is explicitly added as reviewer");
-              ccOrReviewer = true;
-              break;
-            }
-          }
-        }
-        if (!ccOrReviewer && reviewerResult.result.ccs != null) {
-          for (AccountInfo accountInfo : reviewerResult.result.ccs) {
-            if (Objects.equals(id.get(), accountInfo._accountId)) {
-              logger.atFine().log("calling user is explicitly added as cc");
-              ccOrReviewer = true;
-              break;
-            }
-          }
+        if (!ccOrReviewer && reviewerResult.reviewers.contains(id)) {
+          logger.atFine().log("calling user is explicitly added as reviewer or CC");
+          ccOrReviewer = true;
         }
       }
 
@@ -389,8 +373,7 @@
           revision.getChange().getId(), new Op(projectState, revision.getPatchSet().id(), input));
 
       // Notify based on ReviewInput, ignoring the notify settings from any AddReviewerInputs.
-      NotifyResolver.Result notify =
-          notifyResolver.resolve(getNotifyHandling(input, output, revision), input.notifyDetails);
+      NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);
       bu.setNotify(notify);
 
       bu.execute();
@@ -408,17 +391,6 @@
     return Response.ok(output);
   }
 
-  private NotifyHandling getNotifyHandling(
-      ReviewInput input, ReviewResult output, RevisionResource revision) {
-    if (input.notify != null) {
-      return input.notify;
-    }
-    if ((output.ready != null && output.ready) || !revision.getChange().isWorkInProgress()) {
-      return NotifyHandling.ALL;
-    }
-    return NotifyHandling.NONE;
-  }
-
   private NotifyHandling defaultNotify(Change c, ReviewInput in) {
     boolean workInProgress = c.isWorkInProgress();
     if (in.workInProgress) {
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 362fb1b..0b8f441 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -149,7 +149,7 @@
   @Test
   public void addedRobotCommentsAreLinkedToChangeMessages() throws Exception {
     TestTimeUtil.resetWithClockStep(0, TimeUnit.SECONDS);
-    PushOneCommit.Result r = createChange();
+    createChange();
     /* Advancing the time after creating the change so that the first robot comment is not in the same timestamp as with the change creation */
     TestTimeUtil.incrementClock(5, TimeUnit.SECONDS);
 
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 64349a4..b717eb7 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -96,6 +96,15 @@
   private static final byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
   private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
   private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
+  private static final String CONTENT_BINARY_ENCODED_NEW =
+      "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==";
+  private static final byte[] CONTENT_BINARY_DECODED_NEW = "Hello, World!".getBytes(UTF_8);
+  private static final String CONTENT_BINARY_ENCODED_NEW2 =
+      "data:text/plain;base64,VXBsb2FkaW5nIHRvIGFuIGVkaXQgd29ya2VkIQ==";
+  private static final byte[] CONTENT_BINARY_DECODED_NEW2 =
+      "Uploading to an edit worked!".getBytes(UTF_8);
+  private static final String CONTENT_BINARY_ENCODED_NEW3 =
+      "data:text/plain,VXBsb2FkaW5nIHRvIGFuIGVkaXQgd29ya2VkIQ==";
 
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
@@ -316,7 +325,7 @@
         assertThrows(
             BadRequestException.class,
             () -> gApi.changes().id(changeId).edit().modifyFile(Patch.COMMIT_MSG, (RawInput) null));
-    assertThat(ex).hasMessageThat().isEqualTo("new content required");
+    assertThat(ex).hasMessageThat().isEqualTo("either content or binary_content is required");
   }
 
   @Test
@@ -560,11 +569,30 @@
   }
 
   @Test
+  public void createAndUploadBinaryInChangeEditOneRequestRest() throws Exception {
+    FileContentInput in = new FileContentInput();
+    in.binary_content = CONTENT_BINARY_ENCODED_NEW;
+    adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
+    ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW);
+    in.binary_content = CONTENT_BINARY_ENCODED_NEW2;
+    adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
+    ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW2);
+  }
+
+  @Test
+  public void invalidBase64UploadBinaryInChangeEditOneRequestRest() throws Exception {
+    FileContentInput in = new FileContentInput();
+    in.binary_content = CONTENT_BINARY_ENCODED_NEW3;
+    adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertBadRequest();
+  }
+
+  @Test
   public void changeEditNoContentProvidedRest() throws Exception {
     createEmptyEditFor(changeId);
-    adminRestSession
-        .put(urlEditFile(changeId, FILE_NAME), new FileContentInput())
-        .assertBadRequest();
+
+    FileContentInput in = new FileContentInput();
+    in.binary_content = null;
+    adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertBadRequest();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 5bbaf40..18c2e72 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -26,11 +26,6 @@
 public class ElasticReindexIT extends AbstractReindexTests {
 
   @ConfigSuite.Default
-  public static Config elasticsearchV5() {
-    return getConfig(ElasticVersion.V5_6);
-  }
-
-  @ConfigSuite.Config
   public static Config elasticsearchV6() {
     return getConfig(ElasticVersion.V6_8);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 8cfcbab..264ced6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -45,7 +45,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
-import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
@@ -55,11 +54,9 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 @UseClockStep
 @UseTimezone(timezone = "US/Eastern")
-@RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailIT.java
index 0d31a96..c296a5c 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailIT.java
@@ -30,10 +30,8 @@
 import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 @NoHttpd
-@RunWith(ConfigSuite.class)
 public class MailIT extends AbstractDaemonTest {
   private static final String RECEIVEEMAIL = "receiveemail";
   private static final String HOST = "localhost";
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 049fb8b..f130d1f8 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -27,11 +27,6 @@
 public class ElasticIndexIT extends AbstractIndexTests {
 
   @ConfigSuite.Default
-  public static Config elasticsearchV5() {
-    return getConfig(ElasticVersion.V5_6);
-  }
-
-  @ConfigSuite.Config
   public static Config elasticsearchV6() {
     return getConfig(ElasticVersion.V6_8);
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index 9783f52..ab2bb12 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -47,8 +47,6 @@
 
 SUFFIX = "sTest.java"
 
-ELASTICSEARCH_TESTS_V5 = {i: "ElasticV5Query" + i.capitalize() + SUFFIX for i in TYPES}
-
 ELASTICSEARCH_TESTS_V6 = {i: "ElasticV6Query" + i.capitalize() + SUFFIX for i in TYPES}
 
 ELASTICSEARCH_TESTS_V7 = {i: "ElasticV7Query" + i.capitalize() + SUFFIX for i in TYPES}
@@ -60,14 +58,6 @@
 ]
 
 [junit_tests(
-    name = "elasticsearch_query_%ss_test_V5" % name,
-    size = "large",
-    srcs = [src],
-    tags = ELASTICSEARCH_TAGS,
-    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name],
-) for name, src in ELASTICSEARCH_TESTS_V5.items()]
-
-[junit_tests(
     name = "elasticsearch_query_%ss_test_V6" % name,
     size = "large",
     srcs = [src],
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 9d0319c..879c7c5 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -38,8 +38,6 @@
 
   private static String getImageName(ElasticVersion version) {
     switch (version) {
-      case V5_6:
-        return "blacktop/elasticsearch:5.6.16";
       case V6_2:
         return "blacktop/elasticsearch:6.2.4";
       case V6_3:
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 2ae59b0..dcc6880 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -23,22 +23,13 @@
 import org.eclipse.jgit.lib.Config;
 
 public final class ElasticTestUtils {
-  public static void configure(
-      Config config, ElasticContainer container, String prefix, ElasticVersion version) {
+  public static void configure(Config config, ElasticContainer container, String prefix) {
     String hostname = container.getHttpHost().getHostName();
     int port = container.getHttpHost().getPort();
     config.setString("index", null, "type", "elasticsearch");
     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;
-    if (password != null) {
-      config.setString("elasticsearch", null, "password", password);
-    }
-  }
-
-  public static void configure(Config config, ElasticContainer container, String prefix) {
-    configure(config, container, prefix, null);
   }
 
   public static void createAllIndexes(Injector injector) {
@@ -53,7 +44,7 @@
     ElasticContainer container = ElasticContainer.createAndStart(version);
     String indicesPrefix = UUID.randomUUID().toString();
     Config cfg = new Config();
-    configure(cfg, container, indicesPrefix, version);
+    configure(cfg, container, indicesPrefix);
     return cfg;
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
deleted file mode 100644
index ab2b98d..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public class ElasticV5QueryAccountsTest extends AbstractQueryAccountsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName.getSanitizedMethodName();
-    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
deleted file mode 100644
index 97f235c..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.GerritTestName;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-
-public class ElasticV5QueryChangesTest extends AbstractQueryChangesTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Rule public final GerritTestName testName = new GerritTestName();
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName.getSanitizedMethodName();
-    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
deleted file mode 100644
index c490e78..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public class ElasticV5QueryGroupsTest extends AbstractQueryGroupsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName.getSanitizedMethodName();
-    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
deleted file mode 100644
index a6a8605..0000000
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.elasticsearch;
-
-import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.IndexConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-public class ElasticV5QueryProjectsTest extends AbstractQueryProjectsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return IndexConfig.createForElasticsearch();
-  }
-
-  private static ElasticContainer container;
-
-  @BeforeClass
-  public static void startIndexService() {
-    if (container == null) {
-      // Only start Elasticsearch once
-      container = ElasticContainer.createAndStart(ElasticVersion.V5_6);
-    }
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    Config elasticsearchConfig = new Config(config);
-    InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName.getSanitizedMethodName();
-    ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
-  }
-}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 0482211..20c8a17 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -22,9 +22,6 @@
 public class ElasticVersionTest {
   @Test
   public void supportedVersion() throws Exception {
-    assertThat(ElasticVersion.forVersion("5.6.0")).isEqualTo(ElasticVersion.V5_6);
-    assertThat(ElasticVersion.forVersion("5.6.11")).isEqualTo(ElasticVersion.V5_6);
-
     assertThat(ElasticVersion.forVersion("6.2.0")).isEqualTo(ElasticVersion.V6_2);
     assertThat(ElasticVersion.forVersion("6.2.4")).isEqualTo(ElasticVersion.V6_2);
 
@@ -82,7 +79,6 @@
 
   @Test
   public void atLeastMinorVersion() throws Exception {
-    assertThat(ElasticVersion.V5_6.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V6_2.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V6_3.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V6_4.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
@@ -101,7 +97,6 @@
 
   @Test
   public void version6OrLater() throws Exception {
-    assertThat(ElasticVersion.V5_6.isV6OrLater()).isFalse();
     assertThat(ElasticVersion.V6_2.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V6_3.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V6_4.isV6OrLater()).isTrue();
@@ -120,7 +115,6 @@
 
   @Test
   public void version7OrLater() throws Exception {
-    assertThat(ElasticVersion.V5_6.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_2.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_3.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V6_4.isV7OrLater()).isFalse();
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 5e25c13..b32ac19 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestChanges;
 import java.util.Date;
 import java.util.TimeZone;
@@ -34,9 +33,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
-@RunWith(ConfigSuite.class)
 public class CommitMessageOutputTest extends AbstractChangeNotesTest {
   @Test
   public void approvalsCommitFormatSimple() throws Exception {
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index f7f29cd..9f34377 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -25,7 +25,10 @@
 import com.google.gerrit.server.CommentsUtil;
 import java.sql.Timestamp;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
+@RunWith(JUnit4.class)
 public class ListChangeCommentsTest {
 
   @Test
@@ -40,8 +43,6 @@
         getNewChangeMessage("cm2key", "cm2", Timestamp.valueOf("2018-01-01 09:01:15"));
     ChangeMessage cm3 =
         getNewChangeMessage("cm3key", "cm3", Timestamp.valueOf("2018-01-01 09:01:27"));
-    ChangeMessage cm4 =
-        getNewChangeMessage("cm4key", "cm4", Timestamp.valueOf("2018-01-01 09:01:32"));
 
     assertThat(c1.changeMessageId).isNull();
     assertThat(c2.changeMessageId).isNull();
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 92ecf2a..5e4d9b8 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -28,19 +28,6 @@
     "browser": true,
     "es6": true
   },
-  "globals": {
-    "__dirname": false,
-    "app": false,
-    "page": false,
-    "Polymer": false,
-    "process": false,
-    "require": false,
-    "Gerrit": false,
-    "Promise": false,
-    "assert": false,
-    "test": false,
-    "flushAsynchronousOperations": false
-  },
   "rules": {
     "no-confusing-arrow": "error",
     "newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}],
@@ -97,7 +84,9 @@
         "message": "Remove suite.only."
       }
     ],
-    "no-undef": "off",
+    // no-undef disables global variable.
+    // "globals" declares allowed global variables.
+    "no-undef": ["error"],
     "no-useless-escape": "off",
     "no-var": "error",
     "operator-linebreak": "off",
@@ -169,11 +158,131 @@
     "import/no-unused-modules": 2,
     "import/no-default-export": 2
   },
+
+  // List of allowed globals in all files
+  "globals": {
+    // Polygerrit global variables.
+    // You must not add anything new in this list!
+    // Instead export variables from modules
+    // TODO(dmfilippov): Remove global variables from polygerrit
+    "Auth": "readonly",
+    "EventEmitter": "readonly",
+    "FetchPromisesCache": "readonly",
+    "Gerrit": "readonly",
+    "GrAdminApi": "readonly",
+    "GrAnnotation": "readonly",
+    "GrAnnotationActionsContext": "readonly",
+    "GrAnnotationActionsInterface": "readonly",
+    "GrAttributeHelper": "readonly",
+    "GrChangeActionsInterface": "readonly",
+    "GrChangeMetadataApi": "readonly",
+    "GrChangeReplyInterface": "readonly",
+    "GrChangeViewApi": "readonly",
+    "GrCountStringFormatter": "readonly",
+    "GrDiffBuilder": "readonly",
+    "GrDiffBuilderBinary": "readonly",
+    "GrDiffBuilderImage": "readonly",
+    "GrDiffBuilderSideBySide": "readonly",
+    "GrDiffBuilderUnified": "readonly",
+    "GrDiffGroup": "readonly",
+    "GrDiffLine": "readonly",
+    "GrDisplayNameUtils": "readonly",
+    "GrDomHook": "readonly",
+    "GrDomHooksManager": "readonly",
+    "GrEditConstants": "readonly",
+    "GrEmailSuggestionsProvider": "readonly",
+    "GrEtagDecorator": "readonly",
+    "GrEventHelper": "readonly",
+    "GrFileListConstants": "readonly",
+    "GrGroupSuggestionsProvider": "readonly",
+    "GrLinkTextParser": "readonly",
+    "GrPluginActionContext": "readonly",
+    "GrPluginEndpoints": "readonly",
+    "GrPluginRestApi": "readonly",
+    "GrPopupInterface": "readonly",
+    "GrRangeNormalizer": "readonly",
+    "GrRepoApi": "readonly",
+    "GrReporting": "readonly",
+    "GrRestApiHelper": "readonly",
+    "GrReviewerSuggestionsProvider": "readonly",
+    "GrReviewerUpdatesParser": "readonly",
+    "GrSettingsApi": "readonly",
+    "GrStylesApi": "readonly",
+    "GrThemeApi": "readonly",
+    "PluginLoader": "readonly",
+    "SiteBasedCache": "readonly",
+    "util": "readonly",
+    // Global variables from 3rd party libraries.
+    // You should not add anything in this list, always try to import
+    // If import is not possible - you can extend this list
+    "Polymer": "readonly",
+    "ShadyCSS": "readonly",
+    "linkify": "readonly",
+    "moment": "readonly",
+    "page": "readonly",
+    "security": "readonly",
+  },
   "overrides": [
     {
       "files": ["*.html", "test.js", "test-infra.js", "template_test.js"],
       "rules": {
         "jsdoc/require-file-overview": "off"
+      },
+    },
+    {
+      "files": ["*.html", "common-test-setup.js"],
+      // Additional global variables allowed in tests
+      "globals": {
+        // Global variables from 3rd party test libraries/frameworks.
+        // You can extend this list if you want to use other global
+        // variables from these libraries and import is not possible
+        "MockInteractions": "readonly",
+        "_": "readonly",
+        "a11ySuite": "readonly",
+        "assert": "readonly",
+        "expect": "readonly",
+        "fixture": "readonly",
+        "flush": "readonly",
+        "flushAsynchronousOperations": "readonly",
+        "setup": "readonly",
+        "sinon": "readonly",
+        "stub": "readonly",
+        "suite": "readonly",
+        "suiteSetup": "readonly",
+        "teardown": "readonly",
+        "test": "readonly",
+        // Polygerrit global variables.
+        // You must not add anything new in this list!
+        // Instead export variables from modules
+        // TODO(dmfilippov): Remove global variables from polygerrit
+        "isHidden": "readonly",
+        "mockPromise": "readonly",
+
+      }
+    },
+    {
+      "files": "import-href.js",
+      "globals": {
+        "HTMLImports": "readonly",
+      }
+    },
+    {
+      "files": ["test/functional/**/*.js", "wct.conf.js", "template_test.js"],
+      // Settings for functional tests. These scripts are node scripts.
+      // Turn off "no-undef" to allow any global variable
+      "env": {
+        "browser": false,
+        "node": true,
+        "es6": false
+      },
+      "rules": {
+        "no-undef": "off",
+      }
+    },
+    {
+      "files": "test/index.html",
+      "globals": {
+        "WCT": "readonly",
       }
     },
     {
@@ -190,5 +299,5 @@
   ],
   "settings": {
     "html/report-bad-indent": "error"
-  }
+  },
 };
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 7985e46..e4b50a0 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
@@ -46,9 +46,9 @@
     this._getParentIndex =
     Gerrit.PatchSetBehavior.getParentIndex;
 
-    this._comments = comments;
-    this._robotComments = robotComments;
-    this._drafts = drafts;
+    this._comments = comments || {};
+    this._robotComments = robotComments || {};
+    this._drafts = drafts || {};
     this._changeNum = changeNum;
   }
 
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 37c3d52..222109e 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
@@ -337,8 +337,8 @@
     }
     do {
       newIndex = newIndex + delta;
-    } while (newIndex > 0 &&
-             newIndex < this.stops.length - 1 &&
+    } while ((delta > 0 || newIndex > 0) &&
+             (delta < 0 || newIndex < this.stops.length - 1) &&
              opt_condition && !opt_condition(this.stops[newIndex]));
 
     newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));