Merge changes Ideb18201,I0ac9d4de

* changes:
  Add REST API quota checks
  Add QuotaBackend and QuotaEnforcer extension point
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 13e3a53..516cf13 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1362,6 +1362,12 @@
 command, but also to the web UI results pagination size.
 
 
+[[capability_readAs]]
+=== Read As
+
+Allow users to impersonate any user to see which refs they can read.
+
+
 [[capability_runAs]]
 === Run As
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6362597..fc9d405 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2979,7 +2979,7 @@
 for production use. For compatibility information, please refer to the
 link:https://www.gerritcodereview.com/elasticsearch.html[project homepage].
 
-When using Elasticsearch versions 2.4 and 5.6, the open and closed changes are
+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
@@ -3016,6 +3016,22 @@
 +
 Defaults to `30000 ms`.
 
+[[elasticsearch.numberOfShards]]elasticsearch.numberOfShards::
++
+Sets the number of shards to use per index. Refer to the
+link:https://www.elastic.co/guide/en/elasticsearch/reference/current/_basic_concepts.html#getting-started-shards-and-replicas[
+Elasticsearch documentation] for details.
++
+Defaults to 5.
+
+[[elasticsearch.numberOfReplicas]]elasticsearch.numberOfReplicas::
++
+Sets the number of replicas to use per index. Refer to the
+link:https://www.elastic.co/guide/en/elasticsearch/reference/current/_basic_concepts.html#getting-started-shards-and-replicas[
+Elasticsearch documentation] for details.
++
+Defaults to 1.
+
 ==== Elasticsearch Security
 
 When security is enabled in Elasticsearch, the username and password must be provided.
@@ -3023,11 +3039,11 @@
 
 For further information about Elasticsearch security, please refer to the documentation:
 
-* link:https://www.elastic.co/guide/en/elasticsearch/plugins/2.4/security.html[Elasticsearch 2.4]
 * link:https://www.elastic.co/guide/en/x-pack/5.6/security-getting-started.html[Elasticsearch 5.6]
 * link:https://www.elastic.co/guide/en/x-pack/6.2/security-getting-started.html[Elasticsearch 6.2]
 * link:https://www.elastic.co/guide/en/elastic-stack-overview/6.3/security-getting-started.html[Elasticsearch 6.3]
 * link:https://www.elastic.co/guide/en/elastic-stack-overview/6.4/security-getting-started.html[Elasticsearch 6.4]
+* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.5/security-getting-started.html[Elasticsearch 6.5]
 
 [[elasticsearch.username]]elasticsearch.username::
 +
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 39a8d61..e84effd 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2102,6 +2102,16 @@
 `com.google.gerrit.server.git.ChangeReportFormatter` interface, a plugin
 may change the formatting of the report.
 
+[[url-formatting]]
+== URL Formatting
+
+URLs to various parts of Gerrit are usually formed by adding suffixes to
+the canonical web URL.
+
+By implementing the
+`com.google.gerrit.server.config.UrlFormatter` interface, a plugin may
+change the format of the URL.
+
 [[links-to-external-tools]]
 == Links To External Tools
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index b1c9a88..b60e56d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2454,7 +2454,7 @@
 Retrieves a change message including link:#detailed-accounts[detailed account information].
 
 --
-'GET /changes/link:#change-id[\{change-id\}]/message/link:#change-message-id[\{change-message-id\}'
+'GET /changes/link:#change-id[\{change-id\}]/message/link:#change-message-id[\{change-message-id\}]'
 --
 
 As response a link:#change-message-info[ChangeMessageInfo] entity is returned.
@@ -2483,8 +2483,8 @@
 [[delete-change-message]]
 === Delete Change Message
 --
-'DELETE /changes/link:#change-id[\{change-id\}]/message/link:#change-message-id[\{change-message-id\}' +
-'POST /changes/link:#change-id[\{change-id\}]//message/link:#change-message-id[\{change-message-id\}/delete'
+'DELETE /changes/link:#change-id[\{change-id\}]/message/link:#change-message-id[\{change-message-id\}]' +
+'POST /changes/link:#change-id[\{change-id\}]//message/link:#change-message-id[\{change-message-id\}]/delete'
 --
 
 Deletes a change message by replacing the change message with a new message,
diff --git a/WORKSPACE b/WORKSPACE
index 8c8102b..6daa686 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -211,6 +211,12 @@
 )
 
 maven_jar(
+    name = "guava-failureaccess",
+    artifact = "com.google.guava:failureaccess:1.0.1",
+    sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
+)
+
+maven_jar(
     name = "j2objc",
     artifact = "com.google.j2objc:j2objc-annotations:1.1",
     sha1 = "ed28ded51a8b1c6b112568def5f4b455e6809019",
@@ -1016,8 +1022,8 @@
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.5.0",
-    sha1 = "241436d27cf65b84d17126dc7b6b947e8e2c173c",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.5.1",
+    sha1 = "d87892e24ef361b9fff5435246b0f0b8f4561fe8",
 )
 
 JACKSON_VERSION = "2.9.7"
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 68b0699..362803f 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -38,7 +38,6 @@
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupDescription;
@@ -295,7 +294,6 @@
   @Inject private AccountIndexer accountIndexer;
   @Inject private Groups groups;
   @Inject private GroupIndexer groupIndexer;
-  @Inject private ProjectOperations projectOperations;
 
   private ProjectResetter resetter;
   private List<Repository> toClose;
@@ -552,24 +550,7 @@
     return resourcePrefix + name;
   }
 
-  protected Project.NameKey createProject(String nameSuffix) throws RestApiException {
-    return createProject(nameSuffix, null);
-  }
-
-  protected Project.NameKey createProject(String nameSuffix, Project.NameKey parent)
-      throws RestApiException {
-    // Default for createEmptyCommit should match TestProjectConfig.
-    return createProject(nameSuffix, parent, true, null);
-  }
-
-  protected Project.NameKey createProject(
-      String nameSuffix, Project.NameKey parent, boolean createEmptyCommit)
-      throws RestApiException {
-    // Default for createEmptyCommit should match TestProjectConfig.
-    return createProject(nameSuffix, parent, createEmptyCommit, null);
-  }
-
-  protected Project.NameKey createProject(
+  protected Project.NameKey createProjectOverAPI(
       String nameSuffix, Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType)
       throws RestApiException {
     ProjectInput in = new ProjectInput();
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index b0a39cf..7becf99 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -26,6 +26,7 @@
         "//java/com/google/gerrit/pgm/util",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/server/restapi",
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 981ee6b..96a9053 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -228,6 +228,9 @@
 
           // Silence non-critical messages from apache.http.
           .put("org.apache.http", Level.WARN)
+
+          // Silence non-critical messages from Jetty.
+          .put("org.eclipse.jetty", Level.WARN)
           .build();
 
   private static boolean forceLocalDisk() {
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index 6bf4114..f296a69 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -16,24 +16,20 @@
 
 import com.google.gerrit.acceptance.testsuite.project.TestProjectCreation.Builder;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
 import com.google.gerrit.server.project.CreateProjectArgs;
 import com.google.gerrit.server.project.ProjectCreator;
+import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collections;
-import javax.inject.Inject;
 import org.apache.commons.lang.RandomStringUtils;
 import org.eclipse.jgit.lib.Constants;
 
 public class ProjectOperationsImpl implements ProjectOperations {
   private final ProjectCreator projectCreator;
-  private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups;
 
   @Inject
-  ProjectOperationsImpl(
-      ProjectOwnerGroupsProvider.Factory projectOwnerGroups, ProjectCreator projectCreator) {
+  ProjectOperationsImpl(ProjectCreator projectCreator) {
     this.projectCreator = projectCreator;
-    this.projectOwnerGroups = projectOwnerGroups;
   }
 
   @Override
@@ -49,7 +45,8 @@
     args.branch = Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
     args.createEmptyCommit = projectCreation.createEmptyCommit().orElse(true);
     projectCreation.parent().ifPresent(p -> args.newParent = p);
-    args.ownerIds = new ArrayList<>(projectOwnerGroups.create(args.getProject()).get());
+    // ProjectCreator wants non-null owner IDs.
+    args.ownerIds = new ArrayList<>();
     projectCreation.submitType().ifPresent(st -> args.submitType = st);
     projectCreator.createProject(args);
     return new Project.NameKey(name);
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
index aef2625..a0b130e 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -48,13 +48,7 @@
 
     public abstract TestProjectCreation.Builder createEmptyCommit(boolean value);
 
-    /**
-     * Creates empty commit on creation. This is necessary for the project's branches to be born.
-     */
-    public TestProjectCreation.Builder withEmptyCommit() {
-      return createEmptyCommit(true);
-    }
-
+    /** Skips the empty commit on creation. This means that project's branches will not exist. */
     public TestProjectCreation.Builder noEmptyCommit() {
       return createEmptyCommit(false);
     }
diff --git a/java/com/google/gerrit/common/data/AccessSection.java b/java/com/google/gerrit/common/data/AccessSection.java
index c8d8d41..b3da199 100644
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ b/java/com/google/gerrit/common/data/AccessSection.java
@@ -37,7 +37,7 @@
     super(refPattern);
   }
 
-  public List<Permission> getPermissions() {
+  public ImmutableList<Permission> getPermissions() {
     return permissions == null ? ImmutableList.of() : ImmutableList.copyOf(permissions);
   }
 
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
index ff7d25b..be4c33c 100644
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -94,8 +94,7 @@
 
   protected String name;
 
-  // String rather than LabelFunction for backwards compatibility with GWT JSON interface.
-  protected String functionName;
+  protected LabelFunction function;
 
   protected boolean copyMinScore;
   protected boolean copyMaxScore;
@@ -123,7 +122,7 @@
     values = sortValues(valueList);
     defaultValue = 0;
 
-    functionName = LabelFunction.MAX_WITH_BLOCK.getFunctionName();
+    function = LabelFunction.MAX_WITH_BLOCK;
 
     maxNegative = Short.MIN_VALUE;
     maxPositive = Short.MAX_VALUE;
@@ -160,15 +159,11 @@
   }
 
   public LabelFunction getFunction() {
-    if (functionName == null) {
-      return null;
-    }
-    return LabelFunction.parse(functionName)
-        .orElseThrow(() -> new IllegalStateException("Unsupported functionName: " + functionName));
+    return function;
   }
 
   public void setFunction(@Nullable LabelFunction function) {
-    this.functionName = function != null ? function.getFunctionName() : null;
+    this.function = function;
   }
 
   public boolean canOverride() {
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
index a30d412..2e9c2d6 100644
--- a/java/com/google/gerrit/common/data/Permission.java
+++ b/java/com/google/gerrit/common/data/Permission.java
@@ -158,7 +158,7 @@
     exclusiveGroup = newExclusiveGroup;
   }
 
-  public List<PermissionRule> getRules() {
+  public ImmutableList<PermissionRule> getRules() {
     return rules == null ? ImmutableList.of() : ImmutableList.copyOf(rules);
   }
 
diff --git a/java/com/google/gerrit/common/data/ProjectAccess.java b/java/com/google/gerrit/common/data/ProjectAccess.java
index ea17525..a40af22 100644
--- a/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import java.util.List;
@@ -31,7 +32,7 @@
   protected LabelTypes labelTypes;
   protected Map<String, String> capabilities;
   protected Map<AccountGroup.UUID, GroupInfo> groupInfo;
-  protected List<WebLinkInfoCommon> fileHistoryLinks;
+  protected List<WebLinkInfo> fileHistoryLinks;
 
   public ProjectAccess() {}
 
@@ -132,11 +133,11 @@
     groupInfo = m;
   }
 
-  public void setFileHistoryLinks(List<WebLinkInfoCommon> links) {
+  public void setFileHistoryLinks(List<WebLinkInfo> links) {
     fileHistoryLinks = links;
   }
 
-  public List<WebLinkInfoCommon> getFileHistoryLinks() {
+  public List<WebLinkInfo> getFileHistoryLinks() {
     return fileHistoryLinks;
   }
 }
diff --git a/java/com/google/gerrit/common/data/WebLinkInfoCommon.java b/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
deleted file mode 100644
index dd0a70a..0000000
--- a/java/com/google/gerrit/common/data/WebLinkInfoCommon.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2015 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.common.data;
-
-public class WebLinkInfoCommon {
-  public WebLinkInfoCommon() {}
-
-  public String name;
-  public String imageUrl;
-  public String url;
-  public String target;
-}
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 6da19cd..3b589b2 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -107,6 +107,7 @@
     return content;
   }
 
+  private final ElasticConfiguration config;
   private final Schema<V> schema;
   private final SitePaths sitePaths;
   private final String indexNameRaw;
@@ -118,17 +119,18 @@
   protected final ElasticQueryBuilder queryBuilder;
 
   AbstractElasticIndex(
-      ElasticConfiguration cfg,
+      ElasticConfiguration config,
       SitePaths sitePaths,
       Schema<V> schema,
       ElasticRestClientProvider client,
       String indexName,
       String indexType) {
+    this.config = config;
     this.sitePaths = sitePaths;
     this.schema = schema;
     this.gson = new GsonBuilder().setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create();
     this.queryBuilder = new ElasticQueryBuilder();
-    this.indexName = cfg.getIndexName(indexName, schema.getVersion());
+    this.indexName = config.getIndexName(indexName, schema.getVersion());
     this.indexNameRaw = indexName;
     this.client = client;
     this.type = client.adapter().getType(indexType);
@@ -199,7 +201,7 @@
   protected abstract String getMappings();
 
   private String getSettings() {
-    return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting()));
+    return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting(config)));
   }
 
   protected abstract String getId(V v);
@@ -293,8 +295,11 @@
   }
 
   protected String getURI(String type, String request) throws UnsupportedEncodingException {
-    String encodedType = URLEncoder.encode(type, UTF_8.toString());
     String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
+    if (SEARCH.equals(request) && client.adapter().omitTypeFromSearch()) {
+      return encodedIndexName + "/" + request;
+    }
+    String encodedType = URLEncoder.encode(type, UTF_8.toString());
     return encodedIndexName + "/" + encodedType + "/" + request;
   }
 
diff --git a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
index 8d29d21..6863238 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticConfiguration.java
@@ -40,9 +40,13 @@
   static final String KEY_MAX_RETRY_TIMEOUT = "maxRetryTimeout";
   static final String KEY_PREFIX = "prefix";
   static final String KEY_SERVER = "server";
+  static final String KEY_NUMBER_OF_SHARDS = "numberOfShards";
+  static final String KEY_NUMBER_OF_REPLICAS = "numberOfReplicas";
   static final String DEFAULT_PORT = "9200";
   static final String DEFAULT_USERNAME = "elastic";
   static final int DEFAULT_MAX_RETRY_TIMEOUT_MS = 30000;
+  static final int DEFAULT_NUMBER_OF_SHARDS = 5;
+  static final int DEFAULT_NUMBER_OF_REPLICAS = 1;
   static final TimeUnit MAX_RETRY_TIMEOUT_UNIT = TimeUnit.MILLISECONDS;
 
   private final Config cfg;
@@ -51,6 +55,8 @@
   final String username;
   final String password;
   final int maxRetryTimeout;
+  final int numberOfShards;
+  final int numberOfReplicas;
   final String prefix;
 
   @Inject
@@ -71,6 +77,10 @@
                 DEFAULT_MAX_RETRY_TIMEOUT_MS,
                 MAX_RETRY_TIMEOUT_UNIT);
     this.prefix = Strings.nullToEmpty(cfg.getString(SECTION_ELASTICSEARCH, null, KEY_PREFIX));
