Merge "Add @RequiresOptions annotation for dynamic options"
diff --git a/Documentation/config-auto-site-initialization.txt b/Documentation/config-auto-site-initialization.txt
index acd03c9..1be0af9 100644
--- a/Documentation/config-auto-site-initialization.txt
+++ b/Documentation/config-auto-site-initialization.txt
@@ -27,15 +27,14 @@
 run for that site. The database connectivity, in that case, is defined
 in the `etc/gerrit.config`.
 
-If `gerrit.site_path` is not defined then Gerrit will try to find the
-`gerrit.init_path` system property. If defined this property will be
-used to determine the site path. The database connectivity, also for
-this case, is defined by the `jdbc/ReviewDb` JNDI property.
+`gerrit.site_path` system property must be defined to run the init for
+that site.
 
 [WARNING]
 Defining the `jdbc/ReviewDb` JNDI property for an H2 database under the
-path defined by either `gerrit.site_path` or `gerrit.init_path` will
-cause an incomplete auto initialization and Gerrit will fail to start.
+path defined by `gerrit.site_path` will cause an incomplete auto
+initialization and Gerrit will fail to start.
+
 Opening a connection to such a database will create a subfolder under the
 site path folder (in order to create the H2 database) and Gerrit will
 no longer consider that site path to be new and, because of that,
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 765e56e..589071a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3798,6 +3798,20 @@
 If no keys are specified, web-of-trust checks are disabled. This is the
 default behavior.
 
+[[receive.enableProtocolV2]]receive.enableProtocolV2::
++
+Enable support for git protocol version 2.
++
+When this option is enabled, clients may send upload pack using git
+protocol version 2.
++
+The repository must also be configured on the server side to use protocol
+version 2 by setting `protocol.version = 2` either in the gerrit user's
+`~/.gitconfig` file (which will enable it for all repositories) or on
+a per repository basis by setting the option in the `.git/config` file
+of the repository.
++
+Defaults to false, git protocol version 2 is not enabled.
 
 [[repository]]
 === Section repository
@@ -5081,16 +5095,9 @@
 
 The format is one Base-64 encoded public key per line.
 
+== Configuring the Polygerrit UI
 
-== Database system_config
-
-Several columns in the `system_config` table within the metadata
-database may be set to control how Gerrit behaves.
-
-[NOTE]
-The contents of the `system_config` table are cached at startup
-by Gerrit.  If you modify any columns in this table, Gerrit needs
-to be restarted before it will use the new values.
+Please see link:dev-polygerrit.html[UI] on configuring the Polygerrit UI.
 
 === Configurable Parameters
 
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index f7252e0..91d73cc 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -105,9 +105,8 @@
 ----
 
 [TIP]
-Under Jetty, restarting the web application (e.g. after modifying
-`system_config`) is as simple as touching the context config file:
-`'$JETTY_HOME'/contexts/gerrit.xml`
+Under Jetty, restarting the web application is as simple as
+touching the context config file: `'$JETTY_HOME'/contexts/gerrit.xml`
 
 [[tomcat]]
 == Tomcat 7.x
diff --git a/WORKSPACE b/WORKSPACE
index 50714f1..8c8102b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1016,8 +1016,8 @@
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.4.3",
-    sha1 = "5c24325430971ba2fa4769eb446f026b7680d5e7",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.5.0",
+    sha1 = "241436d27cf65b84d17126dc7b6b947e8e2c173c",
 )
 
 JACKSON_VERSION = "2.9.7"
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 0107bae..8c64aa9 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -38,10 +38,8 @@
 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.group.GroupOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelFunction;
@@ -55,7 +53,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
-import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -129,6 +126,7 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.gerrit.testing.FakeGroupAuditService;
 import com.google.gerrit.testing.NoteDbMode;
 import com.google.gerrit.testing.SshMode;
 import com.google.gerrit.testing.TempFileUtil;
@@ -245,6 +243,7 @@
   @Inject protected ChangeNoteUtil changeNoteUtil;
   @Inject protected ChangeResource.Factory changeResourceFactory;
   @Inject protected FakeEmailSender sender;
+  @Inject protected FakeGroupAuditService auditService;
   @Inject protected GerritApi gApi;
   @Inject protected GitRepositoryManager repoManager;
   @Inject protected GroupBackend groupBackend;
@@ -264,7 +263,6 @@
   @Inject protected ChangeNotes.Factory notesFactory;
   @Inject protected BatchAbandon batchAbandon;
   @Inject protected TestSshKeys sshKeys;
-  @Inject protected GroupOperations groupOperations;
 
   protected EventRecorder eventRecorder;
   protected GerritServer server;
@@ -428,11 +426,12 @@
 
     db = reviewDbProvider.open();
 
-    // All groups which were added during the server start (e.g. in SchemaCreator) aren't contained
-    // in the instance of the group index which is available here and in tests. There are two
-    // reasons:
-    // 1) No group index is available in SchemaCreator when using an in-memory database. (This could
-    // be fixed by using the IndexManagerOnInit in InMemoryDatabase similar as BaseInit uses it.)
+    // All groups which were added during the server start (e.g. in ReviewDbSchemaCreator) aren't
+    // contained in the instance of the group index which is available here and in tests. There are
+    // two reasons:
+    // 1) No group index is available in ReviewDbSchemaCreator when using an in-memory database.
+    // (This could be fixed by using the IndexManagerOnInit in InMemoryDatabase similar as BaseInit
+    // uses it.)
     // 2) During the on-init part of the server start, we use another instance of the index than
     // later on. As test indexes are non-permanent, closing an instance and opening another one
     // removes all indexed data.
@@ -1000,15 +999,6 @@
     }
   }
 
-  protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig config = projectConfigFactory.read(md);
-      config.getProject().setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, value);
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
-  }
-
   protected void setUseSignedOffBy(InheritableBoolean value) throws Exception {
     try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
       ProjectConfig config = projectConfigFactory.read(md);
@@ -1229,36 +1219,6 @@
     assertThat(replyTo.getString()).contains(email);
   }
 
-  protected ContributorAgreement configureContributorAgreement(boolean autoVerify)
-      throws Exception {
-    ContributorAgreement ca;
-    String name = autoVerify ? "cla-test-group" : "cla-test-no-auto-verify-group";
-    String g = groupOperations.newGroup().name(name).create().get();
-    GroupApi groupApi = gApi.groups().id(g);
-    groupApi.description("CLA test group");
-    InternalGroup caGroup = group(new AccountGroup.UUID(groupApi.detail().id));
-    GroupReference groupRef = new GroupReference(caGroup.getGroupUUID(), caGroup.getName());
-    PermissionRule rule = new PermissionRule(groupRef);
-    rule.setAction(PermissionRule.Action.ALLOW);
-    if (autoVerify) {
-      ca = new ContributorAgreement("cla-test");
-      ca.setAutoVerify(groupRef);
-      ca.setAccepted(ImmutableList.of(rule));
-    } else {
-      ca = new ContributorAgreement("cla-test-no-auto-verify");
-    }
-    ca.setDescription("description");
-    ca.setAgreementUrl("agreement-url");
-    ca.setAccepted(ImmutableList.of(rule));
-    ca.setExcludeProjectsRegexes(ImmutableList.of("ExcludedProject"));
-
-    try (ProjectConfigUpdate u = updateProject(allProjects)) {
-      u.getConfig().replace(ca);
-      u.save();
-      return ca;
-    }
-  }
-
   protected Map<Branch.NameKey, ObjectId> fetchFromSubmitPreview(String changeId) throws Exception {
     try (BinaryResult result = gApi.changes().id(changeId).current().submitPreview()) {
       return fetchFromBundles(result);
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 9f9cbf9..be8932e 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.testing.FakeEmailSender;
+import com.google.gerrit.testing.FakeGroupAuditService;
 import com.google.gerrit.testing.InMemoryDatabase;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.NoteDbChecker;
@@ -355,6 +356,7 @@
             },
             site);
     daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
+    daemon.setAuditEventModuleForTesting(new FakeGroupAuditService.Module());
     daemon.setAdditionalSysModuleForTesting(testSysModule);
     daemon.setEnableSshd(desc.useSsh());
     daemon.setSlave(isSlave(baseConfig));
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 6ee5c09..0bbf9f1 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -34,8 +34,8 @@
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
 import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gerrit.server.schema.SchemaModule;
-import com.google.gerrit.server.schema.SchemaVersion;
+import com.google.gerrit.server.schema.ReviewDbSchemaModule;
+import com.google.gerrit.server.schema.ReviewDbSchemaVersion;
 import com.google.gerrit.testing.InMemoryDatabase;
 import com.google.gerrit.testing.InMemoryH2Type;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
@@ -101,8 +101,8 @@
     bind(SitePaths.class);
     bind(TrackingFooters.class).toProvider(TrackingFootersProvider.class).in(SINGLETON);
 
-    install(new SchemaModule());
-    bind(SchemaVersion.class).to(SchemaVersion.C);
+    install(new ReviewDbSchemaModule());
+    bind(ReviewDbSchemaVersion.class).to(ReviewDbSchemaVersion.C);
 
     install(new SshdModule());
   }
diff --git a/java/com/google/gerrit/common/audit/Audit.java b/java/com/google/gerrit/common/audit/Audit.java
index 25e4caf..a791e97 100644
--- a/java/com/google/gerrit/common/audit/Audit.java
+++ b/java/com/google/gerrit/common/audit/Audit.java
@@ -23,7 +23,7 @@
  * Audit annotation for JSON/RPC interfaces.
  *
  * <p>Flag with @Audit all the JSON/RPC methods to be traced in audit-trail and submitted to the
- * AuditService.
+ * GroupAuditService.
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD})
diff --git a/java/com/google/gerrit/common/data/AccessSection.java b/java/com/google/gerrit/common/data/AccessSection.java
index 49948f8f..c8d8d41 100644
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ b/java/com/google/gerrit/common/data/AccessSection.java
@@ -16,6 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Project;
 import java.util.ArrayList;
@@ -36,12 +37,8 @@
     super(refPattern);
   }
 
-  // TODO(ekempin): Make this method return an ImmutableList once the GWT UI is gone.
   public List<Permission> getPermissions() {
-    if (permissions == null) {
-      return new ArrayList<>();
-    }
-    return new ArrayList<>(permissions);
+    return permissions == null ? ImmutableList.of() : ImmutableList.copyOf(permissions);
   }
 
   public void setPermissions(List<Permission> list) {
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
index de6108e..a30d412 100644
--- a/java/com/google/gerrit/common/data/Permission.java
+++ b/java/com/google/gerrit/common/data/Permission.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -157,12 +158,8 @@
     exclusiveGroup = newExclusiveGroup;
   }
 
-  // TODO(ekempin): Make this method return an ImmutableList once the GWT UI is gone.
   public List<PermissionRule> getRules() {
-    if (rules == null) {
-      return new ArrayList<>();
-    }
-    return new ArrayList<>(rules);
+    return rules == null ? ImmutableList.of() : ImmutableList.copyOf(rules);
   }
 
   public void setRules(List<PermissionRule> list) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
index 05fd7a7..65d2916 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -40,6 +40,7 @@
       case V6_2:
       case V6_3:
       case V6_4:
+      case V6_5:
         this.searchFilteringName = "_source";
         this.indicesExistParam = "?allow_no_indices=false";
         this.exactFieldType = "keyword";
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index dfa5d21..4c98df1 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -22,7 +22,8 @@
   V5_6("5.6.*"),
   V6_2("6.2.*"),
   V6_3("6.3.*"),
-  V6_4("6.4.*");
+  V6_4("6.4.*"),
+  V6_5("6.5.*");
 
   private final String version;
   private final Pattern pattern;
diff --git a/java/com/google/gerrit/git/BUILD b/java/com/google/gerrit/git/BUILD
new file mode 100644
index 0000000..f0c01de
--- /dev/null
+++ b/java/com/google/gerrit/git/BUILD
@@ -0,0 +1,9 @@
+java_library(
+    name = "git",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib:guava",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+    ],
+)
diff --git a/java/com/google/gerrit/server/git/LockFailureException.java b/java/com/google/gerrit/git/LockFailureException.java
similarity index 97%
rename from java/com/google/gerrit/server/git/LockFailureException.java
rename to java/com/google/gerrit/git/LockFailureException.java
index 02a30e0..b249749 100644
--- a/java/com/google/gerrit/server/git/LockFailureException.java
+++ b/java/com/google/gerrit/git/LockFailureException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.git;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
diff --git a/java/com/google/gerrit/server/update/RefUpdateUtil.java b/java/com/google/gerrit/git/RefUpdateUtil.java
similarity index 82%
rename from java/com/google/gerrit/server/update/RefUpdateUtil.java
rename to java/com/google/gerrit/git/RefUpdateUtil.java
index 3e33677..520d0f2 100644
--- a/java/com/google/gerrit/server/update/RefUpdateUtil.java
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -12,10 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.update;
+package com.google.gerrit.git;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.server.git.LockFailureException;
 import java.io.IOException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -105,6 +104,37 @@
   }
 
   /**
+   * Check results of a single ref update, throwing an exception if there was a failure.
+   *
+   * @param ru ref update; must already have been executed.
+   * @throws IllegalArgumentException if the result was {@code NOT_ATTEMPTED}.
+   * @throws LockFailureException if the result was {@code LOCK_FAILURE}.
+   * @throws IOException if the result failed for another reason.
+   */
+  public static void checkResult(RefUpdate ru) throws IOException {
+    RefUpdate.Result result = ru.getResult();
+    switch (result) {
+      case NOT_ATTEMPTED:
+        throw new IllegalArgumentException("Not attempted: " + ru.getName());
+      case NEW:
+      case FORCED:
+      case NO_CHANGE:
+      case FAST_FORWARD:
+      case RENAMED:
+        return;
+      case LOCK_FAILURE:
+        throw new LockFailureException("Failed to update " + ru.getName() + ": " + result, ru);
+      default:
+      case IO_FAILURE:
+      case REJECTED:
+      case REJECTED_CURRENT_BRANCH:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
+        throw new IOException("Failed to update " + ru.getName() + ": " + ru.getResult());
+    }
+  }
+
+  /**
    * Delete a single ref, throwing a checked exception on failure.
    *
    * <p>Does not require that the ref have any particular old value. Succeeds as a no-op if the ref
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 2294d0e..82547385 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -11,6 +11,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/launcher",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 77ce983..e74c4b2 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.cache.Cache;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -23,6 +25,7 @@
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.audit.HttpAuditEvent;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -30,12 +33,14 @@
 import com.google.gerrit.server.git.UploadPackInitializer;
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.git.validators.UploadValidators;
+import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,6 +49,7 @@
 import com.google.inject.name.Named;
 import java.io.IOException;
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -141,6 +147,30 @@
     addReceivePackFilter(receiveFilter);
   }
 
+  private static String extractWhat(HttpServletRequest request) {
+    StringBuilder commandName = new StringBuilder(request.getRequestURL());
+    if (request.getQueryString() != null) {
+      commandName.append("?").append(request.getQueryString());
+    }
+    return commandName.toString();
+  }
+
+  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]);
+                }
+              });
+    }
+    return multiMap;
+  }
+
   static class Resolver implements RepositoryResolver<HttpServletRequest> {
     private final GitRepositoryManager manager;
     private final PermissionBackend permissionBackend;
@@ -229,6 +259,13 @@
       up.setTimeout(config.getTimeout());
       up.setPreUploadHook(PreUploadHookChain.newChain(Lists.newArrayList(preUploadHooks)));
       up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
+      if (config.enableProtocolV2()) {
+        String header = req.getHeader("Git-Protocol");
+        if (header != null) {
+          String[] params = header.split(":");
+          up.setExtraParameters(Arrays.asList(params));
+        }
+      }
       ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
       for (UploadPackInitializer initializer : uploadPackInitializers) {
         initializer.init(state.getNameKey(), up);
@@ -240,12 +277,19 @@
   static class UploadFilter implements Filter {
     private final UploadValidators.Factory uploadValidatorsFactory;
     private final PermissionBackend permissionBackend;
+    private final Provider<CurrentUser> userProvider;
+    private final GroupAuditService groupAuditService;
 
     @Inject
     UploadFilter(
-        UploadValidators.Factory uploadValidatorsFactory, PermissionBackend permissionBackend) {
+        UploadValidators.Factory uploadValidatorsFactory,
+        PermissionBackend permissionBackend,
+        Provider<CurrentUser> userProvider,
+        GroupAuditService groupAuditService) {
       this.uploadValidatorsFactory = uploadValidatorsFactory;
       this.permissionBackend = permissionBackend;
+      this.userProvider = userProvider;
+      this.groupAuditService = groupAuditService;
     }
 
     @Override
@@ -268,7 +312,22 @@
         return;
       } catch (PermissionBackendException e) {
         throw new ServletException(e);
+      } finally {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        groupAuditService.dispatch(
+            new HttpAuditEvent(
+                httpRequest.getSession().getId(),
+                userProvider.get(),
+                extractWhat(httpRequest),
+                TimeUtil.nowMs(),
+                extractParameters(httpRequest),
+                httpRequest.getMethod(),
+                httpRequest,
+                httpResponse.getStatus(),
+                httpResponse));
       }
+
       // 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 =
@@ -326,15 +385,18 @@
     private final Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache;
     private final PermissionBackend permissionBackend;
     private final Provider<CurrentUser> userProvider;
+    private final GroupAuditService groupAuditService;
 
     @Inject
     ReceiveFilter(
         @Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache,
         PermissionBackend permissionBackend,
-        Provider<CurrentUser> userProvider) {
+        Provider<CurrentUser> userProvider,
+        GroupAuditService groupAuditService) {
       this.cache = cache;
       this.permissionBackend = permissionBackend;
       this.userProvider = userProvider;
+      this.groupAuditService = groupAuditService;
     }
 
     @Override
@@ -365,6 +427,20 @@
         return;
       } catch (PermissionBackendException e) {
         throw new RuntimeException(e);
+      } finally {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        groupAuditService.dispatch(
+            new HttpAuditEvent(
+                httpRequest.getSession().getId(),
+                userProvider.get(),
+                extractWhat(httpRequest),
+                TimeUtil.nowMs(),
+                extractParameters(httpRequest),
+                httpRequest.getMethod(),
+                httpRequest,
+                httpResponse.getStatus(),
+                httpResponse));
       }
 
       if (canUpload != Capable.OK) {
diff --git a/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index abfcc22..ab7bfdf 100644
--- a/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -17,8 +17,8 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.AuditEvent;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.audit.AuditEvent;
 import com.google.gerrit.server.audit.AuditService;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
diff --git a/java/com/google/gerrit/httpd/init/SiteInitializer.java b/java/com/google/gerrit/httpd/init/SiteInitializer.java
index de4f284..67510cd 100644
--- a/java/com/google/gerrit/httpd/init/SiteInitializer.java
+++ b/java/com/google/gerrit/httpd/init/SiteInitializer.java
@@ -14,19 +14,17 @@
 
 package com.google.gerrit.httpd.init;
 
+import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.pgm.init.BaseInit;
 import com.google.gerrit.pgm.init.PluginsDistribution;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
 import java.util.List;
 
 public final class SiteInitializer {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String GERRIT_SITE_PATH = "gerrit.site_path";
 
   private final String sitePath;
   private final String initPath;
@@ -53,42 +51,29 @@
         return;
       }
 
-      try (Connection conn = connectToDb()) {
-        Path site = getSiteFromReviewDb(conn);
-        if (site == null && initPath != null) {
-          site = Paths.get(initPath);
-        }
-        if (site != null) {
-          logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
-          new BaseInit(
-                  site,
-                  new ReviewDbDataSourceProvider(),
-                  false,
-                  false,
-                  pluginsDistribution,
-                  pluginsToInstall)
-              .run();
-        }
+      String path = System.getProperty(GERRIT_SITE_PATH);
+      Path site = null;
+      if (!Strings.isNullOrEmpty(path)) {
+        site = Paths.get(path);
+      }
+
+      if (site == null && initPath != null) {
+        site = Paths.get(initPath);
+      }
+      if (site != null) {
+        logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
+        new BaseInit(
+                site,
+                new ReviewDbDataSourceProvider(),
+                false,
+                false,
+                pluginsDistribution,
+                pluginsToInstall)
+            .run();
       }
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Site init failed");
       throw new RuntimeException(e);
     }
   }
-
-  private Connection connectToDb() throws SQLException {
-    return new ReviewDbDataSourceProvider().get().getConnection();
-  }
-
-  private Path getSiteFromReviewDb(Connection conn) {
-    try (Statement stmt = conn.createStatement();
-        ResultSet rs = stmt.executeQuery("SELECT site_path FROM system_config")) {
-      if (rs.next()) {
-        return Paths.get(rs.getString(1));
-      }
-    } catch (SQLException e) {
-      return null;
-    }
-    return null;
-  }
 }
diff --git a/java/com/google/gerrit/httpd/init/SitePathFromSystemConfigProvider.java b/java/com/google/gerrit/httpd/init/SitePathFromSystemConfigProvider.java
deleted file mode 100644
index 96ba28b..0000000
--- a/java/com/google/gerrit/httpd/init/SitePathFromSystemConfigProvider.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2009 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.httpd.init;
-
-import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.SitePath;
-import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-
-/** Provides {@link Path} annotated with {@link SitePath}. */
-class SitePathFromSystemConfigProvider implements Provider<Path> {
-  private final Path path;
-
-  @Inject
-  SitePathFromSystemConfigProvider(@ReviewDbFactory SchemaFactory<ReviewDb> schemaFactory)
-      throws OrmException {
-    path = read(schemaFactory);
-  }
-
-  @Override
-  public Path get() {
-    return path;
-  }
-
-  private static Path read(SchemaFactory<ReviewDb> schemaFactory) throws OrmException {
-    try (ReviewDb db = schemaFactory.open()) {
-      List<SystemConfig> all = db.systemConfig().all().toList();
-      switch (all.size()) {
-        case 1:
-          return Paths.get(all.get(0).sitePath);
-        case 0:
-          throw new OrmException("system_config table is empty");
-        default:
-          throw new OrmException(
-              "system_config must have exactly 1 row; found " + all.size() + " rows instead");
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 5b7173e..8870d1d 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.api.GerritApiModule;
+import com.google.gerrit.server.api.PluginApiModule;
 import com.google.gerrit.server.audit.AuditModule;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -60,6 +61,7 @@
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritInstanceNameModule;
 import com.google.gerrit.server.config.GerritOptions;
+import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
@@ -88,8 +90,8 @@
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
-import com.google.gerrit.server.schema.SchemaModule;
-import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.schema.ReviewDbSchemaModule;
+import com.google.gerrit.server.schema.ReviewDbSchemaVersionCheck;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.ssh.SshAddressesModule;
@@ -107,6 +109,7 @@
 import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 import com.google.inject.name.Names;
 import com.google.inject.servlet.GuiceFilter;
 import com.google.inject.servlet.GuiceServletContextListener;
@@ -134,6 +137,8 @@
 public class WebAppInitializer extends GuiceServletContextListener implements Filter {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private static final String GERRIT_SITE_PATH = "gerrit.site_path";
+
   private Path sitePath;
   private Injector dbInjector;
   private Injector cfgInjector;
@@ -155,9 +160,11 @@
 
   private synchronized void init() {
     if (manager == null) {
-      final String path = System.getProperty("gerrit.site_path");
+      String path = System.getProperty(GERRIT_SITE_PATH);
       if (path != null) {
         sitePath = Paths.get(path);
+      } else {
+        throw new ProvisionException(GERRIT_SITE_PATH + " must be defined");
       }
 
       if (System.getProperty("gerrit.init") != null) {
@@ -171,7 +178,7 @@
         }
         new SiteInitializer(
                 path,
-                System.getProperty("gerrit.init_path"),
+                System.getProperty(GERRIT_SITE_PATH),
                 new UnzippedDistribution(servletContext),
                 pluginsToInstall)
             .init();
@@ -292,21 +299,6 @@
               listener().to(ReviewDbDataSourceProvider.class);
             }
           });
-
-      // If we didn't get the site path from the system property
-      // we need to get it from the database, as that's our old
-      // method of locating the site path on disk.
-      //
-      modules.add(
-          new AbstractModule() {
-            @Override
-            protected void configure() {
-              bind(Path.class)
-                  .annotatedWith(SitePath.class)
-                  .toProvider(SitePathFromSystemConfigProvider.class)
-                  .in(SINGLETON);
-            }
-          });
       modules.add(new GerritServerConfigModule());
     }
     modules.add(new DatabaseModule());
@@ -317,8 +309,8 @@
 
   private Injector createCfgInjector() {
     final List<Module> modules = new ArrayList<>();
-    modules.add(new SchemaModule());
-    modules.add(SchemaVersionCheck.module());
+    modules.add(new ReviewDbSchemaModule());
+    modules.add(ReviewDbSchemaVersionCheck.module());
     modules.add(new AuthConfigModule());
     return dbInjector.createChildInjector(modules);
   }
@@ -336,6 +328,7 @@
     modules.add(new MimeUtil2Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new GerritApiModule());
+    modules.add(new PluginApiModule());
     modules.add(new SearchingChangeCacheImpl.Module());
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultPermissionBackendModule());
@@ -385,6 +378,7 @@
           @Override
           protected void configure() {
             bind(GerritOptions.class).toInstance(new GerritOptions(false, false, false));
+            bind(GerritRuntime.class).toInstance(GerritRuntime.DAEMON);
           }
         });
     modules.add(new GarbageCollectionModule());
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 519a218..ec71d8f 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -97,6 +97,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams;
 import com.google.gerrit.server.AccessPath;
@@ -108,7 +109,6 @@
 import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
 import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.permissions.GlobalPermission;
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 7284d70..9a24867 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -97,7 +97,7 @@
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.InMemoryAccountPatchReviewStore;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
-import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.schema.ReviewDbSchemaVersionCheck;
 import com.google.gerrit.server.securestore.DefaultSecureStore;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
@@ -199,6 +199,7 @@
   private AbstractModule luceneModule;
   private Module emailModule;
   private Module testSysModule;
+  private Module auditEventModule;
 
   private Runnable serverStarted;
   private IndexType indexType;
@@ -289,7 +290,7 @@
         JythonShell shell = new JythonShell();
         shell.set("m", manager);
         shell.set("ds", dbInjector.getInstance(DataSourceProvider.class));
-        shell.set("schk", dbInjector.getInstance(SchemaVersionCheck.class));
+        shell.set("schk", dbInjector.getInstance(ReviewDbSchemaVersionCheck.class));
         shell.set("d", this);
         shell.run();
       } else {
@@ -320,6 +321,11 @@
   }
 
   @VisibleForTesting
+  public void setAuditEventModuleForTesting(Module module) {
+    auditEventModule = module;
+  }
+
+  @VisibleForTesting
   public void setLuceneModule(LuceneIndexModule m) {
     luceneModule = m;
     inMemoryTest = true;
@@ -391,7 +397,7 @@
 
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<>();
-    modules.add(SchemaVersionCheck.module());
+    modules.add(ReviewDbSchemaVersionCheck.module());
     modules.add(new DropWizardMetricMaker.RestModule());
     modules.add(new LogFileCompressor.Module());
 
@@ -425,7 +431,6 @@
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new GerritApiModule());
     modules.add(new PluginApiModule());
-    modules.add(new AuditModule());
 
     modules.add(new SearchingChangeCacheImpl.Module(slave));
     modules.add(new InternalAccountDirectory.Module());
@@ -438,6 +443,11 @@
     } else {
       modules.add(new SmtpEmailSender.Module());
     }
+    if (auditEventModule != null) {
+      modules.add(auditEventModule);
+    } else {
+      modules.add(new AuditModule());
+    }
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new RestApiModule());
     modules.add(new GpgModule(config));
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index 1739de9..4ea31da 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -245,7 +245,7 @@
   }
 
   private void verifyInstallPluginList(ConsoleUI ui, List<PluginData> plugins) {
-    if (nullOrEmpty(installPlugins) || nullOrEmpty(plugins)) {
+    if (nullOrEmpty(installPlugins)) {
       return;
     }
     Set<String> missing = Sets.newHashSet(installPlugins);
diff --git a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 5bfc00f..6ebd6a3 100644
--- a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -29,7 +29,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
-import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.schema.ReviewDbSchemaVersionCheck;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -55,7 +55,7 @@
   @Override
   public int run() throws Exception {
     Injector dbInjector = createDbInjector(MULTI_USER);
-    manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module()));
+    manager.add(dbInjector, dbInjector.createChildInjector(ReviewDbSchemaVersionCheck.module()));
     manager.start();
     dbInjector
         .createChildInjector(
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index deaf139..71957e1 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -43,7 +43,7 @@
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.plugins.JarScanner;
 import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gerrit.server.schema.SchemaUpdater;
+import com.google.gerrit.server.schema.ReviewDbSchemaUpdater;
 import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
@@ -358,7 +358,7 @@
     public final ConsoleUI ui;
     public final SitePaths site;
     public final InitFlags flags;
-    final SchemaUpdater schemaUpdater;
+    final ReviewDbSchemaUpdater schemaUpdater;
     final SchemaFactory<ReviewDb> schema;
     final GitRepositoryManager repositoryManager;
 
@@ -367,7 +367,7 @@
         ConsoleUI ui,
         SitePaths site,
         InitFlags flags,
-        SchemaUpdater schemaUpdater,
+        ReviewDbSchemaUpdater schemaUpdater,
         @ReviewDbFactory SchemaFactory<ReviewDb> schema,
         GitRepositoryManager repositoryManager) {
       this.ui = ui;
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index 1338efb..65feeab 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -34,7 +34,7 @@
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.DatabaseModule;
-import com.google.gerrit.server.schema.SchemaModule;
+import com.google.gerrit.server.schema.ReviewDbSchemaModule;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
@@ -188,7 +188,7 @@
           }
         });
     modules.add(new DatabaseModule());