+    this.numberOfShards =
+        cfg.getInt(SECTION_ELASTICSEARCH, null, KEY_NUMBER_OF_SHARDS, DEFAULT_NUMBER_OF_SHARDS);
+    this.numberOfReplicas =
+        cfg.getInt(SECTION_ELASTICSEARCH, null, KEY_NUMBER_OF_REPLICAS, DEFAULT_NUMBER_OF_REPLICAS);
     this.hosts = new ArrayList<>();
     for (String server : cfg.getStringList(SECTION_ELASTICSEARCH, null, KEY_SERVER)) {
       try {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
index 65d2916..40c1bbb 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -21,6 +21,7 @@
 
   private final boolean ignoreUnmapped;
   private final boolean usePostV5Type;
+  private final boolean omitTypeFromSearch;
 
   private final String searchFilteringName;
   private final String indicesExistParam;
@@ -31,33 +32,16 @@
   private final String versionDiscoveryUrl;
 
   ElasticQueryAdapter(ElasticVersion version) {
-    this.ignoreUnmapped = version == ElasticVersion.V2_4;
-    this.usePostV5Type = version.isV6();
-    this.versionDiscoveryUrl = version.isV6() ? "/%s*" : "/%s*/_aliases";
-
-    switch (version) {
-      case V5_6:
-      case V6_2:
-      case V6_3:
-      case V6_4:
-      case V6_5:
-        this.searchFilteringName = "_source";
-        this.indicesExistParam = "?allow_no_indices=false";
-        this.exactFieldType = "keyword";
-        this.stringFieldType = "text";
-        this.indexProperty = "true";
-        this.rawFieldsKey = "_source";
-        break;
-      case V2_4:
-      default:
-        this.searchFilteringName = "fields";
-        this.indicesExistParam = "";
-        this.exactFieldType = "string";
-        this.stringFieldType = "string";
-        this.indexProperty = "not_analyzed";
-        this.rawFieldsKey = "fields";
-        break;
-    }
+    this.ignoreUnmapped = false;
+    this.usePostV5Type = version.isV6OrLater();
+    this.omitTypeFromSearch = version.isV7OrLater();
+    this.versionDiscoveryUrl = version.isV6OrLater() ? "/%s*" : "/%s*/_aliases";
+    this.searchFilteringName = "_source";
+    this.indicesExistParam = "?allow_no_indices=false";
+    this.exactFieldType = "keyword";
+    this.stringFieldType = "text";
+    this.indexProperty = "true";
+    this.rawFieldsKey = "_source";
   }
 
   void setIgnoreUnmapped(JsonObject properties) {
@@ -100,8 +84,12 @@
     return usePostV5Type;
   }
 
-  String getType(String preV6Type) {
-    return usePostV5Type() ? POST_V5_TYPE : preV6Type;
+  boolean omitTypeFromSearch() {
+    return omitTypeFromSearch;
+  }
+
+  String getType(String type) {
+    return usePostV5Type() ? POST_V5_TYPE : type;
   }
 
   String getVersionDiscoveryUrl(String name) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticSetting.java b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
index 6fd234d..98c313c 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticSetting.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
@@ -22,33 +22,33 @@
   private static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
       ImmutableMap.of("\\u002E", "\\u0020", "\\u005F", "\\u0020");
 
-  static SettingProperties createSetting() {
-    ElasticSetting.Builder settings = new ElasticSetting.Builder();
-    settings.addCharFilter();
-    settings.addAnalyzer();
-    return settings.build();
+  static SettingProperties createSetting(ElasticConfiguration config) {
+    return new ElasticSetting.Builder().addCharFilter().addAnalyzer().build(config);
   }
 
   static class Builder {
     private final ImmutableMap.Builder<String, FieldProperties> fields =
         new ImmutableMap.Builder<>();
 
-    SettingProperties build() {
+    SettingProperties build(ElasticConfiguration config) {
       SettingProperties properties = new SettingProperties();
       properties.analysis = fields.build();
+      properties.numberOfShards = config.numberOfShards;
+      properties.numberOfReplicas = config.numberOfReplicas;
       return properties;
     }
 
-    void addCharFilter() {
+    Builder addCharFilter() {
       FieldProperties charMapping = new FieldProperties("mapping");
       charMapping.mappings = getCustomCharMappings(CUSTOM_CHAR_MAPPING);
 
       FieldProperties charFilter = new FieldProperties();
       charFilter.customMapping = charMapping;
       fields.put("char_filter", charFilter);
+      return this;
     }
 
-    void addAnalyzer() {
+    Builder addAnalyzer() {
       FieldProperties customAnalyzer = new FieldProperties("custom");
       customAnalyzer.tokenizer = "standard";
       customAnalyzer.charFilter = new String[] {"custom_mapping"};
@@ -57,6 +57,7 @@
       FieldProperties analyzer = new FieldProperties();
       analyzer.customWithCharFilter = customAnalyzer;
       fields.put("analyzer", analyzer);
+      return this;
     }
 
     private static String[] getCustomCharMappings(ImmutableMap<String, String> map) {
@@ -72,6 +73,8 @@
 
   static class SettingProperties {
     Map<String, FieldProperties> analysis;
+    Integer numberOfShards;
+    Integer numberOfReplicas;
   }
 
   static class FieldProperties {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 4c98df1..b69f8f9 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -18,12 +18,12 @@
 import java.util.regex.Pattern;
 
 public enum ElasticVersion {
-  V2_4("2.4.*"),
   V5_6("5.6.*"),
   V6_2("6.2.*"),
   V6_3("6.3.*"),
   V6_4("6.4.*"),
-  V6_5("6.5.*");
+  V6_5("6.5.*"),
+  V7_0("7.0.*");
 
   private final String version;
   private final Pattern pattern;
@@ -56,8 +56,16 @@
     return Joiner.on(", ").join(ElasticVersion.values());
   }
 
-  public boolean isV6() {
-    return version.startsWith("6.");
+  public boolean isV6OrLater() {
+    return isAtLeastVersion(6);
+  }
+
+  public boolean isV7OrLater() {
+    return isAtLeastVersion(7);
+  }
+
+  private boolean isAtLeastVersion(int v) {
+    return Integer.valueOf(version.split("\\.")[0]) >= v;
   }
 
   @Override
diff --git a/java/com/google/gerrit/extensions/client/MenuItem.java b/java/com/google/gerrit/extensions/client/MenuItem.java
index 8375bba..0c7dd88 100644
--- a/java/com/google/gerrit/extensions/client/MenuItem.java
+++ b/java/com/google/gerrit/extensions/client/MenuItem.java
@@ -22,11 +22,6 @@
   public final String target;
   public final String id;
 
-  // Needed for GWT
-  public MenuItem() {
-    this(null, null, null, null);
-  }
-
   public MenuItem(String name, String url) {
     this(name, url, "_blank");
   }
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index e74c4b2..2e1b562 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.cache.Cache;
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
@@ -52,6 +52,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -61,6 +62,7 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.http.server.GitServlet;
 import org.eclipse.jgit.http.server.GitSmartHttpTools;
@@ -130,6 +132,55 @@
     }
   }
 
+  static class HttpServletResponseWithStatusWrapper extends HttpServletResponseWrapper {
+    private int responseStatus;
+
+    HttpServletResponseWithStatusWrapper(HttpServletResponse response) {
+      super(response);
+      /* Even if we could read the status from response, we assume that it is all
+       * fine because we entered the filter without any prior issues.
+       * When Google will have upgraded to Servlet 3.0, we could actually
+       * call response.getStatus() and the code will be clearer.
+       */
+      responseStatus = HttpServletResponse.SC_OK;
+    }
+
+    @Override
+    public void setStatus(int sc) {
+      responseStatus = sc;
+      super.setStatus(sc);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void setStatus(int sc, String sm) {
+      responseStatus = sc;
+      super.setStatus(sc, sm);
+    }
+
+    @Override
+    public void sendError(int sc) throws IOException {
+      this.responseStatus = sc;
+      super.sendError(sc);
+    }
+
+    @Override
+    public void sendError(int sc, String msg) throws IOException {
+      this.responseStatus = sc;
+      super.sendError(sc, msg);
+    }
+
+    @Override
+    public void sendRedirect(String location) throws IOException {
+      this.responseStatus = HttpServletResponse.SC_MOVED_TEMPORARILY;
+      super.sendRedirect(location);
+    }
+
+    public int getResponseStatus() {
+      return responseStatus;
+    }
+  }
+
   @Inject
   GitOverHttpServlet(
       Resolver resolver,
@@ -156,19 +207,15 @@
   }
 
   private static ListMultimap<String, String> extractParameters(HttpServletRequest request) {
-
-    ListMultimap<String, String> multiMap = ArrayListMultimap.create();
-    if (request.getQueryString() != null) {
-      request
-          .getParameterMap()
-          .forEach(
-              (k, v) -> {
-                for (int i = 0; i < v.length; i++) {
-                  multiMap.put(k, v[i]);
-                }
-              });
+    if (request.getQueryString() == null) {
+      return ImmutableListMultimap.of();
     }
-    return multiMap;
+    // Explicit cast is required to compile under Servlet API 2.5, where the return type is raw Map.
+    @SuppressWarnings("cast")
+    Map<String, String[]> parameterMap = (Map<String, String[]>) request.getParameterMap();
+    ImmutableListMultimap.Builder<String, String> b = ImmutableListMultimap.builder();
+    parameterMap.forEach(b::putAll);
+    return b.build();
   }
 
   static class Resolver implements RepositoryResolver<HttpServletRequest> {
@@ -301,41 +348,48 @@
       UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
       PermissionBackend.ForProject perm =
           permissionBackend.currentUser().project(state.getNameKey());
+      HttpServletResponseWithStatusWrapper responseWrapper =
+          new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
+      HttpServletRequest httpRequest = (HttpServletRequest) request;
+      String sessionId = httpRequest.getSession().getId();
+
       try {
-        perm.check(ProjectPermission.RUN_UPLOAD_PACK);
-      } catch (AuthException e) {
-        GitSmartHttpTools.sendError(
-            (HttpServletRequest) request,
-            (HttpServletResponse) response,
-            HttpServletResponse.SC_FORBIDDEN,
-            "upload-pack not permitted on this server");
-        return;
-      } catch (PermissionBackendException e) {
-        throw new ServletException(e);
+        try {
+          perm.check(ProjectPermission.RUN_UPLOAD_PACK);
+        } catch (AuthException e) {
+          GitSmartHttpTools.sendError(
+              (HttpServletRequest) request,
+              responseWrapper,
+              HttpServletResponse.SC_FORBIDDEN,
+              "upload-pack not permitted on this server");
+          return;
+        } catch (PermissionBackendException e) {
+          responseWrapper.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+          throw new ServletException(e);
+        }
+
+        // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
+        // may have been overridden by a proxy server -- we'll try to avoid this.
+        UploadValidators uploadValidators =
+            uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
+        up.setPreUploadHook(
+            PreUploadHookChain.newChain(
+                Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
+        up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
+        next.doFilter(httpRequest, responseWrapper);
       } finally {
-        HttpServletRequest httpRequest = (HttpServletRequest) request;
-        HttpServletResponse httpResponse = (HttpServletResponse) response;
         groupAuditService.dispatch(
             new HttpAuditEvent(
-                httpRequest.getSession().getId(),
+                sessionId,
                 userProvider.get(),
                 extractWhat(httpRequest),
                 TimeUtil.nowMs(),
                 extractParameters(httpRequest),
                 httpRequest.getMethod(),
                 httpRequest,
-                httpResponse.getStatus(),
-                httpResponse));
+                responseWrapper.getResponseStatus(),
+                responseWrapper));
       }
-
-      // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
-      // may have been overridden by a proxy server -- we'll try to avoid this.
-      UploadValidators uploadValidators =
-          uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
-      up.setPreUploadHook(
-          PreUploadHookChain.newChain(Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
-      up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
-      next.doFilter(request, response);
     }
 
     @Override
@@ -411,25 +465,28 @@
       rp.getAdvertiseRefsHook().advertiseRefs(rp);
 
       ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
+      HttpServletResponseWithStatusWrapper responseWrapper =
+          new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
+      HttpServletRequest httpRequest = (HttpServletRequest) request;
       Capable canUpload;
       try {
-        permissionBackend
-            .currentUser()
-            .project(state.getNameKey())
-            .check(ProjectPermission.RUN_RECEIVE_PACK);
-        canUpload = arc.canUpload();
-      } catch (AuthException e) {
-        GitSmartHttpTools.sendError(
-            (HttpServletRequest) request,
-            (HttpServletResponse) response,
-            HttpServletResponse.SC_FORBIDDEN,
-            "receive-pack not permitted on this server");
-        return;
-      } catch (PermissionBackendException e) {
-        throw new RuntimeException(e);
+        try {
+          permissionBackend
+              .currentUser()
+              .project(state.getNameKey())
+              .check(ProjectPermission.RUN_RECEIVE_PACK);
+          canUpload = arc.canUpload();
+        } catch (AuthException e) {
+          GitSmartHttpTools.sendError(
+              httpRequest,
+              responseWrapper,
+              HttpServletResponse.SC_FORBIDDEN,
+              "receive-pack not permitted on this server");
+          return;
+        } catch (PermissionBackendException e) {
+          throw new RuntimeException(e);
+        }
       } finally {
-        HttpServletRequest httpRequest = (HttpServletRequest) request;
-        HttpServletResponse httpResponse = (HttpServletResponse) response;
         groupAuditService.dispatch(
             new HttpAuditEvent(
                 httpRequest.getSession().getId(),
@@ -439,26 +496,26 @@
                 extractParameters(httpRequest),
                 httpRequest.getMethod(),
                 httpRequest,
-                httpResponse.getStatus(),
-                httpResponse));
+                responseWrapper.getResponseStatus(),
+                responseWrapper));
       }
 
       if (canUpload != Capable.OK) {
         GitSmartHttpTools.sendError(
-            (HttpServletRequest) request,
-            (HttpServletResponse) response,
+            httpRequest,
+            responseWrapper,
             HttpServletResponse.SC_FORBIDDEN,
             "\n" + canUpload.getMessage());
         return;
       }
 
       if (!rp.isCheckReferencedObjectsAreReachable()) {
-        chain.doFilter(request, response);
+        chain.doFilter(request, responseWrapper);
         return;
       }
 
       if (!(userProvider.get().isIdentifiedUser())) {
-        chain.doFilter(request, response);
+        chain.doFilter(request, responseWrapper);
         return;
       }
 
@@ -475,7 +532,7 @@
         }
       }
 
-      chain.doFilter(request, response);
+      chain.doFilter(request, responseWrapper);
 
       if (isGet) {
         cache.put(cacheKey, Collections.unmodifiableSet(new HashSet<>(rp.getAdvertisedObjects())));
diff --git a/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java b/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java
index 5a2a033..5a37b7b 100644
--- a/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java
+++ b/java/com/google/gerrit/httpd/restapi/LogRedactUtil.java
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// WARNING: NoteDbUpdateManager cares about the package name RestApiServlet lives in.
-
 package com.google.gerrit.httpd.restapi;
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_AUTHORIZATION;
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 8e06aa1..584d8af 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerIdProvider;
@@ -37,7 +36,6 @@
 import com.google.gerrit.server.group.db.GroupConfig;
 import com.google.gerrit.server.group.db.GroupNameNotes;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.File;
 import java.io.IOException;
@@ -54,9 +52,8 @@
 /**
  * A database accessor for calls related to groups.
  *
- * <p>All calls which read or write group related details to the database <strong>during
- * init</strong> (either ReviewDb or NoteDb) are gathered here. For non-init cases, use {@code
- * Groups} or {@code GroupsUpdate} instead.
+ * <p>All calls which read or write group related details to the NoteDb <strong>during init</strong>
+ * are gathered here. For non-init cases, use {@code Groups} or {@code GroupsUpdate} instead.
  *
  * <p>All methods of this class refer to <em>internal</em> groups.
  */
@@ -76,16 +73,14 @@
   /**
    * Returns the {@code AccountGroup} for the specified {@code GroupReference}.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @param groupReference the {@code GroupReference} of the group
    * @return the {@code InternalGroup} represented by the {@code GroupReference}
-   * @throws OrmException if the group couldn't be retrieved from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
    * @throws NoSuchGroupException if a group with such a name doesn't exist
    */
-  public InternalGroup getExistingGroup(ReviewDb db, GroupReference groupReference)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+  public InternalGroup getExistingGroup(GroupReference groupReference)
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -102,14 +97,11 @@
   /**
    * Returns {@code GroupReference}s for all internal groups.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @return a stream of the {@code GroupReference}s of all internal groups
-   * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
    */
-  public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
-      throws OrmException, IOException, ConfigInvalidException {
+  public Stream<GroupReference> getAllGroupReferences() throws IOException, ConfigInvalidException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -126,14 +118,12 @@
    * <p><strong>Note</strong>: This method doesn't check whether the account exists! It also doesn't
    * update the account index!
    *
-   * @param db the {@code ReviewDb} instance to update
    * @param groupUuid the UUID of the group
    * @param account the account to add
-   * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
    * @throws NoSuchGroupException if the specified group doesn't exist
    */
-  public void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account account)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+  public void addGroupMember(AccountGroup.UUID groupUuid, Account account)
+      throws NoSuchGroupException, IOException, ConfigInvalidException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository repository = new FileRepository(allUsersRepoPath)) {
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index f12fa50..f19cf39 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.SequencesOnInit;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -37,7 +36,6 @@
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -57,7 +55,6 @@
   private final ExternalIdsOnInit externalIds;
   private final SequencesOnInit sequencesOnInit;
   private final GroupsOnInit groupsOnInit;
-  private SchemaFactory<ReviewDb> dbFactory;
   private AccountIndexCollection accountIndexCollection;
   private GroupIndexCollection groupIndexCollection;
 
@@ -84,11 +81,6 @@
   @Override
   public void run() {}
 
-  @Inject(optional = true)
-  void set(SchemaFactory<ReviewDb> dbFactory) {
-    this.dbFactory = dbFactory;
-  }
-
   @Inject
   void set(AccountIndexCollection accountIndexCollection) {
     this.accountIndexCollection = accountIndexCollection;
@@ -106,58 +98,56 @@
       return;
     }
 
-    try (ReviewDb db = dbFactory.open()) {
-      if (!accounts.hasAnyAccount()) {
-        ui.header("Gerrit Administrator");
-        if (ui.yesno(true, "Create administrator user")) {
-          Account.Id id = new Account.Id(sequencesOnInit.nextAccountId(db));
-          String username = ui.readString("admin", "username");
-          String name = ui.readString("Administrator", "name");
-          String httpPassword = ui.readString("secret", "HTTP password");
-          AccountSshKey sshKey = readSshKey(id);
-          String email = readEmail(sshKey);
+    if (!accounts.hasAnyAccount()) {
+      ui.header("Gerrit Administrator");
+      if (ui.yesno(true, "Create administrator user")) {
+        Account.Id id = new Account.Id(sequencesOnInit.nextAccountId());
+        String username = ui.readString("admin", "username");
+        String name = ui.readString("Administrator", "name");
+        String httpPassword = ui.readString("secret", "HTTP password");
+        AccountSshKey sshKey = readSshKey(id);
+        String email = readEmail(sshKey);
 
-          List<ExternalId> extIds = new ArrayList<>(2);
-          extIds.add(ExternalId.createUsername(username, id, httpPassword));
+        List<ExternalId> extIds = new ArrayList<>(2);
+        extIds.add(ExternalId.createUsername(username, id, httpPassword));
 
-          if (email != null) {
-            extIds.add(ExternalId.createEmail(id, email));
-          }
-          externalIds.insert("Add external IDs for initial admin user", extIds);
+        if (email != null) {
+          extIds.add(ExternalId.createEmail(id, email));
+        }
+        externalIds.insert("Add external IDs for initial admin user", extIds);
 
-          Account a = new Account(id, TimeUtil.nowTs());
-          a.setFullName(name);
-          a.setPreferredEmail(email);
-          accounts.insert(a);
+        Account a = new Account(id, TimeUtil.nowTs());
+        a.setFullName(name);
+        a.setPreferredEmail(email);
+        accounts.insert(a);
 
-          // Only two groups should exist at this point in time and hence iterating over all of them
-          // is cheap.
-          Optional<GroupReference> adminGroupReference =
-              groupsOnInit
-                  .getAllGroupReferences(db)
-                  .filter(group -> group.getName().equals("Administrators"))
-                  .findAny();
-          if (!adminGroupReference.isPresent()) {
-            throw new NoSuchGroupException("Administrators");
-          }
-          GroupReference adminGroup = adminGroupReference.get();
-          groupsOnInit.addGroupMember(db, adminGroup.getUUID(), a);
+        // Only two groups should exist at this point in time and hence iterating over all of them
+        // is cheap.
+        Optional<GroupReference> adminGroupReference =
+            groupsOnInit
+                .getAllGroupReferences()
+                .filter(group -> group.getName().equals("Administrators"))
+                .findAny();
+        if (!adminGroupReference.isPresent()) {
+          throw new NoSuchGroupException("Administrators");
+        }
+        GroupReference adminGroup = adminGroupReference.get();
+        groupsOnInit.addGroupMember(adminGroup.getUUID(), a);
 
-          if (sshKey != null) {
-            VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
-            authorizedKeys.addKey(sshKey.sshPublicKey());
-            authorizedKeys.save("Add SSH key for initial admin user\n");
-          }
+        if (sshKey != null) {
+          VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
+          authorizedKeys.addKey(sshKey.sshPublicKey());
+          authorizedKeys.save("Add SSH key for initial admin user\n");
+        }
 
-          AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
-          for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
-            accountIndex.replace(as);
-          }
+        AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
+        for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
+          accountIndex.replace(as);
+        }
 
-          InternalGroup adminInternalGroup = groupsOnInit.getExistingGroup(db, adminGroup);
-          for (GroupIndex groupIndex : groupIndexCollection.getWriteIndexes()) {
-            groupIndex.replace(adminInternalGroup);
-          }
+        InternalGroup adminInternalGroup = groupsOnInit.getExistingGroup(adminGroup);
+        for (GroupIndex groupIndex : groupIndexCollection.getWriteIndexes()) {
+          groupIndex.replace(adminInternalGroup);
         }
       }
     }
diff --git a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
index c9c3a64..1716a3c 100644
--- a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
@@ -35,16 +35,14 @@
     this.allUsersName = allUsersName;
   }
 
-  public int nextAccountId(ReviewDb db) throws OrmException {
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed accountSeed = db::nextAccountId;
+  public int nextAccountId() throws OrmException {
     RepoSequence accountSeq =
         new RepoSequence(
             repoManager,
             GitReferenceUpdated.DISABLED,
             new Project.NameKey(allUsersName.get()),
             Sequences.NAME_ACCOUNTS,
-            accountSeed,
+            () -> ReviewDb.FIRST_ACCOUNT_ID,
             1);
     return accountSeq.next();
   }
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index c7dc420..5ec98fc 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -18,6 +18,7 @@
 import com.google.gwtorm.client.IntKey;
 import com.google.gwtorm.client.StringKey;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 /** Named group of one or more accounts, typically used for access controls. */
@@ -26,12 +27,10 @@
    * Time when the audit subsystem was implemented, used as the default value for {@link #createdOn}
    * when one couldn't be determined from the audit log.
    */
-  // Can't use Instant here because GWT. This is verified against a readable time in the tests,
-  // which don't need to compile under GWT.
-  private static final long AUDIT_CREATION_INSTANT_MS = 1244489460000L;
+  private static final Instant AUDIT_CREATION_INSTANT_MS = Instant.ofEpochMilli(1244489460000L);
 
   public static Timestamp auditCreationInstantTs() {
-    return new Timestamp(AUDIT_CREATION_INSTANT_MS);
+    return Timestamp.from(AUDIT_CREATION_INSTANT_MS);
   }
 
   /** Group name key */
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 8e4292c..90cd066 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.reviewdb.server;
 
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.Relation;
@@ -28,7 +26,6 @@
  * <p>Root entities that are at the top level of some important data graph:
  *
  * <ul>
- *   <li>{@link Account}: Per-user account registration, preferences, identity.
  *   <li>{@link Change}: All review information about a single proposed change.
  * </ul>
  */
@@ -91,22 +88,8 @@
 
   int FIRST_ACCOUNT_ID = 1000000;
 
-  /**
-   * Next unique id for a {@link Account}.
-   *
-   * @deprecated use {@link com.google.gerrit.server.Sequences#nextAccountId()}.
-   */
-  @Sequence(startWith = FIRST_ACCOUNT_ID)
-  @Deprecated
-  int nextAccountId() throws OrmException;
-
   int FIRST_GROUP_ID = 1;
 
-  /** Next unique id for a {@link AccountGroup}. */
-  @Sequence(startWith = FIRST_GROUP_ID)
-  @Deprecated
-  int nextAccountGroupId() throws OrmException;
-
   int FIRST_CHANGE_ID = 1;
 
   /**
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 202729e..c420254 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -136,18 +136,6 @@
 
   @Override
   @SuppressWarnings("deprecation")
-  public int nextAccountId() throws OrmException {
-    return delegate.nextAccountId();
-  }
-
-  @Override
-  @SuppressWarnings("deprecation")
-  public int nextAccountGroupId() throws OrmException {
-    return delegate.nextAccountGroupId();
-  }
-
-  @Override
-  @SuppressWarnings("deprecation")
   public int nextChangeId() throws OrmException {
     return delegate.nextChangeId();
   }
diff --git a/java/com/google/gerrit/server/Sequences.java b/java/com/google/gerrit/server/Sequences.java
index fcf0759..70a02a8 100644
--- a/java/com/google/gerrit/server/Sequences.java
+++ b/java/com/google/gerrit/server/Sequences.java
@@ -93,11 +93,15 @@
         new RepoSequence(
             repoManager, gitRefUpdated, allProjects, NAME_CHANGES, changeSeed, changeBatchSize);
 
-    RepoSequence.Seed groupSeed = () -> nextGroupId(db.get());
     int groupBatchSize = 1;
     groupSeq =
         new RepoSequence(
-            repoManager, gitRefUpdated, allUsers, NAME_GROUPS, groupSeed, groupBatchSize);
+            repoManager,
+            gitRefUpdated,
+            allUsers,
+            NAME_GROUPS,
+            () -> ReviewDb.FIRST_GROUP_ID,
+            groupBatchSize);
 
     nextIdLatency =
         metrics.newTimer(
@@ -158,9 +162,4 @@
   private static int nextChangeId(ReviewDb db) throws OrmException {
     return db.nextChangeId();
   }
-
-  @SuppressWarnings("deprecation")
-  static int nextGroupId(ReviewDb db) throws OrmException {
-    return db.nextAccountGroupId();
-  }
 }
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index 39a2328..589344c 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -19,7 +19,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -54,17 +53,6 @@
         return true;
       };
 
-  private static final Predicate<WebLinkInfoCommon> INVALID_WEBLINK_COMMON =
-      link -> {
-        if (link == null) {
-          return false;
-        } else if (Strings.isNullOrEmpty(link.name) || Strings.isNullOrEmpty(link.url)) {
-          logger.atWarning().log("%s is missing name and/or url", link.getClass().getName());
-          return false;
-        }
-        return true;
-      };
-
   private final DynamicSet<PatchSetWebLink> patchSetLinks;
   private final DynamicSet<ParentWebLink> parentLinks;
   private final DynamicSet<FileWebLink> fileLinks;
@@ -130,25 +118,13 @@
    * @param file File name.
    * @return Links for file history
    */
-  public List<WebLinkInfoCommon> getFileHistoryLinks(String project, String revision, String file) {
+  public List<WebLinkInfo> getFileHistoryLinks(String project, String revision, String file) {
     if (Patch.isMagic(file)) {
       return Collections.emptyList();
     }
     return FluentIterable.from(fileHistoryLinks)
-        .transform(
-            webLink -> {
-              WebLinkInfo info = webLink.getFileHistoryWebLink(project, revision, file);
-              if (info == null) {
-                return null;
-              }
-              WebLinkInfoCommon commonInfo = new WebLinkInfoCommon();
-              commonInfo.name = info.name;
-              commonInfo.imageUrl = info.imageUrl;
-              commonInfo.url = info.url;
-              commonInfo.target = info.target;
-              return commonInfo;
-            })
-        .filter(INVALID_WEBLINK_COMMON)
+        .transform(webLink -> webLink.getFileHistoryWebLink(project, revision, file))
+        .filter(INVALID_WEBLINK)
         .toList();
   }
 
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index 6502569..ef2c926 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -77,12 +78,15 @@
       if (cd == null || !cd.getId().equals(rsrc.getChangeId())) {
         cd = changeDataFactory.create(db.get(), rsrc.getChangeResource().getNotes());
       }
-      ReviewerInfo info =
-          format(
-              new ReviewerInfo(rsrc.getReviewerUser().getAccountId().get()),
-              rsrc.getReviewerUser().getAccountId(),
-              cd);
-      loader.put(info);
+      ReviewerInfo info;
+      if (rsrc.isByEmail()) {
+        Address address = rsrc.getReviewerByEmail();
+        info = ReviewerInfo.byEmail(address.getName(), address.getEmail());
+      } else {
+        Account.Id reviewerAccountId = rsrc.getReviewerUser().getAccountId();
+        info = format(new ReviewerInfo(reviewerAccountId.get()), reviewerAccountId, cd);
+        loader.put(info);
+      }
       infos.add(info);
     }
     loader.fill();
@@ -94,19 +98,21 @@
     return format(ImmutableList.<ReviewerResource>of(rsrc));
   }
 
-  public ReviewerInfo format(ReviewerInfo out, Account.Id reviewer, ChangeData cd)
+  public ReviewerInfo format(ReviewerInfo out, Account.Id reviewerAccountId, ChangeData cd)
       throws OrmException, PermissionBackendException {
     PatchSet.Id psId = cd.change().currentPatchSetId();
     return format(
         out,
-        reviewer,
+        reviewerAccountId,
         cd,
-        approvalsUtil.byPatchSetUser(
-            db.get(), cd.notes(), psId, new Account.Id(out._accountId), null, null));
+        approvalsUtil.byPatchSetUser(db.get(), cd.notes(), psId, reviewerAccountId, null, null));
   }
 
   public ReviewerInfo format(
-      ReviewerInfo out, Account.Id reviewer, ChangeData cd, Iterable<PatchSetApproval> approvals)
+      ReviewerInfo out,
+      Account.Id reviewerAccountId,
+      ChangeData cd,
+      Iterable<PatchSetApproval> approvals)
       throws OrmException, PermissionBackendException {
     LabelTypes labelTypes = cd.getLabelTypes();
 
@@ -123,7 +129,7 @@
     PatchSet ps = cd.currentPatchSet();
     if (ps != null) {
       PermissionBackend.ForChange perm =
-          permissionBackend.absentUser(reviewer).database(db).change(cd);
+          permissionBackend.absentUser(reviewerAccountId).database(db).change(cd);
 
       for (SubmitRecord rec : submitRuleEvaluator.evaluate(cd)) {
         if (rec.labels == null) {
diff --git a/java/com/google/gerrit/server/config/DefaultUrlFormatter.java b/java/com/google/gerrit/server/config/DefaultUrlFormatter.java
index 70fb465..060ee3f 100644
--- a/java/com/google/gerrit/server/config/DefaultUrlFormatter.java
+++ b/java/com/google/gerrit/server/config/DefaultUrlFormatter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -27,7 +28,8 @@
   public static class Module extends AbstractModule {
     @Override
     protected void configure() {
-      bind(UrlFormatter.class).to(DefaultUrlFormatter.class);
+      DynamicItem.itemOf(binder(), UrlFormatter.class);
+      DynamicItem.bind(binder(), UrlFormatter.class).to(DefaultUrlFormatter.class);
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
index be8fcdb..f255ea2 100644
--- a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import java.io.IOException;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.eclipse.jgit.lib.Ref;
@@ -49,13 +49,12 @@
       if (prefixes.isEmpty() || prefixes.get(0).isEmpty()) {
         refs = repo.getAllRefs();
       } else {
-        ImmutableMap.Builder<String, Ref> b = new ImmutableMap.Builder<>();
+        refs = new HashMap<>();
         for (String prefix : prefixes) {
           for (Ref ref : repo.getRefDatabase().getRefsByPrefix(prefix)) {
-            b.put(ref.getName(), ref);
+            refs.put(ref.getName(), ref);
           }
         }
-        refs = b.build();
       }
       return perm.filter(refs, repo, opts);
     } catch (IOException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
index bbe0c62..97beefd 100644
--- a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
+++ b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -33,21 +34,22 @@
 
 /** Helps with the updating of a {@link VersionedMetaData}. */
 public class MetaDataUpdate implements AutoCloseable {
+  @Singleton
   public static class User {
     private final InternalFactory factory;
     private final GitRepositoryManager mgr;
-    private final PersonIdent serverIdent;
+    private final Provider<PersonIdent> serverIdentProvider;
     private final Provider<IdentifiedUser> identifiedUser;
 
     @Inject
     User(
         InternalFactory factory,
         GitRepositoryManager mgr,
-        @GerritPersonIdent PersonIdent serverIdent,
+        @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
         Provider<IdentifiedUser> identifiedUser) {
       this.factory = factory;
       this.mgr = mgr;
-      this.serverIdent = serverIdent;
+      this.serverIdentProvider = serverIdentProvider;
       this.identifiedUser = identifiedUser;
     }
 
@@ -126,29 +128,31 @@
     public MetaDataUpdate create(
         Project.NameKey name, Repository repository, IdentifiedUser user, BatchRefUpdate batch) {
       MetaDataUpdate md = factory.create(name, repository, batch);
-      md.getCommitBuilder().setCommitter(serverIdent);
+      md.getCommitBuilder().setCommitter(serverIdentProvider.get());
       md.setAuthor(user);
       return md;
     }
 
     private PersonIdent createPersonIdent(IdentifiedUser user) {
+      PersonIdent serverIdent = serverIdentProvider.get();
       return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
     }
   }
 
+  @Singleton
   public static class Server {
     private final InternalFactory factory;
     private final GitRepositoryManager mgr;
-    private final PersonIdent serverIdent;
+    private final Provider<PersonIdent> serverIdentProvider;
 
     @Inject
     Server(
         InternalFactory factory,
         GitRepositoryManager mgr,
-        @GerritPersonIdent PersonIdent serverIdent) {
+        @GerritPersonIdent Provider<PersonIdent> serverIdentProvider) {
       this.factory = factory;
       this.mgr = mgr;
-      this.serverIdent = serverIdent;
+      this.serverIdentProvider = serverIdentProvider;
     }
 
     public MetaDataUpdate create(Project.NameKey name)
@@ -162,6 +166,7 @@
       Repository repo = mgr.openRepository(name);
       MetaDataUpdate md = factory.create(name, repo, batch);
       md.setCloseRepository(true);
+      PersonIdent serverIdent = serverIdentProvider.get();
       md.getCommitBuilder().setAuthor(serverIdent);
       md.getCommitBuilder().setCommitter(serverIdent);
       return md;
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index c747533..cdfd00d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -1753,9 +1753,10 @@
           reject(cmd, "cannot use merged with base");
           return;
         }
-        RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
+        RevCommit branchTip = readBranchTip(magicBranch.dest);
         if (branchTip == null) {
-          return; // readBranchTip already rejected cmd.
+          reject(cmd, magicBranch.dest.get() + " not found");
+          return;
         }
         if (!walk.isMergedInto(tip, branchTip)) {
           reject(cmd, "not merged into branch");
@@ -1793,12 +1794,21 @@
           }
         }
       } else if (newChangeForAllNotInTarget) {
-        RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
-        if (branchTip == null) {
-          return; // readBranchTip already rejected cmd.
+        RevCommit branchTip = readBranchTip(magicBranch.dest);
+        if (branchTip != null) {
+          magicBranch.baseCommit = Collections.singletonList(branchTip);
+          logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
+        } else {
+          // The target branch does not exist. Usually pushing changes for review requires that the
+          // target branch exists, but there is an exception for the branch to which HEAD points to
+          // and for refs/meta/config. Pushing for review to these branches is allowed even if the
+          // branch does not exist yet. This allows to push initial code for review to an empty
+          // repository and to review an initial project configuration.
+          if (!ref.equals(readHEAD(repo)) && !ref.equals(RefNames.REFS_CONFIG)) {
+            reject(cmd, magicBranch.dest.get() + " not found");
+            return;
+          }
         }
-        magicBranch.baseCommit = Collections.singletonList(branchTip);
-        logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
       }
     } catch (IOException ex) {
       logger.atWarning().withCause(ex).log(
@@ -1863,17 +1873,18 @@
 
   private static String readHEAD(Repository repo) {
     try {
-      return repo.getFullBranch();
+      String head = repo.getFullBranch();
+      logger.atFine().log("HEAD = %s", head);
+      return head;
     } catch (IOException e) {
       logger.atSevere().withCause(e).log("Cannot read HEAD symref");
       return null;
     }
   }
 
-  private RevCommit readBranchTip(ReceiveCommand cmd, Branch.NameKey branch) throws IOException {
+  private RevCommit readBranchTip(Branch.NameKey branch) throws IOException {
     Ref r = allRefs().get(branch.get());
     if (r == null) {
-      reject(cmd, branch.get() + " not found");
       return null;
     }
     return receivePack.getRevWalk().parseCommit(r.getObjectId());
@@ -2725,10 +2736,13 @@
 
     /** prints a warning if the new PS has the same tree as the previous commit. */
     private void sameTreeWarning() throws IOException {
-      RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+      RevWalk rw = receivePack.getRevWalk();
+      RevCommit newCommit = rw.parseCommit(newCommitId);
       RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
 
       if (newCommit.getTree().equals(priorCommit.getTree())) {
+        rw.parseBody(newCommit);
+        rw.parseBody(priorCommit);
         boolean messageEq =
             Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
         boolean parentsEq = parentsEqual(newCommit, priorCommit);
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 8875d40..0f46535 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -30,13 +30,11 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gerrit.common.data.WebLinkInfoCommon;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
 import com.google.gerrit.extensions.api.access.PermissionInfo;
 import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -149,13 +147,9 @@
 
       // config may have a null revision if the repo doesn't have its own refs/meta/config.
       if (config.getRevision() != null) {
-        // WebLinks operates in terms of the data types used in the GWT UI. Once the GWT UI is
-        // gone, WebLinks should be fixed to use the extension data types.
-        for (WebLinkInfoCommon wl :
+        info.configWebLinks.addAll(
             webLinks.getFileHistoryLinks(
-                projectName.get(), config.getRevision().getName(), ProjectConfig.PROJECT_CONFIG)) {
-          info.configWebLinks.add(new WebLinkInfo(wl.name, wl.imageUrl, wl.url, wl.target));
-        }
+                projectName.get(), config.getRevision().getName(), ProjectConfig.PROJECT_CONFIG));
       }
 
       if (config.updateGroupNames(groupBackend)) {
diff --git a/java/com/google/gerrit/server/schema/Schema_155.java b/java/com/google/gerrit/server/schema/Schema_155.java
index 812d7a6..e9372a5 100644
--- a/java/com/google/gerrit/server/schema/Schema_155.java
+++ b/java/com/google/gerrit/server/schema/Schema_155.java
@@ -40,15 +40,13 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed accountSeed = db::nextAccountId;
     RepoSequence accountSeq =
         new RepoSequence(
             repoManager,
             GitReferenceUpdated.DISABLED,
             allUsersName,
             Sequences.NAME_ACCOUNTS,
-            accountSeed,
+            () -> ReviewDb.FIRST_ACCOUNT_ID,
             1);
 
     // consume one account ID to ensure that the account sequence is initialized in NoteDb
diff --git a/java/com/google/gerrit/server/schema/Schema_163.java b/java/com/google/gerrit/server/schema/Schema_163.java
index 9eb5d5e..4b3659de 100644
--- a/java/com/google/gerrit/server/schema/Schema_163.java
+++ b/java/com/google/gerrit/server/schema/Schema_163.java
@@ -40,15 +40,13 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    @SuppressWarnings("deprecation")
-    RepoSequence.Seed groupSeed = db::nextAccountGroupId;
     RepoSequence groupSeq =
         new RepoSequence(
             repoManager,
             GitReferenceUpdated.DISABLED,
             allUsersName,
             Sequences.NAME_GROUPS,
-            groupSeed,
+            () -> ReviewDb.FIRST_GROUP_ID,
             1);
 
     // consume one account ID to ensure that the group sequence is initialized in NoteDb
diff --git a/java/com/google/gerrit/testing/DisabledReviewDb.java b/java/com/google/gerrit/testing/DisabledReviewDb.java
index 2bf95b0..d06beb9 100644
--- a/java/com/google/gerrit/testing/DisabledReviewDb.java
+++ b/java/com/google/gerrit/testing/DisabledReviewDb.java
@@ -95,16 +95,6 @@
   }
 
   @Override
-  public int nextAccountId() {
-    throw new Disabled();
-  }
-
-  @Override
-  public int nextAccountGroupId() {
-    throw new Disabled();
-  }
-
-  @Override
   public int nextChangeId() {
     throw new Disabled();
   }
diff --git a/java/com/google/gerrit/testing/FakeGroupAuditService.java b/java/com/google/gerrit/testing/FakeGroupAuditService.java
index 7c6674b..ddf03f5 100644
--- a/java/com/google/gerrit/testing/FakeGroupAuditService.java
+++ b/java/com/google/gerrit/testing/FakeGroupAuditService.java
@@ -35,8 +35,8 @@
 @Singleton
 public class FakeGroupAuditService implements GroupAuditService {
 
-  private final PluginSetContext<GroupAuditListener> groupAuditListeners;
-  private final PluginSetContext<AuditListener> auditListeners;
+  protected final PluginSetContext<GroupAuditListener> groupAuditListeners;
+  protected final PluginSetContext<AuditListener> auditListeners;
 
   public static class Module extends AbstractModule {
     @Override
@@ -63,7 +63,10 @@
 
   @Override
   public void dispatch(AuditEvent action) {
-    auditEvents.add(action);
+    synchronized (auditEvents) {
+      auditEvents.add(action);
+      auditEvents.notifyAll();
+    }
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 0f195b5..887035b 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -57,6 +57,7 @@
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -195,35 +196,23 @@
   }
 
   @Inject private Provider<PublicKeyStore> publicKeyStoreProvider;
-
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
-
   @Inject private ExternalIds externalIds;
-
   @Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners;
-
   @Inject private DynamicSet<GitReferenceUpdatedListener> refUpdateListeners;
-
   @Inject private Sequences seq;
-
   @Inject private Provider<InternalAccountQuery> accountQueryProvider;
+  @Inject private StalenessChecker stalenessChecker;
+  @Inject private AccountIndexer accountIndexer;
+  @Inject private GitReferenceUpdated gitReferenceUpdated;
+  @Inject private RetryHelper.Metrics retryMetrics;
+  @Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
+  @Inject private ExternalIdNotes.Factory extIdNotesFactory;
+  @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
+  @Inject private ProjectOperations projectOperations;
 
   @Inject protected Emails emails;
 
-  @Inject private StalenessChecker stalenessChecker;
-
-  @Inject private AccountIndexer accountIndexer;
-
-  @Inject private GitReferenceUpdated gitReferenceUpdated;
-
-  @Inject private RetryHelper.Metrics retryMetrics;
-
-  @Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
-
-  @Inject private ExternalIdNotes.Factory extIdNotesFactory;
-
-  @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
-
   @Inject
   @Named("accounts")
   private LoadingCache<Account.Id, Optional<AccountState>> accountsCache;
@@ -2602,7 +2591,7 @@
   public void deleteAllDraftComments() throws Exception {
     try {
       TestTimeUtil.resetWithClockStep(1, SECONDS);
-      Project.NameKey project2 = createProject("project2");
+      Project.NameKey project2 = projectOperations.newProject().create();
       PushOneCommit.Result r1 = createChange();
 
       TestRepository<?> tr2 = cloneProject(project2);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 744d1e9..bb4b728 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -69,6 +69,7 @@
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
@@ -196,6 +197,7 @@
   @Inject private IndexConfig indexConfig;
 
   @Inject protected GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
 
   private ChangeIndexedCounter changeIndexedCounter;
   private RegistrationHandle changeIndexedCounterHandle;
@@ -1539,7 +1541,7 @@
   @Test
   public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(p)) {
       Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
       Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
@@ -1613,7 +1615,7 @@
   @Test
   public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(p)) {
       Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
       Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
@@ -1657,7 +1659,7 @@
   @Test
   public void addReviewerThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(p)) {
       Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
       Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
@@ -2008,36 +2010,6 @@
   }
 
   @Test
-  public void implicitlyCcOnNonVotingReviewGwtStyle() throws Exception {
-    testImplicitlyCcOnNonVotingReviewGwtStyle(user);
-  }
-
-  @Test
-  public void implicitlyCcOnNonVotingReviewForUserWithoutUserNameGwtStyle() throws Exception {
-    com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
-    assertThat(accountWithoutUsername.username).isNull();
-    testImplicitlyCcOnNonVotingReviewGwtStyle(accountWithoutUsername);
-  }
-
-  private void testImplicitlyCcOnNonVotingReviewGwtStyle(
-      com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
-    PushOneCommit.Result r = createChange();
-    setApiUser(testAccount);
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
-
-    // Exact request format made by GWT UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
-    ReviewInput in = new ReviewInput();
-    in.labels = ImmutableMap.of("Code-Review", (short) 0);
-    in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
-    in.message = "comment";
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
-
-    // If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state.
-    assertThat(getReviewerState(r.getChangeId(), testAccount.id))
-        .hasValue(notesMigration.readChanges() ? CC : REVIEWER);
-  }
-
-  @Test
   public void implicitlyAddReviewerOnVotingReview() throws Exception {
     PushOneCommit.Result r = createChange();
     setApiUser(user);
@@ -2974,7 +2946,7 @@
   @Test
   public void createNewPatchSetWithoutPermission() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSet1");
+    Project.NameKey p = projectOperations.newProject().create();
 
     // Clone separate repositories of the same project as admin and as user
     TestRepository<InMemoryRepository> adminTestRepo = cloneProject(p, admin);
@@ -3020,7 +2992,7 @@
   @Test
   public void createNewPatchSetAsOwnerWithoutPermission() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSet2");
+    Project.NameKey p = projectOperations.newProject().create();
     // Clone separate repositories of the same project as admin and as user
     TestRepository<?> adminTestRepo = cloneProject(project, admin);
 
@@ -3658,7 +3630,7 @@
   @Test
   public void changeCommitMessageWithoutPermissionFails() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSetEdit");
+    Project.NameKey p = projectOperations.newProject().create();
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
     // Block default permission
     block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
index fe7da66..7899ecd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -19,18 +19,21 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.DeprecatedIdentifierException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class ChangeIdIT extends AbstractDaemonTest {
   private ChangeInfo changeInfo;
+  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setup() throws Exception {
@@ -45,7 +48,7 @@
 
   @Test
   public void projectChangeNumberReturnsChangeWhenProjectContainsSlashes() throws Exception {
-    Project.NameKey p = createProject("foo/bar");
+    Project.NameKey p = projectOperations.newProject().create();
     ChangeInfo ci = gApi.changes().create(new ChangeInput(p.get(), "master", "msg")).get();
     ChangeApi cApi = gApi.changes().id(p.get(), ci._number);
     assertThat(cApi.get().changeId).isEqualTo(ci.changeId);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 6f25d28..18eb37a 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -89,14 +89,13 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.sql.Timestamp;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -334,9 +333,6 @@
     String p = createUniqueGroup();
     String g1 = createUniqueGroup();
     String g2 = createUniqueGroup();
-    List<String> groups = new ArrayList<>();
-    groups.add(g1);
-    groups.add(g2);
     gApi.groups().id(p).addGroups(g1, g2);
     assertIncludes(p, g1, g2);
   }
@@ -952,8 +948,8 @@
   }
 
   /**
-   * @Sandboxed is used by this test because it deletes a group reference which introduces an
-   * inconsistency for the group storage. Once group deletion is supported, this test should be
+   * {@code @Sandboxed} is used by this test because it deletes a group reference which introduces
+   * an inconsistency for the group storage. Once group deletion is supported, this test should be
    * updated to use the API instead.
    */
   @Test
@@ -1001,8 +997,7 @@
     TestAccount groupOwner = accountCreator.user2();
     GroupInput in = new GroupInput();
     in.name = name("group");
-    in.members =
-        Collections.singleton(groupOwner).stream().map(u -> u.id.toString()).collect(toList());
+    in.members = Stream.of(groupOwner).map(u -> u.id.toString()).collect(toList());
     in.visibleToAll = true;
     GroupInfo group = gApi.groups().create(in).get();
 
@@ -1045,7 +1040,7 @@
 
   @Test
   public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
-    assertCreateGroupBranch(project, null);
+    assertCreateGroupBranch(project);
     String groupRef =
         RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
     createBranch(project, groupRef);
@@ -1054,7 +1049,7 @@
 
   @Test
   public void pushToDeletedGroupsBranchForNonAllUsersRepo() throws Exception {
-    assertCreateGroupBranch(project, null);
+    assertCreateGroupBranch(project);
     String groupRef =
         RefNames.refsDeletedGroups(
             new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
@@ -1092,8 +1087,7 @@
     }
   }
 
-  private void assertCreateGroupBranch(Project.NameKey project, String expectedErrorOnCreate)
-      throws Exception {
+  private void assertCreateGroupBranch(Project.NameKey project) throws Exception {
     grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
     grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
     TestRepository<InMemoryRepository> repo = cloneProject(project);
@@ -1102,11 +1096,7 @@
             .create(db, admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
             .setParents(ImmutableList.of())
             .to(RefNames.REFS_GROUPS + name("bar"));
-    if (expectedErrorOnCreate != null) {
-      r.assertErrorStatus(expectedErrorOnCreate);
-    } else {
-      r.assertOkStatus();
-    }
+    r.assertOkStatus();
   }
 
   @Test
@@ -1495,7 +1485,7 @@
   private void assertMembers(String group, TestAccount... expectedMembers) throws Exception {
     assertMembers(
         gApi.groups().id(group).members(),
-        TestAccount.names(expectedMembers).stream().toArray(String[]::new));
+        TestAccount.names(expectedMembers).toArray(new String[0]));
     assertAccountInfos(Arrays.asList(expectedMembers), gApi.groups().id(group).members());
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index e4194a3..f0296fc 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
@@ -40,7 +41,7 @@
 import org.junit.Test;
 
 public class CheckAccessIT extends AbstractDaemonTest {
-
+  @Inject private ProjectOperations projectOperations;
   @Inject private GroupOperations groupOperations;
 
   private Project.NameKey normalProject;
@@ -50,9 +51,9 @@
 
   @Before
   public void setUp() throws Exception {
-    normalProject = createProject("normal");
-    secretProject = createProject("secret");
-    secretRefProject = createProject("secretRef");
+    normalProject = projectOperations.newProject().create();
+    secretProject = projectOperations.newProject().create();
+    secretRefProject = projectOperations.newProject().create();
     AccountGroup.UUID privilegedGroupUuid =
         groupOperations.newGroup().name(name("privilegedGroup")).create();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 995f89b..aa1ad7b 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
@@ -73,6 +74,7 @@
   private static final String JIRA_MATCH = "(jira\\\\s+#?)(\\\\d+)";
 
   @Inject private DynamicSet<ProjectIndexedListener> projectIndexedListeners;
+  @Inject private ProjectOperations projectOperations;
 
   @Inject
   @IndexExecutor(BATCH)
@@ -429,7 +431,7 @@
 
   @Test
   public void reindexProject() throws Exception {
-    createProject("child", project);
+    projectOperations.newProject().parent(project).create();
     projectIndexedCounter.clear();
 
     gApi.projects().name(allProjects.get()).index(false);
@@ -438,8 +440,8 @@
 
   @Test
   public void reindexProjectWithChildren() throws Exception {
-    Project.NameKey middle = createProject("middle", project);
-    Project.NameKey leave = createProject("leave", middle);
+    Project.NameKey middle = projectOperations.newProject().parent(project).create();
+    Project.NameKey leave = projectOperations.newProject().parent(middle).create();
     projectIndexedCounter.clear();
 
     gApi.projects().name(project.get()).index(true);
@@ -473,7 +475,7 @@
   @Test
   @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
   public void maxObjectSizeIsInheritedFromParentProject() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
@@ -489,7 +491,7 @@
 
   @Test
   public void maxObjectSizeIsNotInheritedFromParentProject() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
@@ -504,7 +506,7 @@
 
   @Test
   public void maxObjectSizeOverridesParentProjectWhenNotSetOnParent() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("0");
     assertThat(info.maxObjectSizeLimit.value).isNull();
@@ -519,7 +521,7 @@
 
   @Test
   public void maxObjectSizeOverridesParentProjectWhenLower() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("200k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
@@ -535,7 +537,7 @@
   @Test
   @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
   public void maxObjectSizeDoesNotOverrideParentProjectWhenHigher() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
@@ -552,7 +554,7 @@
   @Test
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
   public void maxObjectSizeIsInheritedFromGlobalConfig() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = getConfig();
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
@@ -577,7 +579,7 @@
   @Test
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "300k")
   public void inheritedMaxObjectSizeOverridesGlobalConfigWhenLower() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("200k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
@@ -594,7 +596,7 @@
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
   @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
   public void maxObjectSizeDoesNotOverrideGlobalConfigWhenHigher() throws Exception {
-    Project.NameKey child = createProject(name("child"), project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     ConfigInfo info = setMaxObjectSize("300k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index 6fde012..6b511f6 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.RefState;
@@ -45,6 +46,7 @@
   @Inject private ProjectIndexCollection indexes;
   @Inject private IndexConfig indexConfig;
   @Inject private StalenessChecker stalenessChecker;
+  @Inject private ProjectOperations projectOperations;
 
   private static final ImmutableSet<String> FIELDS =
       ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
@@ -96,8 +98,8 @@
 
   @Test
   public void stalenessChecker_hierarchyChange_isStale() throws Exception {
-    Project.NameKey p1 = createProject("p1", allProjects);
-    Project.NameKey p2 = createProject("p2", allProjects);
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(project)) {
       u.getConfig().getProject().setParentName(p1);
       u.save();
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
index 3295f1a..c21798c 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -27,14 +28,17 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 @NoHttpd
 public class SetParentIT extends AbstractDaemonTest {
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void setParentNotAllowed() throws Exception {
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
     setApiUser(user);
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
@@ -43,7 +47,7 @@
   @Test
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentNotAllowedForNonOwners() throws Exception {
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
     setApiUser(user);
     exception.expect(AuthException.class);
     gApi.projects().name(project.get()).parent(parent);
@@ -52,7 +56,7 @@
   @Test
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentAllowedByAdminWhenAllowProjectOwnersEnabled() throws Exception {
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
 
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
@@ -67,7 +71,7 @@
   @Test
   @GerritConfig(name = "receive.allowProjectOwnersToChangeParent", value = "true")
   public void setParentAllowedForOwners() throws Exception {
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
     setApiUser(user);
     grant(project, "refs/*", Permission.OWNER, false, SystemGroupBackend.REGISTERED_USERS);
     gApi.projects().name(project.get()).parent(parent);
@@ -76,7 +80,7 @@
 
   @Test
   public void setParent() throws Exception {
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
 
     gApi.projects().name(project.get()).parent(parent);
     assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
@@ -104,7 +108,7 @@
 
   @Test
   public void setParentToOwnChildNotAllowed() throws Exception {
-    String child = createProject("child", project, true).get();
+    String child = projectOperations.newProject().parent(project).create().get();
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("cycle exists between");
     gApi.projects().name(project.get()).parent(child);
@@ -112,8 +116,8 @@
 
   @Test
   public void setParentToGrandchildNotAllowed() throws Exception {
-    Project.NameKey child = createProject("child", project, true);
-    String grandchild = createProject("grandchild", child, true).get();
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
+    String grandchild = projectOperations.newProject().parent(child).create().get();
     exception.expect(ResourceConflictException.class);
     exception.expectMessage("cycle exists between");
     gApi.projects().name(project.get()).parent(grandchild);
@@ -137,7 +141,7 @@
   public void setParentForAllUsersMustBeAllProjects() throws Exception {
     gApi.projects().name(allUsers.get()).parent(allProjects.get());
 
-    String parent = createProject("parent", null, true).get();
+    String parent = projectOperations.newProject().create().get();
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("All-Users must inherit from All-Projects");
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 96566f6..37bfd42 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
@@ -93,6 +94,7 @@
   private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
 
   @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
+  @Inject private ProjectOperations projectOperations;
 
   private String changeId;
   private String changeId2;
@@ -679,7 +681,7 @@
   @Test
   public void createEditWithoutPushPatchSetPermission() throws Exception {
     // Create new project with clean permissions
-    Project.NameKey p = createProject("addPatchSetEdit");
+    Project.NameKey p = projectOperations.newProject().create();
     // Clone repository as user
     TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
 
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index a5ff746..7529ee3 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -220,6 +220,60 @@
 
   @Test
   @TestProjectInput(createEmptyCommit = false)
+  public void pushInitialCommitSeriesForMasterBranch() throws Exception {
+    testPushInitialCommitSeriesForMasterBranch();
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void pushInitialCommitSeriesForMasterBranchWithCreateNewChangeForAllNotInTarget()
+      throws Exception {
+    enableCreateNewChangeForAllNotInTarget();
+    testPushInitialCommitSeriesForMasterBranch();
+  }
+
+  private void testPushInitialCommitSeriesForMasterBranch() throws Exception {
+    RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    RevCommit c2 = testRepo.commit().parent(c).message("Second commit").insertChangeId().create();
+    String id2 = GitUtil.getChangeId(testRepo, c2).get();
+    testRepo.reset(c2);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    ChangeInfo change = gApi.changes().id(id).info();
+    assertThat(change.branch).isEqualTo("master");
+    assertThat(change.status).isEqualTo(ChangeStatus.NEW);
+
+    ChangeInfo change2 = gApi.changes().id(id2).info();
+    assertThat(change2.branch).isEqualTo("master");
+    assertThat(change2.status).isEqualTo(ChangeStatus.NEW);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("master")).isNull();
+    }
+
+    gApi.changes().id(change.id).current().review(ReviewInput.approve());
+    gApi.changes().id(change.id).current().submit();
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("master")).isEqualTo(c);
+    }
+
+    gApi.changes().id(change2.id).current().review(ReviewInput.approve());
+    gApi.changes().id(change2.id).current().submit();
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("master")).isEqualTo(c2);
+    }
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
   public void validateConnected() throws Exception {
     RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
     testRepo.reset(c);
@@ -1422,14 +1476,7 @@
 
   @Test
   public void pushSameCommitTwice() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .getProject()
-          .setBooleanConfig(
-              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
-              InheritableBoolean.TRUE);
-      u.save();
-    }
+    enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
         pushFactory.create(
@@ -1451,14 +1498,7 @@
 
   @Test
   public void pushSameCommitTwiceWhenIndexFailed() throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .getProject()
-          .setBooleanConfig(
-              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
-              InheritableBoolean.TRUE);
-      u.save();
-    }
+    enableCreateNewChangeForAllNotInTarget();
 
     PushOneCommit push =
         pushFactory.create(
@@ -2314,6 +2354,113 @@
     assertPushOk(pr, "refs/heads/permitted");
   }
 
+  @Test
+  public void pushCommitsWithSameTreeNoChanges() throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Foo")
+            .parent(getHead(testRepo.getRepository()))
+            .insertChangeId()
+            .create();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    RevCommit amended = testRepo.amend(c).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains(
+            "warning: no changes between prior commit "
+                + c.abbreviate(7).name()
+                + " and new commit "
+                + amended.abbreviate(7).name());
+  }
+
+  @Test
+  public void pushCommitsWithSameTreeNoFilesChangedMessageUpdated() throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Foo")
+            .parent(getHead(testRepo.getRepository()))
+            .insertChangeId()
+            .create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    RevCommit amended =
+        testRepo.amend(c).message("Foo Bar").insertChangeId(id.substring(1)).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains(
+            "warning: " + amended.abbreviate(7).name() + ": no files changed, message updated");
+  }
+
+  @Test
+  public void pushCommitsWithSameTreeNoFilesChangedAuthorChanged() throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("Foo")
+            .parent(getHead(testRepo.getRepository()))
+            .insertChangeId()
+            .create();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    RevCommit amended = testRepo.amend(c).author(user.getIdent()).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains(
+            "warning: " + amended.abbreviate(7).name() + ": no files changed, author changed");
+  }
+
+  @Test
+  public void pushCommitsWithSameTreeNoFilesChangedWasRebased() throws Exception {
+    RevCommit head = getHead(testRepo.getRepository());
+    RevCommit c = testRepo.commit().message("Foo").parent(head).insertChangeId().create();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    testRepo.reset(head);
+    RevCommit newBase = testRepo.commit().message("Base").parent(head).insertChangeId().create();
+    testRepo.reset(newBase);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    testRepo.reset(c);
+    RevCommit amended = testRepo.amend(c).parent(newBase).create();
+    testRepo.reset(amended);
+
+    pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+    assertThat(pr.getMessages())
+        .contains("warning: " + amended.abbreviate(7).name() + ": no files changed, was rebased");
+  }
+
   private DraftInput newDraft(String path, int line, String message) {
     DraftInput d = new DraftInput();
     d.path = path;
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 9e2efd8..58051bb 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -19,7 +19,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -29,6 +29,7 @@
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.StreamSupport;
+import javax.inject.Inject;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -57,6 +58,12 @@
 
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
 
+  protected TestRepository<?> superRepo;
+  protected Project.NameKey superKey;
+  protected TestRepository<?> subRepo;
+  protected Project.NameKey subKey;
+  @Inject protected ProjectOperations projectOperations;
+
   protected SubmitType getSubmitType() {
     return cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
   }
@@ -96,28 +103,23 @@
     return cfg;
   }
 
-  protected Project.NameKey createProjectForPush(
-      String name,
-      @Nullable Project.NameKey parent,
-      boolean createEmptyCommit,
-      SubmitType submitType)
-      throws Exception {
-    Project.NameKey project = createProject(name, parent, createEmptyCommit, submitType);
+  protected void grantPush(Project.NameKey project) throws Exception {
     grant(project, "refs/heads/*", Permission.PUSH);
     grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
+  }
+
+  protected Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
+    Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
+    grantPush(project);
     return project;
   }
 
   private static AtomicInteger contentCounter = new AtomicInteger(0);
-  protected TestRepository<?> superRepo;
-  protected Project.NameKey superKey;
-  protected TestRepository<?> subRepo;
-  protected Project.NameKey subKey;
 
   @Before
   public void setUp() throws Exception {
-    superKey = createProjectForPush("super", null, true, getSubmitType());
-    subKey = createProjectForPush("sub", null, true, getSubmitType());
+    superKey = createProjectForPush(getSubmitType());
+    subKey = createProjectForPush(getSubmitType());
     superRepo = cloneProject(superKey);
     subRepo = cloneProject(subKey);
   }
diff --git a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
index 42e046a..90f4134 100644
--- a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
@@ -17,7 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.server.AuditEvent;
+import com.google.gerrit.server.audit.HttpAuditEvent;
 import java.util.Collections;
+import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
@@ -25,6 +27,7 @@
 import org.junit.Test;
 
 public class GitOverHttpServletIT extends AbstractPushForReview {
+  private static final long AUDIT_EVENT_TIMEOUT = 500L;
 
   @Before
   public void beforeEach() throws Exception {
@@ -42,6 +45,7 @@
         .setRemote("origin")
         .setRefSpecs(new RefSpec("HEAD:refs/for/master"))
         .call();
+    waitForAudit();
 
     // Git smart protocol makes two requests:
     // https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt
@@ -51,11 +55,13 @@
     assertThat(e.who.getAccountId()).isEqualTo(admin.id);
     assertThat(e.what).endsWith("/git-receive-pack");
     assertThat(e.params).isEmpty();
+    assertThat(((HttpAuditEvent) e).httpStatus).isEqualTo(HttpServletResponse.SC_OK);
   }
 
   @Test
   public void uploadPackAuditEventLog() throws Exception {
     testRepo.git().fetch().call();
+    waitForAudit();
 
     assertThat(auditService.auditEvents.size()).isEqualTo(1);
 
@@ -64,5 +70,12 @@
     assertThat(e.params.get("service"))
         .containsExactlyElementsIn(Collections.singletonList("git-upload-pack"));
     assertThat(e.what).endsWith("service=git-upload-pack");
+    assertThat(((HttpAuditEvent) e).httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  private void waitForAudit() throws InterruptedException {
+    synchronized (auditService.auditEvents) {
+      auditService.auditEvents.wait(AUDIT_EVENT_TIMEOUT);
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 94e3c0a..72ba420 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -403,12 +403,15 @@
 
   @Test
   public void subscriptionInheritACL() throws Exception {
-    Project.NameKey configKey = createProjectForPush("config-repo", null, true, getSubmitType());
+    Project.NameKey configKey = projectOperations.newProject().submitType(getSubmitType()).create();
+    grantPush(configKey);
     Project.NameKey config2Key =
-        createProjectForPush("config-repo2", configKey, true, getSubmitType());
+        projectOperations.newProject().parent(configKey).submitType(getSubmitType()).create();
+    grantPush(config2Key);
     cloneProject(config2Key);
 
-    subKey = createProjectForPush("subrepo", config2Key, true, getSubmitType());
+    subKey = projectOperations.newProject().parent(config2Key).submitType(getSubmitType()).create();
+    grantPush(subKey);
     subRepo = cloneProject(subKey);
 
     allowMatchingSubmoduleSubscription(configKey, "refs/heads/*", superKey, "refs/heads/*");
@@ -445,8 +448,7 @@
 
   @Test
   public void subscriptionDeepRelative() throws Exception {
-    Project.NameKey nest =
-        createProjectForPush("nested/subscribed-to-project", null, true, getSubmitType());
+    Project.NameKey nest = createProjectForPush(getSubmitType());
     TestRepository<?> subRepo = cloneProject(nest);
     // master is allowed to be subscribed to any superprojects branch:
     allowMatchingSubmoduleSubscription(nest, "refs/heads/master", superKey, null);
@@ -468,9 +470,9 @@
 
   @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
-  // The value 195 must tuned to the test environment, and is sensitive to the
+  // The value 110 must tuned to the test environment, and is sensitive to the
   // length of the uniquified repository name.
-  @GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "200")
+  @GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "110")
   public void submoduleSubjectCommitMessageSizeLimit() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isFalse();
     testSubmoduleSubjectCommitMessageAndExpectTruncation();
@@ -481,7 +483,6 @@
     // Make sure that the commit is created at an earlier timestamp than the submit timestamp.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-
       allowMatchingSubmoduleSubscription(
           subKey, "refs/heads/master", superKey, "refs/heads/master");
       createSubmoduleSubscription(superRepo, "master", subKey, "master");
@@ -512,8 +513,7 @@
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
 
-      Project.NameKey proj2 =
-          createProjectForPush("subscribed-to-project-2", null, true, getSubmitType());
+      Project.NameKey proj2 = createProjectForPush(getSubmitType());
 
       TestRepository<?> subRepo2 = cloneProject(proj2);
       allowMatchingSubmoduleSubscription(
@@ -560,8 +560,7 @@
     // is afterwards.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-      Project.NameKey proj2 =
-          createProjectForPush("subscribed-to-project-2", null, true, getSubmitType());
+      Project.NameKey proj2 = createProjectForPush(getSubmitType());
       TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
 
       allowMatchingSubmoduleSubscription(
@@ -605,10 +604,8 @@
 
   @Test
   public void updateOnlyRelevantSubmodules() throws Exception {
-    Project.NameKey subkey1 =
-        createProjectForPush("subscribed-to-project-1", null, true, getSubmitType());
-    Project.NameKey subkey2 =
-        createProjectForPush("subscribed-to-project-2", null, true, getSubmitType());
+    Project.NameKey subkey1 = createProjectForPush(getSubmitType());
+    Project.NameKey subkey2 = createProjectForPush(getSubmitType());
     TestRepository<?> subRepo1 = cloneProject(subkey1);
     TestRepository<?> subRepo2 = cloneProject(subkey2);
 
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index d3d1ab3..82b9cd0 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -21,7 +21,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -31,7 +30,6 @@
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.inject.Inject;
 import java.util.ArrayDeque;
 import java.util.Map;
 import org.apache.commons.lang.RandomStringUtils;
@@ -71,8 +69,6 @@
     return submitByRebaseIfNecessaryConfig();
   }
 
-  @Inject ProjectOperations projectOperations;
-
   @Test
   public void subscriptionUpdateOfManyChanges() throws Exception {
     allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
@@ -277,15 +273,15 @@
 
   @Test
   public void updateManySubmodules() throws Exception {
-    Project.NameKey subKey[] = new NameKey[3];
-    TestRepository<?> sub[] = new TestRepository[3];
+    final int NUM = 3;
+    Project.NameKey subKey[] = new NameKey[NUM];
+    TestRepository<?> sub[] = new TestRepository[NUM];
     String prefix = RandomStringUtils.randomAlphabetic(8);
     for (int i = 0; i < subKey.length; i++) {
       subKey[i] =
           projectOperations
               .newProject()
               .name(prefix + "sub" + i)
-              .withEmptyCommit()
               .submitType(getSubmitType())
               .create();
       grant(subKey[i], "refs/heads/*", Permission.PUSH);
@@ -306,7 +302,7 @@
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    ObjectId subId[] = new ObjectId[3];
+    ObjectId subId[] = new ObjectId[NUM];
 
     for (int i = 0; i < sub.length; i++) {
       subId[i] = pushChangeTo(sub[i], "refs/for/master", "some message", "same-topic");
@@ -319,7 +315,7 @@
       expectToHaveSubmoduleState(superRepo, "master", subKey[i], sub[i], "master");
     }
 
-    String heads[] = new String[3];
+    String heads[] = new String[NUM];
     for (int i = 0; i < heads.length; i++) {
       heads[i] =
           sub[i]
@@ -367,8 +363,20 @@
   @Test
   public void doNotUseFastForward() throws Exception {
     // like setup, but without empty commit
-    superKey = createProjectForPush("super-nc", null, false, getSubmitType());
-    subKey = createProjectForPush("sub-nc", null, false, getSubmitType());
+    superKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(superKey);
+    subKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(subKey);
     superRepo = cloneProject(superKey);
     subRepo = cloneProject(subKey);
 
@@ -395,8 +403,20 @@
   @Test
   public void useFastForwardWhenNoSubmodule() throws Exception {
     // like setup, but without empty commit
-    superKey = createProjectForPush("super-nc", null, false, getSubmitType());
-    subKey = createProjectForPush("sub-nc", null, false, getSubmitType());
+    superKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(superKey);
+    subKey =
+        projectOperations
+            .newProject()
+            .submitType(getSubmitType())
+            .createEmptyCommit(false)
+            .create();
+    grantPush(subKey);
     superRepo = cloneProject(superKey);
     subRepo = cloneProject(subKey);
 
@@ -493,7 +513,7 @@
 
   @Test
   public void nonSubmoduleInSameTopic() throws Exception {
-    Project.NameKey standaloneKey = createProjectForPush("standalone", null, true, getSubmitType());
+    Project.NameKey standaloneKey = createProjectForPush(getSubmitType());
     TestRepository<?> standAlone = cloneProject(standaloneKey);
 
     allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
@@ -533,9 +553,9 @@
 
   @Test
   public void recursiveSubmodules() throws Exception {
-    Project.NameKey topKey = createProjectForPush("top-project", null, true, getSubmitType());
-    Project.NameKey midKey = createProjectForPush("mid-project", null, true, getSubmitType());
-    Project.NameKey botKey = createProjectForPush("bottom-project", null, true, getSubmitType());
+    Project.NameKey topKey = createProjectForPush(getSubmitType());
+    Project.NameKey midKey = createProjectForPush(getSubmitType());
+    Project.NameKey botKey = createProjectForPush(getSubmitType());
     TestRepository<?> topRepo = cloneProject(topKey);
     TestRepository<?> midRepo = cloneProject(midKey);
     TestRepository<?> bottomRepo = cloneProject(botKey);
@@ -563,9 +583,9 @@
 
   @Test
   public void triangleSubmodules() throws Exception {
-    Project.NameKey topKey = createProjectForPush("top-project", null, true, getSubmitType());
-    Project.NameKey midKey = createProjectForPush("mid-project", null, true, getSubmitType());
-    Project.NameKey botKey = createProjectForPush("bottom-project", null, true, getSubmitType());
+    Project.NameKey topKey = createProjectForPush(getSubmitType());
+    Project.NameKey midKey = createProjectForPush(getSubmitType());
+    Project.NameKey botKey = createProjectForPush(getSubmitType());
     TestRepository<?> topRepo = cloneProject(topKey);
     TestRepository<?> midRepo = cloneProject(midKey);
     TestRepository<?> bottomRepo = cloneProject(botKey);
@@ -597,9 +617,9 @@
   }
 
   private String prepareBranchCircularSubscription() throws Exception {
-    Project.NameKey topKey = createProjectForPush("top-project", null, true, getSubmitType());
-    Project.NameKey midKey = createProjectForPush("mid-project", null, true, getSubmitType());
-    Project.NameKey botKey = createProjectForPush("bottom-project", null, true, getSubmitType());
+    Project.NameKey topKey = createProjectForPush(getSubmitType());
+    Project.NameKey midKey = createProjectForPush(getSubmitType());
+    Project.NameKey botKey = createProjectForPush(getSubmitType());
     TestRepository<?> topRepo = cloneProject(topKey);
     TestRepository<?> midRepo = cloneProject(midKey);
     TestRepository<?> bottomRepo = cloneProject(botKey);
@@ -662,8 +682,8 @@
 
   @Test
   public void projectNoSubscriptionWholeTopic() throws Exception {
-    Project.NameKey keyA = createProjectForPush("project-a", null, true, getSubmitType());
-    Project.NameKey keyB = createProjectForPush("project-b", null, true, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
 
     TestRepository<?> repoA = cloneProject(keyA);
     TestRepository<?> repoB = cloneProject(keyB);
@@ -733,8 +753,8 @@
 
   @Test
   public void twoProjectsMultipleBranchesWholeTopic() throws Exception {
-    Project.NameKey keyA = createProjectForPush("project-a", null, true, getSubmitType());
-    Project.NameKey keyB = createProjectForPush("project-b", null, true, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
     TestRepository<?> repoA = cloneProject(keyA);
     TestRepository<?> repoB = cloneProject(keyB);
     // bootstrap the dev branch
@@ -782,9 +802,9 @@
   public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
     assume().that(notesMigration.disableChangeReviewDb()).isTrue();
 
-    Project.NameKey subKey1 = createProjectForPush("sub1", null, true, getSubmitType());
+    Project.NameKey subKey1 = createProjectForPush(getSubmitType());
     TestRepository<?> sub1 = cloneProject(subKey1);
-    Project.NameKey subKey2 = createProjectForPush("sub2", null, true, getSubmitType());
+    Project.NameKey subKey2 = createProjectForPush(getSubmitType());
     TestRepository<?> sub2 = cloneProject(subKey2);
 
     allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
@@ -844,9 +864,9 @@
 
   @Test
   public void skipUpdatingBrokenGitlinkPointer() throws Exception {
-    Project.NameKey subKey1 = createProjectForPush("sub1", null, true, getSubmitType());
+    Project.NameKey subKey1 = createProjectForPush(getSubmitType());
     TestRepository<?> sub1 = cloneProject(subKey1);
-    Project.NameKey subKey2 = createProjectForPush("sub2", null, true, getSubmitType());
+    Project.NameKey subKey2 = createProjectForPush(getSubmitType());
     TestRepository<?> sub2 = cloneProject(subKey2);
 
     allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 29a5bd0..bb7cff7 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 elasticsearchV2() {
-    return getConfig(ElasticVersion.V2_4);
-  }
-
-  @ConfigSuite.Config
   public static Config elasticsearchV5() {
     return getConfig(ElasticVersion.V5_6);
   }
@@ -40,6 +35,11 @@
     return getConfig(ElasticVersion.V6_5);
   }
 
+  @ConfigSuite.Config
+  public static Config elasticsearchV7() {
+    return getConfig(ElasticVersion.V7_0);
+  }
+
   @Override
   public void configureIndex(Injector injector) throws Exception {
     createAllIndexes(injector);
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
index bc84593..f9bc539 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -18,21 +18,25 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 
 public class WatchedProjectsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   private static final String NEW_PROJECT_NAME = "newProjectAccess";
 
   @Test
   public void setAndGetWatchedProjects() throws Exception {
-    String projectName1 = createProject(NEW_PROJECT_NAME).get();
-    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+    String projectName1 = projectOperations.newProject().name(NEW_PROJECT_NAME).create().get();
+    String projectName2 =
+        projectOperations.newProject().name(NEW_PROJECT_NAME + "2").create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
 
@@ -57,8 +61,8 @@
 
   @Test
   public void setAndDeleteWatchedProjects() throws Exception {
-    String projectName1 = createProject(NEW_PROJECT_NAME).get();
-    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+    String projectName1 = projectOperations.newProject().create().get();
+    String projectName2 = projectOperations.newProject().create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
@@ -91,7 +95,7 @@
 
   @Test
   public void setConflictingWatches() throws Exception {
-    String projectName = createProject(NEW_PROJECT_NAME).get();
+    String projectName = projectOperations.newProject().create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
@@ -115,7 +119,7 @@
 
   @Test
   public void setAndGetEmptyWatch() throws Exception {
-    String projectName = createProject(NEW_PROJECT_NAME).get();
+    String projectName = projectOperations.newProject().create().get();
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index 0c18dbb..c838cf9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -27,11 +27,13 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
 import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -156,6 +158,7 @@
       ImmutableList.of(RestCall.get("/projects/%s/commits/%s/files/%s/content"));
 
   private static final String FILENAME = "test.txt";
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void projectEndpoints() throws Exception {
@@ -164,7 +167,7 @@
 
   @Test
   public void childProjectEndpoints() throws Exception {
-    Project.NameKey childProject = createProject("test-child-repo", project);
+    Project.NameKey childProject = projectOperations.newProject().parent(project).create();
     RestApiCallHelper.execute(
         adminRestSession, CHILD_PROJECT_ENDPOINTS, project.get(), childProject.get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 08a76e4..bab4c1d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -37,7 +37,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -120,6 +120,7 @@
   @Inject private Submit submitHandler;
 
   @Inject private IdentifiedUser.GenericFactory userFactory;
+  @Inject private ProjectOperations projectOperations;
 
   @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
   private RegistrationHandle onSubmitValidatorHandle;
@@ -315,7 +316,7 @@
   @Test
   public void submitNoPermission() throws Exception {
     // create project where submit is blocked
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     block(p, "refs/*", Permission.SUBMIT, REGISTERED_USERS);
 
     TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -329,7 +330,7 @@
   @Test
   public void noSelfSubmit() throws Exception {
     // create project where submit is blocked for the change owner
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(p)) {
       Util.block(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
       Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
@@ -355,7 +356,7 @@
   @Test
   public void onlySelfSubmit() throws Exception {
     // create project where only the change owner can submit
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(p)) {
       Util.block(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/*");
       Util.allow(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
@@ -385,8 +386,10 @@
     String topic = "test-topic";
 
     // Create test projects
-    TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
-    TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+    TestRepository<?> repoB = cloneProject(keyB);
 
     // Create changes on project-a
     PushOneCommit.Result change1 =
@@ -419,15 +422,15 @@
     String topic = "test-topic";
 
     // Create test project
-    String projectName = "project-a";
-    TestRepository<?> repoA = createProjectWithPush(projectName, null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
 
-    RevCommit initialHead = getRemoteHead(new Project.NameKey(name(projectName)), "master");
+    RevCommit initialHead = getRemoteHead(keyA, "master");
 
     // Create the dev branch on the test project
     BranchInput in = new BranchInput();
     in.revision = initialHead.name();
-    gApi.projects().name(name(projectName)).branch("dev").create(in);
+    gApi.projects().name(keyA.get()).branch("dev").create(in);
 
     // Create changes on master
     PushOneCommit.Result change1 =
@@ -769,8 +772,10 @@
     String topic = "test-topic";
 
     // Create test projects
-    TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
-    TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+    TestRepository<?> repoB = cloneProject(keyB);
 
     // Create changes on project-a
     PushOneCommit.Result change1 =
@@ -815,15 +820,13 @@
           }
         });
     submitWithConflict(change4.getChangeId(), "time to fail");
-    assertThat(projectsCalled).containsExactly(name("project-a"), name("project-b"));
+    assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get());
     for (PushOneCommit.Result change : changes) {
       change.assertChange(Change.Status.NEW, name(topic), admin);
     }
 
     submit(change4.getChangeId());
-    assertThat(projectsCalled)
-        .containsExactly(
-            name("project-a"), name("project-b"), name("project-a"), name("project-b"));
+    assertThat(projectsCalled).containsExactly(keyA.get(), keyB.get(), keyA.get(), keyB.get());
     for (PushOneCommit.Result change : changes) {
       change.assertChange(Change.Status.MERGED, name(topic), admin);
     }
@@ -934,8 +937,10 @@
 
     String topic = "test-topic";
 
-    TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
-    TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+    Project.NameKey keyA = createProjectForPush(getSubmitType());
+    Project.NameKey keyB = createProjectForPush(getSubmitType());
+    TestRepository<?> repoA = cloneProject(keyA);
+    TestRepository<?> repoB = cloneProject(keyB);
 
     PushOneCommit.Result change1 =
         createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
@@ -962,13 +967,13 @@
 
     repoA.git().fetch().call();
     RevWalk rwA = repoA.getRevWalk();
-    RevCommit masterA = rwA.parseCommit(getRemoteHead(name("project-a"), "master"));
+    RevCommit masterA = rwA.parseCommit(getRemoteHead(keyA, "master"));
     RevCommit change1Ps = parseCurrentRevision(rwA, change1.getChangeId());
     assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue();
 
     repoB.git().fetch().call();
     RevWalk rwB = repoB.getRevWalk();
-    RevCommit masterB = rwB.parseCommit(getRemoteHead(name("project-b"), "master"));
+    RevCommit masterB = rwB.parseCommit(getRemoteHead(keyB, "master"));
     RevCommit change2Ps = parseCurrentRevision(rwB, change2.getChangeId());
     assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
 
@@ -1353,12 +1358,12 @@
     }
   }
 
-  private TestRepository<?> createProjectWithPush(
-      String name, @Nullable Project.NameKey parent, SubmitType submitType) throws Exception {
-    Project.NameKey project = createProject(name, parent, true, submitType);
+  // TODO(hanwen): the submodule tests have a similar method; maybe we could share code?
+  protected Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
+    Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
     grant(project, "refs/heads/*", Permission.PUSH);
     grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
-    return cloneProject(project);
+    return project;
   }
 
   protected PushOneCommit.Result createChange(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index ac0d0aa..164fe60 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -19,11 +19,13 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Before;
@@ -32,6 +34,7 @@
 public class ChangeOwnerIT extends AbstractDaemonTest {
 
   private TestAccount user2;
+  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setUp() throws Exception {
@@ -63,7 +66,7 @@
   public void testChangeOwner_OwnerACLGrantedOnParentProject() throws Exception {
     setApiUser(admin);
     grantApproveToChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     setApiUser(user);
     TestRepository<InMemoryRepository> childRepo = cloneProject(child, user);
@@ -74,7 +77,7 @@
   public void testChangeOwner_BlockedOnParentProject() throws Exception {
     setApiUser(admin);
     blockApproveForChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     setApiUser(user);
     grantApproveToAll(child);
@@ -92,7 +95,7 @@
   public void testChangeOwner_BlockedOnParentProjectAndExclusiveAllowOnChild() throws Exception {
     setApiUser(admin);
     blockApproveForChangeOwner(project);
-    Project.NameKey child = createProject("child", project);
+    Project.NameKey child = projectOperations.newProject().parent(project).create();
 
     setApiUser(user);
     grantExclusiveApproveToAll(child);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 257c88b..dc71c1f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -22,8 +22,8 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -35,11 +35,12 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 
-@NoHttpd
 public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
 
   @Before
@@ -96,6 +97,34 @@
   }
 
   @Test
+  public void listReviewersByEmail() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+    AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+    for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+      PushOneCommit.Result r = createChange();
+
+      AddReviewerInput input = new AddReviewerInput();
+      input.reviewer = toRfcAddressString(acc);
+      input.state = state;
+      gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+      RestResponse restResponse =
+          adminRestSession.get("/changes/" + r.getChangeId() + "/reviewers/");
+      restResponse.assertOK();
+      Type type = new TypeToken<List<ReviewerInfo>>() {}.getType();
+      List<ReviewerInfo> reviewers = newGson().fromJson(restResponse.getReader(), type);
+      restResponse.consume();
+
+      assertThat(reviewers).hasSize(1);
+      ReviewerInfo reviewerInfo = Iterables.getOnlyElement(reviewers);
+      assertThat(reviewerInfo._accountId).isNull();
+      assertThat(reviewerInfo.name).isEqualTo(acc.name);
+      assertThat(reviewerInfo.email).isEqualTo(acc.email);
+    }
+  }
+
+  @Test
   public void removeByEmail() throws Exception {
     assume().that(notesMigration.readChanges()).isTrue();
     AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index baf56de..c5ba5e4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -144,8 +144,8 @@
   public void rejectDoubleInheritance() throws Exception {
     setApiUser(admin);
     // Create separate projects to test the config
-    Project.NameKey parent = createProject("projectToInheritFrom");
-    Project.NameKey child = createProject("projectWithMalformedConfig");
+    Project.NameKey parent = createProjectOverAPI("projectToInheritFrom", null, true, null);
+    Project.NameKey child = createProjectOverAPI("projectWithMalformedConfig", null, true, null);
 
     String config =
         gApi.projects()
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index bb114e7..7cdd266 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -35,6 +36,7 @@
 public class IndexChangeIT extends AbstractDaemonTest {
 
   @Inject protected GroupOperations groupOperations;
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void indexChange() throws Exception {
@@ -58,7 +60,7 @@
     gApi.groups().id(group).addMembers("admin", "user", user2.username);
 
     // Create a project and restrict its visibility to the group
-    Project.NameKey p = createProject("p");
+    Project.NameKey p = projectOperations.newProject().create();
     try (ProjectConfigUpdate u = updateProject(p)) {
       Util.allow(
           u.getConfig(),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index 0ece00a..936feed 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -19,12 +19,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -34,11 +36,12 @@
 public class PrivateByDefaultIT extends AbstractDaemonTest {
   private Project.NameKey project1;
   private Project.NameKey project2;
+  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setUp() throws Exception {
-    project1 = createProject("project-1");
-    project2 = createProject("project-2", project1);
+    project1 = projectOperations.newProject().create();
+    project2 = projectOperations.newProject().parent(project1).create();
     setPrivateByDefault(project1, InheritableBoolean.FALSE);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 229122b..d92cf30 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -21,6 +21,7 @@
 
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -34,6 +35,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
 import java.io.File;
 import java.io.InputStream;
 import java.nio.file.Files;
@@ -51,6 +53,7 @@
 import org.junit.Test;
 
 public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
+  @Inject private ProjectOperations projectOperations;
 
   @Override
   protected SubmitType getSubmitType() {
@@ -132,9 +135,9 @@
 
   @Test
   public void submitChangesAcrossRepos() throws Exception {
-    Project.NameKey p1 = createProject("project-where-we-submit");
-    Project.NameKey p2 = createProject("project-impacted-via-topic");
-    Project.NameKey p3 = createProject("project-impacted-indirectly-via-topic");
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
+    Project.NameKey p3 = projectOperations.newProject().create();
 
     RevCommit initialHead2 = getRemoteHead(p2, "master");
     RevCommit initialHead3 = getRemoteHead(p3, "master");
@@ -209,9 +212,9 @@
 
   @Test
   public void submitChangesAcrossReposBlocked() throws Exception {
-    Project.NameKey p1 = createProject("project-where-we-submit");
-    Project.NameKey p2 = createProject("project-impacted-via-topic");
-    Project.NameKey p3 = createProject("project-impacted-indirectly-via-topic");
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
+    Project.NameKey p3 = projectOperations.newProject().create();
 
     TestRepository<?> repo1 = cloneProject(p1);
     TestRepository<?> repo2 = cloneProject(p2);
@@ -388,7 +391,7 @@
             "3",
             "a-topic-here");
 
-    Project.NameKey p3 = createProject("project-related-to-change3");
+    Project.NameKey p3 = projectOperations.newProject().create();
     TestRepository<?> repo3 = cloneProject(p3);
     RevCommit repo3Head = getRemoteHead(p3, "master");
     PushOneCommit.Result change3b =
@@ -710,8 +713,8 @@
     //                    (c2a) <= private
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
-    Project.NameKey p1 = createProject("project-where-we-submit");
-    Project.NameKey p2 = createProject("project-impacted-via-topic");
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
 
     grantLabel("Code-Review", -2, 2, p1, "refs/heads/*", false, REGISTERED_USERS, false);
     grant(p1, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS);
@@ -764,7 +767,7 @@
 
   @Test
   public void testPreviewSubmitTgz() throws Exception {
-    Project.NameKey p1 = createProject("project-name");
+    Project.NameKey p1 = projectOperations.newProject().create();
 
     TestRepository<?> repo1 = cloneProject(p1);
     PushOneCommit.Result change1 = createChange(repo1, "master", "test", "a.txt", "1", "topic");
@@ -789,6 +792,6 @@
         untarredFiles.add(entry.getName());
       }
     }
-    assertThat(untarredFiles).containsExactly(name("project-name") + ".git");
+    assertThat(untarredFiles).containsExactly(p1.get() + ".git");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 4d499f0..af3cf24 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -44,6 +45,7 @@
 
 public class SuggestReviewersIT extends AbstractDaemonTest {
   @Inject private CreateGroup createGroup;
+  @Inject private ProjectOperations projectOperations;
 
   private InternalGroup group1;
   private InternalGroup group2;
@@ -372,7 +374,7 @@
   @Test
   public void reviewerRankingProjectIsolation() throws Exception {
     // Create new project
-    Project.NameKey newProject = createProject("test");
+    Project.NameKey newProject = projectOperations.newProject().create();
 
     // Create users who review changes in both the default and the new project
     String fullName = "Primum Finalis";
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
index 34d87d0..e2fc14f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -18,12 +18,14 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.After;
@@ -33,11 +35,12 @@
 public class WorkInProgressByDefaultIT extends AbstractDaemonTest {
   private Project.NameKey project1;
   private Project.NameKey project2;
+  @Inject private ProjectOperations projectOperations;
 
   @Before
   public void setUp() throws Exception {
-    project1 = createProject("project-1");
-    project2 = createProject("project-2", project1);
+    project1 = projectOperations.newProject().create();
+    project2 = projectOperations.newProject().parent(project1).create();
   }
 
   @After
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 54195d1..e688216 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -76,7 +76,7 @@
 
   @Before
   public void setUp() throws Exception {
-    newProjectName = projectOperations.newProject().withEmptyCommit().create();
+    newProjectName = projectOperations.newProject().create();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
index dad3ca9..9a40346 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -8,6 +8,7 @@
         ":project",
         ":push_tag_util",
         ":refassert",
+        "//lib/commons:lang",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index b426a37..d673f83 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -31,16 +32,18 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 public class DeleteBranchIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   private Branch.NameKey testBranch;
 
   @Before
   public void setUp() throws Exception {
-    project = createProject(name("p"));
+    project = projectOperations.newProject().create();
     testBranch = new Branch.NameKey(project, "test");
     branch(testBranch).create(new BranchInput());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index 78d0270..48527af 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -18,12 +18,14 @@
 import com.google.gerrit.acceptance.GcAssert;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import org.junit.Before;
 import org.junit.Test;
 
 public class GarbageCollectionIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Inject private GcAssert gcAssert;
 
@@ -31,7 +33,7 @@
 
   @Before
   public void setUp() throws Exception {
-    project2 = createProject("p2");
+    project2 = projectOperations.newProject().create();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index d5e811d..d736578 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -18,13 +18,16 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
 import org.junit.Test;
 
 @NoHttpd
 public class GetChildProjectIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void getNonExistingChildProject_NotFound() throws Exception {
@@ -33,15 +36,15 @@
 
   @Test
   public void getNonChildProject_NotFound() throws Exception {
-    Project.NameKey p1 = createProject("p1");
-    Project.NameKey p2 = createProject("p2");
+    Project.NameKey p1 = projectOperations.newProject().create();
+    Project.NameKey p2 = projectOperations.newProject().create();
 
     assertChildNotFound(p1, p2.get());
   }
 
   @Test
   public void getChildProject() throws Exception {
-    Project.NameKey child = createProject("p1");
+    Project.NameKey child = projectOperations.newProject().create();
     ProjectInfo childInfo = gApi.projects().name(allProjects.get()).child(child.get()).get();
 
     assertProjectInfo(projectCache.get(child).getProject(), childInfo);
@@ -49,16 +52,16 @@
 
   @Test
   public void getGrandChildProject_NotFound() throws Exception {
-    Project.NameKey child = createProject("p1");
-    Project.NameKey grandChild = createProject("p1.1", child);
+    Project.NameKey child = projectOperations.newProject().create();
+    Project.NameKey grandChild = projectOperations.newProject().parent(child).create();
 
     assertChildNotFound(allProjects, grandChild.get());
   }
 
   @Test
   public void getGrandChildProjectWithRecursiveFlag() throws Exception {
-    Project.NameKey child = createProject("p1");
-    Project.NameKey grandChild = createProject("p1.1", child);
+    Project.NameKey child = projectOperations.newProject().create();
+    Project.NameKey grandChild = projectOperations.newProject().parent(child).create();
 
     ProjectInfo grandChildInfo =
         gApi.projects().name(allProjects.get()).child(grandChild.get()).get(true);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index dd92a7a..a0bc450 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -18,12 +18,16 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import org.apache.commons.lang.RandomStringUtils;
 import org.junit.Test;
 
 @NoHttpd
 public class ListChildProjectsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void listChildrenOfNonExistingProject_NotFound() throws Exception {
@@ -39,23 +43,27 @@
 
   @Test
   public void listChildren() throws Exception {
-    Project.NameKey child1 = createProject("p1");
-    Project.NameKey child1_1 = createProject("p1.1", child1);
-    Project.NameKey child1_2 = createProject("p1.2", child1);
+    Project.NameKey child1 = projectOperations.newProject().create();
+    Project.NameKey child1_1 = projectOperations.newProject().parent(child1).create();
+    Project.NameKey child1_2 = projectOperations.newProject().parent(child1).create();
 
+    assertThatNameList(gApi.projects().name(child1.get()).children()).isOrdered();
     assertThatNameList(gApi.projects().name(child1.get()).children())
-        .containsExactly(child1_1, child1_2)
-        .inOrder();
+        .containsExactly(child1_1, child1_2);
   }
 
   @Test
   public void listChildrenRecursively() throws Exception {
-    Project.NameKey child1 = createProject("p1");
-    createProject("p2");
-    Project.NameKey child1_1 = createProject("p1.1", child1);
-    Project.NameKey child1_2 = createProject("p1.2", child1);
-    Project.NameKey child1_1_1 = createProject("p1.1.1", child1_1);
-    Project.NameKey child1_1_1_1 = createProject("p1.1.1.1", child1_1_1);
+    String prefix = RandomStringUtils.randomAlphabetic(8);
+    Project.NameKey child1 = projectOperations.newProject().name(prefix + "p1").create();
+    Project.NameKey child1_1 =
+        projectOperations.newProject().parent(child1).name(prefix + "p1.1").create();
+    Project.NameKey child1_2 =
+        projectOperations.newProject().parent(child1).name(prefix + "p1.2").create();
+    Project.NameKey child1_1_1 =
+        projectOperations.newProject().parent(child1_1).name(prefix + "p1.1.1").create();
+    Project.NameKey child1_1_1_1 =
+        projectOperations.newProject().parent(child1_1_1).name(prefix + "p1.1.1.1").create();
 
     assertThatNameList(gApi.projects().name(child1.get()).children(true))
         .containsExactly(child1_1, child1_1_1, child1_1_1_1, child1_2)
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index cd88a56..7009e76 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -33,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.testing.Util;
+import com.google.inject.Inject;
 import java.util.List;
 import java.util.Map;
 import org.junit.Test;
@@ -40,13 +42,14 @@
 @NoHttpd
 @Sandboxed
 public class ListProjectsIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
 
   @Test
   public void listProjects() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    assertThatNameList(filter(gApi.projects().list().get()))
-        .containsExactly(allProjects, allUsers, project, someProject)
-        .inOrder();
+    Project.NameKey someProject = projectOperations.newProject().create();
+    assertThatNameList(gApi.projects().list().get())
+        .containsExactly(allProjects, allUsers, project, someProject);
+    assertThatNameList(gApi.projects().list().get()).isOrdered();
   }
 
   @Test
@@ -59,7 +62,7 @@
       u.save();
     }
 
-    assertThatNameList(filter(gApi.projects().list().get())).doesNotContain(project);
+    assertThatNameList(gApi.projects().list().get()).doesNotContain(project);
   }
 
   @Test
@@ -87,91 +90,94 @@
 
   @Test
   public void listProjectsWithLimit() throws Exception {
-    for (int i = 0; i < 5; i++) {
-      createProject("someProject" + i);
+    String pre = "lpwl-someProject";
+    int n = 6;
+    for (int i = 0; i < n; i++) {
+      projectOperations.newProject().name(pre + i).create();
     }
 
-    String p = name("");
-    // 5, plus p which was automatically created.
-    int n = 6;
     for (int i = 1; i <= n + 2; i++) {
-      assertThatNameList(gApi.projects().list().withPrefix(p).withLimit(i).get())
+      assertThatNameList(gApi.projects().list().withPrefix(pre).withLimit(i).get())
           .hasSize(Math.min(i, n));
     }
   }
 
   @Test
   public void listProjectsWithPrefix() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    Project.NameKey someOtherProject = createProject("some-other-project");
-    createProject("project-awesome");
+    Project.NameKey someProject = projectOperations.newProject().name("listtest-p1").create();
+    Project.NameKey someOtherProject = projectOperations.newProject().name("listtest-p2").create();
+    projectOperations.newProject().name("other-prefix-project").create();
 
-    String p = name("some");
+    String p = "listtest";
     assertBadRequest(gApi.projects().list().withPrefix(p).withRegex(".*"));
     assertBadRequest(gApi.projects().list().withPrefix(p).withSubstring(p));
-    assertThatNameList(filter(gApi.projects().list().withPrefix(p).get()))
-        .containsExactly(someOtherProject, someProject)
-        .inOrder();
-    p = name("SOME");
-    assertThatNameList(filter(gApi.projects().list().withPrefix(p).get())).isEmpty();
+    assertThatNameList(gApi.projects().list().withPrefix(p).get())
+        .containsExactly(someOtherProject, someProject);
+    p = "notlisttest";
+    assertThatNameList(gApi.projects().list().withPrefix(p).get()).isEmpty();
   }
 
   @Test
   public void listProjectsWithRegex() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    Project.NameKey someOtherProject = createProject("some-other-project");
-    Project.NameKey projectAwesome = createProject("project-awesome");
+    Project.NameKey someProject = projectOperations.newProject().name("lpwr-some-project").create();
+    Project.NameKey someOtherProject =
+        projectOperations.newProject().name("lpwr-some-other-project").create();
+    Project.NameKey projectAwesome =
+        projectOperations.newProject().name("lpwr-project-awesome").create();
 
     assertBadRequest(gApi.projects().list().withRegex("[.*"));
     assertBadRequest(gApi.projects().list().withRegex(".*").withPrefix("p"));
     assertBadRequest(gApi.projects().list().withRegex(".*").withSubstring("p"));
 
-    assertThatNameList(filter(gApi.projects().list().withRegex(".*some").get()))
+    assertThatNameList(gApi.projects().list().withRegex(".*some").get())
         .containsExactly(projectAwesome);
-    String r = name("some-project$").replace(".", "\\.");
-    assertThatNameList(filter(gApi.projects().list().withRegex(r).get()))
-        .containsExactly(someProject);
-    assertThatNameList(filter(gApi.projects().list().withRegex(".*").get()))
+    String r = ("lpwr-some-project$").replace(".", "\\.");
+    assertThatNameList(gApi.projects().list().withRegex(r).get()).containsExactly(someProject);
+    assertThatNameList(gApi.projects().list().withRegex(".*").get())
         .containsExactly(
-            allProjects, allUsers, project, projectAwesome, someOtherProject, someProject)
-        .inOrder();
+            allProjects, allUsers, project, projectAwesome, someOtherProject, someProject);
   }
 
   @Test
   public void listProjectsWithStart() throws Exception {
+    String pre = "lpws-";
     for (int i = 0; i < 5; i++) {
-      createProject(new Project.NameKey("someProject" + i).get());
+      projectOperations.newProject().name(pre + i).create();
     }
 
-    String p = name("");
-    List<ProjectInfo> all = gApi.projects().list().withPrefix(p).get();
-    // 5, plus p which was automatically created.
-    int n = 6;
+    List<ProjectInfo> all = gApi.projects().list().withPrefix(pre).get();
+    int n = 5;
     assertThat(all).hasSize(n);
-    assertThatNameList(gApi.projects().list().withPrefix(p).withStart(n - 1).get())
+    assertThatNameList(gApi.projects().list().withPrefix(pre).withStart(n - 1).get())
         .containsExactly(new Project.NameKey(Iterables.getLast(all).name));
   }
 
   @Test
   public void listProjectsWithSubstring() throws Exception {
-    Project.NameKey someProject = createProject("some-project");
-    Project.NameKey someOtherProject = createProject("some-other-project");
-    Project.NameKey projectAwesome = createProject("project-awesome");
+    Project.NameKey someProject = projectOperations.newProject().name("some-project").create();
+    Project.NameKey someOtherProject =
+        projectOperations.newProject().name("some-other-project").create();
+    Project.NameKey projectAwesome =
+        projectOperations.newProject().name("project-awesome").create();
 
     assertBadRequest(gApi.projects().list().withSubstring("some").withRegex(".*"));
     assertBadRequest(gApi.projects().list().withSubstring("some").withPrefix("some"));
-    assertThatNameList(filter(gApi.projects().list().withSubstring("some").get()))
-        .containsExactly(projectAwesome, someOtherProject, someProject)
-        .inOrder();
-    assertThatNameList(filter(gApi.projects().list().withSubstring("SOME").get()))
-        .containsExactly(projectAwesome, someOtherProject, someProject)
-        .inOrder();
+    assertThatNameList(gApi.projects().list().withSubstring("some").get())
+        .containsExactly(projectAwesome, someOtherProject, someProject);
+    assertThatNameList(gApi.projects().list().withSubstring("SOME").get())
+        .containsExactly(projectAwesome, someOtherProject, someProject);
   }
 
   @Test
   public void listProjectsWithTree() throws Exception {
-    Project.NameKey someParentProject = createProject("some-parent-project");
-    Project.NameKey someChildProject = createProject("some-child-project", someParentProject);
+    Project.NameKey someParentProject =
+        projectOperations.newProject().name("some-parent-project").create();
+    Project.NameKey someChildProject =
+        projectOperations
+            .newProject()
+            .name("some-child-project")
+            .parent(someParentProject)
+            .create();
 
     Map<String, ProjectInfo> result = gApi.projects().list().withTree(true).getAsMap();
     assertThat(result).containsKey(someChildProject.get());
@@ -184,15 +190,14 @@
         gApi.projects().list().withType(FilterType.PERMISSIONS).getAsMap();
     assertThat(result.keySet()).containsExactly(allProjects.get(), allUsers.get());
 
-    assertThatNameList(filter(gApi.projects().list().withType(FilterType.ALL).get()))
-        .containsExactly(allProjects, allUsers, project)
-        .inOrder();
+    assertThatNameList(gApi.projects().list().withType(FilterType.ALL).get())
+        .containsExactly(allProjects, allUsers, project);
   }
 
   @Test
   public void listWithHiddenAndReadonlyProjects() throws Exception {
-    Project.NameKey hidden = createProject("project-to-hide");
-    Project.NameKey readonly = createProject("project-to-read");
+    Project.NameKey hidden = projectOperations.newProject().create();
+    Project.NameKey readonly = projectOperations.newProject().create();
 
     // Set project read-only
     ConfigInput input = new ConfigInput();
@@ -203,8 +208,7 @@
     // The hidden project is included because it was not hidden yet.
     // The read-only project is included.
     assertThatNameList(gApi.projects().list().get())
-        .containsExactly(allProjects, allUsers, project, hidden, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, hidden, readonly);
 
     // Hide the project
     input.state = ProjectState.HIDDEN;
@@ -216,18 +220,15 @@
 
     // Hidden project is not included in the list
     assertThatNameList(gApi.projects().list().get())
-        .containsExactly(allProjects, allUsers, project, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, readonly);
 
     // ALL filter applies to type, and doesn't include hidden state
     assertThatNameList(gApi.projects().list().withType(FilterType.ALL).get())
-        .containsExactly(allProjects, allUsers, project, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, readonly);
 
     // "All" boolean option causes hidden projects to be included
     assertThatNameList(gApi.projects().list().withAll(true).get())
-        .containsExactly(allProjects, allUsers, project, hidden, readonly)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project, hidden, readonly);
 
     // "State" option causes only the projects in that state to be included
     assertThatNameList(gApi.projects().list().withState(ProjectState.HIDDEN).get())
@@ -235,8 +236,7 @@
     assertThatNameList(gApi.projects().list().withState(ProjectState.READ_ONLY).get())
         .containsExactly(readonly);
     assertThatNameList(gApi.projects().list().withState(ProjectState.ACTIVE).get())
-        .containsExactly(allProjects, allUsers, project)
-        .inOrder();
+        .containsExactly(allProjects, allUsers, project);
 
     // Cannot use "all" and "state" together
     assertBadRequest(gApi.projects().list().withAll(true).withState(ProjectState.ACTIVE));
@@ -250,16 +250,4 @@
       // Expected.
     }
   }
-
-  private Iterable<ProjectInfo> filter(Iterable<ProjectInfo> infos) {
-    String prefix = name("");
-    return Iterables.filter(
-        infos,
-        p -> {
-          return p.name != null
-              && (p.name.equals(allProjects.get())
-                  || p.name.equals(allUsers.get())
-                  || p.name.startsWith(prefix));
-        });
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index c78b47b..3e51260 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -19,9 +19,11 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
 import java.util.Arrays;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -29,6 +31,8 @@
 import org.junit.Test;
 
 public class ProjectLevelConfigIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Before
   public void setUp() throws Exception {
     fetch(testRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
@@ -82,7 +86,7 @@
         .to(RefNames.REFS_CONFIG)
         .assertOkStatus();
 
-    Project.NameKey childProject = createProject("child", project);
+    Project.NameKey childProject = projectOperations.newProject().parent(project).create();
     TestRepository<?> childTestRepo = cloneProject(childProject);
     fetch(childTestRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
     childTestRepo.reset("refs/heads/config");
@@ -137,7 +141,7 @@
         .to(RefNames.REFS_CONFIG)
         .assertOkStatus();
 
-    Project.NameKey childProject = createProject("child", project);
+    Project.NameKey childProject = projectOperations.newProject().parent(project).create();
     TestRepository<?> childTestRepo = cloneProject(childProject);
     fetch(childTestRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
     childTestRepo.reset("refs/heads/config");
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java b/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java
index a322089..7b0002c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java
@@ -18,8 +18,8 @@
 
 import com.google.auto.value.AutoValue;
 import java.util.Optional;
-import jdk.nashorn.internal.ir.annotations.Ignore;
 import org.apache.commons.lang.StringUtils;
+import org.junit.Ignore;
 
 /** Data container for test REST requests. */
 @Ignore
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 304a1e4..ddc3905 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -29,6 +30,7 @@
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
 import java.util.EnumSet;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -41,6 +43,8 @@
     return submitWholeTopicEnabledConfig();
   }
 
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void doesNotIncludeCurrentFiles() throws Exception {
     RevCommit c1_1 = commitBuilder().add("a.txt", "1").message("subject: 1").create();
@@ -226,7 +230,8 @@
 
   @Test
   public void newBranchTwoChangesTogether() throws Exception {
-    Project.NameKey p1 = createProject("a-new-project", null, false);
+    Project.NameKey p1 = projectOperations.newProject().noEmptyCommit().create();
+
     TestRepository<?> repo1 = cloneProject(p1);
 
     RevCommit c1 =
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
index 87c5ace..6f226c8 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.reviewdb.client.Change;
@@ -94,6 +95,7 @@
 @NoHttpd
 public class OnlineNoteDbMigrationIT extends AbstractDaemonTest {
   private static final String INVALID_STATE = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+  @Inject private ProjectOperations projectOperations;
 
   @ConfigSuite.Default
   public static Config defaultConfig() {
@@ -303,7 +305,7 @@
   public void rebuildSubsetOfProjects() throws Exception {
     setNotesMigrationState(WRITE);
 
-    Project.NameKey p2 = createProject("project2");
+    Project.NameKey p2 = projectOperations.newProject().create();
     TestRepository<?> tr2 = cloneProject(p2, admin);
 
     PushOneCommit.Result r1 = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
index 720eeed..a72cd33 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendConditionIT.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertNotEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
@@ -34,6 +35,7 @@
 public class PermissionBackendConditionIT extends AbstractDaemonTest {
 
   @Inject PermissionBackend pb;
+  @Inject ProjectOperations projectOperations;
 
   @Test
   public void globalPermissions_sameUserAndPermissionEquals() throws Exception {
@@ -110,7 +112,7 @@
 
   @Test
   public void projectPermissions_differentResourceSameUserDoesNotEqual() throws Exception {
-    Project.NameKey project2 = createProject("p2");
+    Project.NameKey project2 = projectOperations.newProject().create();
     BooleanCondition cond1 = pb.user(user()).project(project).testCond(ProjectPermission.READ);
     BooleanCondition cond2 = pb.user(user()).project(project2).testCond(ProjectPermission.READ);
 
@@ -152,7 +154,7 @@
   @Test
   public void refPermissions_differentResourceAndSameUserDoesNotEqual2() throws Exception {
     Branch.NameKey branch1 = new Branch.NameKey(project, "branch");
-    Branch.NameKey branch2 = new Branch.NameKey(createProject("p2"), "branch");
+    Branch.NameKey branch2 = new Branch.NameKey(projectOperations.newProject().create(), "branch");
     BooleanCondition cond1 = pb.user(user()).ref(branch1).testCond(RefPermission.READ);
     BooleanCondition cond2 = pb.user(user()).ref(branch2).testCond(RefPermission.READ);
 
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index cfdd781..f9493fa 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.inject.Inject;
 import java.util.EnumSet;
 import java.util.List;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -40,6 +42,8 @@
 
 @NoHttpd
 public class ProjectWatchIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Test
   public void newPatchSetsNotifyConfig() throws Exception {
     Address addr = new Address("Watcher", "watcher@example.com");
@@ -210,7 +214,7 @@
   @Test
   public void watchProject() throws Exception {
     // watch project
-    String watchedProject = createProject("watchedProject").get();
+    String watchedProject = projectOperations.newProject().create().get();
     setApiUser(user);
     watch(watchedProject);
 
@@ -226,7 +230,7 @@
 
     // push a change to non-watched project -> should not trigger email
     // notification
-    String notWatchedProject = createProject("otherProject").get();
+    String notWatchedProject = projectOperations.newProject().create().get();
     TestRepository<InMemoryRepository> notWatchedRepo =
         cloneProject(new Project.NameKey(notWatchedProject), admin);
     r =
@@ -246,8 +250,8 @@
 
   @Test
   public void watchFile() throws Exception {
-    String watchedProject = createProject("watchedProject").get();
-    String otherWatchedProject = createProject("otherWatchedProject").get();
+    String watchedProject = projectOperations.newProject().create().get();
+    String otherWatchedProject = projectOperations.newProject().create().get();
     setApiUser(user);
 
     // watch file in project as user
@@ -300,7 +304,7 @@
 
   @Test
   public void watchKeyword() throws Exception {
-    String watchedProject = createProject("watchedProject").get();
+    String watchedProject = projectOperations.newProject().create().get();
     setApiUser(user);
 
     // watch keyword in project as user
@@ -339,7 +343,7 @@
 
   @Test
   public void watchAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
+    String anyProject = projectOperations.newProject().create().get();
     setApiUser(user);
 
     // watch the All-Projects project to watch all projects
@@ -366,7 +370,7 @@
 
   @Test
   public void watchFileAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
+    String anyProject = projectOperations.newProject().create().get();
     setApiUser(user);
 
     // watch file in All-Projects project as user to watch the file in all
@@ -417,7 +421,7 @@
 
   @Test
   public void watchKeywordAllProjects() throws Exception {
-    String anyProject = createProject("anyProject").get();
+    String anyProject = projectOperations.newProject().create().get();
     setApiUser(user);
 
     // watch keyword in project as user
@@ -458,7 +462,7 @@
   @Test
   public void watchProjectNoNotificationForIgnoredChange() throws Exception {
     // watch project
-    String watchedProject = createProject("watchedProject").get();
+    String watchedProject = projectOperations.newProject().create().get();
     setApiUser(user);
     watch(watchedProject);
 
@@ -491,7 +495,7 @@
   @Test
   public void watchProjectNoNotificationForPrivateChange() throws Exception {
     // watch project
-    String watchedProject = createProject("watchedProject").get();
+    String watchedProject = projectOperations.newProject().create().get();
     setApiUser(user);
     watch(watchedProject);
 
@@ -511,7 +515,7 @@
 
   @Test
   public void watchProjectNotifyOnPrivateChange() throws Exception {
-    String watchedProject = createProject("watchedProject").get();
+    String watchedProject = projectOperations.newProject().create().get();
 
     // create group that can view all private changes
     GroupInfo groupThatCanViewPrivateChanges =
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 1e60071..4e88955 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -25,11 +25,6 @@
 public class ElasticIndexIT extends AbstractIndexTests {
 
   @ConfigSuite.Default
-  public static Config elasticsearchV2() {
-    return getConfig(ElasticVersion.V2_4);
-  }
-
-  @ConfigSuite.Config
   public static Config elasticsearchV5() {
     return getConfig(ElasticVersion.V5_6);
   }
@@ -39,6 +34,11 @@
     return getConfig(ElasticVersion.V6_5);
   }
 
+  @ConfigSuite.Config
+  public static Config elasticsearchV7() {
+    return getConfig(ElasticVersion.V7_0);
+  }
+
   @Override
   public void configureIndex(Injector injector) throws Exception {
     createAllIndexes(injector);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 4384ab5..c23f889 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GarbageCollection;
@@ -40,14 +41,15 @@
   @Inject private GarbageCollectionQueue gcQueue;
 
   @Inject private GcAssert gcAssert;
+  @Inject private ProjectOperations projectOperations;
 
   private Project.NameKey project2;
   private Project.NameKey project3;
 
   @Before
   public void setUp() throws Exception {
-    project2 = createProject("p2");
-    project3 = createProject("p3");
+    project2 = projectOperations.newProject().create();
+    project3 = projectOperations.newProject().create();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 6dc69be..3f537c0 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -47,7 +47,7 @@
 
   @Test
   public void emptyCommit() throws Exception {
-    Project.NameKey key = projectOperations.newProject().withEmptyCommit().create();
+    Project.NameKey key = projectOperations.newProject().create();
     List<BranchInfo> branches = gApi.projects().name(key.get()).branches().get();
     assertThat(branches).isNotEmpty();
     assertThat(branches.stream().map(x -> x.ref).collect(toList()))
diff --git a/javatests/com/google/gerrit/common/BUILD b/javatests/com/google/gerrit/common/BUILD
index 88ddcd5..29a23c3 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -2,10 +2,7 @@
 
 junit_tests(
     name = "server_tests",
-    srcs = [
-        "AutoValueTest.java",
-        "VersionTest.java",
-    ],
+    srcs = glob(["*.java"]),
     tags = ["no_windows"],
     deps = [
         "//java/com/google/gerrit/common:server",
diff --git a/javatests/com/google/gerrit/common/data/AccessSectionTest.java b/javatests/com/google/gerrit/common/data/AccessSectionTest.java
index b12a9c2..faf9d6c 100644
--- a/javatests/com/google/gerrit/common/data/AccessSectionTest.java
+++ b/javatests/com/google/gerrit/common/data/AccessSectionTest.java
@@ -152,21 +152,6 @@
   }
 
   @Test
-  public void cannotAddPermissionByModifyingListThatWasRetrievedFromAccessSection() {
-    Permission submitPermission = new Permission(Permission.SUBMIT);
-    accessSection.getPermissions().add(submitPermission);
-    assertThat(accessSection.getPermission(Permission.SUBMIT)).isNull();
-
-    List<Permission> permissions = new ArrayList<>();
-    permissions.add(new Permission(Permission.ABANDON));
-    permissions.add(new Permission(Permission.REBASE));
-    accessSection.setPermissions(permissions);
-    assertThat(accessSection.getPermission(Permission.SUBMIT)).isNull();
-    accessSection.getPermissions().add(submitPermission);
-    assertThat(accessSection.getPermission(Permission.SUBMIT)).isNull();
-  }
-
-  @Test
   public void removePermission() {
     Permission abandonPermission = new Permission(Permission.ABANDON);
     Permission rebasePermission = new Permission(Permission.REBASE);
diff --git a/javatests/com/google/gerrit/common/data/BUILD b/javatests/com/google/gerrit/common/data/BUILD
new file mode 100644
index 0000000..776a5e0
--- /dev/null
+++ b/javatests/com/google/gerrit/common/data/BUILD
@@ -0,0 +1,13 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "data_tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/common/data/PermissionTest.java b/javatests/com/google/gerrit/common/data/PermissionTest.java
index 84fb2f0..23380e7 100644
--- a/javatests/com/google/gerrit/common/data/PermissionTest.java
+++ b/javatests/com/google/gerrit/common/data/PermissionTest.java
@@ -187,22 +187,6 @@
   }
 
   @Test
-  public void cannotAddPermissionByModifyingListThatWasRetrievedFromAccessSection() {
-    GroupReference groupReference1 = new GroupReference(new AccountGroup.UUID("uuid-1"), "group1");
-    PermissionRule permissionRule1 = new PermissionRule(groupReference1);
-    permission.getRules().add(permissionRule1);
-    assertThat(permission.getRule(groupReference1)).isNull();
-
-    List<PermissionRule> rules = new ArrayList<>();
-    rules.add(new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-2"), "group2")));
-    rules.add(new PermissionRule(new GroupReference(new AccountGroup.UUID("uuid-3"), "group3")));
-    permission.setRules(rules);
-    assertThat(permission.getRule(groupReference1)).isNull();
-    permission.getRules().add(permissionRule1);
-    assertThat(permission.getRule(groupReference1)).isNull();
-  }
-
-  @Test
   public void getNonExistingRule() {
     GroupReference groupReference = new GroupReference(new AccountGroup.UUID("uuid-1"), "group1");
     assertThat(permission.getRule(groupReference)).isNull();
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index 51d45f2..20ef2fa 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -40,12 +40,12 @@
 
 SUFFIX = "sTest.java"
 
-ELASTICSEARCH_TESTS = {i: "ElasticQuery" + i.capitalize() + SUFFIX for i in TYPES}
-
 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}
+
 ELASTICSEARCH_TAGS = [
     "docker",
     "elastic",
@@ -53,14 +53,6 @@
 ]
 
 [junit_tests(
-    name = "elasticsearch_query_%ss_test" % name,
-    size = "large",
-    srcs = [src],
-    tags = ELASTICSEARCH_TAGS,
-    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name],
-) for name, src in ELASTICSEARCH_TESTS.items()]
-
-[junit_tests(
     name = "elasticsearch_query_%ss_test_V5" % name,
     size = "large",
     srcs = [src],
@@ -76,6 +68,14 @@
     deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name],
 ) for name, src in ELASTICSEARCH_TESTS_V6.items()]
 
+[junit_tests(
+    name = "elasticsearch_query_%ss_test_V7" % name,
+    size = "large",
+    srcs = [src],
+    tags = ELASTICSEARCH_TAGS + ["flaky"],
+    deps = ELASTICSEARCH_DEPS + [QUERY_TESTS_DEP % name],
+) for name, src in ELASTICSEARCH_TESTS_V7.items()]
+
 junit_tests(
     name = "elasticsearch_tests",
     size = "small",
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index c3150f1..79e2853 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -36,14 +36,8 @@
     }
   }
 
-  public static ElasticContainer<?> createAndStart() {
-    return createAndStart(ElasticVersion.V2_4);
-  }
-
   private static String getImageName(ElasticVersion version) {
     switch (version) {
-      case V2_4:
-        return "elasticsearch:2.4.6-alpine";
       case V5_6:
         return "docker.elastic.co/elasticsearch/elasticsearch:5.6.13";
       case V6_2:
@@ -53,7 +47,9 @@
       case V6_4:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.3";
       case V6_5:
-        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.5.0";
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.5.1";
+      case V7_0:
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0-alpha1";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
@@ -65,9 +61,6 @@
   @Override
   protected void configure() {
     addExposedPort(ELASTICSEARCH_DEFAULT_PORT);
-
-    // https://github.com/docker-library/elasticsearch/issues/58
-    addEnv("-Ees.network.host", "0.0.0.0");
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
index 5d2f944..074debc 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
index 5d76162..89d0168 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
index 9ce2e93..79d3ca5 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
index 4184935..247cb29 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(
         elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index eeb4c09..519a9c3 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 7525b65..690734c 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index e8d5683..bf0095a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
index eaaf0c8..f2227a9 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
similarity index 87%
rename from javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
rename to javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index 4f0f8b0..7ac9f21 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// 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.
@@ -25,7 +25,7 @@
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
-public class ElasticQueryAccountsTest extends AbstractQueryAccountsTest {
+public class ElasticV7QueryAccountsTest extends AbstractQueryAccountsTest {
   @ConfigSuite.Default
   public static Config defaultConfig() {
     return IndexConfig.createForElasticsearch();
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart();
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
similarity index 87%
rename from javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
rename to javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index a02d691..ed6f95b 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// 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.
@@ -25,7 +25,7 @@
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
-public class ElasticQueryChangesTest extends AbstractQueryChangesTest {
+public class ElasticV7QueryChangesTest extends AbstractQueryChangesTest {
   @ConfigSuite.Default
   public static Config defaultConfig() {
     return IndexConfig.createForElasticsearch();
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart();
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
similarity index 87%
rename from javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
rename to javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index f13c491..5efaa4a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// 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.
@@ -25,7 +25,7 @@
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
-public class ElasticQueryGroupsTest extends AbstractQueryGroupsTest {
+public class ElasticV7QueryGroupsTest extends AbstractQueryGroupsTest {
   @ConfigSuite.Default
   public static Config defaultConfig() {
     return IndexConfig.createForElasticsearch();
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart();
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
similarity index 87%
rename from javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
rename to javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index dd04010..02b3641 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// 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.
@@ -25,7 +25,7 @@
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
-public class ElasticQueryProjectsTest extends AbstractQueryProjectsTest {
+public class ElasticV7QueryProjectsTest extends AbstractQueryProjectsTest {
   @ConfigSuite.Default
   public static Config defaultConfig() {
     return IndexConfig.createForElasticsearch();
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart();
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_0);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
@@ -52,10 +52,6 @@
     }
   }
 
-  private String testName() {
-    return testName.getMethodName().toLowerCase() + "_";
-  }
-
   @Override
   protected void initAfterLifecycleStart() throws Exception {
     super.initAfterLifecycleStart();
@@ -66,7 +62,7 @@
   protected Injector createInjector() {
     Config elasticsearchConfig = new Config(config);
     InMemoryModule.setDefaults(elasticsearchConfig);
-    String indicesPrefix = testName();
+    String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
     return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 3ab9d5a..baf6c2b 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -22,9 +22,6 @@
 public class ElasticVersionTest extends GerritBaseTests {
   @Test
   public void supportedVersion() throws Exception {
-    assertThat(ElasticVersion.forVersion("2.4.0")).isEqualTo(ElasticVersion.V2_4);
-    assertThat(ElasticVersion.forVersion("2.4.6")).isEqualTo(ElasticVersion.V2_4);
-
     assertThat(ElasticVersion.forVersion("5.6.0")).isEqualTo(ElasticVersion.V5_6);
     assertThat(ElasticVersion.forVersion("5.6.11")).isEqualTo(ElasticVersion.V5_6);
 
@@ -36,6 +33,12 @@
 
     assertThat(ElasticVersion.forVersion("6.4.0")).isEqualTo(ElasticVersion.V6_4);
     assertThat(ElasticVersion.forVersion("6.4.1")).isEqualTo(ElasticVersion.V6_4);
+
+    assertThat(ElasticVersion.forVersion("6.5.0")).isEqualTo(ElasticVersion.V6_5);
+    assertThat(ElasticVersion.forVersion("6.5.1")).isEqualTo(ElasticVersion.V6_5);
+
+    assertThat(ElasticVersion.forVersion("7.0.0")).isEqualTo(ElasticVersion.V7_0);
+    assertThat(ElasticVersion.forVersion("7.0.1")).isEqualTo(ElasticVersion.V7_0);
   }
 
   @Test
@@ -48,9 +51,19 @@
 
   @Test
   public void version6() throws Exception {
-    assertThat(ElasticVersion.V6_2.isV6()).isTrue();
-    assertThat(ElasticVersion.V6_3.isV6()).isTrue();
-    assertThat(ElasticVersion.V6_4.isV6()).isTrue();
-    assertThat(ElasticVersion.V5_6.isV6()).isFalse();
+    assertThat(ElasticVersion.V5_6.isV6OrLater()).isFalse();
+    assertThat(ElasticVersion.V6_2.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V6_3.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V6_4.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V7_0.isV6OrLater()).isTrue();
+  }
+
+  @Test
+  public void version7() 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();
+    assertThat(ElasticVersion.V7_0.isV7OrLater()).isTrue();
   }
 }
diff --git a/lib/BUILD b/lib/BUILD
index 95ca4db..80da4c0 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -80,6 +80,13 @@
 )
 
 java_library(
+    name = "guava-failureaccess",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@guava-failureaccess//jar"],
+)
+
+java_library(
     name = "j2objc",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
@@ -91,6 +98,7 @@
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
     exports = [
+        ":guava-failureaccess",
         ":j2objc",
         "@guava//jar",
     ],
diff --git a/lib/guava.bzl b/lib/guava.bzl
index 1add718..e4c9083 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "26.0-jre"
+GUAVA_VERSION = "27.0.1-jre"
 
-GUAVA_BIN_SHA1 = "6a806eff209f36f635f943e16d97491f00f6bfab"
+GUAVA_BIN_SHA1 = "bd41a290787b5301e63929676d792c507bbc00ae"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/plugins/hooks b/plugins/hooks
index de469e8..25ac76f 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit de469e8e2598779773652abb43a0356650e257b3
+Subproject commit 25ac76fe18537c33f9f27c5463a081449c13ba67
diff --git a/plugins/replication b/plugins/replication
index bc5efb5..3ce09db 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit bc5efb5b60a5a93c25c075f3667841e02532a99c
+Subproject commit 3ce09db0cae09038f2924f1ff8f097e6c91c7a1e
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 9a3fc03..8c152b6 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -165,8 +165,9 @@
         hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
       <template is="dom-if" if="[[change.assignee]]">
         <gr-account-link
+            id="assigneeAccountLink"
             account="[[change.assignee]]"
-            additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
+            additional-text="[[_computeAccountStatusString(change.assignee)]]"></gr-account-link>
       </template>
       <template is="dom-if" if="[[!change.assignee]]">
         <span class="placeholder">--</span>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index aaad362..3637653 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -182,10 +182,13 @@
       element.change = {
         assignee: {
           name: 'test',
+          status: 'test',
         },
       };
       flushAsynchronousOperations();
       assert.isOk(element.$$('.assignee gr-account-link'));
+      assert.equal(Polymer.dom(element.root)
+          .querySelector('#assigneeAccountLink').additionalText, '(test)');
     });
 
     test('_computeAccountStatusString', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 098a4af..42a262a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -124,26 +124,13 @@
           '_groupsChanged(_groups.splices)',
         ],
 
-        attached() {
-          // Setup annotation layers.
-          const layers = [
-            this._createTrailingWhitespaceLayer(),
-            this.$.syntaxLayer,
-            this._createIntralineLayer(),
-            this._createTabIndicatorLayer(),
-            this.$.rangeLayer,
-          ];
-
-          // Get layers from plugins (if any).
-          for (const pluginLayer of this.$.jsAPI.getDiffLayers(
-              this.diffPath, this.changeNum, this.patchNum)) {
-            layers.push(pluginLayer);
-          }
-
-          this._layers = layers;
-        },
-
         render(keyLocations, prefs) {
+          // Setting up annotation layers must happen after plugins are
+          // installed, and |render| satisfies the requirement, however,
+          // |attached| doesn't because in the diff view page, the element is
+          // attached before plugins are installed.
+          this._setupAnnotationLayers();
+
           this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
           this._showTabs = !!prefs.show_tabs;
           this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
@@ -188,6 +175,24 @@
               });
         },
 
+        _setupAnnotationLayers() {
+          const layers = [
+            this._createTrailingWhitespaceLayer(),
+            this.$.syntaxLayer,
+            this._createIntralineLayer(),
+            this._createTabIndicatorLayer(),
+            this.$.rangeLayer,
+          ];
+
+          // Get layers from plugins (if any).
+          for (const pluginLayer of this.$.jsAPI.getDiffLayers(
+              this.diffPath, this.changeNum, this.patchNum)) {
+            layers.push(pluginLayer);
+          }
+
+          this._layers = layers;
+        },
+
         getLineElByChild(node) {
           while (node) {
             if (node instanceof Element) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 1b0ba04..294d085 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -582,13 +582,14 @@
       setup(() => {
         element = fixture('basic');
         element._showTrailingWhitespace = true;
+        element._setupAnnotationLayers();
         initialLayersCount = element._layers.length;
       });
 
       test('no plugin layers', () => {
         const getDiffLayersStub = sinon.stub(element.$.jsAPI, 'getDiffLayers')
                                        .returns([]);
-        element.attached();
+        element._setupAnnotationLayers();
         assert.isTrue(getDiffLayersStub.called);
         assert.equal(element._layers.length, initialLayersCount);
       });
@@ -596,9 +597,9 @@
       test('with plugin layers', () => {
         const getDiffLayersStub = sinon.stub(element.$.jsAPI, 'getDiffLayers')
                                        .returns([{}, {}]);
-        element.attached();
+        element._setupAnnotationLayers();
         assert.isTrue(getDiffLayersStub.called);
-        assert.equal(element._layers.length, initialLayersCount+2);
+        assert.equal(element._layers.length, initialLayersCount + 2);
       });
     });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 8b9d806..fd460cc 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -45,24 +45,6 @@
     return !!(diff.binary && (isA || isB));
   }
 
-  /**
-   * Compare two ranges. Either argument may be falsy, but will only return
-   * true if both are falsy or if neither are falsy and have the same position
-   * values.
-   *
-   * @param {Gerrit.Range=} a range 1
-   * @param {Gerrit.Range=} b range 2
-   * @return {boolean}
-   */
-  function rangesEqual(a, b) {
-    if (!a && !b) { return true; }
-    if (!a || !b) { return false; }
-    return a.start_line === b.start_line &&
-        a.start_character === b.start_character &&
-        a.end_line === b.end_line &&
-        a.end_character === b.end_character;
-  }
-
   /** @enum {string} */
   Gerrit.DiffSide = {
     LEFT: 'left',
@@ -652,7 +634,7 @@
       function matchesRange(threadEl) {
         const threadRange = /** @type {!Gerrit.Range} */(
             JSON.parse(threadEl.getAttribute('range')));
-        return rangesEqual(threadRange, range);
+        return Gerrit.rangesEqual(threadRange, range);
       }
 
       const filteredThreadEls = this._filterThreadElsForLocation(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 996d484..b134d4e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -55,6 +55,24 @@
    *             end_line: number, end_character: number}} */
   Gerrit.Range;
 
+  /**
+   * Compare two ranges. Either argument may be falsy, but will only return
+   * true if both are falsy or if neither are falsy and have the same position
+   * values.
+   *
+   * @param {Gerrit.Range=} a range 1
+   * @param {Gerrit.Range=} b range 2
+   * @return {boolean}
+   */
+  Gerrit.rangesEqual = function(a, b) {
+    if (!a && !b) { return true; }
+    if (!a || !b) { return false; }
+    return a.start_line === b.start_line &&
+        a.start_character === b.start_character &&
+        a.end_line === b.end_line &&
+        a.end_character === b.end_character;
+  };
+
   function isThreadEl(node) {
     return node.nodeType === Node.ELEMENT_NODE &&
         node.classList.contains('comment-thread');
@@ -257,18 +275,14 @@
     _observeNodes() {
       this._nodeObserver = Polymer.dom(this).observeNodes(info => {
         const addedThreadEls = info.addedNodes.filter(isThreadEl);
-        // In principle we should also handle removed nodes, but I have not
-        // figured out how to do that yet without also catching all the removals
-        // caused by further redistribution. Right now, comments are never
-        // removed by no longer slotting them in, so I decided to not handle
-        // this situation until it occurs.
-        this._updateRanges(addedThreadEls);
-        this._updateKeyLocations(addedThreadEls);
+        const removedThreadEls = info.removedNodes.filter(isThreadEl);
+        this._updateRanges(addedThreadEls, removedThreadEls);
+        this._updateKeyLocations(addedThreadEls, removedThreadEls);
         this._redispatchHoverEvents(addedThreadEls);
       });
     },
 
-    _updateRanges(addedThreadEls) {
+    _updateRanges(addedThreadEls, removedThreadEls) {
       function commentRangeFromThreadEl(threadEl) {
         const side = threadEl.getAttribute('comment-side');
         const range = JSON.parse(threadEl.getAttribute('range'));
@@ -278,15 +292,30 @@
       const addedCommentRanges = addedThreadEls
           .map(commentRangeFromThreadEl)
           .filter(({range}) => range);
+      const removedCommentRanges = removedThreadEls
+          .map(commentRangeFromThreadEl)
+          .filter(({range}) => range);
+      for (const removedCommentRange of removedCommentRanges) {
+        const i = this._commentRanges.findIndex(commentRange => {
+          return commentRange.side === removedCommentRange.side &&
+              Gerrit.rangesEqual(commentRange.range, removedCommentRange.range);
+        });
+        this.splice('_commentRanges', i, 1);
+      }
       this.push('_commentRanges', ...addedCommentRanges);
     },
 
-    _updateKeyLocations(addedThreadEls) {
+    _updateKeyLocations(addedThreadEls, removedThreadEls) {
       for (const threadEl of addedThreadEls) {
         const commentSide = threadEl.getAttribute('comment-side');
         const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
         this._keyLocations[commentSide][lineNum] = true;
       }
+      for (const threadEl of removedThreadEls) {
+        const commentSide = threadEl.getAttribute('comment-side');
+        const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
+        this._keyLocations[commentSide][lineNum] = false;
+      }
     },
 
     // Dispatch events that are handled by the gr-diff-highlight.
@@ -628,11 +657,11 @@
     _handleRenderContent() {
       this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => {
         const addedThreadEls = info.addedNodes.filter(isThreadEl);
-        // In principle we should also handle removed nodes, but I have not
-        // figured out how to do that yet without also catching all the removals
-        // caused by further redistribution. Right now, comments are never
-        // removed by no longer slotting them in, so I decided to not handle
-        // this situation until it occurs.
+        // Removed nodes do not need to be handled because all this code does is
+        // adding a slot for the added thread elements, and the extra slots do
+        // not hurt. It's probably a bigger performance cost to remove them than
+        // to keep them around. Medium term we can even consider to add one slot
+        // for each line from the start.
         for (const threadEl of addedThreadEls) {
           const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
           const commentSide = threadEl.getAttribute('comment-side');
diff --git a/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index f2f24e1..439fed7 100644
--- a/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -8,7 +8,6 @@
 ALTER TABLE patch_comments CLUSTER ON patch_comments_pkey;
 ALTER TABLE patch_set_approvals CLUSTER ON patch_set_approvals_pkey;
 
-ALTER TABLE account_group_members CLUSTER ON account_group_members_pkey;
 CLUSTER;