-    modules.add(new SchemaModule());
+    modules.add(new ReviewDbSchemaModule());
     modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
     modules.add(new NotesMigration.Module());
 
diff --git a/java/com/google/gerrit/reviewdb/client/SystemConfig.java b/java/com/google/gerrit/reviewdb/client/SystemConfig.java
deleted file mode 100644
index cd42dd1..0000000
--- a/java/com/google/gerrit/reviewdb/client/SystemConfig.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.StringKey;
-
-/** Global configuration needed to serve web requests. */
-public final class SystemConfig {
-  public static final class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    private static final String VALUE = "X";
-
-    @Column(id = 1, length = 1)
-    protected String one = VALUE;
-
-    public Key() {}
-
-    @Override
-    public String get() {
-      return VALUE;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      assert get().equals(newValue);
-    }
-  }
-
-  /** Construct a new, unconfigured instance. */
-  public static SystemConfig create() {
-    final SystemConfig r = new SystemConfig();
-    r.singleton = new SystemConfig.Key();
-    return r;
-  }
-
-  @Column(id = 1)
-  protected Key singleton;
-
-  /** Local filesystem location of header/footer/CSS configuration files */
-  @Column(id = 3, notNull = false, length = Integer.MAX_VALUE)
-  public transient String sitePath;
-
-  // DO NOT LOOK BELOW THIS LINE. These fields have all been deleted,
-  // but survive to support schema upgrade code.
-
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 2, length = 36, notNull = false)
-  public transient String registerEmailPrivateKey;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 4, notNull = false)
-  public AccountGroup.Id adminGroupId;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 10, notNull = false)
-  public AccountGroup.UUID adminGroupUUID;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 5, notNull = false)
-  public AccountGroup.Id anonymousGroupId;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 6, notNull = false)
-  public AccountGroup.Id registeredGroupId;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 7, notNull = false)
-  public Project.NameKey wildProjectName;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 9, notNull = false)
-  public AccountGroup.Id ownerGroupId;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 8, notNull = false)
-  public AccountGroup.Id batchUsersGroupId;
-  /** DEPRECATED DO NOT USE */
-  @Column(id = 11, notNull = false)
-  public AccountGroup.UUID batchUsersGroupUUID;
-
-  protected SystemConfig() {}
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 4e648b9..8e4292c 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.Relation;
 import com.google.gwtorm.server.Schema;
@@ -31,17 +30,15 @@
  * <ul>
  *   <li>{@link Account}: Per-user account registration, preferences, identity.
  *   <li>{@link Change}: All review information about a single proposed change.
- *   <li>{@link SystemConfig}: Server-wide settings, managed by administrator.
  * </ul>
  */
 public interface ReviewDb extends Schema {
-  /* If you change anything, update SchemaVersion.C to use a new version. */
+  /* If you change anything, update ReviewDbSchemaVersion.C to use a new version. */
 
   @Relation(id = 1)
   SchemaVersionAccess schemaVersion();
 
-  @Relation(id = 2)
-  SystemConfigAccess systemConfig();
+  // Deleted @Relation(id = 2)
 
   // Deleted @Relation(id = 3)
 
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 0deaa57..202729e 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -110,11 +110,6 @@
   }
 
   @Override
-  public SystemConfigAccess systemConfig() {
-    return delegate.systemConfig();
-  }
-
-  @Override
   public ChangeAccess changes() {
     return delegate.changes();
   }
diff --git a/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java b/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
deleted file mode 100644
index a2177fd..0000000
--- a/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-/** Access interface for {@link SystemConfig}. */
-public interface SystemConfigAccess extends Access<SystemConfig, SystemConfig.Key> {
-  @Override
-  @PrimaryKey("singleton")
-  SystemConfig get(SystemConfig.Key key) throws OrmException;
-
-  @Query
-  ResultSet<SystemConfig> all() throws OrmException;
-}
diff --git a/java/com/google/gerrit/server/audit/AuditEvent.java b/java/com/google/gerrit/server/AuditEvent.java
similarity index 97%
rename from java/com/google/gerrit/server/audit/AuditEvent.java
rename to java/com/google/gerrit/server/AuditEvent.java
index 46b2844..773a307 100644
--- a/java/com/google/gerrit/server/audit/AuditEvent.java
+++ b/java/com/google/gerrit/server/AuditEvent.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.audit;
+package com.google.gerrit.server;
 
 import static java.util.Objects.requireNonNull;
 
@@ -20,7 +20,6 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ListMultimap;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.util.time.TimeUtil;
 
 public class AuditEvent {
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 0d48bca..8401852 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -32,6 +32,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 1445dfd..ff1c50b 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -25,6 +25,8 @@
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -35,10 +37,8 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryHelper.Action;
 import com.google.gerrit.server.update.RetryHelper.ActionType;
diff --git a/java/com/google/gerrit/server/audit/AuditListener.java b/java/com/google/gerrit/server/audit/AuditListener.java
index 3f8c298..f555bbd 100644
--- a/java/com/google/gerrit/server/audit/AuditListener.java
+++ b/java/com/google/gerrit/server/audit/AuditListener.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.audit;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.AuditEvent;
 
 @ExtensionPoint
 public interface AuditListener {
diff --git a/java/com/google/gerrit/server/audit/AuditService.java b/java/com/google/gerrit/server/audit/AuditService.java
index cbca65b..425e22a 100644
--- a/java/com/google/gerrit/server/audit/AuditService.java
+++ b/java/com/google/gerrit/server/audit/AuditService.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.AuditEvent;
 import com.google.gerrit.server.audit.group.GroupAuditListener;
 import com.google.gerrit.server.audit.group.GroupMemberAuditEvent;
 import com.google.gerrit.server.audit.group.GroupSubgroupAuditEvent;
@@ -39,6 +40,7 @@
     this.groupAuditListeners = groupAuditListeners;
   }
 
+  @Override
   public void dispatch(AuditEvent action) {
     auditListeners.runEach(l -> l.onAuditableAction(action));
   }
diff --git a/java/com/google/gerrit/server/audit/HttpAuditEvent.java b/java/com/google/gerrit/server/audit/HttpAuditEvent.java
index 11a6b63..5ea2485 100644
--- a/java/com/google/gerrit/server/audit/HttpAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/HttpAuditEvent.java
@@ -14,6 +14,7 @@
 package com.google.gerrit.server.audit;
 
 import com.google.common.collect.ListMultimap;
+import com.google.gerrit.server.AuditEvent;
 import com.google.gerrit.server.CurrentUser;
 
 public class HttpAuditEvent extends AuditEvent {
diff --git a/java/com/google/gerrit/server/audit/SshAuditEvent.java b/java/com/google/gerrit/server/audit/SshAuditEvent.java
index 89f01ac..fee959e 100644
--- a/java/com/google/gerrit/server/audit/SshAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/SshAuditEvent.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.audit;
 
 import com.google.common.collect.ListMultimap;
+import com.google.gerrit.server.AuditEvent;
 import com.google.gerrit.server.CurrentUser;
 
 public class SshAuditEvent extends AuditEvent {
diff --git a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
index 66d6555..0bfe5fd 100644
--- a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
+++ b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
@@ -13,10 +13,11 @@
 // limitations under the License.
 package com.google.gerrit.server.config;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
 import java.util.Collections;
 import java.util.LinkedHashSet;
-import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import org.apache.commons.lang.StringUtils;
@@ -36,6 +37,8 @@
  * (+ various overloaded versions of these)
  */
 public class ConfigUpdatedEvent {
+  public static final Multimap<UpdateResult, ConfigUpdateEntry> NO_UPDATES =
+      new ImmutableMultimap.Builder<UpdateResult, ConfigUpdateEntry>().build();
   private final Config oldConfig;
   private final Config newConfig;
 
@@ -52,25 +55,29 @@
     return this.newConfig;
   }
 
-  public Update accept(ConfigKey entry) {
+  private String getString(ConfigKey key, Config config) {
+    return config.getString(key.section(), key.subsection(), key.name());
+  }
+
+  public Multimap<UpdateResult, ConfigUpdateEntry> accept(ConfigKey entry) {
     return accept(Collections.singleton(entry));
   }
 
-  public Update accept(Set<ConfigKey> entries) {
+  public Multimap<UpdateResult, ConfigUpdateEntry> accept(Set<ConfigKey> entries) {
     return createUpdate(entries, UpdateResult.APPLIED);
   }
 
-  public Update accept(String section) {
+  public Multimap<UpdateResult, ConfigUpdateEntry> accept(String section) {
     Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
     entries.addAll(getEntriesFromSection(newConfig, section));
     return createUpdate(entries, UpdateResult.APPLIED);
   }
 
-  public Update reject(ConfigKey entry) {
+  public Multimap<UpdateResult, ConfigUpdateEntry> reject(ConfigKey entry) {
     return reject(Collections.singleton(entry));
   }
 
-  public Update reject(Set<ConfigKey> entries) {
+  public Multimap<UpdateResult, ConfigUpdateEntry> reject(Set<ConfigKey> entries) {
     return createUpdate(entries, UpdateResult.REJECTED);
   }
 
@@ -87,20 +94,15 @@
     return res;
   }
 
-  private Update createUpdate(Set<ConfigKey> entries, UpdateResult updateResult) {
-    Update update = new Update(updateResult);
+  private Multimap<UpdateResult, ConfigUpdateEntry> createUpdate(
+      Set<ConfigKey> entries, UpdateResult updateResult) {
+    Multimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
     entries
         .stream()
         .filter(this::isValueUpdated)
-        .forEach(
-            key -> {
-              update.addConfigUpdate(
-                  new ConfigUpdateEntry(
-                      key,
-                      oldConfig.getString(key.section(), key.subsection(), key.name()),
-                      newConfig.getString(key.section(), key.subsection(), key.name())));
-            });
-    return update;
+        .map(e -> new ConfigUpdateEntry(e, getString(e, oldConfig), getString(e, newConfig)))
+        .forEach(e -> updates.put(updateResult, e));
+    return updates;
   }
 
   public boolean isSectionUpdated(String section) {
@@ -142,31 +144,6 @@
     }
   }
 
-  /**
-   * One Accepted/Rejected Update have one or more config updates (ConfigUpdateEntry) tied to it.
-   */
-  public static class Update {
-    private UpdateResult result;
-    private final Set<ConfigUpdateEntry> configUpdates;
-
-    public Update(UpdateResult result) {
-      this.configUpdates = new LinkedHashSet<>();
-      this.result = result;
-    }
-
-    public UpdateResult getResult() {
-      return result;
-    }
-
-    public List<ConfigUpdateEntry> getConfigUpdates() {
-      return ImmutableList.copyOf(configUpdates);
-    }
-
-    public void addConfigUpdate(ConfigUpdateEntry entry) {
-      this.configUpdates.add(entry);
-    }
-  }
-
   public enum ConfigEntryType {
     ADDED,
     REMOVED,
diff --git a/java/com/google/gerrit/server/config/GerritConfigListener.java b/java/com/google/gerrit/server/config/GerritConfigListener.java
index 337a962..f5b2976 100644
--- a/java/com/google/gerrit/server/config/GerritConfigListener.java
+++ b/java/com/google/gerrit/server/config/GerritConfigListener.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import java.util.EventListener;
-import java.util.List;
 
 /**
  * Implementations of the GerritConfigListener interface expects to react GerritServerConfig
@@ -24,5 +26,5 @@
  */
 @ExtensionPoint
 public interface GerritConfigListener extends EventListener {
-  List<ConfigUpdatedEvent.Update> configUpdated(ConfigUpdatedEvent event);
+  Multimap<UpdateResult, ConfigUpdateEntry> configUpdated(ConfigUpdatedEvent event);
 }
diff --git a/java/com/google/gerrit/server/config/GerritConfigListenerHelper.java b/java/com/google/gerrit/server/config/GerritConfigListenerHelper.java
index 1dfa3fc..d21e1c3 100644
--- a/java/com/google/gerrit/server/config/GerritConfigListenerHelper.java
+++ b/java/com/google/gerrit/server/config/GerritConfigListenerHelper.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.collect.ImmutableSet;
-import java.util.Collections;
 
 public class GerritConfigListenerHelper {
   public static GerritConfigListener acceptIfChanged(ConfigKey... keys) {
     return e ->
         e.isEntriesUpdated(ImmutableSet.copyOf(keys))
-            ? Collections.singletonList(e.accept(ImmutableSet.copyOf(keys)))
-            : Collections.emptyList();
+            ? e.accept(ImmutableSet.copyOf(keys))
+            : ConfigUpdatedEvent.NO_UPDATES;
   }
 }
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
index 1890de8..09c10740 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.ArrayList;
-import java.util.List;
 
 /** Issues a configuration reload from the GerritServerConfigProvider and notify all listeners. */
 @Singleton
@@ -40,18 +42,20 @@
    * Reloads the Gerrit Server Configuration from disk. Synchronized to ensure that one issued
    * reload is fully completed before a new one starts.
    */
-  public List<ConfigUpdatedEvent.Update> reloadConfig() {
+  public Multimap<UpdateResult, ConfigUpdateEntry> reloadConfig() {
     logger.atInfo().log("Starting server configuration reload");
-    List<ConfigUpdatedEvent.Update> updates = fireUpdatedConfigEvent(configProvider.updateConfig());
+    Multimap<UpdateResult, ConfigUpdateEntry> updates =
+        fireUpdatedConfigEvent(configProvider.updateConfig());
     logger.atInfo().log("Server configuration reload completed succesfully");
     return updates;
   }
 
-  public List<ConfigUpdatedEvent.Update> fireUpdatedConfigEvent(ConfigUpdatedEvent event) {
-    ArrayList<ConfigUpdatedEvent.Update> result = new ArrayList<>();
+  public Multimap<UpdateResult, ConfigUpdateEntry> fireUpdatedConfigEvent(
+      ConfigUpdatedEvent event) {
+    Multimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
     for (GerritConfigListener configListener : configListeners) {
-      result.addAll(configListener.configUpdated(event));
+      updates.putAll(configListener.configUpdated(event));
     }
-    return result;
+    return updates;
   }
 }
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index 4991715..3cb771e 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
diff --git a/java/com/google/gerrit/server/git/NotesBranchUtil.java b/java/com/google/gerrit/server/git/NotesBranchUtil.java
index 24b3727..1636b85 100644
--- a/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -16,10 +16,11 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/git/TransferConfig.java b/java/com/google/gerrit/server/git/TransferConfig.java
index 55b9448..dde524f 100644
--- a/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/java/com/google/gerrit/server/git/TransferConfig.java
@@ -29,6 +29,7 @@
   private final long maxObjectSizeLimit;
   private final String maxObjectSizeLimitFormatted;
   private final boolean inheritProjectMaxObjectSizeLimit;
+  private final boolean enableProtocolV2;
 
   @Inject
   TransferConfig(@GerritServerConfig Config cfg) {
@@ -45,6 +46,7 @@
     maxObjectSizeLimitFormatted = cfg.getString("receive", null, "maxObjectSizeLimit");
     inheritProjectMaxObjectSizeLimit =
         cfg.getBoolean("receive", "inheritProjectMaxObjectSizeLimit", false);
+    enableProtocolV2 = cfg.getBoolean("receive", "enableProtocolV2", false);
 
     packConfig = new PackConfig();
     packConfig.setDeltaCompress(false);
@@ -72,4 +74,8 @@
   public boolean inheritProjectMaxObjectSizeLimit() {
     return inheritProjectMaxObjectSizeLimit;
   }
+
+  public boolean enableProtocolV2() {
+    return enableProtocolV2;
+  }
 }
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 8b14177..196267e 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -19,8 +19,8 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.LockFailureException;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
diff --git a/java/com/google/gerrit/server/group/GroupAuditService.java b/java/com/google/gerrit/server/group/GroupAuditService.java
index c543a6e..4b851ea 100644
--- a/java/com/google/gerrit/server/group/GroupAuditService.java
+++ b/java/com/google/gerrit/server/group/GroupAuditService.java
@@ -18,9 +18,11 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.AuditEvent;
 import java.sql.Timestamp;
 
 public interface GroupAuditService {
+  void dispatch(AuditEvent action);
 
   void dispatchAddMembers(
       Account.Id actor,
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 6477f31..ecb9b2d 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -21,6 +21,8 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -34,12 +36,10 @@
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
diff --git a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
index b250a34..e122754 100644
--- a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
+++ b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.Project;
@@ -27,7 +28,6 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/notedb/IntBlob.java b/java/com/google/gerrit/server/notedb/IntBlob.java
new file mode 100644
index 0000000..9bac2a4
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CharMatcher;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.RefUpdateUtil;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@AutoValue
+public abstract class IntBlob {
+  public static Optional<IntBlob> parse(Repository repo, String refName)
+      throws IOException, OrmException {
+    try (ObjectReader or = repo.newObjectReader()) {
+      return parse(repo, refName, or);
+    }
+  }
+
+  public static Optional<IntBlob> parse(Repository repo, String refName, RevWalk rw)
+      throws IOException, OrmException {
+    return parse(repo, refName, rw.getObjectReader());
+  }
+
+  private static Optional<IntBlob> parse(Repository repo, String refName, ObjectReader or)
+      throws IOException, OrmException {
+    Ref ref = repo.exactRef(refName);
+    if (ref == null) {
+      return Optional.empty();
+    }
+    ObjectId id = ref.getObjectId();
+    ObjectLoader ol = or.open(id, OBJ_BLOB);
+    if (ol.getType() != OBJ_BLOB) {
+      // In theory this should be thrown by open but not all implementations may do it properly
+      // (certainly InMemoryRepository doesn't).
+      throw new IncorrectObjectTypeException(id, OBJ_BLOB);
+    }
+    String str = CharMatcher.whitespace().trimFrom(new String(ol.getCachedBytes(), UTF_8));
+    Integer value = Ints.tryParse(str);
+    if (value == null) {
+      throw new OrmException("invalid value in " + refName + " blob at " + id.name());
+    }
+    return Optional.of(IntBlob.create(id, value));
+  }
+
+  public static RefUpdate tryStore(
+      Repository repo,
+      RevWalk rw,
+      Project.NameKey projectName,
+      String refName,
+      @Nullable ObjectId oldId,
+      int val,
+      GitReferenceUpdated gitRefUpdated)
+      throws IOException {
+    ObjectId newId;
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
+      ins.flush();
+    }
+    RefUpdate ru = repo.updateRef(refName);
+    if (oldId != null) {
+      ru.setExpectedOldObjectId(oldId);
+    }
+    ru.disableRefLog();
+    ru.setNewObjectId(newId);
+    ru.setForceUpdate(true); // Required for non-commitish updates.
+    RefUpdate.Result result = ru.update(rw);
+    if (refUpdated(result)) {
+      gitRefUpdated.fire(projectName, ru, null);
+    }
+    return ru;
+  }
+
+  public static void store(
+      Repository repo,
+      RevWalk rw,
+      Project.NameKey projectName,
+      String refName,
+      @Nullable ObjectId oldId,
+      int val,
+      GitReferenceUpdated gitRefUpdated)
+      throws IOException {
+    RefUpdateUtil.checkResult(tryStore(repo, rw, projectName, refName, oldId, val, gitRefUpdated));
+  }
+
+  private static boolean refUpdated(RefUpdate.Result result) {
+    return result == RefUpdate.Result.NEW || result == RefUpdate.Result.FORCED;
+  }
+
+  private static IntBlob create(ObjectId id, int value) {
+    return new AutoValue_IntBlob(id, value);
+  }
+
+  public abstract ObjectId id();
+
+  public abstract int value();
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 12448fb..046757d 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -30,6 +30,7 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -42,7 +43,6 @@
 import com.google.gerrit.server.git.InsertedObject;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryingRestModifyView;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 4c497ac..56264e9 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -27,13 +27,10 @@
 import com.github.rholder.retry.StopStrategies;
 import com.github.rholder.retry.WaitStrategies;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Predicates;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
-import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.Runnables;
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -42,18 +39,15 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -75,9 +69,9 @@
   }
 
   @VisibleForTesting
-  static RetryerBuilder<RefUpdate.Result> retryerBuilder() {
-    return RetryerBuilder.<RefUpdate.Result>newBuilder()
-        .retryIfResult(Predicates.equalTo(RefUpdate.Result.LOCK_FAILURE))
+  static RetryerBuilder<RefUpdate> retryerBuilder() {
+    return RetryerBuilder.<RefUpdate>newBuilder()
+        .retryIfResult(ru -> ru != null && RefUpdate.Result.LOCK_FAILURE.equals(ru.getResult()))
         .withWaitStrategy(
             WaitStrategies.join(
                 WaitStrategies.exponentialWait(5, TimeUnit.SECONDS),
@@ -85,7 +79,7 @@
         .withStopStrategy(StopStrategies.stopAfterDelay(30, TimeUnit.SECONDS));
   }
 
-  private static final Retryer<RefUpdate.Result> RETRYER = retryerBuilder().build();
+  private static final Retryer<RefUpdate> RETRYER = retryerBuilder().build();
 
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
@@ -95,7 +89,7 @@
   private final int floor;
   private final int batchSize;
   private final Runnable afterReadRef;
-  private final Retryer<RefUpdate.Result> retryer;
+  private final Retryer<RefUpdate> retryer;
 
   // Protects all non-final fields.
   private final Lock counterLock;
@@ -153,7 +147,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate.Result> retryer) {
+      Retryer<RefUpdate> retryer) {
     this(repoManager, gitRefUpdated, projectName, name, seed, batchSize, afterReadRef, retryer, 0);
   }
 
@@ -165,7 +159,7 @@
       Seed seed,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate.Result> retryer,
+      Retryer<RefUpdate> retryer,
       int floor) {
     this.repoManager = requireNonNull(repoManager, "repoManager");
     this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated");
@@ -234,7 +228,7 @@
     try {
       try (Repository repo = repoManager.openRepository(projectName);
           RevWalk rw = new RevWalk(repo)) {
-        checkResult(store(repo, rw, null, val));
+        IntBlob.store(repo, rw, projectName, refName, null, val, gitRefUpdated);
         counter = limit;
       } catch (IOException e) {
         throw new OrmException(e);
@@ -250,7 +244,11 @@
       try (Repository repo = repoManager.openRepository(projectName);
           RevWalk rw = new RevWalk(repo)) {
         TryIncreaseTo attempt = new TryIncreaseTo(repo, rw, val);
-        checkResult(retryer.call(attempt));
+        RefUpdate ru = retryer.call(attempt);
+        // Null update is a sentinel meaning nothing to do.
+        if (ru != null) {
+          RefUpdateUtil.checkResult(ru);
+        }
         counter = limit;
       } catch (ExecutionException | RetryException e) {
         if (e.getCause() != null) {
@@ -269,7 +267,7 @@
     try (Repository repo = repoManager.openRepository(projectName);
         RevWalk rw = new RevWalk(repo)) {
       TryAcquire attempt = new TryAcquire(repo, rw, count);
-      checkResult(retryer.call(attempt));
+      RefUpdateUtil.checkResult(retryer.call(attempt));
       counter = attempt.next;
       limit = counter + count;
       acquireCount++;
@@ -283,17 +281,7 @@
     }
   }
 
-  private void checkResult(RefUpdate.Result result) throws OrmException {
-    if (!refUpdated(result) && result != Result.NO_CHANGE) {
-      throw new OrmException("failed to update " + refName + ": " + result);
-    }
-  }
-
-  private boolean refUpdated(RefUpdate.Result result) {
-    return result == RefUpdate.Result.NEW || result == RefUpdate.Result.FORCED;
-  }
-
-  private class TryAcquire implements Callable<RefUpdate.Result> {
+  private class TryAcquire implements Callable<RefUpdate> {
     private final Repository repo;
     private final RevWalk rw;
     private final int count;
@@ -307,23 +295,23 @@
     }
 
     @Override
-    public RefUpdate.Result call() throws Exception {
-      Ref ref = repo.exactRef(refName);
+    public RefUpdate call() throws Exception {
+      Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
       afterReadRef.run();
       ObjectId oldId;
-      if (ref == null) {
+      if (!blob.isPresent()) {
         oldId = ObjectId.zeroId();
         next = seed.get();
       } else {
-        oldId = ref.getObjectId();
-        next = parse(rw, oldId);
+        oldId = blob.get().id();
+        next = blob.get().value();
       }
       next = Math.max(floor, next);
-      return store(repo, rw, oldId, next + count);
+      return IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
     }
   }
 
-  private class TryIncreaseTo implements Callable<RefUpdate.Result> {
+  private class TryIncreaseTo implements Callable<RefUpdate> {
     private final Repository repo;
     private final RevWalk rw;
     private final int value;
@@ -335,60 +323,26 @@
     }
 
     @Override
-    public RefUpdate.Result call() throws Exception {
-      Ref ref = repo.exactRef(refName);
+    public RefUpdate call() throws Exception {
+      Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
       afterReadRef.run();
       ObjectId oldId;
-      if (ref == null) {
+      if (!blob.isPresent()) {
         oldId = ObjectId.zeroId();
       } else {
-        oldId = ref.getObjectId();
-        int next = parse(rw, oldId);
+        oldId = blob.get().id();
+        int next = blob.get().value();
         if (next >= value) {
-          // a concurrent write updated the ref already to this or a higher value
-          return RefUpdate.Result.NO_CHANGE;
+          // A concurrent write updated the ref already to this or a higher value; return null as a
+          // sentinel meaning nothing to do. Returning RefUpdate doesn't give us the flexibility to
+          // return any other kind of sentinel, since it's a fairly thick object.
+          return null;
         }
       }
-      return store(repo, rw, oldId, value);
+      return IntBlob.tryStore(repo, rw, projectName, refName, oldId, value, gitRefUpdated);
     }
   }
 
-  private int parse(RevWalk rw, ObjectId id) throws IOException, OrmException {
-    ObjectLoader ol = rw.getObjectReader().open(id, OBJ_BLOB);
-    if (ol.getType() != OBJ_BLOB) {
-      // In theory this should be thrown by open but not all implementations
-      // may do it properly (certainly InMemoryRepository doesn't).
-      throw new IncorrectObjectTypeException(id, OBJ_BLOB);
-    }
-    String str = CharMatcher.whitespace().trimFrom(new String(ol.getCachedBytes(), UTF_8));
-    Integer val = Ints.tryParse(str);
-    if (val == null) {
-      throw new OrmException("invalid value in " + refName + " blob at " + id.name());
-    }
-    return val;
-  }
-
-  private RefUpdate.Result store(Repository repo, RevWalk rw, @Nullable ObjectId oldId, int val)
-      throws IOException {
-    ObjectId newId;
-    try (ObjectInserter ins = repo.newObjectInserter()) {
-      newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
-      ins.flush();
-    }
-    RefUpdate ru = repo.updateRef(refName);
-    if (oldId != null) {
-      ru.setExpectedOldObjectId(oldId);
-    }
-    ru.disableRefLog();
-    ru.setNewObjectId(newId);
-    ru.setForceUpdate(true); // Required for non-commitish updates.
-    RefUpdate.Result result = ru.update(rw);
-    if (refUpdated(result)) {
-      gitRefUpdated.fire(projectName, ru, null);
-    }
-    return result;
-  }
-
   public static ReceiveCommand storeNew(ObjectInserter ins, String name, int val)
       throws IOException {
     ObjectId newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
index 225926d..01ac3e2 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -46,6 +46,8 @@
 import com.google.gerrit.common.FormatUtil;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -59,7 +61,6 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.notedb.ChangeBundleReader;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -73,7 +74,6 @@
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gwtorm.server.OrmException;
diff --git a/java/com/google/gerrit/server/project/CommentLinkProvider.java b/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 56cf51e..4987d00 100644
--- a/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -16,15 +16,17 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.gerrit.server.config.GerritConfigListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
@@ -64,11 +66,11 @@
   }
 
   @Override
-  public List<ConfigUpdatedEvent.Update> configUpdated(ConfigUpdatedEvent event) {
+  public Multimap<UpdateResult, ConfigUpdateEntry> configUpdated(ConfigUpdatedEvent event) {
     if (event.isSectionUpdated(ProjectConfig.COMMENTLINK)) {
       commentLinks = parseConfig(event.getNewConfig());
-      return Collections.singletonList(event.accept(ProjectConfig.COMMENTLINK));
+      return event.accept(ProjectConfig.COMMENTLINK);
     }
-    return Collections.emptyList();
+    return ConfigUpdatedEvent.NO_UPDATES;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
index de3c3ee..cab07e3 100644
--- a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
+++ b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
@@ -16,12 +16,12 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
+import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.api.config.ConfigUpdateEntryInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.config.ConfigUpdatedEvent;
 import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
 import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.gerrit.server.config.GerritServerConfigReloader;
@@ -29,10 +29,11 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
-import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 public class ReloadConfig implements RestModifyView<ConfigResource, Input> {
 
@@ -49,25 +50,22 @@
   public Map<String, List<ConfigUpdateEntryInfo>> apply(ConfigResource resource, Input input)
       throws RestApiException, PermissionBackendException {
     permissions.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-
-    List<ConfigUpdatedEvent.Update> updates = config.reloadConfig();
-
-    Map<String, List<ConfigUpdateEntryInfo>> reply = new HashMap<>();
-    for (UpdateResult result : UpdateResult.values()) {
-      reply.put(result.name().toLowerCase(), new ArrayList<>());
-    }
+    Multimap<UpdateResult, ConfigUpdateEntry> updates = config.reloadConfig();
     if (updates.isEmpty()) {
-      return reply;
+      return Collections.emptyMap();
     }
-    updates
+    return updates
+        .asMap()
+        .entrySet()
         .stream()
-        .forEach(u -> reply.get(u.getResult().name().toLowerCase()).addAll(toEntryInfos(u)));
-    return reply;
+        .collect(
+            Collectors.toMap(
+                e -> e.getKey().name().toLowerCase(), e -> toEntryInfos(e.getValue())));
   }
 
-  private static List<ConfigUpdateEntryInfo> toEntryInfos(ConfigUpdatedEvent.Update update) {
-    return update
-        .getConfigUpdates()
+  private static List<ConfigUpdateEntryInfo> toEntryInfos(
+      Collection<ConfigUpdateEntry> updateEntries) {
+    return updateEntries
         .stream()
         .map(ReloadConfig::toConfigUpdateEntryInfo)
         .collect(toImmutableList());
diff --git a/java/com/google/gerrit/server/restapi/project/SetParent.java b/java/com/google/gerrit/server/restapi/project/SetParent.java
index 15cb7f8..12aaf76 100644
--- a/java/com/google/gerrit/server/restapi/project/SetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/SetParent.java
@@ -19,6 +19,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.api.projects.ParentInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -32,6 +33,8 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigKey;
 import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.gerrit.server.config.GerritConfigListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -46,8 +49,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
@@ -175,18 +176,18 @@
   }
 
   @Override
-  public List<ConfigUpdatedEvent.Update> configUpdated(ConfigUpdatedEvent event) {
+  public Multimap<UpdateResult, ConfigUpdateEntry> configUpdated(ConfigUpdatedEvent event) {
     ConfigKey receiveSetParent = ConfigKey.create("receive", "allowProjectOwnersToChangeParent");
     if (!event.isValueUpdated(receiveSetParent)) {
-      return Collections.emptyList();
+      return ConfigUpdatedEvent.NO_UPDATES;
     }
     try {
       boolean enabled =
           event.getNewConfig().getBoolean("receive", "allowProjectOwnersToChangeParent", false);
       this.allowProjectOwnersToChangeParent = enabled;
-      return Collections.singletonList(event.accept(receiveSetParent));
     } catch (IllegalArgumentException iae) {
-      return Collections.singletonList(event.reject(receiveSetParent));
+      return event.reject(receiveSetParent);
     }
+    return event.accept(receiveSetParent);
   }
 }
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index a04def6..93cad19 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
diff --git a/java/com/google/gerrit/server/schema/GroupRebuilder.java b/java/com/google/gerrit/server/schema/GroupRebuilder.java
index 54cbb86..0157025a 100644
--- a/java/com/google/gerrit/server/schema/GroupRebuilder.java
+++ b/java/com/google/gerrit/server/schema/GroupRebuilder.java
@@ -275,8 +275,7 @@
    * Distinct event types.
    *
    * <p>Events at the same time by the same user are batched together by type. The types should
-   * correspond to the possible batch operations supported by {@link
-   * com.google.gerrit.server.audit.AuditService}.
+   * correspond to the possible batch operations supported by AuditService.
    */
   enum Type {
     ADD_MEMBER,
diff --git a/java/com/google/gerrit/server/schema/SchemaCreator.java b/java/com/google/gerrit/server/schema/ReviewDbSchemaCreator.java
similarity index 94%
rename from java/com/google/gerrit/server/schema/SchemaCreator.java
rename to java/com/google/gerrit/server/schema/ReviewDbSchemaCreator.java
index 743019d..3a42f07 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/java/com/google/gerrit/server/schema/ReviewDbSchemaCreator.java
@@ -17,10 +17,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.Sequences;
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -59,7 +58,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 /** Creates the current database schema and populates initial code rows. */
-public class SchemaCreator {
+public class ReviewDbSchemaCreator {
   @SitePath private final Path site_path;
 
   private final GitRepositoryManager repoManager;
@@ -77,7 +76,7 @@
   private final AllProjectsName allProjectsName;
 
   @Inject
-  public SchemaCreator(
+  public ReviewDbSchemaCreator(
       SitePaths site,
       GitRepositoryManager repoManager,
       AllProjectsCreator ap,
@@ -107,7 +106,7 @@
         apName);
   }
 
-  public SchemaCreator(
+  public ReviewDbSchemaCreator(
       @SitePath Path site,
       GitRepositoryManager repoManager,
       AllProjectsCreator ap,
@@ -144,13 +143,12 @@
     }
 
     final CurrentSchemaVersion sVer = CurrentSchemaVersion.create();
-    sVer.versionNbr = SchemaVersion.getBinaryVersion();
+    sVer.versionNbr = ReviewDbSchemaVersion.getBinaryVersion();
     db.schemaVersion().insert(Collections.singleton(sVer));
 
     GroupReference admins = createGroupReference("Administrators");
     GroupReference batchUsers = createGroupReference("Non-Interactive Users");
 
-    initSystemConfig(db);
     allProjectsCreator.setAdministrators(admins).setBatchUsers(batchUsers).create();
     // We have to create the All-Users repository before we can use it to store the groups in it.
     allUsersCreator.setAdministrators(admins).create();
@@ -274,15 +272,4 @@
         .setGroupUUID(groupReference.getUUID())
         .build();
   }
-
-  private SystemConfig initSystemConfig(ReviewDb db) throws OrmException {
-    SystemConfig s = SystemConfig.create();
-    try {
-      s.sitePath = site_path.toRealPath().normalize().toString();
-    } catch (IOException e) {
-      s.sitePath = site_path.toAbsolutePath().normalize().toString();
-    }
-    db.systemConfig().insert(Collections.singleton(s));
-    return s;
-  }
 }
diff --git a/java/com/google/gerrit/server/schema/SchemaModule.java b/java/com/google/gerrit/server/schema/ReviewDbSchemaModule.java
similarity index 97%
rename from java/com/google/gerrit/server/schema/SchemaModule.java
rename to java/com/google/gerrit/server/schema/ReviewDbSchemaModule.java
index 9ce19fe..da8898a4 100644
--- a/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/java/com/google/gerrit/server/schema/ReviewDbSchemaModule.java
@@ -30,7 +30,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 
 /** Validate the schema and connect to Git. */
-public class SchemaModule extends FactoryModule {
+public class ReviewDbSchemaModule extends FactoryModule {
   @Override
   protected void configure() {
     bind(PersonIdent.class)
diff --git a/java/com/google/gerrit/server/schema/SchemaUpdater.java b/java/com/google/gerrit/server/schema/ReviewDbSchemaUpdater.java
similarity index 79%
rename from java/com/google/gerrit/server/schema/SchemaUpdater.java
rename to java/com/google/gerrit/server/schema/ReviewDbSchemaUpdater.java
index 266fbaa..86f4560 100644
--- a/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/java/com/google/gerrit/server/schema/ReviewDbSchemaUpdater.java
@@ -16,7 +16,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -38,28 +37,24 @@
 import com.google.inject.Stage;
 import java.io.IOException;
 import java.sql.SQLException;
-import java.util.Collections;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
 /** Creates or updates the current database schema. */
-public class SchemaUpdater {
+public class ReviewDbSchemaUpdater {
   private final SchemaFactory<ReviewDb> schema;
-  private final SitePaths site;
-  private final SchemaCreator creator;
-  private final Provider<SchemaVersion> updater;
+  private final ReviewDbSchemaCreator creator;
+  private final Provider<ReviewDbSchemaVersion> updater;
 
   @Inject
-  SchemaUpdater(
+  ReviewDbSchemaUpdater(
       @ReviewDbFactory SchemaFactory<ReviewDb> schema,
-      SitePaths site,
-      SchemaCreator creator,
+      ReviewDbSchemaCreator creator,
       Injector parent) {
     this.schema = schema;
-    this.site = site;
     this.creator = creator;
-    this.updater = buildInjector(parent).getProvider(SchemaVersion.class);
+    this.updater = buildInjector(parent).getProvider(ReviewDbSchemaVersion.class);
   }
 
   private static Injector buildInjector(Injector parent) {
@@ -71,7 +66,7 @@
         new AbstractModule() {
           @Override
           protected void configure() {
-            bind(SchemaVersion.class).to(SchemaVersion.C);
+            bind(ReviewDbSchemaVersion.class).to(ReviewDbSchemaVersion.C);
 
             for (Key<?> k :
                 new Key<?>[] {
@@ -104,7 +99,7 @@
   public void update(UpdateUI ui) throws OrmException {
     try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) {
 
-      final SchemaVersion u = updater.get();
+      final ReviewDbSchemaVersion u = updater.get();
       final CurrentSchemaVersion version = getSchemaVersion(db);
       if (version == null) {
         try {
@@ -119,14 +114,12 @@
         } catch (SQLException e) {
           throw new OrmException("Cannot upgrade schema", e);
         }
-
-        updateSystemConfig(db);
       }
     }
   }
 
   @VisibleForTesting
-  public SchemaVersion getLatestSchemaVersion() {
+  public ReviewDbSchemaVersion getLatestSchemaVersion() {
     return updater.get();
   }
 
@@ -137,17 +130,4 @@
       return null;
     }
   }
-
-  private void updateSystemConfig(ReviewDb db) throws OrmException {
-    final SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
-    if (sc == null) {
-      throw new OrmException("No record in system_config table");
-    }
-    try {
-      sc.sitePath = site.site_path.toRealPath().normalize().toString();
-    } catch (IOException e) {
-      sc.sitePath = site.site_path.toAbsolutePath().normalize().toString();
-    }
-    db.systemConfig().update(Collections.singleton(sc));
-  }
 }
diff --git a/java/com/google/gerrit/server/schema/SchemaVersion.java b/java/com/google/gerrit/server/schema/ReviewDbSchemaVersion.java
similarity index 87%
rename from java/com/google/gerrit/server/schema/SchemaVersion.java
rename to java/com/google/gerrit/server/schema/ReviewDbSchemaVersion.java
index 61e9c92..10a2d39 100644
--- a/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/java/com/google/gerrit/server/schema/ReviewDbSchemaVersion.java
@@ -34,18 +34,18 @@
 import java.util.concurrent.TimeUnit;
 
 /** A version of the database schema. */
-public abstract class SchemaVersion {
+public abstract class ReviewDbSchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_169> C = Schema_169.class;
+  public static final Class<Schema_170> C = Schema_170.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
   }
 
-  private final Provider<? extends SchemaVersion> prior;
+  private final Provider<? extends ReviewDbSchemaVersion> prior;
   private final int versionNbr;
 
-  protected SchemaVersion(Provider<? extends SchemaVersion> prior) {
+  protected ReviewDbSchemaVersion(Provider<? extends ReviewDbSchemaVersion> prior) {
     this.prior = prior;
     this.versionNbr = guessVersion(getClass());
   }
@@ -66,7 +66,7 @@
   }
 
   @VisibleForTesting
-  public final SchemaVersion getPrior() {
+  public final ReviewDbSchemaVersion getPrior() {
     return prior.get();
   }
 
@@ -89,7 +89,7 @@
   /** Runs check on the prior schema version, and then upgrades. */
   private void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
       throws OrmException, SQLException {
-    List<SchemaVersion> pending = pending(curr.versionNbr);
+    List<ReviewDbSchemaVersion> pending = pending(curr.versionNbr);
     updateSchema(pending, ui, db);
     migrateData(pending, ui, curr, db);
 
@@ -115,18 +115,18 @@
     }
   }
 
-  private List<SchemaVersion> pending(int curr) {
-    List<SchemaVersion> r = Lists.newArrayListWithCapacity(versionNbr - curr);
-    for (SchemaVersion v = this; curr < v.getVersionNbr(); v = v.prior.get()) {
+  private List<ReviewDbSchemaVersion> pending(int curr) {
+    List<ReviewDbSchemaVersion> r = Lists.newArrayListWithCapacity(versionNbr - curr);
+    for (ReviewDbSchemaVersion v = this; curr < v.getVersionNbr(); v = v.prior.get()) {
       r.add(v);
     }
     Collections.reverse(r);
     return r;
   }
 
-  private void updateSchema(List<SchemaVersion> pending, UpdateUI ui, ReviewDb db)
+  private void updateSchema(List<ReviewDbSchemaVersion> pending, UpdateUI ui, ReviewDb db)
       throws OrmException, SQLException {
-    for (SchemaVersion v : pending) {
+    for (ReviewDbSchemaVersion v : pending) {
       ui.message(String.format("Upgrading schema to %d ...", v.getVersionNbr()));
       v.preUpdateSchema(db);
     }
@@ -147,9 +147,9 @@
   protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {}
 
   private void migrateData(
-      List<SchemaVersion> pending, UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
+      List<ReviewDbSchemaVersion> pending, UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
       throws OrmException, SQLException {
-    for (SchemaVersion v : pending) {
+    for (ReviewDbSchemaVersion v : pending) {
       Stopwatch sw = Stopwatch.createStarted();
       ui.message(String.format("Migrating data to schema %d ...", v.getVersionNbr()));
       v.migrateData(db, ui);
diff --git a/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/java/com/google/gerrit/server/schema/ReviewDbSchemaVersionCheck.java
similarity index 91%
rename from java/com/google/gerrit/server/schema/SchemaVersionCheck.java
rename to java/com/google/gerrit/server/schema/ReviewDbSchemaVersionCheck.java
index bdc15f4..12abc7e 100644
--- a/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/java/com/google/gerrit/server/schema/ReviewDbSchemaVersionCheck.java
@@ -26,12 +26,12 @@
 import com.google.inject.ProvisionException;
 
 /** Validates the current schema version. */
-public class SchemaVersionCheck implements LifecycleListener {
+public class ReviewDbSchemaVersionCheck implements LifecycleListener {
   public static Module module() {
     return new LifecycleModule() {
       @Override
       protected void configure() {
-        listener().to(SchemaVersionCheck.class);
+        listener().to(ReviewDbSchemaVersionCheck.class);
       }
     };
   }
@@ -40,7 +40,7 @@
   private final SitePaths site;
 
   @Inject
-  public SchemaVersionCheck(SchemaFactory<ReviewDb> schemaFactory, SitePaths site) {
+  public ReviewDbSchemaVersionCheck(SchemaFactory<ReviewDb> schemaFactory, SitePaths site) {
     this.schema = schemaFactory;
     this.site = site;
   }
@@ -49,7 +49,7 @@
   public void start() {
     try (ReviewDb db = schema.open()) {
       final CurrentSchemaVersion currentVer = getSchemaVersion(db);
-      final int expectedVer = SchemaVersion.getBinaryVersion();
+      final int expectedVer = ReviewDbSchemaVersion.getBinaryVersion();
 
       if (currentVer == null) {
         throw new ProvisionException(
diff --git a/java/com/google/gerrit/server/schema/Schema_100.java b/java/com/google/gerrit/server/schema/Schema_100.java
index 0902194..b5105a9 100644
--- a/java/com/google/gerrit/server/schema/Schema_100.java
+++ b/java/com/google/gerrit/server/schema/Schema_100.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_100 extends SchemaVersion {
+public class Schema_100 extends ReviewDbSchemaVersion {
   @Inject
   Schema_100(Provider<Schema_99> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_101.java b/java/com/google/gerrit/server/schema/Schema_101.java
index ccbb2de..753d992 100644
--- a/java/com/google/gerrit/server/schema/Schema_101.java
+++ b/java/com/google/gerrit/server/schema/Schema_101.java
@@ -38,7 +38,7 @@
 import java.util.Map;
 import java.util.TreeMap;
 
-public class Schema_101 extends SchemaVersion {
+public class Schema_101 extends ReviewDbSchemaVersion {
 
   private static class PrimaryKey {
     String oldNameInDb;
diff --git a/java/com/google/gerrit/server/schema/Schema_102.java b/java/com/google/gerrit/server/schema/Schema_102.java
index 1c1aa55..2bd52c2 100644
--- a/java/com/google/gerrit/server/schema/Schema_102.java
+++ b/java/com/google/gerrit/server/schema/Schema_102.java
@@ -26,7 +26,7 @@
 import java.util.Set;
 import java.util.regex.Pattern;
 
-public class Schema_102 extends SchemaVersion {
+public class Schema_102 extends ReviewDbSchemaVersion {
   @Inject
   Schema_102(Provider<Schema_101> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_103.java b/java/com/google/gerrit/server/schema/Schema_103.java
index 60a5213..1906b66 100644
--- a/java/com/google/gerrit/server/schema/Schema_103.java
+++ b/java/com/google/gerrit/server/schema/Schema_103.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_103 extends SchemaVersion {
+public class Schema_103 extends ReviewDbSchemaVersion {
   @Inject
   Schema_103(Provider<Schema_102> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_104.java b/java/com/google/gerrit/server/schema/Schema_104.java
index bebdaca..9b4d67c 100644
--- a/java/com/google/gerrit/server/schema/Schema_104.java
+++ b/java/com/google/gerrit/server/schema/Schema_104.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_104 extends SchemaVersion {
+public class Schema_104 extends ReviewDbSchemaVersion {
   @Inject
   Schema_104(Provider<Schema_103> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_105.java b/java/com/google/gerrit/server/schema/Schema_105.java
index dd5e71a7..56e19ef 100644
--- a/java/com/google/gerrit/server/schema/Schema_105.java
+++ b/java/com/google/gerrit/server/schema/Schema_105.java
@@ -28,7 +28,7 @@
 import java.util.Map;
 import java.util.Set;
 
-public class Schema_105 extends SchemaVersion {
+public class Schema_105 extends ReviewDbSchemaVersion {
   private static final String TABLE = "changes";
 
   @Inject
diff --git a/java/com/google/gerrit/server/schema/Schema_106.java b/java/com/google/gerrit/server/schema/Schema_106.java
index 5bb3669..a3f99fc 100644
--- a/java/com/google/gerrit/server/schema/Schema_106.java
+++ b/java/com/google/gerrit/server/schema/Schema_106.java
@@ -40,7 +40,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_106 extends SchemaVersion {
+public class Schema_106 extends ReviewDbSchemaVersion {
   // we can use multiple threads per CPU as we can expect that threads will be
   // waiting for IO
   private static final int THREADS_PER_CPU = 4;
diff --git a/java/com/google/gerrit/server/schema/Schema_107.java b/java/com/google/gerrit/server/schema/Schema_107.java
index dd8868f..bef8c65 100644
--- a/java/com/google/gerrit/server/schema/Schema_107.java
+++ b/java/com/google/gerrit/server/schema/Schema_107.java
@@ -21,7 +21,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_107 extends SchemaVersion {
+public class Schema_107 extends ReviewDbSchemaVersion {
 
   @Inject
   Schema_107(Provider<Schema_106> prior) {
diff --git a/java/com/google/gerrit/server/schema/Schema_108.java b/java/com/google/gerrit/server/schema/Schema_108.java
index b37fab3..b2ab042 100644
--- a/java/com/google/gerrit/server/schema/Schema_108.java
+++ b/java/com/google/gerrit/server/schema/Schema_108.java
@@ -48,7 +48,7 @@
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-public class Schema_108 extends SchemaVersion {
+public class Schema_108 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
 
   @Inject
diff --git a/java/com/google/gerrit/server/schema/Schema_109.java b/java/com/google/gerrit/server/schema/Schema_109.java
index c5a6015..f582741 100644
--- a/java/com/google/gerrit/server/schema/Schema_109.java
+++ b/java/com/google/gerrit/server/schema/Schema_109.java
@@ -20,7 +20,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_109 extends SchemaVersion {
+public class Schema_109 extends ReviewDbSchemaVersion {
   @Inject
   Schema_109(Provider<Schema_108> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_110.java b/java/com/google/gerrit/server/schema/Schema_110.java
index 9e0f112..6da10f6 100644
--- a/java/com/google/gerrit/server/schema/Schema_110.java
+++ b/java/com/google/gerrit/server/schema/Schema_110.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_110 extends SchemaVersion {
+public class Schema_110 extends ReviewDbSchemaVersion {
   @Inject
   Schema_110(Provider<Schema_109> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_111.java b/java/com/google/gerrit/server/schema/Schema_111.java
index 223fdb6..7ba48c2 100644
--- a/java/com/google/gerrit/server/schema/Schema_111.java
+++ b/java/com/google/gerrit/server/schema/Schema_111.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_111 extends SchemaVersion {
+public class Schema_111 extends ReviewDbSchemaVersion {
   @Inject
   Schema_111(Provider<Schema_110> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_112.java b/java/com/google/gerrit/server/schema/Schema_112.java
index 3e879bd..5a764d0 100644
--- a/java/com/google/gerrit/server/schema/Schema_112.java
+++ b/java/com/google/gerrit/server/schema/Schema_112.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_112 extends SchemaVersion {
+public class Schema_112 extends ReviewDbSchemaVersion {
   @Inject
   Schema_112(Provider<Schema_111> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_113.java b/java/com/google/gerrit/server/schema/Schema_113.java
index 32d655e..2876636 100644
--- a/java/com/google/gerrit/server/schema/Schema_113.java
+++ b/java/com/google/gerrit/server/schema/Schema_113.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_113 extends SchemaVersion {
+public class Schema_113 extends ReviewDbSchemaVersion {
   @Inject
   Schema_113(Provider<Schema_112> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_114.java b/java/com/google/gerrit/server/schema/Schema_114.java
index 85c93d2..7bee00c 100644
--- a/java/com/google/gerrit/server/schema/Schema_114.java
+++ b/java/com/google/gerrit/server/schema/Schema_114.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_114 extends SchemaVersion {
+public class Schema_114 extends ReviewDbSchemaVersion {
   @Inject
   Schema_114(Provider<Schema_113> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_115.java b/java/com/google/gerrit/server/schema/Schema_115.java
index 70bc921..28cfe22 100644
--- a/java/com/google/gerrit/server/schema/Schema_115.java
+++ b/java/com/google/gerrit/server/schema/Schema_115.java
@@ -51,7 +51,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-public class Schema_115 extends SchemaVersion {
+public class Schema_115 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager mgr;
   private final AllUsersName allUsersName;
   private final PersonIdent serverUser;
diff --git a/java/com/google/gerrit/server/schema/Schema_116.java b/java/com/google/gerrit/server/schema/Schema_116.java
index 5b018a2..5174d92 100644
--- a/java/com/google/gerrit/server/schema/Schema_116.java
+++ b/java/com/google/gerrit/server/schema/Schema_116.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_116 extends SchemaVersion {
+public class Schema_116 extends ReviewDbSchemaVersion {
   @Inject
   Schema_116(Provider<Schema_115> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_117.java b/java/com/google/gerrit/server/schema/Schema_117.java
index 35e6c8a..d848bec 100644
--- a/java/com/google/gerrit/server/schema/Schema_117.java
+++ b/java/com/google/gerrit/server/schema/Schema_117.java
@@ -24,7 +24,7 @@
 import java.sql.Statement;
 import java.util.Set;
 
-public class Schema_117 extends SchemaVersion {
+public class Schema_117 extends ReviewDbSchemaVersion {
   @Inject
   Schema_117(Provider<Schema_116> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_118.java b/java/com/google/gerrit/server/schema/Schema_118.java
index 8c2c740..e55afda 100644
--- a/java/com/google/gerrit/server/schema/Schema_118.java
+++ b/java/com/google/gerrit/server/schema/Schema_118.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_118 extends SchemaVersion {
+public class Schema_118 extends ReviewDbSchemaVersion {
   @Inject
   Schema_118(Provider<Schema_117> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_119.java b/java/com/google/gerrit/server/schema/Schema_119.java
index e5a6405..9423d81 100644
--- a/java/com/google/gerrit/server/schema/Schema_119.java
+++ b/java/com/google/gerrit/server/schema/Schema_119.java
@@ -58,7 +58,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-public class Schema_119 extends SchemaVersion {
+public class Schema_119 extends ReviewDbSchemaVersion {
   private static final ImmutableMap<String, String> LEGACY_DISPLAYNAME_MAP =
       ImmutableMap.<String, String>of(
           "ANON_GIT", ANON_GIT,
diff --git a/java/com/google/gerrit/server/schema/Schema_120.java b/java/com/google/gerrit/server/schema/Schema_120.java
index 15e34ab..9a1c15b 100644
--- a/java/com/google/gerrit/server/schema/Schema_120.java
+++ b/java/com/google/gerrit/server/schema/Schema_120.java
@@ -39,7 +39,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
 
-public class Schema_120 extends SchemaVersion {
+public class Schema_120 extends ReviewDbSchemaVersion {
 
   private final GitRepositoryManager mgr;
   private final ProjectConfig.Factory projectConfigFactory;
diff --git a/java/com/google/gerrit/server/schema/Schema_121.java b/java/com/google/gerrit/server/schema/Schema_121.java
index 31b42fb..bb994ef 100644
--- a/java/com/google/gerrit/server/schema/Schema_121.java
+++ b/java/com/google/gerrit/server/schema/Schema_121.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_121 extends SchemaVersion {
+public class Schema_121 extends ReviewDbSchemaVersion {
   @Inject
   Schema_121(Provider<Schema_120> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_122.java b/java/com/google/gerrit/server/schema/Schema_122.java
index b5b799d..cce1419 100644
--- a/java/com/google/gerrit/server/schema/Schema_122.java
+++ b/java/com/google/gerrit/server/schema/Schema_122.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_122 extends SchemaVersion {
+public class Schema_122 extends ReviewDbSchemaVersion {
   @Inject
   Schema_122(Provider<Schema_121> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_123.java b/java/com/google/gerrit/server/schema/Schema_123.java
index 31cfd5d..cd84188 100644
--- a/java/com/google/gerrit/server/schema/Schema_123.java
+++ b/java/com/google/gerrit/server/schema/Schema_123.java
@@ -40,7 +40,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
-public class Schema_123 extends SchemaVersion {
+public class Schema_123 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
 
diff --git a/java/com/google/gerrit/server/schema/Schema_124.java b/java/com/google/gerrit/server/schema/Schema_124.java
index 6164fd1..7e2fa42 100644
--- a/java/com/google/gerrit/server/schema/Schema_124.java
+++ b/java/com/google/gerrit/server/schema/Schema_124.java
@@ -50,7 +50,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-public class Schema_124 extends SchemaVersion {
+public class Schema_124 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
   private final PersonIdent serverUser;
diff --git a/java/com/google/gerrit/server/schema/Schema_125.java b/java/com/google/gerrit/server/schema/Schema_125.java
index 474d7ac..cc0d9e7 100644
--- a/java/com/google/gerrit/server/schema/Schema_125.java
+++ b/java/com/google/gerrit/server/schema/Schema_125.java
@@ -44,7 +44,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_125 extends SchemaVersion {
+public class Schema_125 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG =
       "Assign default permissions on user branches\n"
           + "\n"
diff --git a/java/com/google/gerrit/server/schema/Schema_126.java b/java/com/google/gerrit/server/schema/Schema_126.java
index 23de169..f3004ba 100644
--- a/java/com/google/gerrit/server/schema/Schema_126.java
+++ b/java/com/google/gerrit/server/schema/Schema_126.java
@@ -38,7 +38,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_126 extends SchemaVersion {
+public class Schema_126 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG = "Fix default permissions on user branches";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_127.java b/java/com/google/gerrit/server/schema/Schema_127.java
index d246b75..8e4175c 100644
--- a/java/com/google/gerrit/server/schema/Schema_127.java
+++ b/java/com/google/gerrit/server/schema/Schema_127.java
@@ -28,7 +28,7 @@
 import java.sql.Statement;
 import org.eclipse.jgit.lib.Config;
 
-public class Schema_127 extends SchemaVersion {
+public class Schema_127 extends ReviewDbSchemaVersion {
   private static final int MAX_BATCH_SIZE = 1000;
 
   private final SitePaths sitePaths;
diff --git a/java/com/google/gerrit/server/schema/Schema_128.java b/java/com/google/gerrit/server/schema/Schema_128.java
index 4f5f962..cd2718b 100644
--- a/java/com/google/gerrit/server/schema/Schema_128.java
+++ b/java/com/google/gerrit/server/schema/Schema_128.java
@@ -36,7 +36,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_128 extends SchemaVersion {
+public class Schema_128 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG = "Add addPatchSet permission to all projects";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_129.java b/java/com/google/gerrit/server/schema/Schema_129.java
index 73ce3c3..f7cd8c3 100644
--- a/java/com/google/gerrit/server/schema/Schema_129.java
+++ b/java/com/google/gerrit/server/schema/Schema_129.java
@@ -22,7 +22,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_129 extends SchemaVersion {
+public class Schema_129 extends ReviewDbSchemaVersion {
 
   @Inject
   Schema_129(Provider<Schema_128> prior) {
diff --git a/java/com/google/gerrit/server/schema/Schema_130.java b/java/com/google/gerrit/server/schema/Schema_130.java
index 66f2177..0c9d79d 100644
--- a/java/com/google/gerrit/server/schema/Schema_130.java
+++ b/java/com/google/gerrit/server/schema/Schema_130.java
@@ -32,7 +32,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_130 extends SchemaVersion {
+public class Schema_130 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG =
       "Remove force option from 'Push Annotated Tag' permission\n"
           + "\n"
diff --git a/java/com/google/gerrit/server/schema/Schema_131.java b/java/com/google/gerrit/server/schema/Schema_131.java
index e496096..b37ae4b 100644
--- a/java/com/google/gerrit/server/schema/Schema_131.java
+++ b/java/com/google/gerrit/server/schema/Schema_131.java
@@ -33,7 +33,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_131 extends SchemaVersion {
+public class Schema_131 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG =
       "Rename 'Push Annotated/Signed Tag' permission to 'Create Annotated/Signed Tag'";
 
diff --git a/java/com/google/gerrit/server/schema/Schema_132.java b/java/com/google/gerrit/server/schema/Schema_132.java
index 7c1cde8..72ec590 100644
--- a/java/com/google/gerrit/server/schema/Schema_132.java
+++ b/java/com/google/gerrit/server/schema/Schema_132.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_132 extends SchemaVersion {
+public class Schema_132 extends ReviewDbSchemaVersion {
   @Inject
   Schema_132(Provider<Schema_131> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_133.java b/java/com/google/gerrit/server/schema/Schema_133.java
index 31d330b..29cd6e9 100644
--- a/java/com/google/gerrit/server/schema/Schema_133.java
+++ b/java/com/google/gerrit/server/schema/Schema_133.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_133 extends SchemaVersion {
+public class Schema_133 extends ReviewDbSchemaVersion {
   @Inject
   Schema_133(Provider<Schema_132> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_134.java b/java/com/google/gerrit/server/schema/Schema_134.java
index fa01ff3..298ac03 100644
--- a/java/com/google/gerrit/server/schema/Schema_134.java
+++ b/java/com/google/gerrit/server/schema/Schema_134.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_134 extends SchemaVersion {
+public class Schema_134 extends ReviewDbSchemaVersion {
   @Inject
   Schema_134(Provider<Schema_133> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_135.java b/java/com/google/gerrit/server/schema/Schema_135.java
index 6281251..03ef84a 100644
--- a/java/com/google/gerrit/server/schema/Schema_135.java
+++ b/java/com/google/gerrit/server/schema/Schema_135.java
@@ -41,7 +41,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_135 extends SchemaVersion {
+public class Schema_135 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG =
       "Allow admins and project owners to create refs/meta/config";
 
diff --git a/java/com/google/gerrit/server/schema/Schema_136.java b/java/com/google/gerrit/server/schema/Schema_136.java
index a4b1c82..11eda7b 100644
--- a/java/com/google/gerrit/server/schema/Schema_136.java
+++ b/java/com/google/gerrit/server/schema/Schema_136.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_136 extends SchemaVersion {
+public class Schema_136 extends ReviewDbSchemaVersion {
   @Inject
   Schema_136(Provider<Schema_135> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_137.java b/java/com/google/gerrit/server/schema/Schema_137.java
index 1b4102f..a6ea83b 100644
--- a/java/com/google/gerrit/server/schema/Schema_137.java
+++ b/java/com/google/gerrit/server/schema/Schema_137.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /* change the type of SystemConfig#sitePath to CLOB */
-public class Schema_137 extends SchemaVersion {
+public class Schema_137 extends ReviewDbSchemaVersion {
   @Inject
   Schema_137(Provider<Schema_136> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_138.java b/java/com/google/gerrit/server/schema/Schema_138.java
index f824ee1..61d55ec 100644
--- a/java/com/google/gerrit/server/schema/Schema_138.java
+++ b/java/com/google/gerrit/server/schema/Schema_138.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /* Add resolved field to PatchLineComment */
-public class Schema_138 extends SchemaVersion {
+public class Schema_138 extends ReviewDbSchemaVersion {
   @Inject
   Schema_138(Provider<Schema_137> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_139.java b/java/com/google/gerrit/server/schema/Schema_139.java
index cdde7e4..a234be5 100644
--- a/java/com/google/gerrit/server/schema/Schema_139.java
+++ b/java/com/google/gerrit/server/schema/Schema_139.java
@@ -54,7 +54,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 
-public class Schema_139 extends SchemaVersion {
+public class Schema_139 extends ReviewDbSchemaVersion {
   private static final String MSG = "Migrate project watches to git";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_140.java b/java/com/google/gerrit/server/schema/Schema_140.java
index bdc5f55..f59368c 100644
--- a/java/com/google/gerrit/server/schema/Schema_140.java
+++ b/java/com/google/gerrit/server/schema/Schema_140.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Remove ChangeMessage sequence. */
-public class Schema_140 extends SchemaVersion {
+public class Schema_140 extends ReviewDbSchemaVersion {
   @Inject
   Schema_140(Provider<Schema_139> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_141.java b/java/com/google/gerrit/server/schema/Schema_141.java
index c081ea9..ce1c910 100644
--- a/java/com/google/gerrit/server/schema/Schema_141.java
+++ b/java/com/google/gerrit/server/schema/Schema_141.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Add status field to account. */
-public class Schema_141 extends SchemaVersion {
+public class Schema_141 extends ReviewDbSchemaVersion {
   @Inject
   Schema_141(Provider<Schema_140> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_142.java b/java/com/google/gerrit/server/schema/Schema_142.java
index e67ae2f..9e06c52 100644
--- a/java/com/google/gerrit/server/schema/Schema_142.java
+++ b/java/com/google/gerrit/server/schema/Schema_142.java
@@ -29,7 +29,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_142 extends SchemaVersion {
+public class Schema_142 extends ReviewDbSchemaVersion {
   private static final int MAX_BATCH_SIZE = 1000;
 
   @Inject
diff --git a/java/com/google/gerrit/server/schema/Schema_143.java b/java/com/google/gerrit/server/schema/Schema_143.java
index b190b29..10d108f 100644
--- a/java/com/google/gerrit/server/schema/Schema_143.java
+++ b/java/com/google/gerrit/server/schema/Schema_143.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Add isPrivate field to change. */
-public class Schema_143 extends SchemaVersion {
+public class Schema_143 extends ReviewDbSchemaVersion {
   @Inject
   Schema_143(Provider<Schema_142> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_144.java b/java/com/google/gerrit/server/schema/Schema_144.java
index bb0cbca..0aad504 100644
--- a/java/com/google/gerrit/server/schema/Schema_144.java
+++ b/java/com/google/gerrit/server/schema/Schema_144.java
@@ -37,7 +37,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_144 extends SchemaVersion {
+public class Schema_144 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG = "Import external IDs from ReviewDb";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_145.java b/java/com/google/gerrit/server/schema/Schema_145.java
index 6ccb5d8..1207632 100644
--- a/java/com/google/gerrit/server/schema/Schema_145.java
+++ b/java/com/google/gerrit/server/schema/Schema_145.java
@@ -24,7 +24,7 @@
 import java.sql.SQLException;
 
 /** Create account_external_ids_byEmail index. */
-public class Schema_145 extends SchemaVersion {
+public class Schema_145 extends ReviewDbSchemaVersion {
 
   @Inject
   Schema_145(Provider<Schema_144> prior) {
diff --git a/java/com/google/gerrit/server/schema/Schema_146.java b/java/com/google/gerrit/server/schema/Schema_146.java
index 503ed7b..2930408 100644
--- a/java/com/google/gerrit/server/schema/Schema_146.java
+++ b/java/com/google/gerrit/server/schema/Schema_146.java
@@ -54,7 +54,7 @@
  * commit with the registration date as commit time is inserted (if such a commit doesn't exist
  * yet).
  */
-public class Schema_146 extends SchemaVersion {
+public class Schema_146 extends ReviewDbSchemaVersion {
   private static final String CREATE_ACCOUNT_MSG = "Create Account";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_147.java b/java/com/google/gerrit/server/schema/Schema_147.java
index c507fa6..f317e9a 100644
--- a/java/com/google/gerrit/server/schema/Schema_147.java
+++ b/java/com/google/gerrit/server/schema/Schema_147.java
@@ -38,7 +38,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 /** Delete user branches for which no account exists. */
-public class Schema_147 extends SchemaVersion {
+public class Schema_147 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
 
diff --git a/java/com/google/gerrit/server/schema/Schema_148.java b/java/com/google/gerrit/server/schema/Schema_148.java
index 9433da8..949dd5a 100644
--- a/java/com/google/gerrit/server/schema/Schema_148.java
+++ b/java/com/google/gerrit/server/schema/Schema_148.java
@@ -33,7 +33,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_148 extends SchemaVersion {
+public class Schema_148 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG = "Make account IDs of external IDs human-readable";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_149.java b/java/com/google/gerrit/server/schema/Schema_149.java
index f1ccaa6..156091b 100644
--- a/java/com/google/gerrit/server/schema/Schema_149.java
+++ b/java/com/google/gerrit/server/schema/Schema_149.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Add workInProgress field to change. */
-public class Schema_149 extends SchemaVersion {
+public class Schema_149 extends ReviewDbSchemaVersion {
   @Inject
   Schema_149(Provider<Schema_148> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_150.java b/java/com/google/gerrit/server/schema/Schema_150.java
index 456a01a..71736f3 100644
--- a/java/com/google/gerrit/server/schema/Schema_150.java
+++ b/java/com/google/gerrit/server/schema/Schema_150.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Drop ACCOUNT_EXTERNAL_IDS table. */
-public class Schema_150 extends SchemaVersion {
+public class Schema_150 extends ReviewDbSchemaVersion {
   @Inject
   Schema_150(Provider<Schema_149> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_151.java b/java/com/google/gerrit/server/schema/Schema_151.java
index 7d12e58..0e8700f 100644
--- a/java/com/google/gerrit/server/schema/Schema_151.java
+++ b/java/com/google/gerrit/server/schema/Schema_151.java
@@ -29,7 +29,7 @@
 import java.util.Optional;
 
 /** A schema which adds the 'created on' field to groups. */
-public class Schema_151 extends SchemaVersion {
+public class Schema_151 extends ReviewDbSchemaVersion {
   @Inject
   protected Schema_151(Provider<Schema_150> prior) {
     super(prior);
@@ -60,7 +60,7 @@
       PreparedStatement addedOnRetrieval, AccountGroup.Id groupId) throws SQLException {
     addedOnRetrieval.setInt(1, groupId.get());
     try (ResultSet resultSet = addedOnRetrieval.executeQuery()) {
-      if (resultSet.first()) {
+      if (resultSet.next()) {
         return Optional.of(resultSet.getTimestamp(1));
       }
     }
diff --git a/java/com/google/gerrit/server/schema/Schema_152.java b/java/com/google/gerrit/server/schema/Schema_152.java
index c5150a0..b605e90 100644
--- a/java/com/google/gerrit/server/schema/Schema_152.java
+++ b/java/com/google/gerrit/server/schema/Schema_152.java
@@ -24,7 +24,7 @@
 import java.sql.SQLException;
 
 /** Drop unused indexes from accounts table. */
-public class Schema_152 extends SchemaVersion {
+public class Schema_152 extends ReviewDbSchemaVersion {
   @Inject
   Schema_152(Provider<Schema_151> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_153.java b/java/com/google/gerrit/server/schema/Schema_153.java
index 28aeb17..7d1f63e 100644
--- a/java/com/google/gerrit/server/schema/Schema_153.java
+++ b/java/com/google/gerrit/server/schema/Schema_153.java
@@ -21,7 +21,7 @@
 import com.google.inject.Provider;
 
 /** Add reviewStarted field to change. */
-public class Schema_153 extends SchemaVersion {
+public class Schema_153 extends ReviewDbSchemaVersion {
   @Inject
   Schema_153(Provider<Schema_152> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_154.java b/java/com/google/gerrit/server/schema/Schema_154.java
index fab1693..8c97010 100644
--- a/java/com/google/gerrit/server/schema/Schema_154.java
+++ b/java/com/google/gerrit/server/schema/Schema_154.java
@@ -47,7 +47,7 @@
 import org.eclipse.jgit.lib.TextProgressMonitor;
 
 /** Migrate accounts to NoteDb. */
-public class Schema_154 extends SchemaVersion {
+public class Schema_154 extends ReviewDbSchemaVersion {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String TABLE = "accounts";
diff --git a/java/com/google/gerrit/server/schema/Schema_155.java b/java/com/google/gerrit/server/schema/Schema_155.java
index ec16e06..812d7a6 100644
--- a/java/com/google/gerrit/server/schema/Schema_155.java
+++ b/java/com/google/gerrit/server/schema/Schema_155.java
@@ -26,7 +26,7 @@
 import java.sql.SQLException;
 
 /** Create account sequence in NoteDb */
-public class Schema_155 extends SchemaVersion {
+public class Schema_155 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
 
diff --git a/java/com/google/gerrit/server/schema/Schema_156.java b/java/com/google/gerrit/server/schema/Schema_156.java
index fd8fc00..237161c 100644
--- a/java/com/google/gerrit/server/schema/Schema_156.java
+++ b/java/com/google/gerrit/server/schema/Schema_156.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Add revertOf field to change. */
-public class Schema_156 extends SchemaVersion {
+public class Schema_156 extends ReviewDbSchemaVersion {
   @Inject
   Schema_156(Provider<Schema_155> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_157.java b/java/com/google/gerrit/server/schema/Schema_157.java
index f5c5b59..20a8c4b 100644
--- a/java/com/google/gerrit/server/schema/Schema_157.java
+++ b/java/com/google/gerrit/server/schema/Schema_157.java
@@ -24,7 +24,7 @@
 import java.sql.SQLException;
 
 /** Drop unused indexes from accounts table. */
-public class Schema_157 extends SchemaVersion {
+public class Schema_157 extends ReviewDbSchemaVersion {
   @Inject
   Schema_157(Provider<Schema_156> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_158.java b/java/com/google/gerrit/server/schema/Schema_158.java
index ea85444..3b7d9e8 100644
--- a/java/com/google/gerrit/server/schema/Schema_158.java
+++ b/java/com/google/gerrit/server/schema/Schema_158.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Drop ACCOUNTS table. */
-public class Schema_158 extends SchemaVersion {
+public class Schema_158 extends ReviewDbSchemaVersion {
   @Inject
   Schema_158(Provider<Schema_157> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_159.java b/java/com/google/gerrit/server/schema/Schema_159.java
index d6e37d7..ddb5765 100644
--- a/java/com/google/gerrit/server/schema/Schema_159.java
+++ b/java/com/google/gerrit/server/schema/Schema_159.java
@@ -21,7 +21,7 @@
 import com.google.inject.Provider;
 
 /** Migrate draft changes to private or wip changes. */
-public class Schema_159 extends SchemaVersion {
+public class Schema_159 extends ReviewDbSchemaVersion {
 
   private enum DraftWorkflowMigrationStrategy {
     PRIVATE,
diff --git a/java/com/google/gerrit/server/schema/Schema_160.java b/java/com/google/gerrit/server/schema/Schema_160.java
index eb8b70f..10f5c9d 100644
--- a/java/com/google/gerrit/server/schema/Schema_160.java
+++ b/java/com/google/gerrit/server/schema/Schema_160.java
@@ -60,7 +60,7 @@
  * <p>Other menus containing {@code is:draft} in other positions are not affected; this is still a
  * valid predicate that matches no changes.
  */
-public class Schema_160 extends SchemaVersion {
+public class Schema_160 extends ReviewDbSchemaVersion {
   @VisibleForTesting static final ImmutableList<String> DEFAULT_DRAFT_ITEMS;
 
   static {
diff --git a/java/com/google/gerrit/server/schema/Schema_161.java b/java/com/google/gerrit/server/schema/Schema_161.java
index 3077720..4627abe 100644
--- a/java/com/google/gerrit/server/schema/Schema_161.java
+++ b/java/com/google/gerrit/server/schema/Schema_161.java
@@ -39,7 +39,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
-public class Schema_161 extends SchemaVersion {
+public class Schema_161 extends ReviewDbSchemaVersion {
   private static final String MUTE_LABEL = "mute";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_162.java b/java/com/google/gerrit/server/schema/Schema_162.java
index 85220c7..3d3a192 100644
--- a/java/com/google/gerrit/server/schema/Schema_162.java
+++ b/java/com/google/gerrit/server/schema/Schema_162.java
@@ -30,7 +30,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
-public class Schema_162 extends SchemaVersion {
+public class Schema_162 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllProjectsName allProjectsName;
   private final AllUsersName allUsersName;
diff --git a/java/com/google/gerrit/server/schema/Schema_163.java b/java/com/google/gerrit/server/schema/Schema_163.java
index ae05774..9eb5d5e 100644
--- a/java/com/google/gerrit/server/schema/Schema_163.java
+++ b/java/com/google/gerrit/server/schema/Schema_163.java
@@ -26,7 +26,7 @@
 import java.sql.SQLException;
 
 /** Create group sequence in NoteDb */
-public class Schema_163 extends SchemaVersion {
+public class Schema_163 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
 
diff --git a/java/com/google/gerrit/server/schema/Schema_164.java b/java/com/google/gerrit/server/schema/Schema_164.java
index 818f84c..b6c3118 100644
--- a/java/com/google/gerrit/server/schema/Schema_164.java
+++ b/java/com/google/gerrit/server/schema/Schema_164.java
@@ -38,7 +38,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 /** Grant read on group branches */
-public class Schema_164 extends SchemaVersion {
+public class Schema_164 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG = "Grant read permissions on group branches";
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_165.java b/java/com/google/gerrit/server/schema/Schema_165.java
index b4f9523..d80c770 100644
--- a/java/com/google/gerrit/server/schema/Schema_165.java
+++ b/java/com/google/gerrit/server/schema/Schema_165.java
@@ -41,7 +41,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 /** Make default Label-Code-Review permission on user branches exclusive. */
-public class Schema_165 extends SchemaVersion {
+public class Schema_165 extends ReviewDbSchemaVersion {
   private static final String COMMIT_MSG =
       "Make default Label-Code-Review permission on user branches exclusive";
 
diff --git a/java/com/google/gerrit/server/schema/Schema_166.java b/java/com/google/gerrit/server/schema/Schema_166.java
index aa6f4e6..901df56 100644
--- a/java/com/google/gerrit/server/schema/Schema_166.java
+++ b/java/com/google/gerrit/server/schema/Schema_166.java
@@ -28,7 +28,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 /** Set HEAD for All-Users to refs/meta/config. */
-public class Schema_166 extends SchemaVersion {
+public class Schema_166 extends ReviewDbSchemaVersion {
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
 
diff --git a/java/com/google/gerrit/server/schema/Schema_167.java b/java/com/google/gerrit/server/schema/Schema_167.java
index a5066cc..44d89f9 100644
--- a/java/com/google/gerrit/server/schema/Schema_167.java
+++ b/java/com/google/gerrit/server/schema/Schema_167.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -39,7 +40,6 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.group.db.AuditLogFormatter;
 import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -60,7 +60,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 /** Migrate groups from ReviewDb to NoteDb. */
-public class Schema_167 extends SchemaVersion {
+public class Schema_167 extends ReviewDbSchemaVersion {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_168.java b/java/com/google/gerrit/server/schema/Schema_168.java
index 3ea8468..fff4049 100644
--- a/java/com/google/gerrit/server/schema/Schema_168.java
+++ b/java/com/google/gerrit/server/schema/Schema_168.java
@@ -18,7 +18,7 @@
 import com.google.inject.Provider;
 
 /** Drop group tables. */
-public class Schema_168 extends SchemaVersion {
+public class Schema_168 extends ReviewDbSchemaVersion {
   @Inject
   Schema_168(Provider<Schema_167> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_169.java b/java/com/google/gerrit/server/schema/Schema_169.java
index 2779d47..11601e4 100644
--- a/java/com/google/gerrit/server/schema/Schema_169.java
+++ b/java/com/google/gerrit/server/schema/Schema_169.java
@@ -35,7 +35,7 @@
 import org.eclipse.jgit.lib.TextProgressMonitor;
 
 /** Migrate NoteDb inline comments to JSON format. */
-public class Schema_169 extends SchemaVersion {
+public class Schema_169 extends ReviewDbSchemaVersion {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private final CommentJsonMigrator migrator;
   private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/schema/Schema_170.java b/java/com/google/gerrit/server/schema/Schema_170.java
new file mode 100644
index 0000000..6a86494
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_170.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_170 extends ReviewDbSchemaVersion {
+  @Inject
+  Schema_170(Provider<Schema_169> prior) {
+    super(prior);
+  }
+}
diff --git a/java/com/google/gerrit/server/schema/Schema_83.java b/java/com/google/gerrit/server/schema/Schema_83.java
index decbfb1..95b7e6f 100644
--- a/java/com/google/gerrit/server/schema/Schema_83.java
+++ b/java/com/google/gerrit/server/schema/Schema_83.java
@@ -18,14 +18,14 @@
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
-public class Schema_83 extends SchemaVersion {
+public class Schema_83 extends ReviewDbSchemaVersion {
 
   @Inject
   Schema_83() {
     super(
-        new Provider<SchemaVersion>() {
+        new Provider<ReviewDbSchemaVersion>() {
           @Override
-          public SchemaVersion get() {
+          public ReviewDbSchemaVersion get() {
             throw new ProvisionException("Upgrade first to 2.8 or 2.9");
           }
         });
diff --git a/java/com/google/gerrit/server/schema/Schema_84.java b/java/com/google/gerrit/server/schema/Schema_84.java
index c96f650..415b2e3 100644
--- a/java/com/google/gerrit/server/schema/Schema_84.java
+++ b/java/com/google/gerrit/server/schema/Schema_84.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_84 extends SchemaVersion {
+public class Schema_84 extends ReviewDbSchemaVersion {
 
   @Inject
   Schema_84(Provider<Schema_83> prior) {
diff --git a/java/com/google/gerrit/server/schema/Schema_85.java b/java/com/google/gerrit/server/schema/Schema_85.java
index e24e67c..ee8fbdb 100644
--- a/java/com/google/gerrit/server/schema/Schema_85.java
+++ b/java/com/google/gerrit/server/schema/Schema_85.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_85 extends SchemaVersion {
+public class Schema_85 extends ReviewDbSchemaVersion {
   @Inject
   Schema_85(Provider<Schema_84> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_86.java b/java/com/google/gerrit/server/schema/Schema_86.java
index d758189..e468345 100644
--- a/java/com/google/gerrit/server/schema/Schema_86.java
+++ b/java/com/google/gerrit/server/schema/Schema_86.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_86 extends SchemaVersion {
+public class Schema_86 extends ReviewDbSchemaVersion {
   @Inject
   Schema_86(Provider<Schema_85> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_87.java b/java/com/google/gerrit/server/schema/Schema_87.java
index 8a3ea08..79884ba 100644
--- a/java/com/google/gerrit/server/schema/Schema_87.java
+++ b/java/com/google/gerrit/server/schema/Schema_87.java
@@ -28,7 +28,7 @@
 import java.util.Optional;
 import java.util.Set;
 
-public class Schema_87 extends SchemaVersion {
+public class Schema_87 extends ReviewDbSchemaVersion {
   @Inject
   Schema_87(Provider<Schema_86> prior) {
     super(prior);
@@ -59,7 +59,7 @@
       PreparedStatement uuidRetrieval, AccountGroup.Id id) throws SQLException {
     uuidRetrieval.setInt(1, id.get());
     try (ResultSet uuidResults = uuidRetrieval.executeQuery()) {
-      if (uuidResults.first()) {
+      if (uuidResults.next()) {
         Optional.of(new AccountGroup.UUID(uuidResults.getString(1)));
       }
     }
diff --git a/java/com/google/gerrit/server/schema/Schema_88.java b/java/com/google/gerrit/server/schema/Schema_88.java
index 0a7f14c..d5e9994 100644
--- a/java/com/google/gerrit/server/schema/Schema_88.java
+++ b/java/com/google/gerrit/server/schema/Schema_88.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_88 extends SchemaVersion {
+public class Schema_88 extends ReviewDbSchemaVersion {
   @Inject
   Schema_88(Provider<Schema_87> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_89.java b/java/com/google/gerrit/server/schema/Schema_89.java
index de84993..3d352da 100644
--- a/java/com/google/gerrit/server/schema/Schema_89.java
+++ b/java/com/google/gerrit/server/schema/Schema_89.java
@@ -23,7 +23,7 @@
 import com.google.inject.Provider;
 import java.sql.SQLException;
 
-public class Schema_89 extends SchemaVersion {
+public class Schema_89 extends ReviewDbSchemaVersion {
   @Inject
   Schema_89(Provider<Schema_88> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_90.java b/java/com/google/gerrit/server/schema/Schema_90.java
index d8f02ae..3831f33 100644
--- a/java/com/google/gerrit/server/schema/Schema_90.java
+++ b/java/com/google/gerrit/server/schema/Schema_90.java
@@ -20,7 +20,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_90 extends SchemaVersion {
+public class Schema_90 extends ReviewDbSchemaVersion {
   @Inject
   Schema_90(Provider<Schema_89> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_91.java b/java/com/google/gerrit/server/schema/Schema_91.java
index 173793e..6dd2d58 100644
--- a/java/com/google/gerrit/server/schema/Schema_91.java
+++ b/java/com/google/gerrit/server/schema/Schema_91.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_91 extends SchemaVersion {
+public class Schema_91 extends ReviewDbSchemaVersion {
   @Inject
   Schema_91(Provider<Schema_90> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_92.java b/java/com/google/gerrit/server/schema/Schema_92.java
index 5f5c141..9af33c0 100644
--- a/java/com/google/gerrit/server/schema/Schema_92.java
+++ b/java/com/google/gerrit/server/schema/Schema_92.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_92 extends SchemaVersion {
+public class Schema_92 extends ReviewDbSchemaVersion {
   @Inject
   Schema_92(Provider<Schema_91> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_93.java b/java/com/google/gerrit/server/schema/Schema_93.java
index 3132aa4..e9a6691 100644
--- a/java/com/google/gerrit/server/schema/Schema_93.java
+++ b/java/com/google/gerrit/server/schema/Schema_93.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_93 extends SchemaVersion {
+public class Schema_93 extends ReviewDbSchemaVersion {
   @Inject
   Schema_93(Provider<Schema_92> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_94.java b/java/com/google/gerrit/server/schema/Schema_94.java
index d4a189f..1551650 100644
--- a/java/com/google/gerrit/server/schema/Schema_94.java
+++ b/java/com/google/gerrit/server/schema/Schema_94.java
@@ -20,7 +20,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_94 extends SchemaVersion {
+public class Schema_94 extends ReviewDbSchemaVersion {
   @Inject
   Schema_94(Provider<Schema_93> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_95.java b/java/com/google/gerrit/server/schema/Schema_95.java
index 0ce0294..19dfa97 100644
--- a/java/com/google/gerrit/server/schema/Schema_95.java
+++ b/java/com/google/gerrit/server/schema/Schema_95.java
@@ -22,7 +22,7 @@
 import java.sql.SQLException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
-public class Schema_95 extends SchemaVersion {
+public class Schema_95 extends ReviewDbSchemaVersion {
   private final AllUsersCreator allUsersCreator;
 
   @Inject
diff --git a/java/com/google/gerrit/server/schema/Schema_96.java b/java/com/google/gerrit/server/schema/Schema_96.java
index bf19213..2eb9a87 100644
--- a/java/com/google/gerrit/server/schema/Schema_96.java
+++ b/java/com/google/gerrit/server/schema/Schema_96.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_96 extends SchemaVersion {
+public class Schema_96 extends ReviewDbSchemaVersion {
   @Inject
   Schema_96(Provider<Schema_95> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_97.java b/java/com/google/gerrit/server/schema/Schema_97.java
index 0670377..98f548e 100644
--- a/java/com/google/gerrit/server/schema/Schema_97.java
+++ b/java/com/google/gerrit/server/schema/Schema_97.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_97 extends SchemaVersion {
+public class Schema_97 extends ReviewDbSchemaVersion {
   @Inject
   Schema_97(Provider<Schema_96> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_98.java b/java/com/google/gerrit/server/schema/Schema_98.java
index eec3c9f..8a7498f 100644
--- a/java/com/google/gerrit/server/schema/Schema_98.java
+++ b/java/com/google/gerrit/server/schema/Schema_98.java
@@ -20,7 +20,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_98 extends SchemaVersion {
+public class Schema_98 extends ReviewDbSchemaVersion {
   @Inject
   Schema_98(Provider<Schema_97> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/schema/Schema_99.java b/java/com/google/gerrit/server/schema/Schema_99.java
index b7fab7f..ca3a959 100644
--- a/java/com/google/gerrit/server/schema/Schema_99.java
+++ b/java/com/google/gerrit/server/schema/Schema_99.java
@@ -17,7 +17,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_99 extends SchemaVersion {
+public class Schema_99 extends ReviewDbSchemaVersion {
   @Inject
   Schema_99(Provider<Schema_98> prior) {
     super(prior);
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index e27863f9..8e018a5 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
@@ -57,7 +58,6 @@
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.change.NotifyUtil;
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.validators.MergeValidationException;
 import com.google.gerrit.server.git.validators.MergeValidators;
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 10e3455..c8d338b 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -34,13 +34,13 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index c06447d..b859895 100644
--- a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -30,6 +30,7 @@
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.Field;
@@ -48,7 +49,6 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.InsertedObject;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.notedb.ChangeNotes;
diff --git a/java/com/google/gerrit/sshd/AbstractGitCommand.java b/java/com/google/gerrit/sshd/AbstractGitCommand.java
index c49ae82..921b416 100644
--- a/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -29,6 +29,8 @@
 import org.kohsuke.args4j.Argument;
 
 public abstract class AbstractGitCommand extends BaseCommand {
+  private static final String GIT_PROTOCOL = "GIT_PROTOCOL";
+
   @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
   protected ProjectState projectState;
 
@@ -45,9 +47,15 @@
   protected Repository repo;
   protected Project.NameKey projectName;
   protected Project project;
+  protected String[] extraParameters;
 
   @Override
   public void start(Environment env) {
+    String gitProtocol = env.getEnv().get(GIT_PROTOCOL);
+    if (gitProtocol != null) {
+      extraParameters = gitProtocol.split(":");
+    }
+
     Context ctx = context.subContext(newSession(), context.getCommandLine());
     final Context old = sshScope.set(ctx);
     try {
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index 0e34889..df3242c 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.CurrentUser;
@@ -24,6 +25,8 @@
 import com.google.gerrit.server.audit.SshAuditEvent;
 import com.google.gerrit.server.config.ConfigKey;
 import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.gerrit.server.config.GerritConfigListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.ioutil.HexFormat;
@@ -33,8 +36,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.Collections;
-import java.util.List;
 import org.apache.log4j.AsyncAppender;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
@@ -318,25 +319,22 @@
   }
 
   @Override
-  public List<ConfigUpdatedEvent.Update> configUpdated(ConfigUpdatedEvent event) {
+  public Multimap<UpdateResult, ConfigUpdateEntry> configUpdated(ConfigUpdatedEvent event) {
     ConfigKey sshdRequestLog = ConfigKey.create("sshd", "requestLog");
     if (!event.isValueUpdated(sshdRequestLog)) {
-      return Collections.emptyList();
+      return ConfigUpdatedEvent.NO_UPDATES;
     }
     boolean stateUpdated;
     try {
       boolean enabled = event.getNewConfig().getBoolean("sshd", "requestLog", true);
-
       if (enabled) {
         stateUpdated = enableLogging();
       } else {
         stateUpdated = disableLogging();
       }
-      return stateUpdated
-          ? Collections.singletonList(event.accept(sshdRequestLog))
-          : Collections.emptyList();
+      return stateUpdated ? event.accept(sshdRequestLog) : ConfigUpdatedEvent.NO_UPDATES;
     } catch (IllegalArgumentException iae) {
-      return Collections.singletonList(event.reject(sshdRequestLog));
+      return event.reject(sshdRequestLog);
     }
   }
 }
diff --git a/java/com/google/gerrit/sshd/commands/ReloadConfig.java b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
index 1b21230..cbe3c57 100644
--- a/java/com/google/gerrit/sshd/commands/ReloadConfig.java
+++ b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
@@ -16,16 +16,15 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
+import com.google.common.collect.Multimap;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
 import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.gerrit.server.config.GerritServerConfigReloader;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
-import java.util.List;
-import java.util.stream.Collectors;
 
 /** Issues a reload of gerrit.config. */
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@@ -39,31 +38,16 @@
 
   @Override
   protected void run() throws Failure {
-    List<ConfigUpdatedEvent.Update> updates = gerritServerConfigReloader.reloadConfig();
+    Multimap<UpdateResult, ConfigUpdateEntry> updates = gerritServerConfigReloader.reloadConfig();
     if (updates.isEmpty()) {
       stdout.println("No config entries updated!");
       return;
     }
 
     // Print out UpdateResult.{ACCEPTED|REJECTED} entries grouped by their type
-    for (UpdateResult updateResult : UpdateResult.values()) {
-      List<ConfigUpdatedEvent.Update> filteredUpdates = filterUpdates(updates, updateResult);
-      if (filteredUpdates.isEmpty()) {
-        continue;
-      }
-      stdout.println(updateResult.toString() + " configuration changes:");
-      filteredUpdates
-          .stream()
-          .flatMap(update -> update.getConfigUpdates().stream())
-          .forEach(cfgEntry -> stdout.println(cfgEntry.toString()));
+    for (UpdateResult result : updates.keySet()) {
+      stdout.println(result.toString() + " configuration changes:");
+      updates.get(result).forEach(cfgEntry -> stdout.println(cfgEntry.toString()));
     }
   }
-
-  public static List<ConfigUpdatedEvent.Update> filterUpdates(
-      List<ConfigUpdatedEvent.Update> updates, UpdateResult result) {
-    return updates
-        .stream()
-        .filter(update -> update.getResult() == result)
-        .collect(Collectors.toList());
-  }
 }
diff --git a/java/com/google/gerrit/sshd/commands/Upload.java b/java/com/google/gerrit/sshd/commands/Upload.java
index 24a6975..cb5d449 100644
--- a/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/java/com/google/gerrit/sshd/commands/Upload.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -52,7 +53,6 @@
     PermissionBackend.ForProject perm =
         permissionBackend.user(user).project(projectState.getNameKey());
     try {
-
       perm.check(ProjectPermission.RUN_UPLOAD_PACK);
     } catch (AuthException e) {
       throw new Failure(1, "fatal: upload-pack not permitted on this server");
@@ -65,6 +65,9 @@
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
     up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
+    if (config.enableProtocolV2() && extraParameters != null) {
+      up.setExtraParameters(ImmutableList.copyOf(extraParameters));
+    }
 
     List<PreUploadHook> allPreUploadHooks = Lists.newArrayList(preUploadHooks);
     allPreUploadHooks.add(
diff --git a/java/com/google/gerrit/testing/DisabledReviewDb.java b/java/com/google/gerrit/testing/DisabledReviewDb.java
index d902e11..2bf95b0 100644
--- a/java/com/google/gerrit/testing/DisabledReviewDb.java
+++ b/java/com/google/gerrit/testing/DisabledReviewDb.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
-import com.google.gerrit.reviewdb.server.SystemConfigAccess;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.StatementExecutor;
 
@@ -71,11 +70,6 @@
   }
 
   @Override
-  public SystemConfigAccess systemConfig() {
-    throw new Disabled();
-  }
-
-  @Override
   public ChangeAccess changes() {
     throw new Disabled();
   }
diff --git a/java/com/google/gerrit/testing/FakeGroupAuditService.java b/java/com/google/gerrit/testing/FakeGroupAuditService.java
new file mode 100644
index 0000000..7c6674b
--- /dev/null
+++ b/java/com/google/gerrit/testing/FakeGroupAuditService.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testing;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.AuditEvent;
+import com.google.gerrit.server.audit.AuditListener;
+import com.google.gerrit.server.audit.group.GroupAuditListener;
+import com.google.gerrit.server.audit.group.GroupMemberAuditEvent;
+import com.google.gerrit.server.audit.group.GroupSubgroupAuditEvent;
+import com.google.gerrit.server.group.GroupAuditService;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class FakeGroupAuditService implements GroupAuditService {
+
+  private final PluginSetContext<GroupAuditListener> groupAuditListeners;
+  private final PluginSetContext<AuditListener> auditListeners;
+
+  public static class Module extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.setOf(binder(), GroupAuditListener.class);
+      DynamicSet.setOf(binder(), AuditListener.class);
+      bind(GroupAuditService.class).to(FakeGroupAuditService.class);
+    }
+  }
+
+  @Inject
+  public FakeGroupAuditService(
+      PluginSetContext<GroupAuditListener> groupAuditListeners,
+      PluginSetContext<AuditListener> auditListeners) {
+    this.groupAuditListeners = groupAuditListeners;
+    this.auditListeners = auditListeners;
+  }
+
+  public List<AuditEvent> auditEvents = new ArrayList<>();
+
+  public void clearEvents() {
+    auditEvents.clear();
+  }
+
+  @Override
+  public void dispatch(AuditEvent action) {
+    auditEvents.add(action);
+  }
+
+  @Override
+  public void dispatchAddMembers(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<Account.Id> addedMembers,
+      Timestamp addedOn) {
+    GroupMemberAuditEvent event =
+        GroupMemberAuditEvent.create(actor, updatedGroup, addedMembers, addedOn);
+    groupAuditListeners.runEach(l -> l.onAddMembers(event));
+  }
+
+  @Override
+  public void dispatchDeleteMembers(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<Account.Id> deletedMembers,
+      Timestamp deletedOn) {
+    GroupMemberAuditEvent event =
+        GroupMemberAuditEvent.create(actor, updatedGroup, deletedMembers, deletedOn);
+    groupAuditListeners.runEach(l -> l.onDeleteMembers(event));
+  }
+
+  @Override
+  public void dispatchAddSubgroups(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<AccountGroup.UUID> addedSubgroups,
+      Timestamp addedOn) {
+    GroupSubgroupAuditEvent event =
+        GroupSubgroupAuditEvent.create(actor, updatedGroup, addedSubgroups, addedOn);
+    groupAuditListeners.runEach(l -> l.onAddSubgroups(event));
+  }
+
+  @Override
+  public void dispatchDeleteSubgroups(
+      Account.Id actor,
+      AccountGroup.UUID updatedGroup,
+      ImmutableSet<AccountGroup.UUID> deletedSubgroups,
+      Timestamp deletedOn) {
+    GroupSubgroupAuditEvent event =
+        GroupSubgroupAuditEvent.create(actor, updatedGroup, deletedSubgroups, deletedOn);
+    groupAuditListeners.runEach(l -> l.onDeleteSubgroups(event));
+  }
+}
diff --git a/java/com/google/gerrit/testing/InMemoryDatabase.java b/java/com/google/gerrit/testing/InMemoryDatabase.java
index a3d7c17..b489652 100644
--- a/java/com/google/gerrit/testing/InMemoryDatabase.java
+++ b/java/com/google/gerrit/testing/InMemoryDatabase.java
@@ -20,11 +20,10 @@
 import com.google.gerrit.pgm.init.index.elasticsearch.ElasticIndexModuleOnInit;
 import com.google.gerrit.pgm.init.index.lucene.LuceneIndexModuleOnInit;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.schema.SchemaVersion;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaVersion;
 import com.google.gwtorm.jdbc.Database;
 import com.google.gwtorm.jdbc.SimpleDataSource;
 import com.google.gwtorm.server.OrmException;
@@ -62,7 +61,7 @@
     }
   }
 
-  private final SchemaCreator schemaCreator;
+  private final ReviewDbSchemaCreator schemaCreator;
   private final Instance dbInstance;
 
   private boolean created;
@@ -86,7 +85,7 @@
                 }
               }
             });
-    this.schemaCreator = childInjector.getInstance(SchemaCreator.class);
+    this.schemaCreator = childInjector.getInstance(ReviewDbSchemaCreator.class);
     Instance dbInstanceFromInjector = childInjector.getInstance(Instance.class);
     if (dbInstanceFromInjector != null) {
       this.dbInstance = dbInstanceFromInjector;
@@ -96,7 +95,7 @@
     }
   }
 
-  InMemoryDatabase(SchemaCreator schemaCreator) throws OrmException {
+  InMemoryDatabase(ReviewDbSchemaCreator schemaCreator) throws OrmException {
     this.schemaCreator = schemaCreator;
     this.dbInstance = new Instance();
   }
@@ -127,12 +126,6 @@
     return this;
   }
 
-  public SystemConfig getSystemConfig() throws OrmException {
-    try (ReviewDb c = open()) {
-      return c.systemConfig().get(new SystemConfig.Key());
-    }
-  }
-
   public CurrentSchemaVersion getSchemaVersion() throws OrmException {
     try (ReviewDb c = open()) {
       return c.schemaVersion().get(new CurrentSchemaVersion.Key());
@@ -140,7 +133,7 @@
   }
 
   public void assertSchemaVersion() throws OrmException {
-    assertThat(getSchemaVersion().versionNbr).isEqualTo(SchemaVersion.getBinaryVersion());
+    assertThat(getSchemaVersion().versionNbr).isEqualTo(ReviewDbSchemaVersion.getBinaryVersion());
   }
 
   public static class Instance {
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 24ff82c..fc17816 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -85,7 +85,7 @@
 import com.google.gerrit.server.schema.InMemoryAccountPatchReviewStore;
 import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
 import com.google.gerrit.server.schema.ReviewDbFactory;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.securestore.DefaultSecureStore;
 import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.ssh.NoSshKeyCache;
@@ -303,7 +303,7 @@
 
   @Provides
   @Singleton
-  InMemoryDatabase getInMemoryDatabase(SchemaCreator schemaCreator) throws OrmException {
+  InMemoryDatabase getInMemoryDatabase(ReviewDbSchemaCreator schemaCreator) throws OrmException {
     return new InMemoryDatabase(schemaCreator);
   }
 
diff --git a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
index cebd139..02be071 100644
--- a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
+++ b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gwtorm.server.SchemaFactory;
@@ -50,7 +50,7 @@
   @Inject private AccountManager accountManager;
   @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private SchemaFactory<ReviewDb> schemaFactory;
-  @Inject private SchemaCreator schemaCreator;
+  @Inject private ReviewDbSchemaCreator schemaCreator;
   @Inject private ThreadLocalRequestContext requestContext;
   // Only for use in setting up/tearing down injector.
   @Inject private InMemoryDatabase inMemoryDatabase;
diff --git a/java/gerrit/PRED_commit_delta_4.java b/java/gerrit/PRED_commit_delta_4.java
index 7c26632..d2634ea 100644
--- a/java/gerrit/PRED_commit_delta_4.java
+++ b/java/gerrit/PRED_commit_delta_4.java
@@ -102,7 +102,7 @@
         String oldName = patch.getOldName();
         Patch.ChangeType changeType = patch.getChangeType();
 
-        if (newName.equals("/COMMIT_MSG")) {
+        if (Patch.isMagic(newName)) {
           continue;
         }
 
diff --git a/java/gerrit/PRED_commit_edits_2.java b/java/gerrit/PRED_commit_edits_2.java
index c196026..f46a6487 100644
--- a/java/gerrit/PRED_commit_edits_2.java
+++ b/java/gerrit/PRED_commit_edits_2.java
@@ -14,6 +14,7 @@
 
 package gerrit;
 
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.Text;
@@ -90,7 +91,7 @@
         String newName = entry.getNewName();
         String oldName = entry.getOldName();
 
-        if (newName.equals("/COMMIT_MSG")) {
+        if (Patch.isMagic(newName)) {
           continue;
         }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 9886db5..0f195b5 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -93,6 +93,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.gpg.Fingerprint;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.testing.TestKey;
@@ -118,7 +119,6 @@
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.LockFailureException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.account.StalenessChecker;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index c939ac1..b4416302 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -16,13 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -34,8 +43,14 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.junit.AfterClass;
@@ -46,6 +61,46 @@
 public class AgreementsIT extends AbstractDaemonTest {
   private ContributorAgreement caAutoVerify;
   private ContributorAgreement caNoAutoVerify;
+  @Inject private GroupOperations groupOperations;
+
+  protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig config = projectConfigFactory.read(md);
+      config.getProject().setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, value);
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
+  }
+
+  protected ContributorAgreement configureContributorAgreement(boolean autoVerify)
+      throws Exception {
+    ContributorAgreement ca;
+    String name = autoVerify ? "cla-test-group" : "cla-test-no-auto-verify-group";
+    AccountGroup.UUID g = groupOperations.newGroup().name(name).create();
+    GroupApi groupApi = gApi.groups().id(g.get());
+    groupApi.description("CLA test group");
+    InternalGroup caGroup = group(new AccountGroup.UUID(groupApi.detail().id));
+    GroupReference groupRef = new GroupReference(caGroup.getGroupUUID(), caGroup.getName());
+    PermissionRule rule = new PermissionRule(groupRef);
+    rule.setAction(PermissionRule.Action.ALLOW);
+    if (autoVerify) {
+      ca = new ContributorAgreement("cla-test");
+      ca.setAutoVerify(groupRef);
+      ca.setAccepted(ImmutableList.of(rule));
+    } else {
+      ca = new ContributorAgreement("cla-test-no-auto-verify");
+    }
+    ca.setDescription("description");
+    ca.setAgreementUrl("agreement-url");
+    ca.setAccepted(ImmutableList.of(rule));
+    ca.setExcludeProjectsRegexes(ImmutableList.of("ExcludedProject"));
+
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      u.getConfig().replace(ca);
+      u.save();
+      return ca;
+    }
+  }
 
   @ConfigSuite.Config
   public static Config enableAgreementsConfig() {
@@ -291,4 +346,33 @@
     in.project = project.get();
     return in;
   }
+
+  @Test
+  @GerritConfig(name = "auth.contributorAgreements", value = "true")
+  public void anonymousAccessServerInfoEvenWithCLAs() throws Exception {
+    setApiUserAnonymous();
+    gApi.config().server().getInfo();
+  }
+
+  @Test
+  public void publishEditRestWithoutCLA() throws Exception {
+    String filename = "foo";
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, "subject1", filename, "contentold");
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+    String changeId = result.getChangeId();
+
+    gApi.changes().id(changeId).edit().create();
+    gApi.changes()
+        .id(changeId)
+        .edit()
+        .modifyFile(filename, RawInputUtil.create("newcontent".getBytes(UTF_8)));
+
+    String url = "/changes/" + changeId + "/edit:publish";
+    setUseContributorAgreements(InheritableBoolean.TRUE);
+    userRestSession.post(url).assertForbidden();
+    setUseContributorAgreements(InheritableBoolean.FALSE);
+    userRestSession.post(url).assertNoContent();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
index 31f3f91..2167d27 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -9,6 +9,7 @@
         "no_windows",
     ],
     deps = [
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/server/util/time",
     ],
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 4a4ee2a..ee22141 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
@@ -126,6 +127,7 @@
   @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
   @Inject private Sequences seq;
   @Inject private AccountOperations accountOperations;
+  @Inject private GroupOperations groupOperations;
 
   @Before
   public void setTimeForTesting() {
@@ -1510,9 +1512,9 @@
   }
 
   private static void assertIncludes(Iterable<GroupInfo> includes, String... expectedNames) {
-    assertThat(Iterables.transform(includes, i -> i.name))
-        .containsExactlyElementsIn(Arrays.asList(expectedNames))
-        .inOrder();
+    Iterable<String> names = Iterables.transform(includes, i -> i.name);
+    assertThat(names).containsExactlyElementsIn(Arrays.asList(expectedNames));
+    assertThat(names).isOrdered();
   }
 
   private void assertNoIncludes(String group) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 9ad14ee..96566f6 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -238,16 +237,6 @@
   }
 
   @Test
-  public void publishEditRestWithoutCLA() throws Exception {
-    configureContributorAgreement(true);
-    createArbitraryEditFor(changeId);
-    setUseContributorAgreements(InheritableBoolean.TRUE);
-    adminRestSession.post(urlPublish(changeId)).assertForbidden();
-    setUseContributorAgreements(InheritableBoolean.FALSE);
-    adminRestSession.post(urlPublish(changeId)).assertNoContent();
-  }
-
-  @Test
   public void rebaseEdit() throws Exception {
     PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
     createEmptyEditFor(changeId2);
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 66a09f8..f306932 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -53,6 +53,7 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.junit.Before;
 
 public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
 
@@ -95,7 +96,7 @@
     return cfg;
   }
 
-  protected TestRepository<?> createProjectWithPush(
+  protected Project.NameKey createProjectForPush(
       String name,
       @Nullable Project.NameKey parent,
       boolean createEmptyCommit,
@@ -104,24 +105,40 @@
     Project.NameKey project = createProject(name, parent, createEmptyCommit, submitType);
     grant(project, "refs/heads/*", Permission.PUSH);
     grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
-    return cloneProject(project);
-  }
-
-  protected TestRepository<?> createProjectWithPush(String name, @Nullable Project.NameKey parent)
-      throws Exception {
-    return createProjectWithPush(name, parent, true, getSubmitType());
-  }
-
-  protected TestRepository<?> createProjectWithPush(String name, boolean createEmptyCommit)
-      throws Exception {
-    return createProjectWithPush(name, null, createEmptyCommit, getSubmitType());
+    return project;
   }
 
   protected TestRepository<?> createProjectWithPush(String name) throws Exception {
     return createProjectWithPush(name, null, true, getSubmitType());
   }
 
+  protected TestRepository<?> createProjectWithPush(
+      String name,
+      @Nullable Project.NameKey parent,
+      boolean createEmptyCommit,
+      SubmitType submitType)
+      throws Exception {
+    return cloneProject(createProjectForPush(name, parent, createEmptyCommit, submitType));
+  }
+
+  protected TestRepository<?> createProjectWithPush(String name, boolean createEmptyCommit)
+      throws Exception {
+    return cloneProject(createProjectForPush(name, null, createEmptyCommit, getSubmitType()));
+  }
+
   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());
+    superRepo = cloneProject(superKey);
+    subRepo = cloneProject(subKey);
+  }
 
   protected ObjectId pushChangeTo(
       TestRepository<?> repo, String ref, String file, String content, String message, String topic)
@@ -184,16 +201,31 @@
   protected void allowSubmoduleSubscription(
       String submodule, String subBranch, String superproject, String superBranch, boolean match)
       throws Exception {
-    Project.NameKey sub = new Project.NameKey(name(submodule));
-    Project.NameKey superName = new Project.NameKey(name(superproject));
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(sub)) {
+    allowSubmoduleSubscription(
+        nameKey(submodule), subBranch, nameKey(superproject), superBranch, match);
+  }
+
+  protected void allowMatchingSubmoduleSubscription(
+      Project.NameKey submodule, String subBranch, Project.NameKey superproject, String superBranch)
+      throws Exception {
+    allowSubmoduleSubscription(submodule, subBranch, superproject, superBranch, true);
+  }
+
+  protected void allowSubmoduleSubscription(
+      Project.NameKey submodule,
+      String subBranch,
+      Project.NameKey superproject,
+      String superBranch,
+      boolean match)
+      throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(submodule)) {
       md.setMessage("Added superproject subscription");
       SubscribeSection s;
       ProjectConfig pc = projectConfigFactory.read(md);
-      if (pc.getSubscribeSections().containsKey(superName)) {
-        s = pc.getSubscribeSections().get(superName);
+      if (pc.getSubscribeSections().containsKey(superproject)) {
+        s = pc.getSubscribeSections().get(superproject);
       } else {
-        s = new SubscribeSection(superName);
+        s = new SubscribeSection(superproject);
       }
       String refspec;
       if (superBranch == null) {
@@ -223,6 +255,15 @@
   protected void createSubmoduleSubscription(
       TestRepository<?> repo, String branch, String subscribeToRepo, String subscribeToBranch)
       throws Exception {
+    createSubmoduleSubscription(repo, branch, nameKey(subscribeToRepo), subscribeToBranch);
+  }
+
+  protected void createSubmoduleSubscription(
+      TestRepository<?> repo,
+      String branch,
+      Project.NameKey subscribeToRepo,
+      String subscribeToBranch)
+      throws Exception {
     Config config = new Config();
     prepareSubmoduleConfigEntry(config, subscribeToRepo, subscribeToBranch);
     pushSubmoduleConfig(repo, branch, config);
@@ -246,12 +287,20 @@
       String subscribeToRepoPrefix,
       String subscribeToRepo,
       String subscribeToBranch) {
-    subscribeToRepo = name(subscribeToRepo);
-    String url = subscribeToRepoPrefix + subscribeToRepo;
-    config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
-    config.setString("submodule", subscribeToRepo, "url", url);
+    prepareRelativeSubmoduleConfigEntry(
+        config, subscribeToRepoPrefix, nameKey(subscribeToRepo), subscribeToBranch);
+  }
+
+  protected void prepareRelativeSubmoduleConfigEntry(
+      Config config,
+      String subscribeToRepoPrefix,
+      Project.NameKey subscribeToRepo,
+      String subscribeToBranch) {
+    String url = subscribeToRepoPrefix + subscribeToRepo.get();
+    config.setString("submodule", subscribeToRepo.get(), "path", subscribeToRepo.get());
+    config.setString("submodule", subscribeToRepo.get(), "url", url);
     if (subscribeToBranch != null) {
-      config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+      config.setString("submodule", subscribeToRepo.get(), "branch", subscribeToBranch);
     }
   }
 
@@ -260,21 +309,41 @@
     // The submodule subscription module checks for gerrit.canonicalWebUrl to
     // detect if it's configured for automatic updates. It doesn't matter if
     // it serves from that URL.
+    prepareSubmoduleConfigEntry(
+        config, nameKey(subscribeToRepo), nameKey(subscribeToRepo), subscribeToBranch);
+  }
+
+  protected void prepareSubmoduleConfigEntry(
+      Config config, Project.NameKey subscribeToRepo, String subscribeToBranch) {
+    // The submodule subscription module checks for gerrit.canonicalWebUrl to
+    // detect if it's configured for automatic updates. It doesn't matter if
+    // it serves from that URL.
     prepareSubmoduleConfigEntry(config, subscribeToRepo, subscribeToRepo, subscribeToBranch);
   }
 
+  protected Project.NameKey nameKey(String s) {
+    return new Project.NameKey(name(s));
+  }
+
   protected void prepareSubmoduleConfigEntry(
       Config config, String subscribeToRepo, String subscribeToRepoPath, String subscribeToBranch) {
-    subscribeToRepo = name(subscribeToRepo);
-    subscribeToRepoPath = name(subscribeToRepoPath);
+    prepareSubmoduleConfigEntry(
+        config, nameKey(subscribeToRepo), nameKey(subscribeToRepoPath), subscribeToBranch);
+  }
+
+  protected void prepareSubmoduleConfigEntry(
+      Config config,
+      Project.NameKey subscribeToRepo,
+      Project.NameKey subscribeToRepoPath,
+      String subscribeToBranch) {
     // The submodule subscription module checks for gerrit.canonicalWebUrl to
     // detect if it's configured for automatic updates. It doesn't matter if
     // it serves from that URL.
     String url = cfg.getString("gerrit", null, "canonicalWebUrl") + "/" + subscribeToRepo;
-    config.setString("submodule", subscribeToRepoPath, "path", subscribeToRepoPath);
-    config.setString("submodule", subscribeToRepoPath, "url", url);
+    config.setString("submodule", subscribeToRepoPath.get(), "path", subscribeToRepoPath.get());
+    config.setString("submodule", subscribeToRepoPath.get(), "url", url);
     if (subscribeToBranch != null) {
-      config.setString("submodule", subscribeToRepoPath, "branch", subscribeToBranch);
+      config.setString("submodule", subscribeToRepoPath.get(), "branch", subscribeToBranch);
     }
   }
 
@@ -302,8 +371,17 @@
       TestRepository<?> subRepo,
       String subBranch)
       throws Exception {
+    expectToHaveSubmoduleState(repo, branch, nameKey(submodule), subRepo, subBranch);
+  }
 
-    submodule = name(submodule);
+  protected void expectToHaveSubmoduleState(
+      TestRepository<?> repo,
+      String branch,
+      Project.NameKey submodule,
+      TestRepository<?> subRepo,
+      String subBranch)
+      throws Exception {
+
     ObjectId commitId =
         repo.git()
             .fetch()
@@ -326,7 +404,7 @@
     rw.parseBody(c.getTree());
 
     RevTree tree = c.getTree();
-    RevObject actualId = repo.get(tree, submodule);
+    RevObject actualId = repo.get(tree, submodule.get());
 
     assertThat(actualId).isEqualTo(subHead);
   }
@@ -334,8 +412,12 @@
   protected void expectToHaveSubmoduleState(
       TestRepository<?> repo, String branch, String submodule, ObjectId expectedId)
       throws Exception {
+    expectToHaveSubmoduleState(repo, branch, nameKey(submodule), expectedId);
+  }
 
-    submodule = name(submodule);
+  protected void expectToHaveSubmoduleState(
+      TestRepository<?> repo, String branch, Project.NameKey submodule, ObjectId expectedId)
+      throws Exception {
     ObjectId commitId =
         repo.git()
             .fetch()
@@ -349,7 +431,7 @@
     rw.parseBody(c.getTree());
 
     RevTree tree = c.getTree();
-    RevObject actualId = repo.get(tree, submodule);
+    RevObject actualId = repo.get(tree, submodule.get());
 
     assertThat(actualId).isEqualTo(expectedId);
   }
@@ -410,8 +492,11 @@
 
   protected boolean hasSubmodule(TestRepository<?> repo, String branch, String submodule)
       throws Exception {
+    return hasSubmodule(repo, branch, new Project.NameKey(name(submodule)));
+  }
 
-    submodule = name(submodule);
+  protected boolean hasSubmodule(TestRepository<?> repo, String branch, Project.NameKey submodule)
+      throws Exception {
     Ref branchTip =
         repo.git().fetch().setRemote("origin").call().getAdvertisedRef("refs/heads/" + branch);
     if (branchTip == null) {
@@ -426,7 +511,7 @@
 
     RevTree tree = c.getTree();
     try {
-      repo.get(tree, submodule);
+      repo.get(tree, submodule.get());
       return true;
     } catch (AssertionError e) {
       return false;
@@ -446,7 +531,8 @@
 
     RevWalk rw = repo.getRevWalk();
     RevCommit c = rw.parseCommit(commitId);
-    assertThat(c.getFullMessage()).isEqualTo(expectedMessage);
+    String msg = c.getFullMessage();
+    assertThat(msg).isEqualTo(expectedMessage);
   }
 
   protected PersonIdent getAuthor(TestRepository<?> repo, String branch) throws Exception {
@@ -465,8 +551,13 @@
 
   protected void directUpdateSubmodule(String project, String refName, String path, AnyObjectId id)
       throws Exception {
-    path = name(path);
-    try (Repository serverRepo = repoManager.openRepository(new Project.NameKey(name(project)));
+    directUpdateSubmodule(nameKey(project), refName, nameKey(path), id);
+  }
+
+  protected void directUpdateSubmodule(
+      Project.NameKey project, String refName, Project.NameKey path, AnyObjectId id)
+      throws Exception {
+    try (Repository serverRepo = repoManager.openRepository(project);
         ObjectInserter ins = serverRepo.newObjectInserter();
         RevWalk rw = new RevWalk(serverRepo)) {
       Ref ref = serverRepo.exactRef(refName);
@@ -480,7 +571,7 @@
       b.finish();
       DirCacheEditor e = dc.editor();
       e.add(
-          new PathEdit(path) {
+          new PathEdit(path.get()) {
             @Override
             public void apply(DirCacheEntry ent) {
               ent.setFileMode(FileMode.GITLINK);
diff --git a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
new file mode 100644
index 0000000..42e046a
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.AuditEvent;
+import java.util.Collections;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitOverHttpServletIT extends AbstractPushForReview {
+
+  @Before
+  public void beforeEach() throws Exception {
+    CredentialsProvider.setDefault(
+        new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword));
+    selectProtocol(AbstractPushForReview.Protocol.HTTP);
+    auditService.clearEvents();
+  }
+
+  @Test
+  public void receivePackAuditEventLog() throws Exception {
+    testRepo
+        .git()
+        .push()
+        .setRemote("origin")
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master"))
+        .call();
+
+    // Git smart protocol makes two requests:
+    // https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt
+    assertThat(auditService.auditEvents.size()).isEqualTo(2);
+
+    AuditEvent e = auditService.auditEvents.get(1);
+    assertThat(e.who.getAccountId()).isEqualTo(admin.id);
+    assertThat(e.what).endsWith("/git-receive-pack");
+    assertThat(e.params).isEmpty();
+  }
+
+  @Test
+  public void uploadPackAuditEventLog() throws Exception {
+    testRepo.git().fetch().call();
+
+    assertThat(auditService.auditEvents.size()).isEqualTo(1);
+
+    AuditEvent e = auditService.auditEvents.get(0);
+    assertThat(e.who.toString()).isEqualTo("ANONYMOUS");
+    assertThat(e.params.get("service"))
+        .containsExactlyElementsIn(Collections.singletonList("git-upload-pack"));
+    assertThat(e.what).endsWith("service=git-upload-pack");
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 03b1165..d76b310 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -47,198 +48,169 @@
   @Test
   @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
   public void testSubscriptionWithoutGlobalServerSetting() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionWithoutSpecificSubscription() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionToEmptyRepo() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isTrue();
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
   }
 
   @Test
   public void subscriptionToExistingRepo() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isTrue();
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
   }
 
   @Test
   public void subscriptionWildcardACLForSingleBranch() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
     // master is allowed to be subscribed to master branch only:
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", null);
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, null);
     // create 'branch':
     pushChangeTo(superRepo, "branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "master");
 
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
-    assertThat(hasSubmodule(superRepo, "branch", "subscribed-to-project")).isFalse();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
+    assertThat(hasSubmodule(superRepo, "branch", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionWildcardACLForMissingProject() throws Exception {
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
     allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "not-existing-super-project", "refs/heads/*");
+        subKey, "refs/heads/*", new Project.NameKey("not-existing-super-project"), "refs/heads/*");
     pushChangeTo(subRepo, "master");
   }
 
   @Test
   public void subscriptionWildcardACLForMissingBranch() throws Exception {
-    createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", "refs/heads/*");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
     pushChangeTo(subRepo, "foo");
   }
 
   @Test
   public void subscriptionWildcardACLForMissingGitmodules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", "refs/heads/*");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
     pushChangeTo(superRepo, "master");
     pushChangeTo(subRepo, "master");
   }
 
   @Test
   public void subscriptionWildcardACLOneOnOneMapping() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
     // any branch is allowed to be subscribed to the same superprojects branch:
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", "refs/heads/*");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
 
     // create 'branch' in both repos:
     pushChangeTo(superRepo, "branch");
     pushChangeTo(subRepo, "branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "branch");
 
     ObjectId subHEAD1 = pushChangeTo(subRepo, "master");
     ObjectId subHEAD2 = pushChangeTo(subRepo, "branch");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD1);
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD2);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD2);
 
     // Now test that cross subscriptions do not work:
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "master", subKey, "branch");
     ObjectId subHEAD3 = pushChangeTo(subRepo, "branch");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD1);
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD3);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD3);
   }
 
   @Test
   public void subscriptionWildcardACLForManyBranches() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     // Any branch is allowed to be subscribed to any superproject branch:
-    allowSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/*", "super-project", null, false);
+    allowSubmoduleSubscription(subKey, "refs/heads/*", superKey, null, false);
     pushChangeTo(superRepo, "branch");
     pushChangeTo(subRepo, "another-branch");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "another-branch");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "another-branch");
     ObjectId subHEAD = pushChangeTo(subRepo, "another-branch");
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
   }
 
   @Test
   public void subscriptionWildcardACLOneToManyBranches() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     // Any branch is allowed to be subscribed to any superproject branch:
-    allowSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/*", false);
+    allowSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/*", false);
     pushChangeTo(superRepo, "branch");
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
 
-    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "branch");
+    createSubmoduleSubscription(superRepo, "branch", subKey, "branch");
     pushChangeTo(subRepo, "branch");
 
     // no change expected, as only master is subscribed:
-    expectToHaveSubmoduleState(superRepo, "branch", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
   }
 
   @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "false")
   public void testSubmoduleShortCommitMessage() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     // The first update doesn't include any commit messages
     ObjectId subRepoId = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
     expectToHaveCommitMessage(superRepo, "master", "Update git submodules\n\n");
 
     // Any following update also has a short message
     subRepoId = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
     expectToHaveCommitMessage(superRepo, "master", "Update git submodules\n\n");
   }
 
   @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
   public void testSubmoduleSubjectCommitMessage() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
     // The first update doesn't include the rev log
@@ -248,7 +220,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName());
 
@@ -261,7 +233,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName()
             + "\n  - "
@@ -270,13 +242,11 @@
 
   @Test
   public void submoduleCommitMessage() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
 
     // The first update doesn't include the rev log
@@ -286,7 +256,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName());
 
@@ -299,7 +269,7 @@
         "master",
         "Update git submodules\n\n"
             + "* Update "
-            + name("subscribed-to-project")
+            + subKey.get()
             + " from branch 'master'\n  to "
             + subHEAD.getName()
             + "\n  - "
@@ -308,171 +278,151 @@
 
   @Test
   public void subscriptionUnsubscribe() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
 
     deleteAllSubscriptions(superRepo, "master");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
 
     pushChangeTo(superRepo, "refs/heads/master", "commit after unsubscribe", "");
     pushChangeTo(subRepo, "refs/heads/master", "commit after unsubscribe", "");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
   }
 
   @Test
   public void subscriptionUnsubscribeByDeletingGitModules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     pushChangeTo(subRepo, "master");
     ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
 
     deleteGitModulesFile(superRepo, "master");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
 
     pushChangeTo(superRepo, "refs/heads/master", "commit after unsubscribe", "");
     pushChangeTo(subRepo, "refs/heads/master", "commit after unsubscribe", "");
-    expectToHaveSubmoduleState(
-        superRepo, "master", "subscribed-to-project", subHEADbeforeUnsubscribing);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
   }
 
   @Test
   public void subscriptionToDifferentBranches() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/foo", "super-project", "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "foo");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/foo", superKey, "refs/heads/master");
+
+    createSubmoduleSubscription(superRepo, "master", subKey, "foo");
     ObjectId subFoo = pushChangeTo(subRepo, "foo");
     pushChangeTo(subRepo, "master");
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subFoo);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subFoo);
   }
 
   @Test
   public void branchCircularSubscription() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "super-project", "refs/heads/master", "subscribed-to-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(superKey, "refs/heads/master", subKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(subRepo, "master", "super-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(subRepo, "master", superKey, "master");
 
     pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
 
-    assertThat(hasSubmodule(subRepo, "master", "super-project")).isFalse();
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(subRepo, "master", superKey)).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void projectCircularSubscription() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "super-project", "refs/heads/dev", "subscribed-to-project", "refs/heads/dev");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");
 
     pushChangeTo(subRepo, "master");
     pushChangeTo(superRepo, "master");
     pushChangeTo(subRepo, "dev");
     pushChangeTo(superRepo, "dev");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(subRepo, "dev", superKey, "dev");
 
     ObjectId subMasterHead = pushChangeTo(subRepo, "master");
     ObjectId superDevHead = pushChangeTo(superRepo, "dev");
 
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isTrue();
-    assertThat(hasSubmodule(subRepo, "dev", "super-project")).isTrue();
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subMasterHead);
-    expectToHaveSubmoduleState(subRepo, "dev", "super-project", superDevHead);
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
+    assertThat(hasSubmodule(subRepo, "dev", superKey)).isTrue();
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subMasterHead);
+    expectToHaveSubmoduleState(subRepo, "dev", superKey, superDevHead);
   }
 
   @Test
   public void subscriptionFailOnMissingACL() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionFailOnWrongProjectACL() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
     allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "wrong-super-project", "refs/heads/master");
+        subKey,
+        "refs/heads/master",
+        new Project.NameKey("wrong-super-project"),
+        "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionFailOnWrongBranchACL() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
     allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/wrong-branch");
+        subKey, "refs/heads/master", superKey, "refs/heads/wrong-branch");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     pushChangeTo(subRepo, "master");
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionInheritACL() throws Exception {
-    createProjectWithPush("config-repo");
-    createProjectWithPush("config-repo2", new Project.NameKey(name("config-repo")));
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo =
-        createProjectWithPush("subscribed-to-project", new Project.NameKey(name("config-repo2")));
-    allowMatchingSubmoduleSubscription(
-        "config-repo", "refs/heads/*", "super-project", "refs/heads/*");
+    Project.NameKey configKey = createProjectForPush("config-repo", null, true, getSubmitType());
+    Project.NameKey config2Key =
+        createProjectForPush("config-repo2", configKey, true, getSubmitType());
+    cloneProject(config2Key);
+
+    subKey = createProjectForPush("subrepo", config2Key, true, getSubmitType());
+    subRepo = cloneProject(subKey);
+
+    allowMatchingSubmoduleSubscription(configKey, "refs/heads/*", superKey, "refs/heads/*");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
   }
 
   @Test
   public void allowedButNotSubscribed() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     subRepo
@@ -490,16 +440,16 @@
     assertThat(r.getRemoteUpdate("refs/heads/master").getStatus())
         .isEqualTo(RemoteRefUpdate.Status.OK);
 
-    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+    assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
   }
 
   @Test
   public void subscriptionDeepRelative() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("nested/subscribed-to-project");
+    Project.NameKey nest =
+        createProjectForPush("nested/subscribed-to-project", null, true, getSubmitType());
+    TestRepository<?> subRepo = cloneProject(nest);
     // master is allowed to be subscribed to any superprojects branch:
-    allowMatchingSubmoduleSubscription(
-        "nested/subscribed-to-project", "refs/heads/master", "super-project", null);
+    allowMatchingSubmoduleSubscription(nest, "refs/heads/master", superKey, null);
 
     pushChangeTo(subRepo, "master");
     createRelativeSubmoduleSubscription(
@@ -519,7 +469,9 @@
 
   @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
-  @GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "220")
+  // The value 195 must tuned to the test environment, and is sensitive to the
+  // length of the uniquified repository name.
+  @GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "200")
   public void submoduleSubjectCommitMessageSizeLimit() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isFalse();
     testSubmoduleSubjectCommitMessageAndExpectTruncation();
@@ -530,11 +482,10 @@
     // Make sure that the commit is created at an earlier timestamp than the submit timestamp.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-      TestRepository<?> superRepo = createProjectWithPush("super-project");
-      TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
       allowMatchingSubmoduleSubscription(
-          "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-      createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+          subKey, "refs/heads/master", superKey, "refs/heads/master");
+      createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
       PushOneCommit.Result pushResult =
           createChange(subRepo, "refs/heads/master", "Change", "a.txt", "some content", null);
@@ -561,18 +512,18 @@
     // is afterwards.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-      TestRepository<?> superRepo = createProjectWithPush("super-project");
-      TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-      TestRepository<?> subRepo2 = createProjectWithPush("subscribed-to-project-2");
 
+      Project.NameKey proj2 =
+          createProjectForPush("subscribed-to-project-2", null, true, getSubmitType());
+
+      TestRepository<?> subRepo2 = cloneProject(proj2);
       allowMatchingSubmoduleSubscription(
-          "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-      allowMatchingSubmoduleSubscription(
-          "subscribed-to-project-2", "refs/heads/master", "super-project", "refs/heads/master");
+          subKey, "refs/heads/master", superKey, "refs/heads/master");
+      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
       Config config = new Config();
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project", "master");
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project-2", "master");
+      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
       pushSubmoduleConfig(superRepo, "master", config);
 
       String topic = "foo";
@@ -610,21 +561,17 @@
     // is afterwards.
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     try {
-      TestRepository<?> superRepo = createProjectWithPush("super-project");
-      TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-
-      createProjectWithPush("subscribed-to-project-2");
-      TestRepository<?> subRepo2 =
-          cloneProject(new Project.NameKey(name("subscribed-to-project-2")), user);
+      Project.NameKey proj2 =
+          createProjectForPush("subscribed-to-project-2", null, true, getSubmitType());
+      TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
 
       allowMatchingSubmoduleSubscription(
-          "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-      allowMatchingSubmoduleSubscription(
-          "subscribed-to-project-2", "refs/heads/master", "super-project", "refs/heads/master");
+          subKey, "refs/heads/master", superKey, "refs/heads/master");
+      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
       Config config = new Config();
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project", "master");
-      prepareSubmoduleConfigEntry(config, "subscribed-to-project-2", "master");
+      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
       pushSubmoduleConfig(superRepo, "master", config);
 
       String topic = "foo";
@@ -636,7 +583,7 @@
 
       // Create change as user.
       PushOneCommit push =
-          pushFactory.create(db, user.getIdent(), subRepo2, "Change 2", "b.txt", "other content");
+          pushFactory.create(db, user.getIdent(), repo2, "Change 2", "b.txt", "other content");
       PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
       approve(pushResult2.getChangeId());
 
@@ -659,14 +606,15 @@
 
   @Test
   public void updateOnlyRelevantSubmodules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo1 = createProjectWithPush("subscribed-to-project-1");
-    TestRepository<?> subRepo2 = createProjectWithPush("subscribed-to-project-2");
+    Project.NameKey subkey1 =
+        createProjectForPush("subscribed-to-project-1", null, true, getSubmitType());
+    Project.NameKey subkey2 =
+        createProjectForPush("subscribed-to-project-2", null, true, getSubmitType());
+    TestRepository<?> subRepo1 = cloneProject(subkey1);
+    TestRepository<?> subRepo2 = cloneProject(subkey2);
 
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project-1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project-2", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subkey1, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subkey2, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
     prepareSubmoduleConfigEntry(config, "subscribed-to-project-1", "master");
@@ -689,41 +637,35 @@
 
   @Test
   public void skipUpdatingBrokenGitlinkPointer() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     // Push once to initialize submodule.
     ObjectId subTip = pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subTip);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subTip);
 
     // Write an invalid SHA-1 directly to the gitlink.
     ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    directUpdateSubmodule("super-project", "refs/heads/master", "subscribed-to-project", badId);
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", badId);
+    directUpdateSubmodule(superKey, "refs/heads/master", subKey, badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, badId);
 
     // Push succeeds, but gitlink update is skipped.
     pushChangeTo(subRepo, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, badId);
   }
 
   private ObjectId directUpdateRef(String project, String ref) throws Exception {
-    try (Repository serverRepo = repoManager.openRepository(new Project.NameKey(name(project)))) {
+    try (Repository serverRepo = repoManager.openRepository(nameKey(project))) {
       return new TestRepository<>(serverRepo).branch(ref).commit().create().copy();
     }
   }
 
   private void testSubmoduleSubjectCommitMessageAndExpectTruncation() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
     // The first update doesn't include the rev log, so we ignore it
     pushChangeTo(subRepo, "master");
 
@@ -736,6 +678,6 @@
         "master",
         String.format(
             "Update git submodules\n\n* Update %s from branch 'master'\n  to %s\n  - %s\n\n[...]",
-            name("subscribed-to-project"), subHEAD.getName(), subCommitMsg.getShortMessage()));
+            subKey.get(), subHEAD.getName(), subCommitMsg.getShortMessage()));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index eef3295..0b583bf 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -68,12 +68,8 @@
 
   @Test
   public void subscriptionUpdateOfManyChanges() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     ObjectId subHEAD =
         subRepo
@@ -150,15 +146,13 @@
             .getAdvertisedRef("refs/heads/master")
             .getObjectId();
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
 
     // As the submodules have changed commits, the superproject tree will be
     // different, so we cannot directly compare the trees here, so make
     // assumptions only about the changed branches:
-    Project.NameKey p1 = new Project.NameKey(name("super-project"));
-    Project.NameKey p2 = new Project.NameKey(name("subscribed-to-project"));
-    assertThat(preview).containsKey(new Branch.NameKey(p1, "refs/heads/master"));
-    assertThat(preview).containsKey(new Branch.NameKey(p2, "refs/heads/master"));
+    assertThat(preview).containsKey(new Branch.NameKey(superKey, "refs/heads/master"));
+    assertThat(preview).containsKey(new Branch.NameKey(subKey, "refs/heads/master"));
 
     if ((getSubmitType() == SubmitType.CHERRY_PICK)
         || (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
@@ -176,12 +170,9 @@
 
   @Test
   public void subscriptionUpdateIncludingChangeInSuperproject() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     ObjectId subHEAD =
         subRepo
@@ -274,27 +265,27 @@
             .getAdvertisedRef("refs/heads/master")
             .getObjectId();
 
-    expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subRepoId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
   }
 
   @Test
   public void updateManySubmodules() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub1 = createProjectWithPush("sub1");
-    TestRepository<?> sub2 = createProjectWithPush("sub2");
-    TestRepository<?> sub3 = createProjectWithPush("sub3");
+    Project.NameKey subKey1 = createProjectForPush("sub1", null, true, getSubmitType());
+    Project.NameKey subKey2 = createProjectForPush("sub2", null, true, getSubmitType());
+    Project.NameKey subKey3 = createProjectForPush("sub3", null, true, getSubmitType());
 
-    allowMatchingSubmoduleSubscription(
-        "sub1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub2", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub3", "refs/heads/master", "super-project", "refs/heads/master");
+    TestRepository<?> sub1 = cloneProject(subKey1);
+    TestRepository<?> sub2 = cloneProject(subKey2);
+    TestRepository<?> sub3 = cloneProject(subKey3);
+
+    allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey2, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey3, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub1", "master");
-    prepareSubmoduleConfigEntry(config, "sub2", "master");
-    prepareSubmoduleConfigEntry(config, "sub3", "master");
+    prepareSubmoduleConfigEntry(config, subKey1, "master");
+    prepareSubmoduleConfigEntry(config, subKey2, "master");
+    prepareSubmoduleConfigEntry(config, subKey3, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
@@ -309,9 +300,9 @@
 
     gApi.changes().id(getChangeId(sub1, sub1Id).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey1, sub1, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey3, sub3, "master");
 
     String sub1HEAD =
         sub1.git()
@@ -346,15 +337,15 @@
           "master",
           "Update git submodules\n\n"
               + "* Update "
-              + name("sub1")
+              + subKey1.get()
               + " from branch 'master'\n  to "
               + sub1HEAD
               + "\n\n* Update "
-              + name("sub2")
+              + subKey2.get()
               + " from branch 'master'\n  to "
               + sub2HEAD
               + "\n\n* Update "
-              + name("sub3")
+              + subKey3.get()
               + " from branch 'master'\n  to "
               + sub3HEAD);
     }
@@ -374,73 +365,74 @@
 
   @Test
   public void doNotUseFastForward() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project", false);
-    TestRepository<?> sub = createProjectWithPush("sub", false);
+    // like setup, but without empty commit
+    superKey = createProjectForPush("super-nc", null, false, getSubmitType());
+    subKey = createProjectForPush("sub-nc", null, false, getSubmitType());
+    superRepo = cloneProject(superKey);
+    subRepo = cloneProject(subKey);
 
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "sub", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
 
     ObjectId superId = pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
 
-    String subChangeId = getChangeId(sub, subId).get();
+    String subChangeId = getChangeId(subRepo, subId).get();
     approve(subChangeId);
     approve(getChangeId(superRepo, superId).get());
 
     gApi.changes().id(subChangeId).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
-    RevCommit superHead = getRemoteHead(name("super-project"), "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
+    RevCommit superHead = getRemoteHead(superKey, "master");
     assertThat(superHead.getShortMessage()).contains("some message");
     assertThat(superHead.getId()).isNotEqualTo(superId);
   }
 
   @Test
   public void useFastForwardWhenNoSubmodule() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project", false);
-    TestRepository<?> sub = createProjectWithPush("sub", false);
+    // like setup, but without empty commit
+    superKey = createProjectForPush("super-nc", null, false, getSubmitType());
+    subKey = createProjectForPush("sub-nc", null, false, getSubmitType());
+    superRepo = cloneProject(superKey);
+    subRepo = cloneProject(subKey);
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
-
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
     ObjectId superId = pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
 
-    String subChangeId = getChangeId(sub, subId).get();
+    String subChangeId = getChangeId(subRepo, subId).get();
     approve(subChangeId);
     approve(getChangeId(superRepo, superId).get());
 
     gApi.changes().id(subChangeId).current().submit();
 
-    RevCommit superHead = getRemoteHead(name("super-project"), "master");
+    RevCommit superHead = getRemoteHead(superKey, "master");
     assertThat(superHead.getShortMessage()).isEqualTo("some message");
     assertThat(superHead.getId()).isEqualTo(superId);
   }
 
   @Test
   public void sameProjectSameBranchDifferentPaths() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub = createProjectWithPush("sub");
-
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub", "master");
-    prepareSubmoduleConfigEntry(config, "sub", "sub-copy", "master");
+    prepareSubmoduleConfigEntry(config, subKey, "master");
+    Project.NameKey copyKey = nameKey("sub-copy");
+    prepareSubmoduleConfigEntry(config, subKey, copyKey, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "");
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "");
 
-    approve(getChangeId(sub, subId).get());
+    approve(getChangeId(subRepo, subId).get());
 
-    gApi.changes().id(getChangeId(sub, subId).get()).current().submit();
+    gApi.changes().id(getChangeId(subRepo, subId).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub-copy", sub, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master", copyKey, subRepo, "master");
 
     superRepo
         .git()
@@ -457,37 +449,33 @@
 
   @Test
   public void sameProjectDifferentBranchDifferentPaths() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub = createProjectWithPush("sub");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/dev", superKey, "refs/heads/master");
 
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/dev", "super-project", "refs/heads/master");
-
-    ObjectId devHead = pushChangeTo(sub, "dev");
+    ObjectId devHead = pushChangeTo(subRepo, "dev");
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub", "sub-master", "master");
-    prepareSubmoduleConfigEntry(config, "sub", "sub-dev", "dev");
+    prepareSubmoduleConfigEntry(config, subKey, nameKey("sub-master"), "master");
+    prepareSubmoduleConfigEntry(config, subKey, nameKey("sub-dev"), "dev");
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId subMasterId =
-        pushChangeTo(sub, "refs/for/master", "some message", "b.txt", "content b", "same-topic");
+        pushChangeTo(
+            subRepo, "refs/for/master", "some message", "b.txt", "content b", "same-topic");
 
-    sub.reset(devHead);
+    subRepo.reset(devHead);
     ObjectId subDevId =
         pushChangeTo(
-            sub, "refs/for/dev", "some message in dev", "b.txt", "content b", "same-topic");
+            subRepo, "refs/for/dev", "some message in dev", "b.txt", "content b", "same-topic");
 
-    approve(getChangeId(sub, subMasterId).get());
-    approve(getChangeId(sub, subDevId).get());
+    approve(getChangeId(subRepo, subMasterId).get());
+    approve(getChangeId(subRepo, subDevId).get());
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    gApi.changes().id(getChangeId(sub, subMasterId).get()).current().submit();
+    gApi.changes().id(getChangeId(subRepo, subMasterId).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub-master", sub, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub-dev", sub, "dev");
+    expectToHaveSubmoduleState(superRepo, "master", nameKey("sub-master"), subRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master", nameKey("sub-dev"), subRepo, "dev");
 
     superRepo
         .git()
@@ -504,29 +492,27 @@
 
   @Test
   public void nonSubmoduleInSameTopic() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub = createProjectWithPush("sub");
-    TestRepository<?> standAlone = createProjectWithPush("standalone");
+    Project.NameKey standaloneKey = createProjectForPush("standalone", null, true, getSubmitType());
+    TestRepository<?> standAlone = cloneProject(standaloneKey);
 
-    allowMatchingSubmoduleSubscription(
-        "sub", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
 
-    createSubmoduleSubscription(superRepo, "master", "sub", "master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
     ObjectId standAloneId =
         pushChangeTo(standAlone, "refs/for/master", "some message", "same-topic");
 
-    String subChangeId = getChangeId(sub, subId).get();
+    String subChangeId = getChangeId(subRepo, subId).get();
     String standAloneChangeId = getChangeId(standAlone, standAloneId).get();
     approve(subChangeId);
     approve(standAloneChangeId);
 
     gApi.changes().id(subChangeId).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
 
     ChangeStatus status = gApi.changes().id(standAloneChangeId).info().status;
     assertThat(status).isEqualTo(ChangeStatus.MERGED);
@@ -546,17 +532,18 @@
 
   @Test
   public void recursiveSubmodules() throws Exception {
-    TestRepository<?> topRepo = createProjectWithPush("top-project");
-    TestRepository<?> midRepo = createProjectWithPush("mid-project");
-    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+    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());
+    TestRepository<?> topRepo = cloneProject(topKey);
+    TestRepository<?> midRepo = cloneProject(midKey);
+    TestRepository<?> bottomRepo = cloneProject(botKey);
 
-    allowMatchingSubmoduleSubscription(
-        "mid-project", "refs/heads/master", "top-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "mid-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
 
-    createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
-    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+    createSubmoduleSubscription(topRepo, "master", midKey, "master");
+    createSubmoduleSubscription(midRepo, "master", botKey, "master");
 
     ObjectId bottomHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
     ObjectId topHead = pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
@@ -569,27 +556,27 @@
 
     gApi.changes().id(id1).current().submit();
 
-    expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
-    expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
+    expectToHaveSubmoduleState(midRepo, "master", botKey, bottomRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", midKey, midRepo, "master");
   }
 
   @Test
   public void triangleSubmodules() throws Exception {
-    TestRepository<?> topRepo = createProjectWithPush("top-project");
-    TestRepository<?> midRepo = createProjectWithPush("mid-project");
-    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+    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());
+    TestRepository<?> topRepo = cloneProject(topKey);
+    TestRepository<?> midRepo = cloneProject(midKey);
+    TestRepository<?> bottomRepo = cloneProject(botKey);
 
-    allowMatchingSubmoduleSubscription(
-        "mid-project", "refs/heads/master", "top-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "mid-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "top-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", topKey, "refs/heads/master");
 
-    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+    createSubmoduleSubscription(midRepo, "master", botKey, "master");
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "bottom-project", "master");
-    prepareSubmoduleConfigEntry(config, "mid-project", "master");
+    prepareSubmoduleConfigEntry(config, botKey, "master");
+    prepareSubmoduleConfigEntry(config, midKey, "master");
     pushSubmoduleConfig(topRepo, "master", config);
 
     ObjectId bottomHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
@@ -603,26 +590,26 @@
 
     gApi.changes().id(id1).current().submit();
 
-    expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
-    expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
-    expectToHaveSubmoduleState(topRepo, "master", "bottom-project", bottomRepo, "master");
+    expectToHaveSubmoduleState(midRepo, "master", botKey, bottomRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", midKey, midRepo, "master");
+    expectToHaveSubmoduleState(topRepo, "master", botKey, bottomRepo, "master");
   }
 
   private String prepareBranchCircularSubscription() throws Exception {
-    TestRepository<?> topRepo = createProjectWithPush("top-project");
-    TestRepository<?> midRepo = createProjectWithPush("mid-project");
-    TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+    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());
+    TestRepository<?> topRepo = cloneProject(topKey);
+    TestRepository<?> midRepo = cloneProject(midKey);
+    TestRepository<?> bottomRepo = cloneProject(botKey);
 
-    createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
-    createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
-    createSubmoduleSubscription(bottomRepo, "master", "top-project", "master");
+    createSubmoduleSubscription(midRepo, "master", botKey, "master");
+    createSubmoduleSubscription(topRepo, "master", midKey, "master");
+    createSubmoduleSubscription(bottomRepo, "master", topKey, "master");
 
-    allowMatchingSubmoduleSubscription(
-        "bottom-project", "refs/heads/master", "mid-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "mid-project", "refs/heads/master", "top-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "top-project", "refs/heads/master", "bottom-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(topKey, "refs/heads/master", botKey, "refs/heads/master");
 
     ObjectId bottomMasterHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "");
     String changeId = getChangeId(bottomRepo, bottomMasterHead).get();
@@ -649,19 +636,14 @@
 
   @Test
   public void projectCircularSubscriptionWholeTopic() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
-
-    allowMatchingSubmoduleSubscription(
-        "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "super-project", "refs/heads/dev", "subscribed-to-project", "refs/heads/dev");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");
 
     pushChangeTo(subRepo, "dev");
     pushChangeTo(superRepo, "dev");
 
-    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
-    createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    createSubmoduleSubscription(subRepo, "dev", superKey, "dev");
 
     ObjectId subMasterHead =
         pushChangeTo(
@@ -672,15 +654,15 @@
     approve(getChangeId(superRepo, superDevHead).get());
 
     exception.expectMessage("Project level circular subscriptions detected");
-    exception.expectMessage("subscribed-to-project");
-    exception.expectMessage("super-project");
+    exception.expectMessage(subKey.get());
+    exception.expectMessage(superKey.get());
     gApi.changes().id(getChangeId(subRepo, subMasterHead).get()).current().submit();
   }
 
   @Test
   public void projectNoSubscriptionWholeTopic() throws Exception {
-    TestRepository<?> repoA = createProjectWithPush("project-a");
-    TestRepository<?> repoB = createProjectWithPush("project-b");
+    TestRepository<?> repoA = createProjectWithPush("project-a", null, true, getSubmitType());
+    TestRepository<?> repoB = createProjectWithPush("project-b", null, true, getSubmitType());
     // bootstrap the dev branch
     ObjectId a0 = pushChangeTo(repoA, "dev");
 
@@ -747,8 +729,8 @@
 
   @Test
   public void twoProjectsMultipleBranchesWholeTopic() throws Exception {
-    TestRepository<?> repoA = createProjectWithPush("project-a");
-    TestRepository<?> repoB = createProjectWithPush("project-b");
+    TestRepository<?> repoA = createProjectWithPush("project-a", null, true, getSubmitType());
+    TestRepository<?> repoB = createProjectWithPush("project-b", null, true, getSubmitType());
     // bootstrap the dev branch
     pushChangeTo(repoA, "dev");
 
@@ -796,18 +778,17 @@
   public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
     assume().that(notesMigration.disableChangeReviewDb()).isTrue();
 
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub1 = createProjectWithPush("sub1");
-    TestRepository<?> sub2 = createProjectWithPush("sub2");
+    Project.NameKey subKey1 = createProjectForPush("sub1", null, true, getSubmitType());
+    TestRepository<?> sub1 = cloneProject(subKey1);
+    Project.NameKey subKey2 = createProjectForPush("sub2", null, true, getSubmitType());
+    TestRepository<?> sub2 = cloneProject(subKey2);
 
-    allowMatchingSubmoduleSubscription(
-        "sub1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub2", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey2, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub1", "master");
-    prepareSubmoduleConfigEntry(config, "sub2", "master");
+    prepareSubmoduleConfigEntry(config, subKey1, "master");
+    prepareSubmoduleConfigEntry(config, subKey2, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
@@ -837,20 +818,20 @@
 
     sub1.git().fetch().call();
     RevWalk rw1 = sub1.getRevWalk();
-    RevCommit master1 = rw1.parseCommit(getRemoteHead(name("sub1"), "master"));
+    RevCommit master1 = rw1.parseCommit(getRemoteHead(subKey1.get(), "master"));
     RevCommit change1Ps = parseCurrentRevision(rw1, changeId1);
     assertThat(rw1.isMergedInto(change1Ps, master1)).isTrue();
 
     sub2.git().fetch().call();
     RevWalk rw2 = sub2.getRevWalk();
-    RevCommit master2 = rw2.parseCommit(getRemoteHead(name("sub2"), "master"));
+    RevCommit master2 = rw2.parseCommit(getRemoteHead(subKey2.get(), "master"));
     RevCommit change2Ps = parseCurrentRevision(rw2, changeId2);
     assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue();
 
     assertThat(input.generateLockFailures).containsExactly(false);
 
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
-    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey1, sub1, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");
 
     assertWithMessage("submodule subscription update should have made one commit")
         .that(superRepo.getRepository().resolve("origin/master^"))
@@ -859,24 +840,23 @@
 
   @Test
   public void skipUpdatingBrokenGitlinkPointer() throws Exception {
-    TestRepository<?> superRepo = createProjectWithPush("super-project");
-    TestRepository<?> sub1 = createProjectWithPush("sub1");
-    TestRepository<?> sub2 = createProjectWithPush("sub2");
+    Project.NameKey subKey1 = createProjectForPush("sub1", null, true, getSubmitType());
+    TestRepository<?> sub1 = cloneProject(subKey1);
+    Project.NameKey subKey2 = createProjectForPush("sub2", null, true, getSubmitType());
+    TestRepository<?> sub2 = cloneProject(subKey2);
 
-    allowMatchingSubmoduleSubscription(
-        "sub1", "refs/heads/master", "super-project", "refs/heads/master");
-    allowMatchingSubmoduleSubscription(
-        "sub2", "refs/heads/master", "super-project", "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey2, "refs/heads/master", superKey, "refs/heads/master");
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, "sub1", "master");
-    prepareSubmoduleConfigEntry(config, "sub2", "master");
+    prepareSubmoduleConfigEntry(config, subKey1, "master");
+    prepareSubmoduleConfigEntry(config, subKey2, "master");
     pushSubmoduleConfig(superRepo, "master", config);
 
     // Write an invalid SHA-1 directly to one of the gitlinks.
     ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
-    directUpdateSubmodule("super-project", "refs/heads/master", "sub1", badId);
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", badId);
+    directUpdateSubmodule(superKey, "refs/heads/master", subKey1, badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey1, badId);
 
     String topic = "same-topic";
     ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic);
@@ -893,7 +873,7 @@
     assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED);
 
     // sub1 was skipped but sub2 succeeded.
-    expectToHaveSubmoduleState(superRepo, "master", "sub1", badId);
-    expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+    expectToHaveSubmoduleState(superRepo, "master", subKey1, badId);
+    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 0d5d2cd..29a5bd0 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -14,28 +14,17 @@
 
 package com.google.gerrit.acceptance.pgm;
 
-import com.google.gerrit.elasticsearch.ElasticContainer;
-import com.google.gerrit.elasticsearch.ElasticTestUtils;
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import static com.google.gerrit.elasticsearch.ElasticTestUtils.createAllIndexes;
+import static com.google.gerrit.elasticsearch.ElasticTestUtils.getConfig;
+
 import com.google.gerrit.elasticsearch.ElasticVersion;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Injector;
-import java.util.UUID;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 
 public class ElasticReindexIT extends AbstractReindexTests {
 
-  private static Config getConfig(ElasticVersion version) {
-    ElasticNodeInfo elasticNodeInfo;
-    ElasticContainer<?> container = ElasticContainer.createAndStart(version);
-    elasticNodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
-    String indicesPrefix = UUID.randomUUID().toString();
-    Config cfg = new Config();
-    ElasticTestUtils.configure(cfg, elasticNodeInfo.port, indicesPrefix, version);
-    return cfg;
-  }
-
   @ConfigSuite.Default
   public static Config elasticsearchV2() {
     return getConfig(ElasticVersion.V2_4);
@@ -48,12 +37,12 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV6() {
-    return getConfig(ElasticVersion.V6_4);
+    return getConfig(ElasticVersion.V6_5);
   }
 
   @Override
   public void configureIndex(Injector injector) throws Exception {
-    ElasticTestUtils.createAllIndexes(injector);
+    createAllIndexes(injector);
   }
 
   @Before
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 6c12dd6..6c0b707 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -196,13 +196,4 @@
     // user
     assertThat(i.user.anonymousCowardName).isEqualTo(AnonymousCowardNameProvider.DEFAULT);
   }
-
-  @Test
-  @GerritConfig(name = "auth.contributorAgreements", value = "true")
-  public void anonymousAccess() throws Exception {
-    configureContributorAgreement(true);
-
-    setApiUserAnonymous();
-    gApi.config().server().getInfo();
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 9d69955..1e60071 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -14,27 +14,16 @@
 
 package com.google.gerrit.acceptance.ssh;
 
-import com.google.gerrit.elasticsearch.ElasticContainer;
-import com.google.gerrit.elasticsearch.ElasticTestUtils;
-import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
+import static com.google.gerrit.elasticsearch.ElasticTestUtils.createAllIndexes;
+import static com.google.gerrit.elasticsearch.ElasticTestUtils.getConfig;
+
 import com.google.gerrit.elasticsearch.ElasticVersion;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Injector;
-import java.util.UUID;
 import org.eclipse.jgit.lib.Config;
 
 public class ElasticIndexIT extends AbstractIndexTests {
 
-  private static Config getConfig(ElasticVersion version) {
-    ElasticNodeInfo elasticNodeInfo;
-    ElasticContainer<?> container = ElasticContainer.createAndStart(version);
-    elasticNodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
-    String indicesPrefix = UUID.randomUUID().toString();
-    Config cfg = new Config();
-    ElasticTestUtils.configure(cfg, elasticNodeInfo.port, indicesPrefix, version);
-    return cfg;
-  }
-
   @ConfigSuite.Default
   public static Config elasticsearchV2() {
     return getConfig(ElasticVersion.V2_4);
@@ -47,11 +36,11 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV6() {
-    return getConfig(ElasticVersion.V6_4);
+    return getConfig(ElasticVersion.V6_5);
   }
 
   @Override
   public void configureIndex(Injector injector) throws Exception {
-    ElasticTestUtils.createAllIndexes(injector);
+    createAllIndexes(injector);
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 93e97c4..c3150f1 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -52,6 +52,8 @@
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.3.2";
       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";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index b46e040..9f7b60c 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -21,6 +21,7 @@
 import com.google.inject.TypeLiteral;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.UUID;
 import org.eclipse.jgit.lib.Config;
 
 public final class ElasticTestUtils {
@@ -55,6 +56,16 @@
     }
   }
 
+  public static Config getConfig(ElasticVersion version) {
+    ElasticNodeInfo elasticNodeInfo;
+    ElasticContainer<?> container = ElasticContainer.createAndStart(version);
+    elasticNodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
+    String indicesPrefix = UUID.randomUUID().toString();
+    Config cfg = new Config();
+    configure(cfg, elasticNodeInfo.port, indicesPrefix, version);
+    return cfg;
+  }
+
   private ElasticTestUtils() {
     // hide default constructor
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index b8154ce..eeb4c09 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_4);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 3445b36..7525b65 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_4);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index 851b27d..e8d5683 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_4);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/git/BUILD b/javatests/com/google/gerrit/git/BUILD
new file mode 100644
index 0000000..458bdf9
--- /dev/null
+++ b/javatests/com/google/gerrit/git/BUILD
@@ -0,0 +1,34 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+MEDIUM_TESTS = ["RefUpdateUtilRepoTest.java"]
+
+junit_tests(
+    name = "medium_tests",
+    size = "medium",
+    timeout = "short",
+    srcs = MEDIUM_TESTS,
+    tags = ["no_windows"],
+    deps = [
+        "//java/com/google/gerrit/git",
+        "//lib:guava",
+        "//lib:junit",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/truth",
+    ],
+)
+
+junit_tests(
+    name = "small_tests",
+    size = "small",
+    srcs = glob(
+        ["*.java"],
+        exclude = MEDIUM_TESTS,
+    ),
+    deps = [
+        "//java/com/google/gerrit/git",
+        "//lib:guava",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/truth",
+    ],
+)
diff --git a/javatests/com/google/gerrit/server/update/RefUpdateUtilRepoTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
similarity index 98%
rename from javatests/com/google/gerrit/server/update/RefUpdateUtilRepoTest.java
rename to javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
index fe9d522..99247b8 100644
--- a/javatests/com/google/gerrit/server/update/RefUpdateUtilRepoTest.java
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilRepoTest.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.update;
+package com.google.gerrit.git;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/javatests/com/google/gerrit/server/update/RefUpdateUtilTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
similarity index 97%
rename from javatests/com/google/gerrit/server/update/RefUpdateUtilTest.java
rename to javatests/com/google/gerrit/git/RefUpdateUtilTest.java
index fc8696a..fe40fb4 100644
--- a/javatests/com/google/gerrit/server/update/RefUpdateUtilTest.java
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
@@ -12,14 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.update;
+package com.google.gerrit.git;
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.server.git.LockFailureException;
 import java.io.IOException;
 import java.util.function.Consumer;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
diff --git a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 685f42d..689d786 100644
--- a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -41,7 +41,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.InMemoryDatabase;
@@ -80,7 +80,7 @@
 
   @Inject private InMemoryDatabase schemaFactory;
 
-  @Inject private SchemaCreator schemaCreator;
+  @Inject private ReviewDbSchemaCreator schemaCreator;
 
   @Inject private ThreadLocalRequestContext requestContext;
 
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 8749b7a..88edc2e 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -38,6 +38,7 @@
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/lifecycle",
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index eb6f189..982bd14 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -44,7 +44,7 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -71,7 +71,7 @@
   @Inject private LabelNormalizer norm;
   @Inject private MetaDataUpdate.User metaDataUpdateFactory;
   @Inject private ProjectCache projectCache;
-  @Inject private SchemaCreator schemaCreator;
+  @Inject private ReviewDbSchemaCreator schemaCreator;
   @Inject protected ThreadLocalRequestContext requestContext;
   @Inject private ChangeNotes.Factory changeNotesFactory;
   @Inject private ProjectConfig.Factory projectConfigFactory;
diff --git a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
index 7e12439..7ed6c50 100644
--- a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
@@ -22,10 +22,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestTimeUtil;
 import java.io.IOException;
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
index f3dd5d6..d02fa1b 100644
--- a/javatests/com/google/gerrit/server/group/db/BUILD
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -10,6 +10,7 @@
         "//java/com/google/gerrit/common/data/testing:common-data-test-util",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+        "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/group/db/testing",
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 42d01e2..7364c95 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.common.data.testing.GroupReferenceSubject;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.testing.CommitInfoSubject;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.GitTestUtil;
 import com.google.gerrit.testing.TestTimeUtil;
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index a21f5ba..64fd875 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -46,7 +46,7 @@
 
 public class RepoSequenceTest {
   // Don't sleep in tests.
-  private static final Retryer<RefUpdate.Result> RETRYER =
+  private static final Retryer<RefUpdate> RETRYER =
       RepoSequence.retryerBuilder().withBlockStrategy(t -> {}).build();
 
   @Rule public ExpectedException exception = ExpectedException.none();
@@ -200,11 +200,11 @@
             1,
             10,
             () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
-            RetryerBuilder.<RefUpdate.Result>newBuilder()
+            RetryerBuilder.<RefUpdate>newBuilder()
                 .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                 .build());
     exception.expect(OrmException.class);
-    exception.expectMessage("failed to update refs/sequences/id: LOCK_FAILURE");
+    exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
     s.next();
   }
 
@@ -335,11 +335,11 @@
             1,
             10,
             () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
-            RetryerBuilder.<RefUpdate.Result>newBuilder()
+            RetryerBuilder.<RefUpdate>newBuilder()
                 .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                 .build());
     exception.expect(OrmException.class);
-    exception.expectMessage("failed to update refs/sequences/id: LOCK_FAILURE");
+    exception.expectMessage("Failed to update refs/sequences/id: LOCK_FAILURE");
     s.increaseTo(2);
   }
 
@@ -352,7 +352,7 @@
       final int start,
       int batchSize,
       Runnable afterReadRef,
-      Retryer<RefUpdate.Result> retryer) {
+      Retryer<RefUpdate> retryer) {
     return new RepoSequence(
         repoManager,
         GitReferenceUpdated.DISABLED,
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 1d2d04c..38e9578 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -63,7 +63,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.project.testing.Util;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.InMemoryDatabase;
@@ -199,7 +199,7 @@
 
   @Inject private PermissionBackend permissionBackend;
   @Inject private CapabilityCollection.Factory capabilityCollectionFactory;
-  @Inject private SchemaCreator schemaCreator;
+  @Inject private ReviewDbSchemaCreator schemaCreator;
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private ThreadLocalRequestContext requestContext;
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 9d01405..26cc3f8 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -74,7 +74,7 @@
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.index.account.AccountIndexer;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
@@ -120,7 +120,7 @@
 
   @Inject protected InMemoryDatabase schemaFactory;
 
-  @Inject protected SchemaCreator schemaCreator;
+  @Inject protected ReviewDbSchemaCreator schemaCreator;
 
   @Inject protected ThreadLocalRequestContext requestContext;
 
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index f362e5a..de2234e 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -109,7 +109,7 @@
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -177,7 +177,7 @@
   @Inject protected PatchSetUtil psUtil;
   @Inject protected ChangeNotes.Factory changeNotesFactory;
   @Inject protected Provider<ChangeQueryProcessor> queryProcessorProvider;
-  @Inject protected SchemaCreator schemaCreator;
+  @Inject protected ReviewDbSchemaCreator schemaCreator;
   @Inject protected SchemaFactory<ReviewDb> schemaFactory;
   @Inject protected Sequences seq;
   @Inject protected ThreadLocalRequestContext requestContext;
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 750813a..7e4a0a4 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -52,7 +52,7 @@
 import com.google.gerrit.server.index.group.GroupField;
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
@@ -91,7 +91,7 @@
 
   @Inject protected InMemoryDatabase schemaFactory;
 
-  @Inject protected SchemaCreator schemaCreator;
+  @Inject protected ReviewDbSchemaCreator schemaCreator;
 
   @Inject protected ThreadLocalRequestContext requestContext;
 
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 420c323..3219dfd 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -50,7 +50,7 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.schema.ReviewDbSchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.RequestContext;
@@ -88,7 +88,7 @@
 
   @Inject protected InMemoryDatabase schemaFactory;
 
-  @Inject protected SchemaCreator schemaCreator;
+  @Inject protected ReviewDbSchemaCreator schemaCreator;
 
   @Inject protected ThreadLocalRequestContext requestContext;
 
diff --git a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java b/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
index bf6953a..6a8a55a 100644
--- a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
+++ b/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
@@ -37,7 +38,6 @@
 import com.google.gerrit.server.group.db.AuditLogFormatter;
 import com.google.gerrit.server.group.db.AuditLogReader;
 import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.GitTestUtil;
diff --git a/javatests/com/google/gerrit/server/schema/SchemaCreatorTest.java b/javatests/com/google/gerrit/server/schema/ReviewDbSchemaCreatorTest.java
similarity index 96%
rename from javatests/com/google/gerrit/server/schema/SchemaCreatorTest.java
rename to javatests/com/google/gerrit/server/schema/ReviewDbSchemaCreatorTest.java
index 9cb9333..bb358cc 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/javatests/com/google/gerrit/server/schema/ReviewDbSchemaCreatorTest.java
@@ -31,7 +31,6 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.File;
-import java.io.IOException;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.ArrayList;
@@ -43,7 +42,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class SchemaCreatorTest {
+public class ReviewDbSchemaCreatorTest {
   @Inject private AllProjectsName allProjects;
 
   @Inject private GitRepositoryManager repoManager;
@@ -63,7 +62,7 @@
   }
 
   @Test
-  public void getCauses_CreateSchema() throws OrmException, SQLException, IOException {
+  public void getCauses_CreateSchema() throws OrmException, SQLException {
     // Initially the schema should be empty.
     String[] types = {"TABLE", "VIEW"};
     try (JdbcSchema d = (JdbcSchema) db.open();
@@ -82,7 +81,6 @@
     if (sitePath.getName().equals(".")) {
       sitePath = sitePath.getParentFile();
     }
-    assertThat(db.getSystemConfig().sitePath).isEqualTo(sitePath.getCanonicalPath());
   }
 
   private LabelTypes getLabelTypes() throws Exception {
diff --git a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/ReviewDbSchemaUpdaterTest.java
similarity index 93%
rename from javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
rename to javatests/com/google/gerrit/server/schema/ReviewDbSchemaUpdaterTest.java
index c4844b1..4133558 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/ReviewDbSchemaUpdaterTest.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -56,7 +55,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class SchemaUpdaterTest {
+public class ReviewDbSchemaUpdaterTest {
   private LifecycleManager lifecycle;
   private InMemoryDatabase db;
 
@@ -81,7 +80,7 @@
 
     final Path site = Paths.get(UUID.randomUUID().toString());
     final SitePaths paths = new SitePaths(site);
-    SchemaUpdater u =
+    ReviewDbSchemaUpdater u =
         Guice.createInjector(
                 new FactoryModule() {
                   @Override
@@ -126,9 +125,11 @@
                     bind(MetricMaker.class).to(DisabledMetricMaker.class);
                   }
                 })
-            .getInstance(SchemaUpdater.class);
+            .getInstance(ReviewDbSchemaUpdater.class);
 
-    for (SchemaVersion s = u.getLatestSchemaVersion(); s.getVersionNbr() > 1; s = s.getPrior()) {
+    for (ReviewDbSchemaVersion s = u.getLatestSchemaVersion();
+        s.getVersionNbr() > 1;
+        s = s.getPrior()) {
       try {
         assertThat(s.getPrior().getVersionNbr())
             .named(
@@ -146,7 +147,5 @@
     u.update(new TestUpdateUI());
 
     db.assertSchemaVersion();
-    final SystemConfig sc = db.getSystemConfig();
-    assertThat(sc.sitePath).isEqualTo(paths.site_path.toAbsolutePath().toString());
   }
 }
diff --git a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java
index 57689b3..f17550e 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java
@@ -206,7 +206,7 @@
     }
   }
 
-  private void executeSchemaMigration(SchemaVersion schema) throws Exception {
+  private void executeSchemaMigration(ReviewDbSchemaVersion schema) throws Exception {
     schema.migrateData(db, new TestUpdateUI());
   }
 
diff --git a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
index 78fef39..890ae32 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
@@ -920,7 +920,7 @@
     }
   }
 
-  private void executeSchemaMigration(SchemaVersion schema, AccountGroup... groupsToVerify)
+  private void executeSchemaMigration(ReviewDbSchemaVersion schema, AccountGroup... groupsToVerify)
       throws Exception {
     executeSchemaMigration(
         schema,
@@ -929,7 +929,7 @@
             .toArray(AccountGroup.UUID[]::new));
   }
 
-  private void executeSchemaMigration(SchemaVersion schema, GroupInfo... groupsToVerify)
+  private void executeSchemaMigration(ReviewDbSchemaVersion schema, GroupInfo... groupsToVerify)
       throws Exception {
     executeSchemaMigration(
         schema,
@@ -938,8 +938,8 @@
             .toArray(AccountGroup.UUID[]::new));
   }
 
-  private void executeSchemaMigration(SchemaVersion schema, AccountGroup.UUID... groupsToVerify)
-      throws Exception {
+  private void executeSchemaMigration(
+      ReviewDbSchemaVersion schema, AccountGroup.UUID... groupsToVerify) throws Exception {
     List<GroupBundle> reviewDbBundles = new ArrayList<>();
     for (AccountGroup.UUID groupUuid : groupsToVerify) {
       reviewDbBundles.add(GroupBundle.Factory.fromReviewDb(db, groupUuid));
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index cef3d63..9117e85 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -1,30 +1,9 @@
 load("//tools/bzl:junit.bzl", "junit_tests")
 
-MEDIUM_TESTS = ["RefUpdateUtilRepoTest.java"]
-
-junit_tests(
-    name = "medium_tests",
-    size = "medium",
-    timeout = "short",
-    srcs = MEDIUM_TESTS,
-    tags = ["no_windows"],
-    deps = [
-        "//java/com/google/gerrit/server",
-        "//lib:guava",
-        "//lib:junit",
-        "//lib/jgit/org.eclipse.jgit:jgit",
-        "//lib/jgit/org.eclipse.jgit.junit:junit",
-        "//lib/truth",
-    ],
-)
-
 junit_tests(
     name = "small_tests",
     size = "small",
-    srcs = glob(
-        ["*.java"],
-        exclude = MEDIUM_TESTS,
-    ),
+    srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/lifecycle",
diff --git a/plugins/BUILD b/plugins/BUILD
index 137be6e..1ce0fba7 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -31,6 +31,7 @@
     "//java/com/google/gerrit/common:annotations",
     "//java/com/google/gerrit/common:server",
     "//java/com/google/gerrit/extensions:api",
+    "//java/com/google/gerrit/git",
     "//java/com/google/gerrit/index",
     "//java/com/google/gerrit/index:query_exception",
     "//java/com/google/gerrit/lifecycle",
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 131f578..041ac8d 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 131f5782a6723d1e017f53cf4e56ff363263b076
+Subproject commit 041ac8d9fb0c0e5adff0aa37773a1931aced5a9a
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 5fa8187..6ce1a09 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -56,7 +56,6 @@
     <style include="gr-form-styles"></style>
     <main class="gr-form-styles read-only">
       <style include="shared-styles"></style>
-      <style include="dashboard-header-styles"></style>
       <div class="info">
         <h1 id="Title" class$="name">
           [[repo]]
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 6443095..77a1e2a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -307,9 +307,9 @@
         commands.push({
           title,
           command: commandObj[title]
-              .replace('${project}', repo)
+              .replace('${project}', encodeURI(repo))
               .replace('${project-base-name}',
-              repo.substring(repo.lastIndexOf('/') + 1)),
+              encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
         });
       }
       return commands;
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
index 86e3903..715ddc0 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -126,13 +126,15 @@
       let name;
       let value;
       const generateStatusStr = function(account) {
-        return account.status ? ' (' + account.status + ')' : '';
+        return account.status ? '(' + account.status + ')' : '';
       };
       if (reviewer.account) {
         // Reviewer is an account suggestion from getChangeSuggestedReviewers.
         const reviewerName = this._accountOrAnon(reviewer.account);
-        name = reviewerName + ' <' + reviewer.account.email + '>' +
-            generateStatusStr(reviewer.account);
+        const reviewerEmail = this._reviewerEmail(reviewer.account.email);
+        const reviewerStatus = generateStatusStr(reviewer.account);
+        name = [reviewerName, reviewerEmail, reviewerStatus]
+            .filter(p => p.length > 0).join(' ');
         value = reviewer;
       } else if (reviewer.group) {
         // Reviewer is a group suggestion from getChangeSuggestedReviewers.
@@ -141,8 +143,10 @@
       } else if (reviewer._account_id) {
         // Reviewer is an account suggestion from getSuggestedAccounts.
         const reviewerName = this._accountOrAnon(reviewer);
-        name = reviewerName + ' <' + reviewer.email + '>' +
-            generateStatusStr(reviewer);
+        const reviewerEmail = this._reviewerEmail(reviewer.email);
+        const reviewerStatus = generateStatusStr(reviewer);
+        name = [reviewerName, reviewerEmail, reviewerStatus]
+            .filter(p => p.length > 0).join(' ');
         value = {account: reviewer, count: 1};
       }
       return {name, value};
@@ -168,5 +172,13 @@
             .map(this._makeSuggestion.bind(this));
       });
     },
+
+    _reviewerEmail(email) {
+      if (typeof email !== 'undefined') {
+        return '<' + email + '>';
+      }
+
+      return '';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
index 03a0be8..20d127d 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -56,6 +56,15 @@
         status: opt_status,
       };
     };
+    let _nextAccountId3 = 0;
+    const makeAccount3 = function(opt_status) {
+      const accountId3 = ++_nextAccountId3;
+      return {
+        _account_id: accountId3,
+        name: 'name ' + accountId3,
+        status: opt_status,
+      };
+    };
 
     let owner;
     let existingReviewer1;
@@ -115,6 +124,7 @@
       test('_makeSuggestion formats account or group accordingly', () => {
         let account = makeAccount();
         const account2 = makeAccount2();
+        const account3 = makeAccount3();
         let suggestion = element._makeSuggestion({account});
         assert.deepEqual(suggestion, {
           name: account.name + ' <' + account.email + '>',
@@ -154,6 +164,22 @@
           name: account.name + ' <' + account.email + '> (OOO)',
           value: {account, count: 1},
         });
+
+        sandbox.stub(element, '_reviewerEmail',
+            () => { return ''; });
+
+        suggestion = element._makeSuggestion(account3);
+        assert.deepEqual(suggestion, {
+          name: account3.name,
+          value: {account: account3, count: 1},
+        });
+      });
+
+      test('_reviewerEmail', () => {
+        assert.equal(
+            element._reviewerEmail('email@gerritreview.com'),
+            '<email@gerritreview.com>');
+        assert.equal(element._reviewerEmail(undefined), '');
       });
 
       test('_getReviewerSuggestions excludes owner+reviewers', done => {
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 420a14f..6aea35b 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
@@ -51,16 +51,6 @@
     (function() {
       'use strict';
 
-      const Defs = {};
-
-      /**
-       * @typedef {{
-       *  number: number,
-       *  leftSide: {boolean}
-       * }}
-       */
-      Defs.LineOfInterest;
-
       const DiffViewMode = {
         SIDE_BY_SIDE: 'SIDE_BY_SIDE',
         UNIFIED: 'UNIFIED_DIFF',
@@ -115,10 +105,6 @@
           parentIndex: Number,
           path: String,
           projectName: String,
-          /**
-           * @type {Defs.LineOfInterest|null}
-           */
-          lineOfInterest: Object,
 
           _builder: Object,
           _groups: Array,
@@ -127,6 +113,7 @@
           /** @type {!Array<!Gerrit.HoveredRange>} */
           commentRanges: {
             type: Array,
+            value: () => [],
           },
         },
 
@@ -161,7 +148,7 @@
           this._layers = layers;
         },
 
-        render(comments, prefs) {
+        render(keyLocations, prefs) {
           this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
           this._showTabs = !!prefs.show_tabs;
           this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
@@ -172,8 +159,7 @@
           this._builder = this._getDiffBuilder(this.diff, prefs);
 
           this.$.processor.context = prefs.context;
-          this.$.processor.keyLocations = this._getKeyLocations(comments,
-              this.lineOfInterest);
+          this.$.processor.keyLocations = keyLocations;
 
           this._clearDiffContent();
           this._builder.addColumns(this.diffElement, prefs.font_size);
@@ -332,33 +318,6 @@
           this.diffElement.innerHTML = null;
         },
 
-        /**
-         * @param {!Object} comments
-         * @param {Defs.LineOfInterest|null} lineOfInterest
-         */
-        _getKeyLocations(comments, lineOfInterest) {
-          const result = {
-            left: {},
-            right: {},
-          };
-          for (const side in comments) {
-            if (side !== GrDiffBuilder.Side.LEFT &&
-                side !== GrDiffBuilder.Side.RIGHT) {
-              continue;
-            }
-            for (const c of comments[side]) {
-              result[side][c.line || GrDiffLine.FILE] = true;
-            }
-          }
-
-          if (lineOfInterest) {
-            const side = lineOfInterest.leftSide ? 'left' : 'right';
-            result[side][lineOfInterest.number] = true;
-          }
-
-          return result;
-        },
-
         _groupsChanged(changeRecord) {
           if (!changeRecord) { return; }
           for (const splice of changeRecord.indexSplices) {
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 fd74d55..c7a1140 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
@@ -396,33 +396,6 @@
         `Fix in diff preferences`);
     });
 
-    test('_getKeyLocations', () => {
-      assert.deepEqual(element._getKeyLocations({left: [], right: []}, null),
-          {left: {}, right: {}});
-      const comments = {
-        left: [{line: 123}, {}],
-        right: [{line: 456}],
-      };
-      assert.deepEqual(element._getKeyLocations(comments, null), {
-        left: {FILE: true, 123: true},
-        right: {456: true},
-      });
-
-      const lineOfInterest = {number: 789, leftSide: true};
-      assert.deepEqual(
-          element._getKeyLocations(comments, lineOfInterest), {
-            left: {FILE: true, 123: true, 789: true},
-            right: {456: true},
-          });
-
-      delete lineOfInterest.leftSide;
-      assert.deepEqual(
-          element._getKeyLocations(comments, lineOfInterest), {
-            left: {FILE: true, 123: true},
-            right: {456: true, 789: true},
-          });
-    });
-
     suite('_isTotal', () => {
       test('is total for add', () => {
         const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
@@ -852,7 +825,7 @@
 
     suite('rendering text, images and binary files', () => {
       let processStub;
-      let comments;
+      let keyLocations;
       let prefs;
       let content;
 
@@ -862,7 +835,7 @@
         processStub = sandbox.stub(element.$.processor, 'process')
             .returns(Promise.resolve());
         sandbox.stub(element, '_anyLineTooLong').returns(true);
-        comments = {left: [], right: [], meta: {patchRange: undefined}};
+        keyLocations = {left: {}, right: {}};
         prefs = {
           line_length: 10,
           show_tabs: true,
@@ -883,7 +856,7 @@
 
       test('text', () => {
         element.diff = {content};
-        return element.render(comments, prefs).then(() => {
+        return element.render(keyLocations, prefs).then(() => {
           assert.isTrue(processStub.calledOnce);
           assert.isFalse(processStub.lastCall.args[1]);
         });
@@ -892,7 +865,7 @@
       test('image', () => {
         element.diff = {content, binary: true};
         element.isImageDiff = true;
-        return element.render(comments, prefs).then(() => {
+        return element.render(keyLocations, prefs).then(() => {
           assert.isTrue(processStub.calledOnce);
           assert.isTrue(processStub.lastCall.args[1]);
         });
@@ -900,7 +873,7 @@
 
       test('binary', () => {
         element.diff = {content, binary: true};
-        return element.render(comments, prefs).then(() => {
+        return element.render(keyLocations, prefs).then(() => {
           assert.isTrue(processStub.calledOnce);
           assert.isTrue(processStub.lastCall.args[1]);
         });
@@ -910,7 +883,7 @@
     suite('rendering', () => {
       let content;
       let outputEl;
-      let comments;
+      let keyLocations;
 
       setup(done => {
         const prefs = {
@@ -938,7 +911,7 @@
         });
         element = fixture('basic');
         outputEl = element.queryEffectiveChildren('#diffTable');
-        comments = {left: [], right: [], meta: {patchRange: undefined}};
+        keyLocations = {left: {}, right: {}};
         sandbox.stub(element, '_getDiffBuilder', () => {
           const builder = new GrDiffBuilder({content}, [], prefs, outputEl);
           sandbox.stub(builder, 'addColumns');
@@ -952,7 +925,7 @@
           return builder;
         });
         element.diff = {content};
-        element.render(comments, prefs).then(done);
+        element.render(keyLocations, prefs).then(done);
       });
 
       test('reporting', done => {
@@ -977,7 +950,7 @@
       });
 
       test('addColumns is called', done => {
-        element.render(comments, {}).then(done);
+        element.render(keyLocations, {}).then(done);
         assert.isTrue(element._builder.addColumns.called);
       });
 
@@ -1001,7 +974,7 @@
 
       test('render-start and render are fired', done => {
         const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
-        element.render(comments, {}).then(() => {
+        element.render(keyLocations, {}).then(() => {
           const firedEventTypes = dispatchEventStub.getCalls()
               .map(c => { return c.args[0].type; });
           assert.include(firedEventTypes, 'render-start');
@@ -1029,7 +1002,7 @@
           context: -1,
           syntax_highlighting: true,
         };
-        element.render(comments, prefs);
+        element.render(keyLocations, prefs);
       });
 
       test('cancel', () => {
@@ -1046,7 +1019,7 @@
       let builder;
       let diff;
       let prefs;
-      let comments;
+      let keyLocations;
 
       setup(done => {
         element = fixture('mock-diff');
@@ -1058,9 +1031,9 @@
           show_tabs: true,
           tab_size: 4,
         };
-        comments = {left: [], right: [], meta: {patchRange: undefined}};
+        keyLocations = {left: {}, right: {}};
 
-        element.render(comments, prefs).then(() => {
+        element.render(keyLocations, prefs).then(() => {
           builder = element._builder;
           done();
         });
@@ -1170,7 +1143,7 @@
       test('_getNextContentOnSide unified left', done => {
         // Re-render as unified:
         element.viewMode = 'UNIFIED_DIFF';
-        element.render(comments, prefs).then(() => {
+        element.render(keyLocations, prefs).then(() => {
           builder = element._builder;
 
           const startElem = builder.getContentByLine(5, 'left',
@@ -1190,7 +1163,7 @@
       test('_getNextContentOnSide unified right', done => {
         // Re-render as unified:
         element.viewMode = 'UNIFIED_DIFF';
-        element.render(comments, prefs).then(() => {
+        element.render(keyLocations, prefs).then(() => {
           builder = element._builder;
 
           const startElem = builder.getContentByLine(5, 'right',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
index d335e7a..3eb7b37 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
@@ -37,7 +37,6 @@
         commit-range="[[commitRange]]"
         hidden$="[[hidden]]"
         no-render-on-prefs-change="[[noRenderOnPrefsChange]]"
-        comments="[[comments]]"
         line-wrapping="[[lineWrapping]]"
         view-mode="[[viewMode]]"
         line-of-interest="[[lineOfInterest]]"
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 814c7268..5839141 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
@@ -199,7 +199,7 @@
 
       _threadEls: {
         type: Array,
-        value: [],
+        value: () => [],
       },
     },
 
@@ -208,7 +208,16 @@
     ],
 
     listeners: {
+      // These are named inconsistently for a reason:
+      // The create-comment event is fired to indicate that we should
+      // create a comment.
+      // The comment-* events are just notifying that the comments did already
+      // change in some way, and that we should update any models we may want
+      // to keep in sync.
       'create-comment': '_handleCreateComment',
+      'comment-discard': '_handleCommentDiscard',
+      'comment-update': '_handleCommentUpdate',
+      'comment-save': '_handleCommentSave',
     },
 
     observers: [
@@ -723,5 +732,76 @@
           this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
     },
 
+    _handleCommentSave(e) {
+      const comment = e.detail.comment;
+      const side = e.detail.comment.__commentSide;
+      const idx = this._findDraftIndex(comment, side);
+      this.set(['comments', side, idx], comment);
+      this._handleCommentSaveOrDiscard();
+    },
+
+    _handleCommentDiscard(e) {
+      const comment = e.detail.comment;
+      this._removeComment(comment);
+      this._handleCommentSaveOrDiscard();
+    },
+
+    /**
+     * Closure annotation for Polymer.prototype.push is off. Submitted PR:
+     * https://github.com/Polymer/polymer/pull/4776
+     * but for not supressing annotations.
+     *
+     * @suppress {checkTypes}
+     */
+    _handleCommentUpdate(e) {
+      const comment = e.detail.comment;
+      const side = e.detail.comment.__commentSide;
+      let idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
+      }
+      if (idx !== -1) { // Update draft or comment.
+        this.set(['comments', side, idx], comment);
+      } else { // Create new draft.
+        this.push(['comments', side], comment);
+      }
+    },
+
+    _handleCommentSaveOrDiscard() {
+      this.dispatchEvent(new CustomEvent('diff-comments-modified',
+          {bubbles: true}));
+    },
+
+    _removeComment(comment) {
+      const side = comment.__commentSide;
+      this._removeCommentFromSide(comment, side);
+    },
+
+    _removeCommentFromSide(comment, side) {
+      let idx = this._findCommentIndex(comment, side);
+      if (idx === -1) {
+        idx = this._findDraftIndex(comment, side);
+      }
+      if (idx !== -1) {
+        this.splice('comments.' + side, idx, 1);
+      }
+    },
+
+    /** @return {number} */
+    _findCommentIndex(comment, side) {
+      if (!comment.id || !this.comments[side]) {
+        return -1;
+      }
+      return this.comments[side].findIndex(item => item.id === comment.id);
+    },
+
+    /** @return {number} */
+    _findDraftIndex(comment, side) {
+      if (!comment.__draftID || !this.comments[side]) {
+        return -1;
+      }
+      return this.comments[side].findIndex(
+          item => item.__draftID === comment.__draftID);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index 423bdc6..5344256 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -46,15 +46,195 @@
         async getLoggedIn() { return getLoggedIn; },
       });
       element = fixture('basic');
-      // For reasons beyond me, fixture reuses elements, cleans out some
-      // stuff but not that list.
-      element._threadEls = [];
     });
 
     teardown(() => {
       sandbox.restore();
     });
 
+    suite('handle comment-update', () => {
+      setup(() => {
+        sandbox.stub(element, '_commentsChanged');
+        element.comments = {
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+            {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          ],
+          right: [
+            {id: 'c1', __commentSide: 'right'},
+            {id: 'c2', __commentSide: 'right'},
+            {id: 'd1', __draft: true, __commentSide: 'right'},
+            {id: 'd2', __draft: true, __commentSide: 'right'},
+          ],
+        };
+      });
+
+      test('creating a draft', () => {
+        const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
+          __commentSide: 'left'};
+        element.fire('comment-update', {comment});
+        assert.include(element.comments.left, comment);
+      });
+
+      test('discarding a draft', () => {
+        const draftID = 'tempID';
+        const id = 'savedID';
+        const comment = {
+          __draft: true,
+          __draftID: draftID,
+          side: 'PARENT',
+          __commentSide: 'left',
+        };
+        const diffCommentsModifiedStub = sandbox.stub();
+        element.addEventListener('diff-comments-modified',
+            diffCommentsModifiedStub);
+        element.comments.left.push(comment);
+        comment.id = id;
+        element.fire('comment-discard', {comment});
+        const drafts = element.comments.left.filter(item => {
+          return item.__draftID === draftID;
+        });
+        assert.equal(drafts.length, 0);
+        assert.isTrue(diffCommentsModifiedStub.called);
+      });
+
+      test('saving a draft', () => {
+        const draftID = 'tempID';
+        const id = 'savedID';
+        const comment = {
+          __draft: true,
+          __draftID: draftID,
+          side: 'PARENT',
+          __commentSide: 'left',
+        };
+        const diffCommentsModifiedStub = sandbox.stub();
+        element.addEventListener('diff-comments-modified',
+            diffCommentsModifiedStub);
+        element.comments.left.push(comment);
+        comment.id = id;
+        element.fire('comment-save', {comment});
+        const drafts = element.comments.left.filter(item => {
+          return item.__draftID === draftID;
+        });
+        assert.equal(drafts.length, 1);
+        assert.equal(drafts[0].id, id);
+        assert.isTrue(diffCommentsModifiedStub.called);
+      });
+    });
+
+    test('remove comment', () => {
+      sandbox.stub(element, '_commentsChanged');
+      element.comments = {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+          {id: 'd2', __draft: true, __commentSide: 'right'},
+        ],
+      };
+
+      element._removeComment({});
+      // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
+      // to believe that one object deepEquals another even when they do :-/.
+      assert.equal(JSON.stringify(element.comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+          {id: 'd2', __draft: true, __commentSide: 'right'},
+        ],
+      }));
+
+      element._removeComment({id: 'bc2', side: 'PARENT',
+        __commentSide: 'left'});
+      assert.deepEqual(element.comments, {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+          {id: 'd2', __draft: true, __commentSide: 'right'},
+        ],
+      });
+
+      element._removeComment({id: 'd2', __commentSide: 'right'});
+      assert.deepEqual(element.comments, {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+          {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+        ],
+        right: [
+          {id: 'c1', __commentSide: 'right'},
+          {id: 'c2', __commentSide: 'right'},
+          {id: 'd1', __draft: true, __commentSide: 'right'},
+        ],
+      });
+    });
+
     test('thread-discard handling', () => {
       const threads = [
         {comments: [{id: 4711}]},
@@ -685,12 +865,6 @@
       assert.equal(element.$.diff.noRenderOnPrefsChange, value);
     });
 
-    test('passes in comments', () => {
-      const value = {left: [], right: []};
-      element.comments = value;
-      assert.equal(element.$.diff.comments, value);
-    });
-
     test('passes in lineWrapping', () => {
       const value = true;
       element.lineWrapping = value;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index fd54af4..5a56069 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -1040,6 +1040,7 @@
     },
 
     _handleNextUnreviewedFile(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       this._setReviewed(true);
       // Ensure that the currently viewed file always appears in unreviewedFiles
       // so we resolve the right "next" file.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 862db10..e587953 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -293,8 +293,7 @@
               line-wrapping="[[lineWrapping]]"
               is-image-diff="[[isImageDiff]]"
               base-image="[[baseImage]]"
-              revision-image="[[revisionImage]]"
-              line-of-interest="[[lineOfInterest]]">
+              revision-image="[[revisionImage]]">
             <slot></slot>
             <table
                 id="diffTable"
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 f87e46f..a8cf320 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -35,6 +35,18 @@
     RIGHT: 'right',
   };
 
+  const Defs = {};
+
+  /**
+   * Special line number which should not be collapsed into a shared region.
+   *
+   * @typedef {{
+   *  number: number,
+   *  leftSide: boolean
+   * }}
+   */
+  Defs.LineOfInterest;
+
   const LARGE_DIFF_THRESHOLD_LINES = 10000;
   const FULL_CONTEXT = -1;
   const LIMITED_CONTEXT = 10;
@@ -62,12 +74,6 @@
      * @event show-auth-required
      */
 
-    /**
-     * Fired when a comment is saved or discarded
-     *
-     * @event diff-comments-modified
-     */
-
      /**
       * Fired when a comment is created
       *
@@ -101,14 +107,10 @@
         reflectToAttribute: true,
       },
       noRenderOnPrefsChange: Boolean,
-      comments: {
-        type: Object,
-        value: {left: [], right: []},
-      },
       /** @type {!Array<!Gerrit.HoveredRange>} */
       _commentRanges: {
         type: Array,
-        value: [],
+        value: () => [],
       },
       lineWrapping: {
         type: Boolean,
@@ -121,15 +123,24 @@
         observer: '_viewModeObserver',
       },
 
-      /**
-       * Special line number which should not be collapsed into a shared region.
-       * @type {{
-       *  number: number,
-       *  leftSide: {boolean}
-       * }|null}
-       */
+       /** @type ?Defs.LineOfInterest */
       lineOfInterest: Object,
 
+      /**
+       * The key locations based on the comments and line of interests,
+       * where lines should not be collapsed.
+       *
+       * @type {{left: Object<(string|number), number>,
+       *     right: Object<(string|number), number>}}
+       */
+      _keyLocations: {
+        type: Object,
+        value: () => ({
+          left: {},
+          right: {},
+        }),
+      },
+
       loading: {
         type: Boolean,
         value: false,
@@ -216,15 +227,12 @@
     ],
 
     listeners: {
-      'comment-discard': '_handleCommentDiscard',
-      'comment-update': '_handleCommentUpdate',
-      'comment-save': '_handleCommentSave',
       'create-range-comment': '_handleCreateRangeComment',
       'render-content': '_handleRenderContent',
     },
 
     attached() {
-      this._updateRangesWhenNodesChange();
+      this._observeNodes();
     },
 
     detached() {
@@ -232,25 +240,38 @@
       this._unobserveNodes();
     },
 
-    _updateRangesWhenNodesChange() {
+    _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);
+      });
+    },
+
+    _updateRanges(addedThreadEls) {
       function commentRangeFromThreadEl(threadEl) {
         const side = threadEl.getAttribute('comment-side');
         const range = JSON.parse(threadEl.getAttribute('range'));
         return {side, range, hovering: false};
       }
 
-      this._nodeObserver = Polymer.dom(this).observeNodes(info => {
-        const addedThreadEls = info.addedNodes.filter(isThreadEl);
-        const addedCommentRanges = addedThreadEls
-            .map(commentRangeFromThreadEl)
-            .filter(({range}) => range);
-        this.push('_commentRanges', ...addedCommentRanges);
-        // In principal 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.
-      });
+      const addedCommentRanges = addedThreadEls
+          .map(commentRangeFromThreadEl)
+          .filter(({range}) => range);
+      this.push('_commentRanges', ...addedCommentRanges);
+    },
+
+    _updateKeyLocations(addedThreadEls) {
+      for (const threadEl of addedThreadEls) {
+        const commentSide = threadEl.getAttribute('comment-side');
+        const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
+        this._keyLocations[commentSide][lineNum] = true;
+      }
     },
 
     /** Cancel any remaining diff builder rendering work. */
@@ -285,11 +306,6 @@
       }
     },
 
-    _handleCommentSaveOrDiscard() {
-      this.dispatchEvent(new CustomEvent('diff-comments-modified',
-          {bubbles: true}));
-    },
-
     /** @return {string} */
     _computeContainerClass(loggedIn, viewMode, displayLine) {
       const classes = ['diffContainer'];
@@ -484,75 +500,6 @@
       return side;
     },
 
-    _handleCommentDiscard(e) {
-      const comment = e.detail.comment;
-      this._removeComment(comment);
-      this._handleCommentSaveOrDiscard();
-    },
-
-    _removeComment(comment) {
-      const side = comment.__commentSide;
-      this._removeCommentFromSide(comment, side);
-    },
-
-    _handleCommentSave(e) {
-      const comment = e.detail.comment;
-      const side = e.detail.comment.__commentSide;
-      const idx = this._findDraftIndex(comment, side);
-      this.set(['comments', side, idx], comment);
-      this._handleCommentSaveOrDiscard();
-    },
-
-    /**
-     * Closure annotation for Polymer.prototype.push is off. Submitted PR:
-     * https://github.com/Polymer/polymer/pull/4776
-     * but for not supressing annotations.
-     *
-     * @suppress {checkTypes} */
-    _handleCommentUpdate(e) {
-      const comment = e.detail.comment;
-      const side = e.detail.comment.__commentSide;
-      let idx = this._findCommentIndex(comment, side);
-      if (idx === -1) {
-        idx = this._findDraftIndex(comment, side);
-      }
-      if (idx !== -1) { // Update draft or comment.
-        this.set(['comments', side, idx], comment);
-      } else { // Create new draft.
-        this.push(['comments', side], comment);
-      }
-    },
-
-    _removeCommentFromSide(comment, side) {
-      let idx = this._findCommentIndex(comment, side);
-      if (idx === -1) {
-        idx = this._findDraftIndex(comment, side);
-      }
-      if (idx !== -1) {
-        this.splice('comments.' + side, idx, 1);
-      }
-    },
-
-    /** @return {number} */
-    _findCommentIndex(comment, side) {
-      if (!comment.id || !this.comments[side]) {
-        return -1;
-      }
-      return this.comments[side].findIndex(item => {
-        return item.id === comment.id;
-      });
-    },
-
-    /** @return {number} */
-    _findDraftIndex(comment, side) {
-      if (!comment.__draftID || !this.comments[side]) {
-        return -1;
-      }
-      return this.comments[side].findIndex(item => {
-        return item.__draftID === comment.__draftID;
-      });
-    },
-
     _prefsObserver(newPrefs, oldPrefs) {
       // Scan the preference objects one level deep to see if they differ.
       let differ = !oldPrefs;
@@ -612,7 +559,7 @@
 
       this.updateStyles(stylesToUpdate);
 
-      if (this.diff && this.comments && !this.noRenderOnPrefsChange) {
+      if (this.diff && !this.noRenderOnPrefsChange) {
         this._renderDiffTable();
       }
     },
@@ -639,13 +586,18 @@
       }
 
       this._showWarning = false;
-      this.$.diffBuilder.render(this.comments, this._getBypassPrefs());
+
+      if (this.lineOfInterest) {
+        const side = this.lineOfInterest.leftSide ? 'left' : 'right';
+        this._keyLocations[side][this.lineOfInterest.number] = true;
+      }
+      this.$.diffBuilder.render(this._keyLocations, this._getBypassPrefs());
     },
 
     _handleRenderContent() {
       this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => {
         const addedThreadEls = info.addedNodes.filter(isThreadEl);
-        // In principal we should also handle removed nodes, but I have not
+        // 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
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 62284ad..99f2ded9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -192,107 +192,6 @@
             element.$$('.diffContainer').classList.contains('displayLine'));
       });
 
-      test('remove comment', () => {
-        element.comments = {
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-            {id: 'd2', __draft: true, __commentSide: 'right'},
-          ],
-        };
-
-        element._removeComment({});
-        // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
-        // to believe that one object deepEquals another even when they do :-/.
-        assert.equal(JSON.stringify(element.comments), JSON.stringify({
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-            {id: 'd2', __draft: true, __commentSide: 'right'},
-          ],
-        }));
-
-        element._removeComment({id: 'bc2', side: 'PARENT',
-          __commentSide: 'left'});
-        assert.deepEqual(element.comments, {
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-            {id: 'd2', __draft: true, __commentSide: 'right'},
-          ],
-        });
-
-        element._removeComment({id: 'd2', __commentSide: 'right'});
-        assert.deepEqual(element.comments, {
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-          ],
-          right: [
-            {id: 'c1', __commentSide: 'right'},
-            {id: 'c2', __commentSide: 'right'},
-            {id: 'd1', __draft: true, __commentSide: 'right'},
-          ],
-        });
-      });
-
       test('thread groups', () => {
         const contentEl = document.createElement('div');
 
@@ -333,11 +232,6 @@
           };
 
           element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
-          element.comments = {
-            left: [],
-            right: [],
-            meta: {patchRange: undefined},
-          };
           element.isImageDiff = true;
           element.prefs = {
             auto_hide_diff_table_header: true,
@@ -663,11 +557,6 @@
         const setupDiff = function() {
           const mock = document.createElement('mock-diff-response');
           element.diff = mock.diffResponse;
-          element.comments = {
-            left: [],
-            right: [],
-            meta: {patchRange: undefined},
-          };
           element.prefs = {
             context: 10,
             tab_size: 8,
@@ -766,29 +655,6 @@
             change_type: 'MODIFIED',
             content: [{skip: 66}],
           };
-          element.comments = {
-            meta: {
-              changeNum: '42',
-              patchRange: {
-                basePatchNum: 'PARENT',
-                patchNum: 3,
-              },
-              path: '/path/to/foo',
-              projectConfig: {foo: 'bar'},
-            },
-            left: [
-              {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            ],
-            right: [
-              {id: 'c1', __commentSide: 'right'},
-              {id: 'c2', __commentSide: 'right'},
-              {id: 'd1', __draft: true, __commentSide: 'right'},
-              {id: 'd2', __draft: true, __commentSide: 'right'},
-            ],
-          };
         });
 
         test('change in preferences re-renders diff', () => {
@@ -807,86 +673,6 @@
           assert.isFalse(element._renderDiffTable.called);
         });
       });
-
-      suite('handle comment-update', () => {
-        setup(() => {
-          element.comments = {
-            meta: {
-              changeNum: '42',
-              patchRange: {
-                basePatchNum: 'PARENT',
-                patchNum: 3,
-              },
-              path: '/path/to/foo',
-              projectConfig: {foo: 'bar'},
-            },
-            left: [
-              {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
-              {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
-            ],
-            right: [
-              {id: 'c1', __commentSide: 'right'},
-              {id: 'c2', __commentSide: 'right'},
-              {id: 'd1', __draft: true, __commentSide: 'right'},
-              {id: 'd2', __draft: true, __commentSide: 'right'},
-            ],
-          };
-        });
-
-        test('creating a draft', () => {
-          const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
-            __commentSide: 'left'};
-          element.fire('comment-update', {comment});
-          assert.include(element.comments.left, comment);
-        });
-
-        test('discarding a draft', () => {
-          const draftID = 'tempID';
-          const id = 'savedID';
-          const comment = {
-            __draft: true,
-            __draftID: draftID,
-            side: 'PARENT',
-            __commentSide: 'left',
-          };
-          const diffCommentsModifiedStub = sandbox.stub();
-          element.addEventListener('diff-comments-modified',
-              diffCommentsModifiedStub);
-          element.comments.left.push(comment);
-          comment.id = id;
-          element.fire('comment-discard', {comment});
-          const drafts = element.comments.left.filter(item => {
-            return item.__draftID === draftID;
-          });
-          assert.equal(drafts.length, 0);
-          assert.isTrue(diffCommentsModifiedStub.called);
-        });
-
-        test('saving a draft', () => {
-          const draftID = 'tempID';
-          const id = 'savedID';
-          const comment = {
-            __draft: true,
-            __draftID: draftID,
-            side: 'PARENT',
-            __commentSide: 'left',
-          };
-          const diffCommentsModifiedStub = sandbox.stub();
-          element.addEventListener('diff-comments-modified',
-              diffCommentsModifiedStub);
-          element.comments.left.push(comment);
-          comment.id = id;
-          element.fire('comment-save', {comment});
-          const drafts = element.comments.left.filter(item => {
-            return item.__draftID === draftID;
-          });
-          assert.equal(drafts.length, 1);
-          assert.equal(drafts[0].id, id);
-          assert.isTrue(diffCommentsModifiedStub.called);
-        });
-      });
     });
 
     suite('diff header', () => {
@@ -946,7 +732,6 @@
         const mock = document.createElement('mock-diff-response');
         sandbox.stub(element.$.diffBuilder, 'getDiffLength').returns(10000);
         element.diff = mock.diffResponse;
-        element.comments = {left: [], right: []};
         element.noRenderOnPrefsChange = true;
       });
 
@@ -1127,6 +912,57 @@
         assert.equal(element._computeNewlineWarningClass('foo', false), shown);
       });
     });
+
+    suite('key locations', () => {
+      let renderStub;
+
+      setup(() => {
+        element = fixture('basic');
+        element.prefs = {};
+        renderStub = sandbox.stub(element.$.diffBuilder, 'render');
+      });
+
+      test('lineOfInterest is a key location', () => {
+        element.lineOfInterest = {number: 789, leftSide: true};
+        element._renderDiffTable();
+        assert.isTrue(renderStub.called);
+        assert.deepEqual(renderStub.lastCall.args[0], {
+          left: {789: true},
+          right: {},
+        });
+      });
+
+      test('line comments are key locations', () => {
+        const threadEl = document.createElement('div');
+        threadEl.className = 'comment-thread';
+        threadEl.setAttribute('comment-side', 'right');
+        threadEl.setAttribute('line-num', 3);
+        Polymer.dom(element).appendChild(threadEl);
+        Polymer.dom.flush();
+
+        element._renderDiffTable();
+        assert.isTrue(renderStub.called);
+        assert.deepEqual(renderStub.lastCall.args[0], {
+          left: {},
+          right: {3: true},
+        });
+      });
+
+      test('file comments are key locations', () => {
+        const threadEl = document.createElement('div');
+        threadEl.className = 'comment-thread';
+        threadEl.setAttribute('comment-side', 'left');
+        Polymer.dom(element).appendChild(threadEl);
+        Polymer.dom.flush();
+
+        element._renderDiffTable();
+        assert.isTrue(renderStub.called);
+        assert.deepEqual(renderStub.lastCall.args[0], {
+          left: {FILE: true},
+          right: {},
+        });
+      });
+    });
   });
 
   a11ySuite('basic');
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index c5faa49..75f632d 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -56,6 +56,9 @@
       <name>Luca Milanesio</name>
     </developer>
     <developer>
+      <name>Marco Miller</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 52b11c1..cb8494b 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -56,6 +56,9 @@
       <name>Luca Milanesio</name>
     </developer>
     <developer>
+      <name>Marco Miller</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index d22c3ee..f58a6c7 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -56,6 +56,9 @@
       <name>Luca Milanesio</name>
     </developer>
     <developer>
+      <name>Marco Miller</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index e6c04e2..9849237 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -56,6 +56,9 @@
       <name>Luca Milanesio</name>
     </developer>
     <developer>
+      <name>Marco Miller</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/webapp/WEB-INF/web.xml b/webapp/WEB-INF/web.xml
index 386eb07..e901357 100644
--- a/webapp/WEB-INF/web.xml
+++ b/webapp/WEB-INF/web.xml
@@ -8,7 +8,7 @@
 
   <filter>
     <filter-name>guiceFilter</filter-name>
-    <filter-class>com.google.gerrit.httpd.WebAppInitializer</filter-class>
+    <filter-class>com.google.gerrit.httpd.init.WebAppInitializer</filter-class>
   </filter>
   <filter-mapping>
     <filter-name>guiceFilter</filter-name>