Merge "Use page-error in gr-repo-detail-list and gr-plugin-list"
diff --git a/Documentation/cmd-index-start.txt b/Documentation/cmd-index-start.txt
index 769360d..5a002f3 100644
--- a/Documentation/cmd-index-start.txt
+++ b/Documentation/cmd-index-start.txt
@@ -34,6 +34,8 @@
   Currently supported values:
     * changes
     * accounts
+    * groups
+    * projects
 
 --force::
   Force an online re-index.
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index de73930..cc485d5 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2441,6 +2441,21 @@
 +
 By default, true.
 
+[[httpd.gracefulStopTimeout]]httpd.gracefulStopTimeout::
++
+Set a graceful stop time. If set, the daemon ensures that all incoming
+calls are preserved for a maximum period of time, before starting
+the graceful shutdown process. Sites behind a workload balancer such as
+HAProxy would need this to be set for avoiding serving errors during
+rolling restarts.
++
+Values should use common unit suffixes to express their setting:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
++
+By default, 0 seconds (immediate shutdown).
+
 [[httpd.inheritChannel]]httpd.inheritChannel::
 +
 If true, permits the daemon to inherit its server socket channel
@@ -2760,7 +2775,7 @@
 simultaneous writes that may cause one of the writes to not be reflected in the
 index. The check to avoid this does consume some resources.
 +
-Defaults to true.
+Defaults to false.
 
 [[index.scheduledIndexer]]
 ==== Subsection index.scheduledIndexer
@@ -4032,7 +4047,7 @@
 Only used when `sendemail.from` is set to `USER`.
 List of allowed domains. If user's email matches one of the domains, emails will
 be sent as USER, otherwise as MIXED mode. Wildcards may be specified by
-including `*` to match any number of characters, for example `*.example.com`
+including `\*` to match any number of characters, for example `*.example.com`
 matches any subdomain of `example.com`.
 +
 By default, `*`.
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 84e4062..6272b54 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -217,9 +217,9 @@
 
 * `AnyWithBlock`
 +
-The lowest possible negative value, if present, blocks a submit, Any
-other value enables a submit. To permit blocking submits, ensure
-that a negative value is defined.
+The label is not mandatory but the lowest possible negative value,
+if present, blocks a submit. To permit blocking submits, ensure that a
+negative value is defined.
 
 * `MaxNoBlock`
 +
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index 2d0812e..de5171c 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -424,10 +424,13 @@
 
 As project owner you can administrate the branches of your project in
 the Gerrit Web UI under `Projects` > `List` > <your project> >
-`Branches`. In the Web UI both link:project-configuration.html#branch-creation[
-branch creation] and link:project-configuration.html#branch-deletion[branch
-deletion] are allowed for project owners without requiring any
-additional access rights.
+`Branches`. In the Web UI link:project-configuration.html#branch-creation[
+branch creation] is allowed if you have
+link:access-control.html#category_create[Create Reference] access right and
+link:project-configuration.html#branch-deletion[branch deletion] is allowed if
+you have the link:access-control.html#category_delete[Delete Reference] or the
+link:access-control.html#category_push[Push] access right with the `force`
+option.
 
 By setting `HEAD` on the project you can define its
 link:project-configuration.html#default-branch[default branch]. For convenience
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 3805c80..6ce7f1f 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -19,7 +19,7 @@
 [[tools]]
 == Tools
 
-Gerrit speaks the git protocol. This means in order to work with Gerrit
+Gerrit uses the git protocol. This means in order to work with Gerrit
 you do *not* need to install any Gerrit client, but having a regular
 git client, such as the link:http://git-scm.com/[git command line] or
 link:http://eclipse.org/egit/[EGit] in Eclipse, is sufficient.
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 2b34185..7360bd4 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -157,7 +157,8 @@
 
 oldRev:: The old value of the ref, prior to the update.
 
-newRev:: The new value the ref was updated to.
+newRev:: The new value the ref was updated to. Zero value (`0000000000000000000000000000000000000000`)
+indicates that the ref was deleted.
 
 refName:: Full ref name within project.
 
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 5c79c1b..f76b5e4 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -257,9 +257,7 @@
 
 To be able to create new branches the user must have the
 link:access-control.html#category_create[Create Reference] access
-right. In addition, project owners and Gerrit administrators can create
-new branches from the Web UI or via REST even without having the
-`Create Reference` access right.
+right.
 
 When using the Web UI, the REST endpoint or the SSH command it is only
 possible to create branches on commits that already exist in the
@@ -295,9 +293,7 @@
 To be able to delete branches, the user must have the
 link:access-control.html#category_delete[Delete Reference] or the
 link:access-control.html#category_push[Push] access right with the
-`force` option. In addition, project owners and Gerrit administrators
-can delete branches from the Web UI or via REST even without having the
-`Force Push` access right.
+`force` option.
 
 [[default-branch]]
 === Default Branch
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 49d5ee5..00fd81f 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -1549,8 +1549,11 @@
 |Field Name    ||Description
 |`id`          ||The URL encoded UUID of the group.
 |`name`        |
-not set if returned in a map where the group name is used as map key|
-The name of the group.
+optional, not set if returned in a map where the group name is used as map key|
+The name of the group. +
+For external groups the group name is missing if there is no group
+backend that can resolve the group UUID. E.g. this can happen when a
+plugin that provided a group backend was uninstalled.
 |`url`         |optional|
 URL to information about the group. Typically a URL to a web page that
 permits users to apply to join the group, or manage their membership.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 0f030a6..2ad8cbd 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -4,9 +4,7 @@
 
 Most basic searches can be viewed by clicking on a link along the top
 menu bar.  The link will prefill the search box with a common search
-query, execute it, and present the results.  If exactly one change
-matches the search, the change will be presented instead of a list.
-
+query, execute it, and present the results.
 
 [options="header"]
 |=================================================
@@ -14,7 +12,7 @@
 |All > Open           | status:open '(or is:open)'
 |All > Merged         | status:merged
 |All > Abandoned      | status:abandoned
-|My > Watched Changes | status:open is:watched
+|My > Watched Changes | is:watched is:open
 |My > Starred Changes | is:starred
 |My > Draft Comments  | has:draft
 |Open changes in Foo  | status:open project:Foo
@@ -36,6 +34,9 @@
 |Approval requirement             | Code-Review>=+2, Verified=1
 |=============================================================
 
+For change searches (i.e. those using a numerical id, Change-Id, or commit
+SHA1), if the search results in a single change that change will be
+presented instead of a list.
 
 [[search-operators]]
 == Search Operators
@@ -601,7 +602,7 @@
 link:#submittable[submittable:ok].)
 
 `is:open (label:Verified-1 OR label:Code-Review-2)`::
-`is:open (label:Verified=reject OR label:Code-Review:reject)`::
+`is:open (label:Verified=reject OR label:Code-Review=reject)`::
 +
 Changes that are blocked from submission due to a blocking score.
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
index b056afa..5e38a14 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
@@ -113,17 +113,19 @@
           GroupInfo member = auditEvent.memberAsGroup();
           if (AccountGroup.isInternalGroup(member.getGroupUUID())) {
             table.setWidget(
-                row, 3, new Hyperlink(member.name(), Dispatcher.toGroup(member.getGroupUUID())));
+                row,
+                3,
+                new Hyperlink(formatGroup(member), Dispatcher.toGroup(member.getGroupUUID())));
             fmt.getElement(row, 3).setTitle(null);
           } else if (member.url() != null) {
             Anchor a = new Anchor();
-            a.setText(member.name());
+            a.setText(formatGroup(member));
             a.setHref(member.url());
             a.setTitle("UUID " + member.getGroupUUID().get());
             table.setWidget(row, 3, a);
             fmt.getElement(row, 3).setTitle(null);
           } else {
-            table.setText(row, 3, member.name());
+            table.setText(row, 3, formatGroup(member));
             fmt.getElement(row, 3).setTitle("UUID " + member.getGroupUUID().get());
           }
           break;
@@ -148,4 +150,10 @@
     b.append(")");
     return b.toString();
   }
+
+  private static String formatGroup(GroupInfo group) {
+    return group.name() != null && !group.name().isEmpty()
+        ? group.name()
+        : group.getGroupUUID().get();
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 54ba802..6233dbb 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -360,8 +360,8 @@
     accountIndexer.index(accountId);
   }
 
-  private void reindexAllGroups() throws OrmException, IOException, ConfigInvalidException {
-    Iterable<GroupReference> allGroups = groups.getAllGroupReferences(db)::iterator;
+  private void reindexAllGroups() throws IOException, ConfigInvalidException {
+    Iterable<GroupReference> allGroups = groups.getAllGroupReferences()::iterator;
     for (GroupReference group : allGroups) {
       groupIndexer.index(group.getUUID());
     }
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index dc5e59d..c6e03a8 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
@@ -35,7 +34,6 @@
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -57,7 +55,6 @@
 public class AccountCreator {
   private final Map<String, TestAccount> accounts;
 
-  private final SchemaFactory<ReviewDb> reviewDbProvider;
   private final Sequences sequences;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
@@ -68,7 +65,6 @@
 
   @Inject
   AccountCreator(
-      SchemaFactory<ReviewDb> schema,
       Sequences sequences,
       @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       VersionedAuthorizedKeys.Accessor authorizedKeys,
@@ -77,7 +73,6 @@
       SshKeyCache sshKeyCache,
       @SshEnabled boolean sshEnabled) {
     accounts = new HashMap<>();
-    reviewDbProvider = schema;
     this.sequences = sequences;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.authorizedKeys = authorizedKeys;
@@ -98,51 +93,49 @@
     if (account != null) {
       return account;
     }
-    try (ReviewDb db = reviewDbProvider.open()) {
-      Account.Id id = new Account.Id(sequences.nextAccountId());
+    Account.Id id = new Account.Id(sequences.nextAccountId());
 
-      List<ExternalId> extIds = new ArrayList<>(2);
-      String httpPass = null;
-      if (username != null) {
-        httpPass = "http-pass";
-        extIds.add(ExternalId.createUsername(username, id, httpPass));
-      }
-
-      if (email != null) {
-        extIds.add(ExternalId.createEmail(id, email));
-      }
-
-      accountsUpdateProvider
-          .get()
-          .insert(
-              "Create Test Account",
-              id,
-              u -> u.setFullName(fullName).setPreferredEmail(email).addExternalIds(extIds));
-
-      if (groupNames != null) {
-        for (String n : groupNames) {
-          AccountGroup.NameKey k = new AccountGroup.NameKey(n);
-          Optional<InternalGroup> group = groupCache.get(k);
-          if (!group.isPresent()) {
-            throw new NoSuchGroupException(n);
-          }
-          addGroupMember(db, group.get().getGroupUUID(), id);
-        }
-      }
-
-      KeyPair sshKey = null;
-      if (sshEnabled && username != null) {
-        sshKey = genSshKey();
-        authorizedKeys.addKey(id, publicKey(sshKey, email));
-        sshKeyCache.evict(username);
-      }
-
-      account = new TestAccount(id, username, email, fullName, sshKey, httpPass);
-      if (username != null) {
-        accounts.put(username, account);
-      }
-      return account;
+    List<ExternalId> extIds = new ArrayList<>(2);
+    String httpPass = null;
+    if (username != null) {
+      httpPass = "http-pass";
+      extIds.add(ExternalId.createUsername(username, id, httpPass));
     }
+
+    if (email != null) {
+      extIds.add(ExternalId.createEmail(id, email));
+    }
+
+    accountsUpdateProvider
+        .get()
+        .insert(
+            "Create Test Account",
+            id,
+            u -> u.setFullName(fullName).setPreferredEmail(email).addExternalIds(extIds));
+
+    if (groupNames != null) {
+      for (String n : groupNames) {
+        AccountGroup.NameKey k = new AccountGroup.NameKey(n);
+        Optional<InternalGroup> group = groupCache.get(k);
+        if (!group.isPresent()) {
+          throw new NoSuchGroupException(n);
+        }
+        addGroupMember(group.get().getGroupUUID(), id);
+      }
+    }
+
+    KeyPair sshKey = null;
+    if (sshEnabled && username != null) {
+      sshKey = genSshKey();
+      authorizedKeys.addKey(id, publicKey(sshKey, email));
+      sshKeyCache.evict(username);
+    }
+
+    account = new TestAccount(id, username, email, fullName, sshKey, httpPass);
+    if (username != null) {
+      accounts.put(username, account);
+    }
+    return account;
   }
 
   public TestAccount create(@Nullable String username, String group) throws Exception {
@@ -193,12 +186,12 @@
     return out.toString(US_ASCII.name()).trim();
   }
 
-  private void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account.Id accountId)
+  private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
       throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId)))
             .build();
-    groupsUpdateProvider.get().updateGroup(db, groupUuid, groupUpdate);
+    groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
   }
 }
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 37ef20a..c056454 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -36,7 +36,6 @@
 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.GroupNoteDbMode;
 import com.google.gerrit.testing.InMemoryDatabase;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.NoteDbChecker;
@@ -419,7 +418,6 @@
     cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
 
     NoteDbMode.newNotesMigrationFromEnv().setConfigValues(cfg);
-    GroupNoteDbMode.get().getGroupsMigration().setConfigValuesIfNotSetYet(cfg);
   }
 
   private static Injector createTestInjector(Daemon daemon) throws Exception {
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 58dfa94..83a3874 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.config.TrackingFootersProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeBundleReader;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.schema.DataSourceType;
@@ -93,7 +92,6 @@
     bind(DataSourceType.class).to(InMemoryH2Type.class);
 
     install(new NotesMigration.Module());
-    install(new GroupsMigration.Module());
     TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
         new TypeLiteral<SchemaFactory<ReviewDb>>() {};
     bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 24d4b6b..7e2796a 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -208,7 +208,6 @@
   }
 
   private static class Upload implements UploadPackFactory<Context> {
-    private final Provider<CurrentUser> userProvider;
     private final TransferConfig transferConfig;
     private final DynamicSet<UploadPackInitializer> uploadPackInitializers;
     private final DynamicSet<PreUploadHook> preUploadHooks;
@@ -219,7 +218,6 @@
 
     @Inject
     Upload(
-        Provider<CurrentUser> userProvider,
         TransferConfig transferConfig,
         DynamicSet<UploadPackInitializer> uploadPackInitializers,
         DynamicSet<PreUploadHook> preUploadHooks,
@@ -227,7 +225,6 @@
         ThreadLocalRequestContext threadContext,
         ProjectCache projectCache,
         PermissionBackend permissionBackend) {
-      this.userProvider = userProvider;
       this.transferConfig = transferConfig;
       this.uploadPackInitializers = uploadPackInitializers;
       this.preUploadHooks = preUploadHooks;
@@ -246,7 +243,7 @@
       threadContext.setContext(req);
       current.set(req);
 
-      PermissionBackend.ForProject perm = permissionBackend.user(userProvider).project(req.project);
+      PermissionBackend.ForProject perm = permissionBackend.currentUser().project(req.project);
       try {
         perm.check(ProjectPermission.RUN_UPLOAD_PACK);
       } catch (AuthException e) {
@@ -318,7 +315,7 @@
       current.set(req);
       try {
         permissionBackend
-            .user(userProvider)
+            .currentUser()
             .project(req.project)
             .check(ProjectPermission.RUN_RECEIVE_PACK);
       } catch (AuthException e) {
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index 09ffe9d..b754351 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -35,9 +35,13 @@
 import com.google.inject.Injector;
 import com.google.inject.Module;
 import com.google.inject.Provider;
+import java.io.File;
 import java.util.Arrays;
 import java.util.Collections;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
 import org.junit.Rule;
 import org.junit.rules.RuleChain;
 import org.junit.rules.TemporaryFolder;
@@ -109,8 +113,12 @@
           return new Statement() {
             @Override
             public void evaluate() throws Throwable {
-              beforeTest(description);
-              base.evaluate();
+              try {
+                beforeTest(description);
+                base.evaluate();
+              } finally {
+                afterTest();
+              }
             }
           };
         }
@@ -122,13 +130,65 @@
   protected Account.Id adminId;
 
   private GerritServer.Description serverDesc;
+  private SystemReader oldSystemReader;
 
   private void beforeTest(Description description) throws Exception {
+    // SystemReader must be overridden before creating any repos, since they read the user/system
+    // configs at initialization time, and are then stored in the RepositoryCache forever.
+    oldSystemReader = setFakeSystemReader(tempSiteDir.getRoot());
+
     serverDesc = GerritServer.Description.forTestMethod(description, configName);
     sitePaths = new SitePaths(tempSiteDir.getRoot().toPath());
     GerritServer.init(serverDesc, baseConfig, sitePaths.site_path);
   }
 
+  private static SystemReader setFakeSystemReader(File tempDir) {
+    SystemReader oldSystemReader = SystemReader.getInstance();
+    SystemReader.setInstance(
+        new SystemReader() {
+          @Override
+          public String getHostname() {
+            return oldSystemReader.getHostname();
+          }
+
+          @Override
+          public String getenv(String variable) {
+            return oldSystemReader.getenv(variable);
+          }
+
+          @Override
+          public String getProperty(String key) {
+            return oldSystemReader.getProperty(key);
+          }
+
+          @Override
+          public FileBasedConfig openUserConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
+          }
+
+          @Override
+          public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
+          }
+
+          @Override
+          public long getCurrentTime() {
+            return oldSystemReader.getCurrentTime();
+          }
+
+          @Override
+          public int getTimezone(long when) {
+            return oldSystemReader.getTimezone(when);
+          }
+        });
+    return oldSystemReader;
+  }
+
+  private void afterTest() throws Exception {
+    SystemReader.setInstance(oldSystemReader);
+    oldSystemReader = null;
+  }
+
   protected ServerContext startServer() throws Exception {
     return startServer(null);
   }
@@ -153,7 +213,10 @@
   }
 
   protected static void runGerrit(String... args) throws Exception {
-    assertThat(GerritLauncher.mainImpl(args))
+    // Use invokeProgram with the current classloader, rather than mainImpl, which would create a
+    // new classloader. This is necessary so that static state, particularly the SystemReader, is
+    // shared with the test method.
+    assertThat(GerritLauncher.invokeProgram(StandaloneSiteTest.class.getClassLoader(), args))
         .named("gerrit.war " + Arrays.stream(args).collect(joining(" ")))
         .isEqualTo(0);
   }
diff --git a/java/com/google/gerrit/common/data/LabelFunction.java b/java/com/google/gerrit/common/data/LabelFunction.java
index 0ce2c29..7d13c70 100644
--- a/java/com/google/gerrit/common/data/LabelFunction.java
+++ b/java/com/google/gerrit/common/data/LabelFunction.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -27,15 +28,15 @@
  * rules, in which case the choice of function in the project config is ignored.
  *
  * <p>Function semantics are documented in {@code config-labels.txt}, and actual behavior is
- * implemented in Prolog in {@code gerrit_common.pl}.
+ * implemented both in Prolog in {@code gerrit_common.pl} and in the {@link #check} method.
  */
 public enum LabelFunction {
-  MAX_WITH_BLOCK("MaxWithBlock", true),
-  ANY_WITH_BLOCK("AnyWithBlock", true),
-  MAX_NO_BLOCK("MaxNoBlock", false),
-  NO_BLOCK("NoBlock", false),
-  NO_OP("NoOp", false),
-  PATCH_SET_LOCK("PatchSetLock", false);
+  ANY_WITH_BLOCK("AnyWithBlock", true, false, false),
+  MAX_WITH_BLOCK("MaxWithBlock", true, true, true),
+  MAX_NO_BLOCK("MaxNoBlock", false, true, true),
+  NO_BLOCK("NoBlock"),
+  NO_OP("NoOp"),
+  PATCH_SET_LOCK("PatchSetLock");
 
   public static final Map<String, LabelFunction> ALL;
 
@@ -53,10 +54,18 @@
 
   private final String name;
   private final boolean isBlock;
+  private final boolean isRequired;
+  private final boolean requiresMaxValue;
 
-  private LabelFunction(String name, boolean isBlock) {
+  LabelFunction(String name) {
+    this(name, false, false, false);
+  }
+
+  LabelFunction(String name, boolean isBlock, boolean isRequired, boolean requiresMaxValue) {
     this.name = name;
     this.isBlock = isBlock;
+    this.isRequired = isRequired;
+    this.requiresMaxValue = requiresMaxValue;
   }
 
   /** The function name as defined in documentation and {@code project.config}. */
@@ -68,4 +77,47 @@
   public boolean isBlock() {
     return isBlock;
   }
+
+  /** Whether the label is a mandatory label, meaning absence of votes will prevent submission. */
+  public boolean isRequired() {
+    return isRequired;
+  }
+
+  /** Whether the label requires a vote with the maximum value to allow submission. */
+  public boolean isMaxValueRequired() {
+    return requiresMaxValue;
+  }
+
+  public SubmitRecord.Label check(LabelType labelType, Iterable<PatchSetApproval> approvals) {
+    SubmitRecord.Label submitRecordLabel = new SubmitRecord.Label();
+    submitRecordLabel.label = labelType.getName();
+
+    submitRecordLabel.status = SubmitRecord.Label.Status.MAY;
+    if (isRequired) {
+      submitRecordLabel.status = SubmitRecord.Label.Status.NEED;
+    }
+
+    for (PatchSetApproval a : approvals) {
+      if (a.getValue() == 0) {
+        continue;
+      }
+
+      if (isBlock && labelType.isMaxNegative(a)) {
+        submitRecordLabel.appliedBy = a.getAccountId();
+        submitRecordLabel.status = SubmitRecord.Label.Status.REJECT;
+        return submitRecordLabel;
+      }
+
+      if (labelType.isMaxPositive(a) || !requiresMaxValue) {
+        submitRecordLabel.appliedBy = a.getAccountId();
+
+        submitRecordLabel.status = SubmitRecord.Label.Status.MAY;
+        if (isRequired) {
+          submitRecordLabel.status = SubmitRecord.Label.Status.OK;
+        }
+      }
+    }
+
+    return submitRecordLabel;
+  }
 }
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 5bdd9ca..739726e 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -184,7 +184,7 @@
       try {
         Project.NameKey nameKey = new Project.NameKey(projectName);
         ProjectState state = projectCache.checkedGet(nameKey);
-        if (state == null) {
+        if (state == null || !state.statePermitsRead()) {
           throw new RepositoryNotFoundException(nameKey.get());
         }
         req.setAttribute(ATT_STATE, state);
@@ -241,16 +241,12 @@
   static class UploadFilter implements Filter {
     private final UploadValidators.Factory uploadValidatorsFactory;
     private final PermissionBackend permissionBackend;
-    private final Provider<CurrentUser> userProvider;
 
     @Inject
     UploadFilter(
-        UploadValidators.Factory uploadValidatorsFactory,
-        PermissionBackend permissionBackend,
-        Provider<CurrentUser> userProvider) {
+        UploadValidators.Factory uploadValidatorsFactory, PermissionBackend permissionBackend) {
       this.uploadValidatorsFactory = uploadValidatorsFactory;
       this.permissionBackend = permissionBackend;
-      this.userProvider = userProvider;
     }
 
     @Override
@@ -261,7 +257,7 @@
       ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
       UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
       PermissionBackend.ForProject perm =
-          permissionBackend.user(userProvider).project(state.getNameKey());
+          permissionBackend.currentUser().project(state.getNameKey());
       try {
         perm.check(ProjectPermission.RUN_UPLOAD_PACK);
       } catch (AuthException e) {
@@ -356,7 +352,7 @@
       Capable s;
       try {
         permissionBackend
-            .user(userProvider)
+            .currentUser()
             .project(state.getNameKey())
             .check(ProjectPermission.RUN_RECEIVE_PACK);
         s = arc.canUpload();
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index b39f027..ba2a063 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -112,11 +112,11 @@
       GitRepositoryManager repoManager,
       ProjectCache projectCache,
       PermissionBackend permissionBackend,
-      Provider<AnonymousUser> anonymousUserProvider,
       Provider<CurrentUser> userProvider,
       SitePaths site,
       @GerritServerConfig Config cfg,
       SshInfo sshInfo,
+      Provider<AnonymousUser> anonymousUserProvider,
       GitwebConfig gitwebConfig,
       GitwebCgiConfig gitwebCgiConfig)
       throws IOException {
@@ -423,7 +423,10 @@
       }
 
       projectState.checkStatePermitsRead();
-      permissionBackend.user(userProvider).project(nameKey).check(ProjectPermission.READ);
+      permissionBackend
+          .user(anonymousUserProvider.get())
+          .project(nameKey)
+          .check(ProjectPermission.READ);
     } catch (AuthException e) {
       sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND);
       return;
@@ -584,7 +587,7 @@
 
     if (projectState.statePermitsRead()
         && permissionBackend
-            .user(anonymousUserProvider)
+            .user(anonymousUserProvider.get())
             .project(nameKey)
             .testOrFalse(ProjectPermission.READ)) {
       env.set("GERRIT_ANONYMOUS_READ", "1");
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index bf1dd35..bac3fd0 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -71,7 +71,6 @@
 import com.google.gerrit.server.mail.receive.MailReceiver;
 import com.google.gerrit.server.mail.send.SmtpEmailSender;
 import com.google.gerrit.server.mime.MimeUtil2Module;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
@@ -309,7 +308,6 @@
     }
     modules.add(new DatabaseModule());
     modules.add(new NotesMigration.Module());
-    modules.add(new GroupsMigration.Module());
     modules.add(new DropWizardMetricMaker.ApiModule());
     return Guice.createInjector(PRODUCTION, modules);
   }
diff --git a/java/com/google/gerrit/httpd/raw/CatServlet.java b/java/com/google/gerrit/httpd/raw/CatServlet.java
index 013c8e9..4b5c227 100644
--- a/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
@@ -54,7 +53,6 @@
 @Singleton
 public class CatServlet extends HttpServlet {
   private final Provider<ReviewDb> requestDb;
-  private final Provider<CurrentUser> userProvider;
   private final ChangeEditUtil changeEditUtil;
   private final PatchSetUtil psUtil;
   private final ChangeNotes.Factory changeNotesFactory;
@@ -64,14 +62,12 @@
   @Inject
   CatServlet(
       Provider<ReviewDb> sf,
-      Provider<CurrentUser> usrprv,
       ChangeEditUtil ceu,
       PatchSetUtil psu,
       ChangeNotes.Factory cnf,
       PermissionBackend pb,
       ProjectCache pc) {
     requestDb = sf;
-    userProvider = usrprv;
     changeEditUtil = ceu;
     psUtil = psu;
     changeNotesFactory = cnf;
@@ -132,7 +128,7 @@
     try {
       ChangeNotes notes = changeNotesFactory.createChecked(changeId);
       permissionBackend
-          .user(userProvider)
+          .currentUser()
           .change(notes)
           .database(requestDb)
           .check(ChangePermission.READ);
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 131dbf9..515624f 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -295,7 +295,7 @@
       RestCollection<RestResource, RestResource> rc = members.get();
       globals
           .permissionBackend
-          .user(globals.currentUser)
+          .user(globals.currentUser.get())
           .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
 
       viewData = new ViewData(null, null);
@@ -839,10 +839,10 @@
     }
 
     Object obj = createInstance(type);
-    Field[] fields = obj.getClass().getDeclaredFields();
-    if (fields.length == 0 && Strings.isNullOrEmpty(value)) {
+    if (Strings.isNullOrEmpty(value)) {
       return obj;
     }
+    Field[] fields = obj.getClass().getDeclaredFields();
     for (Field f : fields) {
       if (f.getAnnotation(DefaultInput.class) != null && f.getType() == String.class) {
         f.setAccessible(true);
@@ -1189,7 +1189,7 @@
       throws AuthException, PermissionBackendException {
     globals
         .permissionBackend
-        .user(globals.currentUser)
+        .user(globals.currentUser.get())
         .checkAny(GlobalPermission.fromAnnotation(d.pluginName, d.view.getClass()));
   }
 
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 48e06e9..f117b24 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupControl;
@@ -51,7 +50,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -70,7 +68,6 @@
   private final GroupBackend groupBackend;
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final GroupControl.Factory groupControlFactory;
   private final MetaDataUpdate.Server metaDataUpdateFactory;
   private final AllProjectsName allProjectsName;
@@ -83,7 +80,6 @@
       GroupBackend groupBackend,
       ProjectCache projectCache,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       GroupControl.Factory groupControlFactory,
       MetaDataUpdate.Server metaDataUpdateFactory,
       AllProjectsName allProjectsName,
@@ -92,7 +88,6 @@
     this.groupBackend = groupBackend;
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.groupControlFactory = groupControlFactory;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allProjectsName = allProjectsName;
@@ -131,7 +126,7 @@
     List<AccessSection> local = new ArrayList<>();
     Set<String> ownerOf = new HashSet<>();
     Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>();
-    PermissionBackend.ForProject perm = permissionBackend.user(user).project(projectName);
+    PermissionBackend.ForProject perm = permissionBackend.currentUser().project(projectName);
     boolean checkReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
     boolean canWriteProjectConfig = true;
     try {
@@ -217,7 +212,7 @@
     detail.setInheritsFrom(config.getProject().getParent(allProjectsName));
 
     if (projectName.equals(allProjectsName)
-        && permissionBackend.user(user).testOrFalse(ADMINISTRATE_SERVER)) {
+        && permissionBackend.currentUser().testOrFalse(ADMINISTRATE_SERVER)) {
       ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
     }
 
@@ -264,8 +259,14 @@
       throws NoSuchProjectException, IOException, PermissionBackendException,
           ResourceConflictException {
     ProjectState state = projectCache.checkedGet(projectName);
+    // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+    // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+    // be allowed for other users). Allowing project owners to access here will help them to view
+    // and update the config of hidden projects easily.
+    ProjectPermission permissionToCheck =
+        state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
     try {
-      permissionBackend.user(user).project(projectName).check(ProjectPermission.ACCESS);
+      permissionBackend.currentUser().project(projectName).check(permissionToCheck);
     } catch (AuthException e) {
       throw new NoSuchProjectException(projectName);
     }
@@ -285,7 +286,7 @@
 
   private boolean isAdmin() throws PermissionBackendException {
     try {
-      permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
       return true;
     } catch (AuthException e) {
       return false;
diff --git a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index e44d680..6c26020 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.common.errors.UpdateParentFailedException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -109,13 +108,9 @@
   @Override
   public final T call()
       throws NoSuchProjectException, IOException, ConfigInvalidException, InvalidNameException,
-          NoSuchGroupException, OrmException, UpdateParentFailedException,
-          PermissionDeniedException, PermissionBackendException, ResourceConflictException {
-    try {
-      contributorAgreements.check(projectName, user);
-    } catch (AuthException e) {
-      throw new PermissionDeniedException(e.getMessage());
-    }
+          NoSuchGroupException, OrmException, UpdateParentFailedException, AuthException,
+          PermissionBackendException, ResourceConflictException {
+    contributorAgreements.check(projectName, user);
 
     try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
       ProjectConfig config = ProjectConfig.read(md, base);
@@ -195,7 +190,7 @@
   protected abstract T updateProjectConfig(
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, NoSuchProjectException, ConfigInvalidException, OrmException,
-          PermissionDeniedException, PermissionBackendException, ResourceConflictException;
+          AuthException, PermissionBackendException, ResourceConflictException;
 
   private void replace(ProjectConfig config, Set<String> toDelete, AccessSection section)
       throws NoSuchGroupException {
diff --git a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 43dddf9..23b80ca 100644
--- a/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -133,16 +132,16 @@
   @Override
   protected Change.Id updateProjectConfig(
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
-      throws IOException, OrmException, PermissionDeniedException, PermissionBackendException,
+      throws IOException, OrmException, AuthException, PermissionBackendException,
           ConfigInvalidException, ResourceConflictException {
     PermissionBackend.ForProject perm = permissionBackend.user(user).project(config.getName());
     if (!check(perm, ProjectPermission.READ_CONFIG)) {
-      throw new PermissionDeniedException(RefNames.REFS_CONFIG + " not visible");
+      throw new AuthException(RefNames.REFS_CONFIG + " not visible");
     }
 
     if (!check(perm, ProjectPermission.WRITE_CONFIG)
         && !check(perm.ref(RefNames.REFS_CONFIG), RefPermission.CREATE_CHANGE)) {
-      throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG);
+      throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG);
     }
 
     projectCache.checkedGet(config.getName()).checkStatePermitsWrite();
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index e8892be..b7d232d 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -64,6 +64,17 @@
     System.exit(mainImpl(argv));
   }
 
+  /**
+   * Invokes a proram.
+   *
+   * <p>Creates a new classloader to load and run the program class. To reuse a classloader across
+   * calls (e.g. from tests), use {@link #invokeProgram(ClassLoader, String[])}.
+   *
+   * @param argv arguments, as would be passed to {@code gerrit.war}. The first argument is the
+   *     program name.
+   * @return program return code.
+   * @throws Exception if any error occurs.
+   */
   public static int mainImpl(String[] argv) throws Exception {
     if (argv.length == 0) {
       File me;
@@ -164,7 +175,16 @@
     }
   }
 
-  private static int invokeProgram(ClassLoader loader, String[] origArgv) throws Exception {
+  /**
+   * Invokes a proram in the provided {@code ClassLoader}.
+   *
+   * @param loader classloader to load program class from.
+   * @param origArgv arguments, as would be passed to {@code gerrit.war}. The first argument is the
+   *     program name.
+   * @return program return code.
+   * @throws Exception if any error occurs.
+   */
+  public static int invokeProgram(ClassLoader loader, String[] origArgv) throws Exception {
     String name = origArgv[0];
     final String[] argv = new String[origArgv.length - 1];
     System.arraycopy(origArgv, 1, argv, 0, argv.length);
diff --git a/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java b/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
index 6abf17c..55c932c 100644
--- a/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
+++ b/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -35,7 +34,6 @@
   private final DynamicMap<RestView<MetricResource>> views;
   private final Provider<ListMetrics> list;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final DropWizardMetricMaker metrics;
 
   @Inject
@@ -43,12 +41,10 @@
       DynamicMap<RestView<MetricResource>> views,
       Provider<ListMetrics> list,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       DropWizardMetricMaker metrics) {
     this.views = views;
     this.list = list;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.metrics = metrics;
   }
 
@@ -65,7 +61,7 @@
   @Override
   public MetricResource parse(ConfigResource parent, IdString id)
       throws ResourceNotFoundException, AuthException, PermissionBackendException {
-    permissionBackend.user(user).check(GlobalPermission.VIEW_CACHES);
+    permissionBackend.currentUser().check(GlobalPermission.VIEW_CACHES);
 
     Metric metric = metrics.getMetric(id.get());
     if (metric == null) {
diff --git a/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
index 22fdb2c..10761c7 100644
--- a/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -29,13 +29,16 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.index.DummyIndexModule;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.notedb.rebuild.GcAllUsers;
 import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import org.kohsuke.args4j.Option;
@@ -99,6 +102,7 @@
   private LifecycleManager dbManager;
   private LifecycleManager sysManager;
 
+  @Inject private GcAllUsers gcAllUsers;
   @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
 
   @Override
@@ -137,6 +141,9 @@
           migrator.migrate();
         }
       }
+      try (PrintWriter w = new PrintWriter(System.out, true)) {
+        gcAllUsers.run(w);
+      }
     } finally {
       stop();
     }
@@ -190,6 +197,7 @@
             install(dbInjector.getInstance(BatchProgramModule.class));
             install(new DummyIndexModule());
             factory(ChangeResource.Factory.class);
+            factory(GarbageCollection.Factory.class);
           }
         });
   }
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 3895d16..b6eac05 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -39,6 +39,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import javax.servlet.DispatcherType;
 import javax.servlet.Filter;
 import org.eclipse.jetty.http.HttpScheme;
@@ -56,6 +57,7 @@
 import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
 import org.eclipse.jetty.server.session.SessionHandler;
 import org.eclipse.jetty.servlet.DefaultServlet;
 import org.eclipse.jetty.servlet.FilterHolder;
@@ -149,6 +151,15 @@
       httpd.addBean(mbean);
     }
 
+    long gracefulStopTimeout =
+        cfg.getTimeUnit("httpd", null, "gracefulStopTimeout", 0L, TimeUnit.MILLISECONDS);
+    if (gracefulStopTimeout > 0) {
+      StatisticsHandler statsHandler = new StatisticsHandler();
+      statsHandler.setHandler(app);
+      app = statsHandler;
+      httpd.setStopTimeout(gracefulStopTimeout);
+    }
+
     httpd.setHandler(app);
     httpd.setStopAtShutdown(false);
   }
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 4b752e6..1d3e516 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -16,11 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
@@ -28,8 +25,6 @@
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -41,17 +36,13 @@
 import com.google.gerrit.server.group.db.AuditLogFormatter;
 import com.google.gerrit.server.group.db.GroupConfig;
 import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gerrit.server.notedb.GroupsMigration;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.sql.Timestamp;
-import java.util.List;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
@@ -74,14 +65,12 @@
   private final InitFlags flags;
   private final SitePaths site;
   private final String allUsers;
-  private final GroupsMigration groupsMigration;
 
   @Inject
   public GroupsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) {
     this.flags = flags;
     this.site = site;
     this.allUsers = allUsers.get();
-    this.groupsMigration = new GroupsMigration(flags.cfg);
   }
 
   /**
@@ -97,15 +86,6 @@
    */
   public InternalGroup getExistingGroup(ReviewDb db, GroupReference groupReference)
       throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
-    if (groupsMigration.readFromNoteDb()) {
-      return getExistingGroupFromNoteDb(groupReference);
-    }
-
-    return getExistingGroupFromReviewDb(db, groupReference);
-  }
-
-  private InternalGroup getExistingGroupFromNoteDb(GroupReference groupReference)
-      throws IOException, ConfigInvalidException, NoSuchGroupException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -119,23 +99,6 @@
     throw new NoSuchGroupException(groupReference.getUUID());
   }
 
-  private static InternalGroup getExistingGroupFromReviewDb(
-      ReviewDb db, GroupReference groupReference) throws OrmException, NoSuchGroupException {
-    String groupName = groupReference.getName();
-    AccountGroupName accountGroupName =
-        db.accountGroupNames().get(new AccountGroup.NameKey(groupName));
-    if (accountGroupName == null) {
-      throw new NoSuchGroupException(groupName);
-    }
-
-    AccountGroup.Id groupId = accountGroupName.getId();
-    AccountGroup group = db.accountGroups().get(groupId);
-    if (group == null) {
-      throw new NoSuchGroupException(groupName);
-    }
-    return Groups.asInternalGroup(db, group);
-  }
-
   /**
    * Returns {@code GroupReference}s for all internal groups.
    *
@@ -147,18 +110,13 @@
    */
   public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
       throws OrmException, IOException, ConfigInvalidException {
-    if (groupsMigration.readFromNoteDb()) {
-      File allUsersRepoPath = getPathToAllUsersRepository();
-      if (allUsersRepoPath != null) {
-        try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
-          return GroupNameNotes.loadAllGroups(allUsersRepo).stream();
-        }
+    File allUsersRepoPath = getPathToAllUsersRepository();
+    if (allUsersRepoPath != null) {
+      try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
+        return GroupNameNotes.loadAllGroups(allUsersRepo).stream();
       }
-      return Stream.empty();
     }
-
-    return Streams.stream(db.accountGroups().all())
-        .map(group -> new GroupReference(group.getGroupUUID(), group.getName()));
+    return Stream.empty();
   }
 
   /**
@@ -176,49 +134,6 @@
    */
   public void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account account)
       throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
-    addGroupMemberInReviewDb(db, groupUuid, account.getId());
-    if (!groupsMigration.writeToNoteDb()) {
-      return;
-    }
-    addGroupMemberInNoteDb(groupUuid, account);
-  }
-
-  private static void addGroupMemberInReviewDb(
-      ReviewDb db, AccountGroup.UUID groupUuid, Account.Id accountId)
-      throws OrmException, NoSuchGroupException {
-    AccountGroup group = getExistingGroup(db, groupUuid);
-    AccountGroup.Id groupId = group.getId();
-
-    if (isMember(db, groupId, accountId)) {
-      return;
-    }
-
-    db.accountGroupMembers()
-        .insert(
-            ImmutableList.of(
-                new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId))));
-  }
-
-  private static AccountGroup getExistingGroup(ReviewDb db, AccountGroup.UUID groupUuid)
-      throws OrmException, NoSuchGroupException {
-    List<AccountGroup> accountGroups = db.accountGroups().byUUID(groupUuid).toList();
-    if (accountGroups.size() == 1) {
-      return Iterables.getOnlyElement(accountGroups);
-    } else if (accountGroups.isEmpty()) {
-      throw new NoSuchGroupException(groupUuid);
-    } else {
-      throw new OrmDuplicateKeyException("Duplicate group UUID " + groupUuid);
-    }
-  }
-
-  private static boolean isMember(ReviewDb db, AccountGroup.Id groupId, Account.Id accountId)
-      throws OrmException {
-    AccountGroupMember.Key key = new AccountGroupMember.Key(accountId, groupId);
-    return db.accountGroupMembers().get(key) != null;
-  }
-
-  private void addGroupMemberInNoteDb(AccountGroup.UUID groupUuid, Account account)
-      throws IOException, ConfigInvalidException, NoSuchGroupException {
     File allUsersRepoPath = getPathToAllUsersRepository();
     if (allUsersRepoPath != null) {
       try (Repository repository = new FileRepository(allUsersRepoPath)) {
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 357674d..ffec375 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -72,6 +72,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.restapi.group.GroupModule;
+import com.google.gerrit.server.rules.DefaultSubmitRule;
 import com.google.gerrit.server.rules.PrologModule;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.inject.Inject;
@@ -172,6 +173,7 @@
     // Submit rule evaluator
     factory(SubmitRuleEvaluator.Factory.class);
     install(new PrologModule());
+    install(new DefaultSubmitRule.Module());
 
     bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
     bind(EventUtil.class).toProvider(Providers.<EventUtil>of(null));
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index afabcf6..b59e085 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.schema.DataSourceModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
@@ -184,7 +183,6 @@
     modules.add(new SchemaModule());
     modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
     modules.add(new NotesMigration.Module());
-    modules.add(new GroupsMigration.Module());
 
     try {
       return Guice.createInjector(PRODUCTION, modules);
diff --git a/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java
deleted file mode 100644
index 1bfbd37..0000000
--- a/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java
+++ /dev/null
@@ -1,306 +0,0 @@
-// Copyright (C) 2017 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.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-
-public class DisallowReadFromGroupsReviewDbWrapper extends ReviewDbWrapper {
-  private static final String MSG = "This table has been migrated to NoteDb";
-
-  private final Groups groups;
-  private final GroupNames groupNames;
-  private final GroupMembers groupMembers;
-  private final GroupMemberAudits groupMemberAudits;
-  private final ByIds byIds;
-  private final ByIdAudits byIdAudits;
-
-  public DisallowReadFromGroupsReviewDbWrapper(ReviewDb db) {
-    super(db);
-    groups = new Groups(delegate.accountGroups());
-    groupNames = new GroupNames(delegate.accountGroupNames());
-    groupMembers = new GroupMembers(delegate.accountGroupMembers());
-    groupMemberAudits = new GroupMemberAudits(delegate.accountGroupMembersAudit());
-    byIds = new ByIds(delegate.accountGroupById());
-    byIdAudits = new ByIdAudits(delegate.accountGroupByIdAud());
-  }
-
-  @Override
-  public AccountGroupAccess accountGroups() {
-    return groups;
-  }
-
-  @Override
-  public AccountGroupNameAccess accountGroupNames() {
-    return groupNames;
-  }
-
-  @Override
-  public AccountGroupMemberAccess accountGroupMembers() {
-    return groupMembers;
-  }
-
-  @Override
-  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
-    return groupMemberAudits;
-  }
-
-  @Override
-  public AccountGroupByIdAccess accountGroupById() {
-    return byIds;
-  }
-
-  @Override
-  public AccountGroupByIdAudAccess accountGroupByIdAud() {
-    return byIdAudits;
-  }
-
-  private static class Groups extends AccountGroupAccessWrapper {
-    protected Groups(AccountGroupAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<AccountGroup> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroup, OrmException> getAsync(
-        AccountGroup.Id key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroup> get(Iterable<AccountGroup.Id> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public AccountGroup get(AccountGroup.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroup> all() {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class GroupNames extends AccountGroupNameAccessWrapper {
-    protected GroupNames(AccountGroupNameAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<AccountGroupName> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupName, OrmException> getAsync(
-        AccountGroup.NameKey key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupName> get(Iterable<AccountGroup.NameKey> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public AccountGroupName get(AccountGroup.NameKey name) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupName> all() {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class GroupMembers extends AccountGroupMemberAccessWrapper {
-    protected GroupMembers(AccountGroupMemberAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupMember, OrmException>
-        getAsync(AccountGroupMember.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> get(Iterable<AccountGroupMember.Key> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public AccountGroupMember get(AccountGroupMember.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> byAccount(Account.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class GroupMemberAudits extends AccountGroupMemberAuditAccessWrapper {
-    protected GroupMemberAudits(AccountGroupMemberAuditAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupMemberAudit, OrmException>
-        getAsync(AccountGroupMemberAudit.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> get(Iterable<AccountGroupMemberAudit.Key> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> byGroupAccount(
-        AccountGroup.Id groupId, Account.Id accountId) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class ByIds extends AccountGroupByIdAccessWrapper {
-    protected ByIds(AccountGroupByIdAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupById, OrmException> getAsync(
-        AccountGroupById.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> get(Iterable<AccountGroupById.Key> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public AccountGroupById get(AccountGroupById.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> all() {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-
-  private static class ByIdAudits extends AccountGroupByIdAudAccessWrapper {
-    protected ByIdAudits(AccountGroupByIdAudAccess delegate) {
-      super(delegate);
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> iterateAllEntities() {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public com.google.common.util.concurrent.CheckedFuture<AccountGroupByIdAud, OrmException>
-        getAsync(AccountGroupByIdAud.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> get(Iterable<AccountGroupByIdAud.Key> keys) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public AccountGroupByIdAud get(AccountGroupByIdAud.Key key) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> byGroupInclude(
-        AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) {
-      throw new UnsupportedOperationException(MSG);
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) {
-      throw new UnsupportedOperationException(MSG);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 22a9cf3..4e648b9 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -53,17 +53,13 @@
 
   // Deleted @Relation(id = 8)
 
-  @Relation(id = 10)
-  AccountGroupAccess accountGroups();
+  // Deleted @Relation(id = 10)
 
-  @Relation(id = 11)
-  AccountGroupNameAccess accountGroupNames();
+  // Deleted @Relation(id = 11)
 
-  @Relation(id = 12)
-  AccountGroupMemberAccess accountGroupMembers();
+  // Deleted @Relation(id = 12)
 
-  @Relation(id = 13)
-  AccountGroupMemberAuditAccess accountGroupMembersAudit();
+  // Deleted @Relation(id = 13)
 
   // Deleted @Relation(id = 17)
 
@@ -92,11 +88,9 @@
 
   // Deleted @Relation(id = 28)
 
-  @Relation(id = 29)
-  AccountGroupByIdAccess accountGroupById();
+  // Deleted @Relation(id = 29)
 
-  @Relation(id = 30)
-  AccountGroupByIdAudAccess accountGroupByIdAud();
+  // Deleted @Relation(id = 30)
 
   int FIRST_ACCOUNT_ID = 1000000;
 
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java b/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
index ef057eb..aed9778 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
@@ -53,9 +53,6 @@
     if (db instanceof DisallowReadFromChangesReviewDbWrapper) {
       return unwrapDb(((DisallowReadFromChangesReviewDbWrapper) db).unsafeGetDelegate());
     }
-    if (db instanceof DisallowReadFromGroupsReviewDbWrapper) {
-      return unwrapDb(((DisallowReadFromGroupsReviewDbWrapper) db).unsafeGetDelegate());
-    }
     return db;
   }
 
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 788c4d4..f8e93ae 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -115,26 +115,6 @@
   }
 
   @Override
-  public AccountGroupAccess accountGroups() {
-    return delegate.accountGroups();
-  }
-
-  @Override
-  public AccountGroupNameAccess accountGroupNames() {
-    return delegate.accountGroupNames();
-  }
-
-  @Override
-  public AccountGroupMemberAccess accountGroupMembers() {
-    return delegate.accountGroupMembers();
-  }
-
-  @Override
-  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
-    return delegate.accountGroupMembersAudit();
-  }
-
-  @Override
   public ChangeAccess changes() {
     return delegate.changes();
   }
@@ -160,16 +140,6 @@
   }
 
   @Override
-  public AccountGroupByIdAccess accountGroupById() {
-    return delegate.accountGroupById();
-  }
-
-  @Override
-  public AccountGroupByIdAudAccess accountGroupByIdAud() {
-    return delegate.accountGroupByIdAud();
-  }
-
-  @Override
   @SuppressWarnings("deprecation")
   public int nextAccountId() throws OrmException {
     return delegate.nextAccountId();
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 79c57484..8e00130 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.ServerInitiated;
@@ -43,7 +42,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -65,7 +63,6 @@
 public class AccountManager {
   private static final Logger log = LoggerFactory.getLogger(AccountManager.class);
 
-  private final SchemaFactory<ReviewDb> schema;
   private final Sequences sequences;
   private final Accounts accounts;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
@@ -82,7 +79,6 @@
 
   @Inject
   AccountManager(
-      SchemaFactory<ReviewDb> schema,
       Sequences sequences,
       @GerritServerConfig Config cfg,
       Accounts accounts,
@@ -95,7 +91,6 @@
       ExternalIds externalIds,
       GroupsUpdate.Factory groupsUpdateFactory,
       SetInactiveFlag setInactiveFlag) {
-    this.schema = schema;
     this.sequences = sequences;
     this.accounts = accounts;
     this.accountsUpdateProvider = accountsUpdateProvider;
@@ -140,51 +135,49 @@
       throw e;
     }
     try {
-      try (ReviewDb db = schema.open()) {
-        ExternalId id = externalIds.get(who.getExternalIdKey());
-        if (id == null) {
-          if (who.getUserName().isPresent()) {
-            ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, who.getUserName().get());
-            ExternalId existingId = externalIds.get(key);
-            if (existingId != null) {
-              // An inconsistency is detected in the database, having a record for scheme "username:"
-              // but no record for scheme "gerrit:". Try to recover by linking
-              // "gerrit:" identity to the existing account.
-              log.warn(
-                  "User {} already has an account; link new identity to the existing account.",
-                  who.getUserName());
-              return link(existingId.accountId(), who);
-            }
+      ExternalId id = externalIds.get(who.getExternalIdKey());
+      if (id == null) {
+        if (who.getUserName().isPresent()) {
+          ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, who.getUserName().get());
+          ExternalId existingId = externalIds.get(key);
+          if (existingId != null) {
+            // An inconsistency is detected in the database, having a record for scheme "username:"
+            // but no record for scheme "gerrit:". Try to recover by linking
+            // "gerrit:" identity to the existing account.
+            log.warn(
+                "User {} already has an account; link new identity to the existing account.",
+                who.getUserName());
+            return link(existingId.accountId(), who);
           }
-          // New account, automatically create and return.
-          log.info("External ID not found. Attempting to create new account.");
-          return create(db, who);
         }
-
-        Optional<AccountState> accountState = byIdCache.get(id.accountId());
-        if (!accountState.isPresent()) {
-          log.error(
-              String.format(
-                  "Authentication with external ID %s failed. Account %s doesn't exist.",
-                  id.key().get(), id.accountId().get()));
-          throw new AccountException("Authentication error, account not found");
-        }
-
-        // Account exists
-        Optional<Account> act = updateAccountActiveStatus(who, accountState.get().getAccount());
-        if (!act.isPresent()) {
-          // The account was deleted since we checked for it last time. This should never happen
-          // since we don't support deletion of accounts.
-          throw new AccountException("Authentication error, account not found");
-        }
-        if (!act.get().isActive()) {
-          throw new AccountException("Authentication error, account inactive");
-        }
-
-        // return the identity to the caller.
-        update(who, id);
-        return new AuthResult(id.accountId(), who.getExternalIdKey(), false);
+        // New account, automatically create and return.
+        log.info("External ID not found. Attempting to create new account.");
+        return create(who);
       }
+
+      Optional<AccountState> accountState = byIdCache.get(id.accountId());
+      if (!accountState.isPresent()) {
+        log.error(
+            String.format(
+                "Authentication with external ID %s failed. Account %s doesn't exist.",
+                id.key().get(), id.accountId().get()));
+        throw new AccountException("Authentication error, account not found");
+      }
+
+      // Account exists
+      Optional<Account> act = updateAccountActiveStatus(who, accountState.get().getAccount());
+      if (!act.isPresent()) {
+        // The account was deleted since we checked for it last time. This should never happen
+        // since we don't support deletion of accounts.
+        throw new AccountException("Authentication error, account not found");
+      }
+      if (!act.get().isActive()) {
+        throw new AccountException("Authentication error, account inactive");
+      }
+
+      // return the identity to the caller.
+      update(who, id);
+      return new AuthResult(id.accountId(), who.getExternalIdKey(), false);
     } catch (OrmException | ConfigInvalidException e) {
       throw new AccountException("Authentication error", e);
     }
@@ -289,7 +282,7 @@
     }
   }
 
-  private AuthResult create(ReviewDb db, AuthRequest who)
+  private AuthResult create(AuthRequest who)
       throws OrmException, AccountException, IOException, ConfigInvalidException {
     Account.Id newId = new Account.Id(sequences.nextAccountId());
 
@@ -349,7 +342,7 @@
               .getPermission(GlobalCapability.ADMINISTRATE_SERVER);
 
       AccountGroup.UUID adminGroupUuid = admin.getRules().get(0).getGroup().getUUID();
-      addGroupMember(db, adminGroupUuid, user);
+      addGroupMember(adminGroupUuid, user);
     }
 
     realm.onCreateAccount(who, accountState.getAccount());
@@ -369,7 +362,7 @@
     return ExternalId.create(SCHEME_USERNAME, username, accountId);
   }
 
-  private void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, IdentifiedUser user)
+  private void addGroupMember(AccountGroup.UUID groupUuid, IdentifiedUser user)
       throws OrmException, IOException, ConfigInvalidException, AccountException {
     // The user initiated this request by logging in. -> Attribute all modifications to that user.
     GroupsUpdate groupsUpdate = groupsUpdateFactory.create(user);
@@ -379,7 +372,7 @@
                 memberIds -> Sets.union(memberIds, ImmutableSet.of(user.getAccountId())))
             .build();
     try {
-      groupsUpdate.updateGroup(db, groupUuid, groupUpdate);
+      groupsUpdate.updateGroup(groupUuid, groupUpdate);
     } catch (NoSuchGroupException e) {
       throw new AccountException(String.format("Group %s not found", groupUuid));
     }
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 58eaadc..a20aab7 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -17,12 +17,10 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -166,20 +164,16 @@
   }
 
   static class ByUUIDLoader extends CacheLoader<String, Optional<InternalGroup>> {
-    private final SchemaFactory<ReviewDb> schema;
     private final Groups groups;
 
     @Inject
-    ByUUIDLoader(SchemaFactory<ReviewDb> sf, Groups groups) {
-      schema = sf;
+    ByUUIDLoader(Groups groups) {
       this.groups = groups;
     }
 
     @Override
     public Optional<InternalGroup> load(String uuid) throws Exception {
-      try (ReviewDb db = schema.open()) {
-        return groups.getGroup(db, new AccountGroup.UUID(uuid));
-      }
+      return groups.getGroup(new AccountGroup.UUID(uuid));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index d8472a6..ba81c6a 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -23,13 +23,11 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
@@ -180,20 +178,16 @@
   }
 
   static class AllExternalLoader extends CacheLoader<String, ImmutableList<AccountGroup.UUID>> {
-    private final SchemaFactory<ReviewDb> schema;
     private final Groups groups;
 
     @Inject
-    AllExternalLoader(SchemaFactory<ReviewDb> sf, Groups groups) {
-      schema = sf;
+    AllExternalLoader(Groups groups) {
       this.groups = groups;
     }
 
     @Override
     public ImmutableList<AccountGroup.UUID> load(String key) throws Exception {
-      try (ReviewDb db = schema.open()) {
-        return groups.getExternalGroups(db).collect(toImmutableList());
-      }
+      return groups.getExternalGroups().collect(toImmutableList());
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/InternalGroupBackend.java b/java/com/google/gerrit/server/account/InternalGroupBackend.java
index 4547807b..ea6eb87 100644
--- a/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -20,15 +20,12 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.group.db.GroupsNoteDbConsistencyChecker;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -43,7 +40,6 @@
   private final GroupControl.Factory groupControlFactory;
   private final GroupCache groupCache;
   private final Groups groups;
-  private final SchemaFactory<ReviewDb> schema;
   private final IncludingGroupMembership.Factory groupMembershipFactory;
 
   @Inject
@@ -51,12 +47,10 @@
       GroupControl.Factory groupControlFactory,
       GroupCache groupCache,
       Groups groups,
-      SchemaFactory<ReviewDb> schema,
       IncludingGroupMembership.Factory groupMembershipFactory) {
     this.groupControlFactory = groupControlFactory;
     this.groupCache = groupCache;
     this.groups = groups;
-    this.schema = schema;
     this.groupMembershipFactory = groupMembershipFactory;
   }
 
@@ -77,13 +71,13 @@
 
   @Override
   public Collection<GroupReference> suggest(String name, ProjectState project) {
-    try (ReviewDb db = schema.open()) {
+    try {
       return groups
-          .getAllGroupReferences(db)
+          .getAllGroupReferences()
           .filter(group -> startsWithIgnoreCase(group, name))
           .filter(this::isVisible)
           .collect(toList());
-    } catch (OrmException | IOException | ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException e) {
       return ImmutableList.of();
     }
   }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index ffd413a..442bc2a 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -278,8 +278,6 @@
    */
   public static ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
       throws ConfigInvalidException {
-    checkNotNull(blobId);
-
     Config externalIdConfig = new Config();
     try {
       externalIdConfig.fromText(new String(raw, UTF_8));
@@ -287,6 +285,13 @@
       throw invalidConfig(noteId, e.getMessage());
     }
 
+    return parse(noteId, externalIdConfig, blobId);
+  }
+
+  public static ExternalId parse(String noteId, Config externalIdConfig, ObjectId blobId)
+      throws ConfigInvalidException {
+    checkNotNull(blobId);
+
     Set<String> externalIdKeys = externalIdConfig.getSubsections(EXTERNAL_ID_SECTION);
     if (externalIdKeys.size() != 1) {
       throw invalidConfig(
@@ -439,11 +444,17 @@
     // c.setString(...) ensures that account IDs are human readable.
     c.setString(
         EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, Integer.toString(accountId().get()));
+
     if (email() != null) {
       c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email());
+    } else {
+      c.unset(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY);
     }
+
     if (password() != null) {
       c.setString(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY, password());
+    } else {
+      c.unset(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 972dfbd..ecb201f 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toSet;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
@@ -371,9 +372,9 @@
 
     Set<ExternalId> newExtIds = new HashSet<>();
     noteMapUpdates.add(
-        (rw, n) -> {
+        (rw, n, f) -> {
           for (ExternalId extId : extIds) {
-            ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+            ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
             newExtIds.add(insertedExtId);
           }
         });
@@ -399,9 +400,9 @@
     Set<ExternalId> removedExtIds = get(ExternalId.Key.from(extIds));
     Set<ExternalId> updatedExtIds = new HashSet<>();
     noteMapUpdates.add(
-        (rw, n) -> {
+        (rw, n, f) -> {
           for (ExternalId extId : extIds) {
-            ExternalId updatedExtId = upsert(rw, inserter, noteMap, extId);
+            ExternalId updatedExtId = upsert(rw, inserter, noteMap, f, extId);
             updatedExtIds.add(updatedExtId);
           }
         });
@@ -429,9 +430,9 @@
     checkLoaded();
     Set<ExternalId> removedExtIds = new HashSet<>();
     noteMapUpdates.add(
-        (rw, n) -> {
+        (rw, n, f) -> {
           for (ExternalId extId : extIds) {
-            remove(rw, noteMap, extId);
+            remove(rw, noteMap, f, extId);
             removedExtIds.add(extId);
           }
         });
@@ -458,9 +459,9 @@
     checkLoaded();
     Set<ExternalId> removedExtIds = new HashSet<>();
     noteMapUpdates.add(
-        (rw, n) -> {
+        (rw, n, f) -> {
           for (ExternalId.Key extIdKey : extIdKeys) {
-            ExternalId removedExtId = remove(rw, noteMap, extIdKey, accountId);
+            ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, accountId);
             removedExtIds.add(removedExtId);
           }
         });
@@ -476,9 +477,9 @@
     checkLoaded();
     Set<ExternalId> removedExtIds = new HashSet<>();
     noteMapUpdates.add(
-        (rw, n) -> {
+        (rw, n, f) -> {
           for (ExternalId.Key extIdKey : extIdKeys) {
-            ExternalId extId = remove(rw, noteMap, extIdKey, null);
+            ExternalId extId = remove(rw, noteMap, f, extIdKey, null);
             removedExtIds.add(extId);
           }
         });
@@ -506,16 +507,16 @@
     Set<ExternalId> removedExtIds = new HashSet<>();
     Set<ExternalId> updatedExtIds = new HashSet<>();
     noteMapUpdates.add(
-        (rw, n) -> {
+        (rw, n, f) -> {
           for (ExternalId.Key extIdKey : toDelete) {
-            ExternalId removedExtId = remove(rw, noteMap, extIdKey, accountId);
+            ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, accountId);
             if (removedExtId != null) {
               removedExtIds.add(removedExtId);
             }
           }
 
           for (ExternalId extId : toAdd) {
-            ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+            ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
             updatedExtIds.add(insertedExtId);
           }
         });
@@ -540,14 +541,14 @@
     Set<ExternalId> removedExtIds = new HashSet<>();
     Set<ExternalId> updatedExtIds = new HashSet<>();
     noteMapUpdates.add(
-        (rw, n) -> {
+        (rw, n, f) -> {
           for (ExternalId.Key extIdKey : toDelete) {
-            ExternalId removedExtId = remove(rw, noteMap, extIdKey, null);
+            ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, null);
             removedExtIds.add(removedExtId);
           }
 
           for (ExternalId extId : toAdd) {
-            ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+            ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
             updatedExtIds.add(insertedExtId);
           }
         });
@@ -660,14 +661,22 @@
     }
 
     try (RevWalk rw = new RevWalk(repo)) {
+      Set<String> footers = new HashSet<>();
       for (NoteMapUpdate noteMapUpdate : noteMapUpdates) {
         try {
-          noteMapUpdate.execute(rw, noteMap);
+          noteMapUpdate.execute(rw, noteMap, footers);
         } catch (DuplicateExternalIdKeyException e) {
           throw new IOException(e);
         }
       }
       noteMapUpdates.clear();
+      if (!footers.isEmpty()) {
+        commit.setMessage(
+            footers
+                .stream()
+                .sorted()
+                .collect(joining("\n", commit.getMessage().trim() + "\n\n", "")));
+      }
 
       RevTree oldTree = revision != null ? rw.parseTree(revision) : null;
       ObjectId newTreeId = noteMap.writeTree(inserter);
@@ -718,14 +727,17 @@
    * <p>If the external ID already exists it is overwritten.
    */
   private static ExternalId upsert(
-      RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
+      RevWalk rw, ObjectInserter ins, NoteMap noteMap, Set<String> footers, ExternalId extId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extId.key().sha1();
     Config c = new Config();
     if (noteMap.contains(extId.key().sha1())) {
-      byte[] raw = readNoteData(rw, noteMap.get(noteId));
+      ObjectId noteDataId = noteMap.get(noteId);
+      byte[] raw = readNoteData(rw, noteDataId);
       try {
         c = new BlobBasedConfig(null, raw);
+        ExternalId oldExtId = ExternalId.parse(noteId.name(), c, noteDataId);
+        addFooters(footers, oldExtId);
       } catch (ConfigInvalidException e) {
         throw new ConfigInvalidException(
             String.format("Invalid external id config for note %s: %s", noteId, e.getMessage()));
@@ -735,7 +747,9 @@
     byte[] raw = c.toText().getBytes(UTF_8);
     ObjectId noteData = ins.insert(OBJ_BLOB, raw);
     noteMap.set(noteId, noteData);
-    return ExternalId.create(extId, noteData);
+    ExternalId newExtId = ExternalId.create(extId, noteData);
+    addFooters(footers, newExtId);
+    return newExtId;
   }
 
   /**
@@ -744,7 +758,8 @@
    * @throws IllegalStateException is thrown if there is an existing external ID that has the same
    *     key, but otherwise doesn't match the specified external ID.
    */
-  private static ExternalId remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
+  private static ExternalId remove(
+      RevWalk rw, NoteMap noteMap, Set<String> footers, ExternalId extId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extId.key().sha1();
     if (!noteMap.contains(noteId)) {
@@ -760,6 +775,7 @@
         extId.toString(),
         actualExtId.toString());
     noteMap.remove(noteId);
+    addFooters(footers, actualExtId);
     return actualExtId;
   }
 
@@ -772,7 +788,11 @@
    *     exists
    */
   private static ExternalId remove(
-      RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
+      RevWalk rw,
+      NoteMap noteMap,
+      Set<String> footers,
+      ExternalId.Key extIdKey,
+      Account.Id expectedAccountId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extIdKey.sha1();
     if (!noteMap.contains(noteId)) {
@@ -792,9 +812,17 @@
           extId.accountId().get());
     }
     noteMap.remove(noteId);
+    addFooters(footers, extId);
     return extId;
   }
 
+  private static void addFooters(Set<String> footers, ExternalId extId) {
+    footers.add("Account: " + extId.accountId().get());
+    if (extId.email() != null) {
+      footers.add("Email: " + extId.email());
+    }
+  }
+
   private void checkExternalIdsDontExist(Collection<ExternalId> extIds)
       throws DuplicateExternalIdKeyException, IOException {
     checkExternalIdKeysDontExist(ExternalId.Key.from(extIds));
@@ -823,7 +851,7 @@
 
   @FunctionalInterface
   private interface NoteMapUpdate {
-    void execute(RevWalk rw, NoteMap noteMap)
+    void execute(RevWalk rw, NoteMap noteMap, Set<String> footers)
         throws IOException, ConfigInvalidException, DuplicateExternalIdKeyException;
   }
 
diff --git a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index f5f1a34..44b6610 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -100,7 +100,7 @@
     }
     try {
       CreateAccount impl = createAccount.create(in.username);
-      permissionBackend.user(self).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
+      permissionBackend.currentUser().checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
       AccountInfo info = impl.apply(TopLevelResource.INSTANCE, in).value();
       return id(info._accountId);
     } catch (Exception e) {
diff --git a/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index 0b3bc64..247be44 100644
--- a/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectResource;
@@ -49,7 +48,6 @@
   private final ProjectsCollection projects;
   private final Provider<ListGroups> listGroups;
   private final Provider<QueryGroups> queryGroups;
-  private final Provider<CurrentUser> user;
   private final PermissionBackend permissionBackend;
   private final CreateGroup.Factory createGroup;
   private final GroupApiImpl.Factory api;
@@ -61,7 +59,6 @@
       ProjectsCollection projects,
       Provider<ListGroups> listGroups,
       Provider<QueryGroups> queryGroups,
-      Provider<CurrentUser> user,
       PermissionBackend permissionBackend,
       CreateGroup.Factory createGroup,
       GroupApiImpl.Factory api) {
@@ -70,7 +67,6 @@
     this.projects = projects;
     this.listGroups = listGroups;
     this.queryGroups = queryGroups;
-    this.user = user;
     this.permissionBackend = permissionBackend;
     this.createGroup = createGroup;
     this.api = api;
@@ -95,7 +91,7 @@
     }
     try {
       CreateGroup impl = createGroup.create(in.name);
-      permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
+      permissionBackend.currentUser().checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
       GroupInfo info = impl.apply(TopLevelResource.INSTANCE, in);
       return id(info.id);
     } catch (Exception e) {
diff --git a/java/com/google/gerrit/server/args4j/ProjectHandler.java b/java/com/google/gerrit/server/args4j/ProjectHandler.java
index 8959d97..1d40b53 100644
--- a/java/com/google/gerrit/server/args4j/ProjectHandler.java
+++ b/java/com/google/gerrit/server/args4j/ProjectHandler.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -25,7 +24,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import org.kohsuke.args4j.CmdLineException;
@@ -42,20 +40,17 @@
 
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
   public ProjectHandler(
       ProjectCache projectCache,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       @Assisted final CmdLineParser parser,
       @Assisted final OptionDef option,
       @Assisted final Setter<ProjectState> setter) {
     super(parser, option, setter);
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
-    this.user = user;
   }
 
   @Override
@@ -84,7 +79,13 @@
       if (state == null) {
         throw new CmdLineException(owner, String.format("project %s not found", nameWithoutSuffix));
       }
-      permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
+      // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+      // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+      // be allowed for other users). Allowing project owners to access here will help them to view
+      // and update the config of hidden projects easily.
+      ProjectPermission permissionToCheck =
+          state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+      permissionBackend.currentUser().project(nameKey).check(permissionToCheck);
     } catch (AuthException e) {
       throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage());
     } catch (PermissionBackendException | IOException e) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 9536d55..21e4cb1 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -155,7 +155,6 @@
 import com.google.gerrit.server.mail.send.SetAssigneeSender;
 import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.notedb.NoteDbModule;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
 import com.google.gerrit.server.patch.PatchScriptFactory;
@@ -175,6 +174,7 @@
 import com.google.gerrit.server.query.change.ConflictsCacheImpl;
 import com.google.gerrit.server.restapi.config.ConfigRestModule;
 import com.google.gerrit.server.restapi.group.GroupModule;
+import com.google.gerrit.server.rules.DefaultSubmitRule;
 import com.google.gerrit.server.rules.PrologModule;
 import com.google.gerrit.server.rules.RulesCache;
 import com.google.gerrit.server.rules.SubmitRule;
@@ -204,14 +204,11 @@
 public class GerritGlobalModule extends FactoryModule {
   private final Config cfg;
   private final AuthModule authModule;
-  private final GroupsMigration groupsMigration;
 
   @Inject
-  GerritGlobalModule(
-      @GerritServerConfig Config cfg, AuthModule authModule, GroupsMigration groupsMigration) {
+  GerritGlobalModule(@GerritServerConfig Config cfg, AuthModule authModule) {
     this.cfg = cfg;
     this.authModule = authModule;
-    this.groupsMigration = groupsMigration;
   }
 
   @Override
@@ -246,6 +243,7 @@
     install(new GroupModule());
     install(new NoteDbModule(cfg));
     install(new PrologModule());
+    install(new DefaultSubmitRule.Module());
     install(new ReceiveCommitsModule());
     install(new SshAddressesModule());
     install(ThreadLocalRequestContext.module());
@@ -307,7 +305,6 @@
     install(new com.google.gerrit.server.restapi.access.Module());
     install(new ConfigRestModule());
     install(new com.google.gerrit.server.restapi.change.Module());
-    install(new com.google.gerrit.server.group.Module(groupsMigration));
     install(new com.google.gerrit.server.restapi.account.Module());
     install(new com.google.gerrit.server.restapi.project.Module());
     install(new com.google.gerrit.server.restapi.group.Module());
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 64f5ae7..9880dae 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -400,7 +400,7 @@
     }
     try {
       permissionBackend
-          .user(currentUser)
+          .currentUser()
           .database(reviewDb)
           .change(notes)
           .check(ChangePermission.ADD_PATCH_SET);
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 8f12cb3..62e8d12 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -150,6 +150,11 @@
 
   protected boolean isVisibleTo(Project.NameKey project, CurrentUser user) {
     try {
+      ProjectState state = projectCache.get(project);
+      if (state == null || !state.statePermitsRead()) {
+        return false;
+      }
+
       permissionBackend.user(user).project(project).check(ProjectPermission.ACCESS);
       return true;
     } catch (AuthException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/extensions/webui/UiActions.java b/java/com/google/gerrit/server/extensions/webui/UiActions.java
index 6714055..043c2e4 100644
--- a/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -31,13 +31,11 @@
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.extensions.webui.UiAction.Description;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendCondition;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -57,12 +55,10 @@
   }
 
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> userProvider;
 
   @Inject
-  UiActions(PermissionBackend permissionBackend, Provider<CurrentUser> userProvider) {
+  UiActions(PermissionBackend permissionBackend) {
     this.permissionBackend = permissionBackend;
-    this.userProvider = userProvider;
   }
 
   public <R extends RestResource> Iterable<UiAction.Description> from(
@@ -146,7 +142,7 @@
       return null;
     }
     if (!globalRequired.isEmpty()) {
-      PermissionBackend.WithUser withUser = permissionBackend.user(userProvider);
+      PermissionBackend.WithUser withUser = permissionBackend.currentUser();
       Iterator<GlobalOrPluginPermission> i = globalRequired.iterator();
       BooleanCondition p = withUser.testCond(i.next());
       while (i.hasNext()) {
diff --git a/java/com/google/gerrit/server/git/receive/NoteDbPushOption.java b/java/com/google/gerrit/server/git/receive/NoteDbPushOption.java
new file mode 100644
index 0000000..8142d0a
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/NoteDbPushOption.java
@@ -0,0 +1,47 @@
+// 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.git.receive;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Optional;
+
+/** Possible values for {@code -o notedb=X} push option. */
+public enum NoteDbPushOption {
+  DISALLOW,
+  ALLOW;
+
+  public static final String OPTION_NAME = "notedb";
+
+  private static final ImmutableMap<String, NoteDbPushOption> ALL =
+      Arrays.stream(values()).collect(toImmutableMap(NoteDbPushOption::value, o -> o));
+
+  /**
+   * Parses an option value from a lowercase string representation.
+   *
+   * @param value input value.
+   * @return parsed value, or empty if no value matched.
+   */
+  public static Optional<NoteDbPushOption> parse(String value) {
+    return Optional.ofNullable(ALL.get(value));
+  }
+
+  public String value() {
+    return name().toLowerCase(Locale.US);
+  }
+}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 0e54e59..8eaa9de 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -387,6 +387,7 @@
   private boolean newChangeForAllNotInTarget;
   private String setFullNameTo;
   private boolean setChangeAsPrivate;
+  private Optional<NoteDbPushOption> noteDbPushOption;
 
   // Handles for outputting back over the wire to the end user.
   private Task newProgress;
@@ -561,6 +562,7 @@
     commandProgress = progress.beginSubTask("refs", UNKNOWN);
 
     try {
+      parsePushOptions();
       parseCommands(commands);
     } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
       for (ReceiveCommand cmd : actualCommands) {
@@ -811,8 +813,7 @@
     return sb.append(":\n").append(error.get()).toString();
   }
 
-  private void parseCommands(Collection<ReceiveCommand> commands)
-      throws PermissionBackendException, NoSuchProjectException, IOException {
+  private void parsePushOptions() {
     List<String> optionList = rp.getPushOptions();
     if (optionList != null) {
       for (String option : optionList) {
@@ -825,6 +826,22 @@
       }
     }
 
+    List<String> noteDbValues = pushOptions.get("notedb");
+    if (!noteDbValues.isEmpty()) {
+      // These semantics for duplicates/errors are somewhat arbitrary and may not match e.g. the
+      // CommandLineParser behavior used by MagicBranchInput.
+      String value = noteDbValues.get(noteDbValues.size() - 1);
+      noteDbPushOption = NoteDbPushOption.parse(value);
+      if (!noteDbPushOption.isPresent()) {
+        addError("Invalid value in -o " + NoteDbPushOption.OPTION_NAME + "=" + value);
+      }
+    } else {
+      noteDbPushOption = Optional.of(NoteDbPushOption.DISALLOW);
+    }
+  }
+
+  private void parseCommands(Collection<ReceiveCommand> commands)
+      throws PermissionBackendException, NoSuchProjectException, IOException {
     logDebug("Parsing {} commands", commands.size());
     for (ReceiveCommand cmd : commands) {
       if (cmd.getResult() != NOT_ATTEMPTED) {
@@ -875,6 +892,38 @@
         continue;
       }
 
+      if (RefNames.isNoteDbMetaRef(cmd.getRefName())) {
+        // Reject pushes to NoteDb refs without a special option and permission. Note that this
+        // prohibition doesn't depend on NoteDb being enabled in any way, since all sites will
+        // migrate to NoteDb eventually, and we don't want garbage data waiting there when the
+        // migration finishes.
+        logDebug(
+            "{} NoteDb ref {} with {}={}",
+            cmd.getType(),
+            cmd.getRefName(),
+            NoteDbPushOption.OPTION_NAME,
+            noteDbPushOption);
+        if (!Optional.of(NoteDbPushOption.ALLOW).equals(noteDbPushOption)) {
+          // Only reject this command, not the whole push. This supports the use case of "git clone
+          // --mirror" followed by "git push --mirror", when the user doesn't really intend to clone
+          // or mirror the NoteDb data; there is no single refspec that describes all refs *except*
+          // NoteDb refs.
+          reject(
+              cmd,
+              "NoteDb update requires -o "
+                  + NoteDbPushOption.OPTION_NAME
+                  + "="
+                  + NoteDbPushOption.ALLOW.value());
+          continue;
+        }
+        try {
+          permissionBackend.user(user).check(GlobalPermission.ACCESS_DATABASE);
+        } catch (AuthException e) {
+          reject(cmd, "NoteDb update requires access database permission");
+          continue;
+        }
+      }
+
       switch (cmd.getType()) {
         case CREATE:
           parseCreate(cmd);
diff --git a/java/com/google/gerrit/server/group/DbGroupAuditListener.java b/java/com/google/gerrit/server/group/DbGroupAuditListener.java
deleted file mode 100644
index 8b53348..0000000
--- a/java/com/google/gerrit/server/group/DbGroupAuditListener.java
+++ /dev/null
@@ -1,265 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.group;
-
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
-
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.UniversalGroupBackend;
-import com.google.gerrit.server.audit.group.GroupAuditEvent;
-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.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import java.sql.Timestamp;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import org.slf4j.Logger;
-
-class DbGroupAuditListener implements GroupAuditListener {
-  private static final Logger log = org.slf4j.LoggerFactory.getLogger(DbGroupAuditListener.class);
-
-  private final SchemaFactory<ReviewDb> schema;
-  private final AccountCache accountCache;
-  private final GroupCache groupCache;
-  private final UniversalGroupBackend groupBackend;
-
-  @Inject
-  DbGroupAuditListener(
-      SchemaFactory<ReviewDb> schema,
-      AccountCache accountCache,
-      GroupCache groupCache,
-      UniversalGroupBackend groupBackend) {
-    this.schema = schema;
-    this.accountCache = accountCache;
-    this.groupCache = groupCache;
-    this.groupBackend = groupBackend;
-  }
-
-  @Override
-  public void onAddMembers(GroupMemberAuditEvent event) {
-    Optional<InternalGroup> updatedGroup = groupCache.get(event.getUpdatedGroup());
-    if (!updatedGroup.isPresent()) {
-      logFailToLoadUpdatedGroup(event);
-      return;
-    }
-
-    InternalGroup group = updatedGroup.get();
-    try (ReviewDb db = unwrapDb(schema.open())) {
-      db.accountGroupMembersAudit().insert(toAccountGroupMemberAudits(event, group.getId()));
-    } catch (OrmException e) {
-      logOrmException(
-          "Cannot log add accounts to group event performed by user", event, group.getName(), e);
-    }
-  }
-
-  @Override
-  public void onDeleteMembers(GroupMemberAuditEvent event) {
-    Optional<InternalGroup> updatedGroup = groupCache.get(event.getUpdatedGroup());
-    if (!updatedGroup.isPresent()) {
-      logFailToLoadUpdatedGroup(event);
-      return;
-    }
-
-    InternalGroup group = updatedGroup.get();
-    List<AccountGroupMemberAudit> auditInserts = new ArrayList<>();
-    List<AccountGroupMemberAudit> auditUpdates = new ArrayList<>();
-    try (ReviewDb db = unwrapDb(schema.open())) {
-      for (Account.Id accountId : event.getModifiedMembers()) {
-        AccountGroupMemberAudit audit = null;
-        ResultSet<AccountGroupMemberAudit> audits =
-            db.accountGroupMembersAudit().byGroupAccount(group.getId(), accountId);
-        for (AccountGroupMemberAudit a : audits) {
-          if (a.isActive()) {
-            audit = a;
-            break;
-          }
-        }
-
-        if (audit != null) {
-          audit.removed(event.getActor(), event.getTimestamp());
-          auditUpdates.add(audit);
-          continue;
-        }
-        AccountGroupMember.Key key = new AccountGroupMember.Key(accountId, group.getId());
-        audit =
-            new AccountGroupMemberAudit(
-                new AccountGroupMember(key), event.getActor(), event.getTimestamp());
-        audit.removedLegacy();
-        auditInserts.add(audit);
-      }
-      db.accountGroupMembersAudit().update(auditUpdates);
-      db.accountGroupMembersAudit().insert(auditInserts);
-    } catch (OrmException e) {
-      logOrmException(
-          "Cannot log delete accounts from group event performed by user",
-          event,
-          group.getName(),
-          e);
-    }
-  }
-
-  @Override
-  public void onAddSubgroups(GroupSubgroupAuditEvent event) {
-    Optional<InternalGroup> updatedGroup = groupCache.get(event.getUpdatedGroup());
-    if (!updatedGroup.isPresent()) {
-      logFailToLoadUpdatedGroup(event);
-      return;
-    }
-
-    InternalGroup group = updatedGroup.get();
-    try (ReviewDb db = unwrapDb(schema.open())) {
-      db.accountGroupByIdAud().insert(toAccountGroupByIdAudits(event, group.getId()));
-    } catch (OrmException e) {
-      logOrmException(
-          "Cannot log add groups to group event performed by user", event, group.getName(), e);
-    }
-  }
-
-  @Override
-  public void onDeleteSubgroups(GroupSubgroupAuditEvent event) {
-    Optional<InternalGroup> updatedGroup = groupCache.get(event.getUpdatedGroup());
-    if (!updatedGroup.isPresent()) {
-      logFailToLoadUpdatedGroup(event);
-      return;
-    }
-
-    InternalGroup group = updatedGroup.get();
-    List<AccountGroupByIdAud> auditUpdates = new ArrayList<>();
-    try (ReviewDb db = unwrapDb(schema.open())) {
-      for (AccountGroup.UUID uuid : event.getModifiedSubgroups()) {
-        AccountGroupByIdAud audit = null;
-        ResultSet<AccountGroupByIdAud> audits =
-            db.accountGroupByIdAud().byGroupInclude(updatedGroup.get().getId(), uuid);
-        for (AccountGroupByIdAud a : audits) {
-          if (a.isActive()) {
-            audit = a;
-            break;
-          }
-        }
-
-        if (audit != null) {
-          audit.removed(event.getActor(), event.getTimestamp());
-          auditUpdates.add(audit);
-        }
-      }
-      db.accountGroupByIdAud().update(auditUpdates);
-    } catch (OrmException e) {
-      logOrmException(
-          "Cannot log delete groups from group event performed by user", event, group.getName(), e);
-    }
-  }
-
-  private void logFailToLoadUpdatedGroup(GroupAuditEvent event) {
-    ImmutableList<String> descriptions = createEventDescriptions(event, "(fail to load group)");
-    String message =
-        createErrorMessage("Fail to load the updated group", event.getActor(), descriptions);
-    log.error(message);
-  }
-
-  private void logOrmException(
-      String header, GroupAuditEvent event, String updatedGroupName, OrmException e) {
-    ImmutableList<String> descriptions = createEventDescriptions(event, updatedGroupName);
-    String message = createErrorMessage(header, event.getActor(), descriptions);
-    log.error(message, e);
-  }
-
-  private ImmutableList<String> createEventDescriptions(
-      GroupAuditEvent event, String updatedGroupName) {
-    ImmutableList.Builder<String> builder = ImmutableList.builder();
-    if (event instanceof GroupMemberAuditEvent) {
-      GroupMemberAuditEvent memberAuditEvent = (GroupMemberAuditEvent) event;
-      for (Account.Id accountId : memberAuditEvent.getModifiedMembers()) {
-        String userName = getUserName(accountId).orElse("");
-        builder.add(
-            MessageFormat.format(
-                "account {0}/{1}, group {2}/{3}",
-                accountId, userName, event.getUpdatedGroup(), updatedGroupName));
-      }
-    } else if (event instanceof GroupSubgroupAuditEvent) {
-      GroupSubgroupAuditEvent subgroupAuditEvent = (GroupSubgroupAuditEvent) event;
-      for (AccountGroup.UUID groupUuid : subgroupAuditEvent.getModifiedSubgroups()) {
-        String groupName = groupBackend.get(groupUuid).getName();
-        builder.add(
-            MessageFormat.format(
-                "group {0}/{1}, group {2}/{3}",
-                groupUuid, groupName, subgroupAuditEvent.getUpdatedGroup(), updatedGroupName));
-      }
-    }
-
-    return builder.build();
-  }
-
-  private String createErrorMessage(
-      String header, Account.Id me, ImmutableList<String> descriptions) {
-    StringBuilder message = new StringBuilder(header);
-    message.append(" ");
-    message.append(me);
-    message.append("/");
-    message.append(getUserName(me).orElse(null));
-    message.append(": ");
-    message.append(Joiner.on("; ").join(descriptions));
-    return message.toString();
-  }
-
-  private Optional<String> getUserName(Account.Id accountId) {
-    return accountCache.get(accountId).map(AccountState::getUserName).orElse(Optional.empty());
-  }
-
-  private static ImmutableSet<AccountGroupMemberAudit> toAccountGroupMemberAudits(
-      GroupMemberAuditEvent event, AccountGroup.Id updatedGroupId) {
-    Timestamp timestamp = event.getTimestamp();
-    Account.Id actor = event.getActor();
-    return event
-        .getModifiedMembers()
-        .stream()
-        .map(
-            member ->
-                new AccountGroupMemberAudit(
-                    new AccountGroupMemberAudit.Key(member, updatedGroupId, timestamp), actor))
-        .collect(toImmutableSet());
-  }
-
-  private static ImmutableSet<AccountGroupByIdAud> toAccountGroupByIdAudits(
-      GroupSubgroupAuditEvent event, AccountGroup.Id updatedGroupId) {
-    Timestamp timestamp = event.getTimestamp();
-    Account.Id actor = event.getActor();
-    return event
-        .getModifiedSubgroups()
-        .stream()
-        .map(
-            subgroup ->
-                new AccountGroupByIdAud(
-                    new AccountGroupByIdAud.Key(updatedGroupId, subgroup, timestamp), actor))
-        .collect(toImmutableSet());
-  }
-}
diff --git a/java/com/google/gerrit/server/group/Module.java b/java/com/google/gerrit/server/group/Module.java
deleted file mode 100644
index a1d8378b..0000000
--- a/java/com/google/gerrit/server/group/Module.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.google.gerrit.server.group;
-
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.audit.group.GroupAuditListener;
-import com.google.gerrit.server.notedb.GroupsMigration;
-
-public class Module extends FactoryModule {
-  private final GroupsMigration groupsMigration;
-
-  public Module(GroupsMigration groupsMigration) {
-    this.groupsMigration = groupsMigration;
-  }
-
-  @Override
-  protected void configure() {
-    if (!groupsMigration.disableGroupReviewDb()) {
-      // DbGroupAuditListener is used solely for the ReviewDb audit log. It does not respect
-      // ReviewDb wrappers that disable reads. Hence, we don't want to bind it if ReviewDb is
-      // disabled.
-      DynamicSet.bind(binder(), GroupAuditListener.class).to(DbGroupAuditListener.class);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 91cc11c..ebbb7a1 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StartupCheck;
 import com.google.gerrit.server.StartupException;
@@ -34,8 +33,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -191,13 +188,11 @@
   public static class NameCheck implements StartupCheck {
     private final Config cfg;
     private final Groups groups;
-    private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    NameCheck(@GerritServerConfig Config cfg, Groups groups, SchemaFactory<ReviewDb> schema) {
+    NameCheck(@GerritServerConfig Config cfg, Groups groups) {
       this.cfg = cfg;
       this.groups = groups;
-      this.schema = schema;
     }
 
     @Override
@@ -216,14 +211,14 @@
       }
 
       Optional<GroupReference> conflictingGroup;
-      try (ReviewDb db = schema.open()) {
+      try {
         conflictingGroup =
             groups
-                .getAllGroupReferences(db)
+                .getAllGroupReferences()
                 .filter(group -> hasConfiguredName(byLowerCaseConfiguredName, group))
                 .findAny();
 
-      } catch (OrmException | IOException | ConfigInvalidException ignored) {
+      } catch (IOException | ConfigInvalidException ignored) {
         return;
       }
 
diff --git a/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java b/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java
index 63ae8ca..62cc20d 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfigCommitMessage.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
 import com.google.gerrit.server.group.InternalGroup;
 import java.util.Optional;
 import java.util.Set;
@@ -55,9 +56,12 @@
 
     StringJoiner footerJoiner = new StringJoiner("\n", "\n\n", "");
     footerJoiner.setEmptyValue("");
-    getFooterForRename().ifPresent(footerJoiner::add);
-    getFootersForMemberModifications().forEach(footerJoiner::add);
-    getFootersForSubgroupModifications().forEach(footerJoiner::add);
+    Streams.concat(
+            Streams.stream(getFooterForRename()),
+            getFootersForMemberModifications(),
+            getFootersForSubgroupModifications())
+        .sorted()
+        .forEach(footerJoiner::add);
     String footer = footerJoiner.toString();
 
     return summaryLine + footer;
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index bbdc8d2..46fa998 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -14,28 +14,15 @@
 
 package com.google.gerrit.server.group.db;
 
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.notedb.GroupsMigration;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -48,10 +35,10 @@
 /**
  * A database accessor for read calls related to groups.
  *
- * <p>All calls which read group related details from the database (either ReviewDb or NoteDb) are
- * gathered here. Other classes should always use this class instead of accessing the database
- * directly. There are a few exceptions though: schema classes, wrapper classes, and classes
- * executed during init. The latter ones should use {@code GroupsOnInit} instead.
+ * <p>All calls which read group related details from the database are gathered here. Other classes
+ * should always use this class instead of accessing the database directly. There are a few
+ * exceptions though: schema classes, wrapper classes, and classes executed during init. The latter
+ * ones should use {@code GroupsOnInit} instead.
  *
  * <p>Most callers should not need to read groups directly from the database; they should use the
  * {@link com.google.gerrit.server.account.GroupCache GroupCache} instead.
@@ -60,18 +47,13 @@
  */
 @Singleton
 public class Groups {
-  private final GroupsMigration groupsMigration;
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
   private final AuditLogReader auditLogReader;
 
   @Inject
   public Groups(
-      GroupsMigration groupsMigration,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      AuditLogReader auditLogReader) {
-    this.groupsMigration = groupsMigration;
+      GitRepositoryManager repoManager, AllUsersName allUsersName, AuditLogReader auditLogReader) {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.auditLogReader = auditLogReader;
@@ -80,27 +62,16 @@
   /**
    * Returns the {@code InternalGroup} for the specified UUID if it exists.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @param groupUuid the UUID of the group
    * @return the found {@code InternalGroup} if it exists, or else an empty {@code Optional}
-   * @throws OrmDuplicateKeyException if multiple groups are found for the specified UUID
-   * @throws OrmException if the group couldn't be retrieved from ReviewDb
    * @throws IOException if the group couldn't be retrieved from NoteDb
    * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
    */
-  public Optional<InternalGroup> getGroup(ReviewDb db, AccountGroup.UUID groupUuid)
-      throws OrmException, IOException, ConfigInvalidException {
-    if (groupsMigration.readFromNoteDb()) {
-      try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-        return getGroupFromNoteDb(allUsersRepo, groupUuid);
-      }
+  public Optional<InternalGroup> getGroup(AccountGroup.UUID groupUuid)
+      throws IOException, ConfigInvalidException {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+      return getGroupFromNoteDb(allUsersRepo, groupUuid);
     }
-
-    Optional<AccountGroup> accountGroup = getGroupFromReviewDb(db, groupUuid);
-    if (!accountGroup.isPresent()) {
-      return Optional.empty();
-    }
-    return Optional.of(asInternalGroup(db, accountGroup.get()));
   }
 
   private static Optional<InternalGroup> getGroupFromNoteDb(
@@ -116,130 +87,31 @@
     return loadedGroup;
   }
 
-  public static InternalGroup asInternalGroup(ReviewDb db, AccountGroup accountGroup)
-      throws OrmException {
-    ImmutableSet<Account.Id> members =
-        getMembersFromReviewDb(db, accountGroup.getId()).collect(toImmutableSet());
-    ImmutableSet<AccountGroup.UUID> subgroups =
-        getSubgroupsFromReviewDb(db, accountGroup.getId()).collect(toImmutableSet());
-    return InternalGroup.create(accountGroup, members, subgroups);
-  }
-
-  /**
-   * Returns the {@code AccountGroup} for the specified UUID.
-   *
-   * @param db the {@code ReviewDb} instance to use for lookups
-   * @param groupUuid the UUID of the group
-   * @return the {@code AccountGroup} which has the specified UUID
-   * @throws OrmDuplicateKeyException if multiple groups are found for the specified UUID
-   * @throws OrmException if the group couldn't be retrieved from ReviewDb
-   * @throws NoSuchGroupException if a group with such a UUID doesn't exist
-   */
-  static AccountGroup getExistingGroupFromReviewDb(ReviewDb db, AccountGroup.UUID groupUuid)
-      throws OrmException, NoSuchGroupException {
-    Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
-    return group.orElseThrow(() -> new NoSuchGroupException(groupUuid));
-  }
-
-  /**
-   * Returns the {@code AccountGroup} for the specified UUID if it exists.
-   *
-   * @param db the {@code ReviewDb} instance to use for lookups
-   * @param groupUuid the UUID of the group
-   * @return the found {@code AccountGroup} if it exists, or else an empty {@code Optional}
-   * @throws OrmDuplicateKeyException if multiple groups are found for the specified UUID
-   * @throws OrmException if the group couldn't be retrieved from ReviewDb
-   */
-  private static Optional<AccountGroup> getGroupFromReviewDb(
-      ReviewDb db, AccountGroup.UUID groupUuid) throws OrmException {
-    List<AccountGroup> accountGroups = db.accountGroups().byUUID(groupUuid).toList();
-    if (accountGroups.size() == 1) {
-      return Optional.of(Iterables.getOnlyElement(accountGroups));
-    } else if (accountGroups.isEmpty()) {
-      return Optional.empty();
-    } else {
-      throw new OrmDuplicateKeyException("Duplicate group UUID " + groupUuid);
-    }
-  }
-
   /**
    * Returns {@code GroupReference}s for all internal groups.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @return a stream of the {@code GroupReference}s of all internal groups
-   * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
    */
-  public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
-      throws OrmException, IOException, ConfigInvalidException {
-    if (groupsMigration.readFromNoteDb()) {
-      try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-        return GroupNameNotes.loadAllGroups(allUsersRepo).stream();
-      }
+  public Stream<GroupReference> getAllGroupReferences() throws IOException, ConfigInvalidException {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+      return GroupNameNotes.loadAllGroups(allUsersRepo).stream();
     }
-
-    return Streams.stream(db.accountGroups().all())
-        .map(group -> new GroupReference(group.getGroupUUID(), group.getName()));
-  }
-
-  /**
-   * Returns the members (accounts) of a group.
-   *
-   * <p><strong>Note</strong>: This method doesn't check whether the accounts exist!
-   *
-   * @param db the {@code ReviewDb} instance to use for lookups
-   * @param groupId the ID of the group
-   * @return a stream of the IDs of the members
-   * @throws OrmException if an error occurs while reading from ReviewDb
-   */
-  static Stream<Account.Id> getMembersFromReviewDb(ReviewDb db, AccountGroup.Id groupId)
-      throws OrmException {
-    ResultSet<AccountGroupMember> accountGroupMembers = db.accountGroupMembers().byGroup(groupId);
-    return Streams.stream(accountGroupMembers).map(AccountGroupMember::getAccountId);
-  }
-
-  /**
-   * Returns the subgroups of a group.
-   *
-   * <p>This parent group must be an internal group whereas the subgroups can either be internal or
-   * external groups.
-   *
-   * <p><strong>Note</strong>: This method doesn't check whether the subgroups exist!
-   *
-   * @param db the {@code ReviewDb} instance to use for lookups
-   * @param groupId the ID of the group
-   * @return a stream of the UUIDs of the subgroups
-   * @throws OrmException if an error occurs while reading from ReviewDb
-   */
-  static Stream<AccountGroup.UUID> getSubgroupsFromReviewDb(ReviewDb db, AccountGroup.Id groupId)
-      throws OrmException {
-    ResultSet<AccountGroupById> accountGroupByIds = db.accountGroupById().byGroup(groupId);
-    return Streams.stream(accountGroupByIds).map(AccountGroupById::getIncludeUUID).distinct();
   }
 
   /**
    * Returns all known external groups. External groups are 'known' when they are specified as a
    * subgroup of an internal group.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @return a stream of the UUIDs of the known external groups
-   * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
    */
-  public Stream<AccountGroup.UUID> getExternalGroups(ReviewDb db)
-      throws OrmException, IOException, ConfigInvalidException {
-    if (groupsMigration.readFromNoteDb()) {
-      try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-        return getExternalGroupsFromNoteDb(allUsersRepo);
-      }
+  public Stream<AccountGroup.UUID> getExternalGroups() throws IOException, ConfigInvalidException {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+      return getExternalGroupsFromNoteDb(allUsersRepo);
     }
-
-    return Streams.stream(db.accountGroupById().all())
-        .map(AccountGroupById::getIncludeUUID)
-        .distinct()
-        .filter(groupUuid -> !AccountGroup.isInternalGroup(groupUuid));
   }
 
   private static Stream<AccountGroup.UUID> getExternalGroupsFromNoteDb(Repository allUsersRepo)
@@ -259,50 +131,28 @@
   /**
    * Returns the membership audit records for a given group.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @param repo All-Users repository.
    * @param groupUuid the UUID of the group
    * @return the audit records, in arbitrary order; empty if the group does not exist
-   * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
    */
-  public List<AccountGroupMemberAudit> getMembersAudit(
-      ReviewDb db, Repository repo, AccountGroup.UUID groupUuid)
-      throws OrmException, IOException, ConfigInvalidException {
-    if (groupsMigration.readFromNoteDb()) {
-      return auditLogReader.getMembersAudit(repo, groupUuid);
-    }
-    Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
-    if (!group.isPresent()) {
-      return ImmutableList.of();
-    }
-
-    return db.accountGroupMembersAudit().byGroup(group.get().getId()).toList();
+  public List<AccountGroupMemberAudit> getMembersAudit(Repository repo, AccountGroup.UUID groupUuid)
+      throws IOException, ConfigInvalidException {
+    return auditLogReader.getMembersAudit(repo, groupUuid);
   }
 
   /**
    * Returns the subgroup audit records for a given group.
    *
-   * @param db the {@code ReviewDb} instance to use for lookups
    * @param repo All-Users repository.
    * @param groupUuid the UUID of the group
    * @return the audit records, in arbitrary order; empty if the group does not exist
-   * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws IOException if an error occurs while reading from NoteDb
    * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
    */
-  public List<AccountGroupByIdAud> getSubgroupsAudit(
-      ReviewDb db, Repository repo, AccountGroup.UUID groupUuid)
-      throws OrmException, IOException, ConfigInvalidException {
-    if (groupsMigration.readFromNoteDb()) {
-      return auditLogReader.getSubgroupsAudit(repo, groupUuid);
-    }
-    Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
-    if (!group.isPresent()) {
-      return ImmutableList.of();
-    }
-
-    return db.accountGroupByIdAud().byGroup(group.get().getId()).toList();
+  public List<AccountGroupByIdAud> getSubgroupsAudit(Repository repo, AccountGroup.UUID groupUuid)
+      throws IOException, ConfigInvalidException {
+    return auditLogReader.getSubgroupsAudit(repo, groupUuid);
   }
 }
diff --git a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
index 2335d32..9b86221 100644
--- a/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsConsistencyChecker.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -52,7 +51,6 @@
   private final Accounts accounts;
   private final GitRepositoryManager repoManager;
   private final GroupsNoteDbConsistencyChecker globalChecker;
-  private final GroupsMigration groupsMigration;
 
   @Inject
   GroupsConsistencyChecker(
@@ -60,22 +58,16 @@
       GroupBackend groupBackend,
       Accounts accounts,
       GitRepositoryManager repositoryManager,
-      GroupsNoteDbConsistencyChecker globalChecker,
-      GroupsMigration groupsMigration) {
+      GroupsNoteDbConsistencyChecker globalChecker) {
     this.allUsersName = allUsersName;
     this.groupBackend = groupBackend;
     this.accounts = accounts;
     this.repoManager = repositoryManager;
     this.globalChecker = globalChecker;
-    this.groupsMigration = groupsMigration;
   }
 
   /** Checks that all internal group references exist, and that no groups have cycles. */
   public List<ConsistencyProblemInfo> check() throws IOException {
-    if (!groupsMigration.writeToNoteDb()) {
-      return new ArrayList<>();
-    }
-
     try (Repository repo = repoManager.openRepository(allUsersName)) {
       GroupsNoteDbConsistencyChecker.Result result = globalChecker.check(repo);
       if (!result.problems.isEmpty()) {
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index f10409e..1fad1fb 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -14,14 +14,10 @@
 
 package com.google.gerrit.server.group.db;
 
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.server.group.db.Groups.getExistingGroupFromReviewDb;
-
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
@@ -29,12 +25,7 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -51,11 +42,9 @@
 import com.google.gerrit.server.git.RenameGroupOp;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -75,10 +64,10 @@
 /**
  * A database accessor for write calls related to groups.
  *
- * <p>All calls which write group related details to the database (either ReviewDb or NoteDb) are
- * gathered here. Other classes should always use this class instead of accessing the database
- * directly. There are a few exceptions though: schema classes, wrapper classes, and classes
- * executed during init. The latter ones should use {@code GroupsOnInit} instead.
+ * <p>All calls which write group related details to the database are gathered here. Other classes
+ * should always use this class instead of accessing the database directly. There are a few
+ * exceptions though: schema classes, wrapper classes, and classes executed during init. The latter
+ * ones should use {@code GroupsOnInit} instead.
  *
  * <p>If not explicitly stated, all methods of this class refer to <em>internal</em> groups.
  */
@@ -109,7 +98,6 @@
   private final AuditLogFormatter auditLogFormatter;
   private final PersonIdent authorIdent;
   private final MetaDataUpdateFactory metaDataUpdateFactory;
-  private final GroupsMigration groupsMigration;
   private final GitReferenceUpdated gitRefUpdated;
   private final RetryHelper retryHelper;
 
@@ -127,7 +115,6 @@
       @GerritServerId String serverId,
       @GerritPersonIdent PersonIdent serverIdent,
       MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
-      GroupsMigration groupsMigration,
       GitReferenceUpdated gitRefUpdated,
       RetryHelper retryHelper,
       @Assisted @Nullable IdentifiedUser currentUser) {
@@ -138,7 +125,6 @@
     this.indexer = indexer;
     this.auditService = auditService;
     this.renameGroupOpFactory = renameGroupOpFactory;
-    this.groupsMigration = groupsMigration;
     this.gitRefUpdated = gitRefUpdated;
     this.retryHelper = retryHelper;
     this.currentUser = currentUser;
@@ -184,37 +170,18 @@
   /**
    * Creates the specified group for the specified members (accounts).
    *
-   * @param db the {@code ReviewDb} instance to update
    * @param groupCreation an {@code InternalGroupCreation} which specifies all mandatory properties
    *     of the group
    * @param groupUpdate an {@code InternalGroupUpdate} which specifies optional properties of the
    *     group. If this {@code InternalGroupUpdate} updates a property which was already specified
    *     by the {@code InternalGroupCreation}, the value of this {@code InternalGroupUpdate} wins.
-   * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
    * @throws OrmDuplicateKeyException if a group with the chosen name already exists
    * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
    * @return the created {@code InternalGroup}
    */
   public InternalGroup createGroup(
-      ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmException, IOException, ConfigInvalidException {
-    if (!groupsMigration.disableGroupReviewDb()) {
-      if (!groupUpdate.getUpdatedOn().isPresent()) {
-        // Set updatedOn to a specific value so that the same timestamp is used for ReviewDb and
-        // NoteDb.
-        groupUpdate = groupUpdate.toBuilder().setUpdatedOn(TimeUtil.nowTs()).build();
-      }
-
-      InternalGroup createdGroupInReviewDb =
-          createGroupInReviewDb(ReviewDbUtil.unwrapDb(db), groupCreation, groupUpdate);
-
-      if (!groupsMigration.writeToNoteDb()) {
-        updateCachesOnGroupCreation(createdGroupInReviewDb);
-        dispatchAuditEventsOnGroupCreation(createdGroupInReviewDb);
-        return createdGroupInReviewDb;
-      }
-    }
-
+      InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
+      throws OrmDuplicateKeyException, IOException, ConfigInvalidException {
     InternalGroup createdGroup = createGroupInNoteDbWithRetry(groupCreation, groupUpdate);
     updateCachesOnGroupCreation(createdGroup);
     dispatchAuditEventsOnGroupCreation(createdGroup);
@@ -224,67 +191,32 @@
   /**
    * Updates the specified group.
    *
-   * @param db the {@code ReviewDb} instance to update
    * @param groupUuid the UUID of the group to update
    * @param groupUpdate an {@code InternalGroupUpdate} which indicates the desired updates on the
    *     group
-   * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
-   * @throws com.google.gwtorm.server.OrmDuplicateKeyException if the new name of the group is used
-   *     by another group
+   * @throws OrmDuplicateKeyException if the new name of the group is used by another group
    * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
    * @throws NoSuchGroupException if the specified group doesn't exist
    */
-  public void updateGroup(ReviewDb db, AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
+  public void updateGroup(AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
+      throws OrmDuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException {
     Optional<Timestamp> updatedOn = groupUpdate.getUpdatedOn();
     if (!updatedOn.isPresent()) {
-      // Set updatedOn to a specific value so that the same timestamp is used for ReviewDb and
-      // NoteDb. This timestamp is also used by audit events.
       updatedOn = Optional.of(TimeUtil.nowTs());
       groupUpdate = groupUpdate.toBuilder().setUpdatedOn(updatedOn.get()).build();
     }
 
-    UpdateResult result = updateGroupInDb(db, groupUuid, groupUpdate);
+    UpdateResult result = updateGroupInDb(groupUuid, groupUpdate);
     updateCachesOnGroupUpdate(result);
     dispatchAuditEventsOnGroupUpdate(result, updatedOn.get());
   }
 
   @VisibleForTesting
-  public UpdateResult updateGroupInDb(
-      ReviewDb db, AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
-    UpdateResult reviewDbUpdateResult = null;
-    if (!groupsMigration.disableGroupReviewDb()) {
-      AccountGroup group = getExistingGroupFromReviewDb(ReviewDbUtil.unwrapDb(db), groupUuid);
-      reviewDbUpdateResult = updateGroupInReviewDb(ReviewDbUtil.unwrapDb(db), group, groupUpdate);
-
-      if (!groupsMigration.writeToNoteDb()) {
-        return reviewDbUpdateResult;
-      }
-    }
-
+  public UpdateResult updateGroupInDb(AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
+      throws OrmDuplicateKeyException, NoSuchGroupException, IOException, ConfigInvalidException {
     Optional<UpdateResult> noteDbUpdateResult =
         updateGroupInNoteDbWithRetry(groupUuid, groupUpdate);
-    return noteDbUpdateResult.orElse(reviewDbUpdateResult);
-  }
-
-  private InternalGroup createGroupInReviewDb(
-      ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmException {
-
-    AccountGroupName gn = new AccountGroupName(groupCreation.getNameKey(), groupCreation.getId());
-    // first insert the group name to validate that the group name hasn't
-    // already been used to create another group
-    db.accountGroupNames().insert(ImmutableList.of(gn));
-
-    Timestamp createdOn = groupUpdate.getUpdatedOn().orElseGet(TimeUtil::nowTs);
-    AccountGroup group = createAccountGroup(groupCreation, createdOn);
-    UpdateResult updateResult = updateGroupInReviewDb(db, group, groupUpdate);
-    return InternalGroup.create(
-        group,
-        updateResult.getAddedMembers(),
-        updateResult.getAddedSubgroups(),
-        updateResult.getRefState());
+    return noteDbUpdateResult.orElse(null);
   }
 
   public static AccountGroup createAccountGroup(
@@ -308,157 +240,9 @@
     groupUpdate.getVisibleToAll().ifPresent(group::setVisibleToAll);
   }
 
-  private UpdateResult updateGroupInReviewDb(
-      ReviewDb db, AccountGroup group, InternalGroupUpdate groupUpdate) throws OrmException {
-    AccountGroup.NameKey originalName = group.getNameKey();
-    applyUpdate(group, groupUpdate);
-    AccountGroup.NameKey updatedName = group.getNameKey();
-
-    // The name must be inserted first so that we stop early for already used names.
-    updateNameInReviewDb(db, group.getId(), originalName, updatedName);
-    db.accountGroups().upsert(ImmutableList.of(group));
-
-    ImmutableSet<Account.Id> originalMembers =
-        Groups.getMembersFromReviewDb(db, group.getId()).collect(toImmutableSet());
-    ImmutableSet<Account.Id> updatedMembers =
-        ImmutableSet.copyOf(groupUpdate.getMemberModification().apply(originalMembers));
-    ImmutableSet<AccountGroup.UUID> originalSubgroups =
-        Groups.getSubgroupsFromReviewDb(db, group.getId()).collect(toImmutableSet());
-    ImmutableSet<AccountGroup.UUID> updatedSubgroups =
-        ImmutableSet.copyOf(groupUpdate.getSubgroupModification().apply(originalSubgroups));
-
-    Set<Account.Id> addedMembers =
-        addGroupMembersInReviewDb(db, group.getId(), originalMembers, updatedMembers);
-    Set<Account.Id> deletedMembers =
-        deleteGroupMembersInReviewDb(db, group.getId(), originalMembers, updatedMembers);
-    Set<AccountGroup.UUID> addedSubgroups =
-        addSubgroupsInReviewDb(db, group.getId(), originalSubgroups, updatedSubgroups);
-    Set<AccountGroup.UUID> deletedSubgroups =
-        deleteSubgroupsInReviewDb(db, group.getId(), originalSubgroups, updatedSubgroups);
-
-    UpdateResult.Builder resultBuilder =
-        UpdateResult.builder()
-            .setGroupUuid(group.getGroupUUID())
-            .setGroupId(group.getId())
-            .setGroupName(group.getNameKey())
-            .setAddedMembers(addedMembers)
-            .setDeletedMembers(deletedMembers)
-            .setAddedSubgroups(addedSubgroups)
-            .setDeletedSubgroups(deletedSubgroups);
-    if (!Objects.equals(originalName, updatedName)) {
-      resultBuilder.setPreviousGroupName(originalName);
-    }
-    return resultBuilder.build();
-  }
-
-  private static void updateNameInReviewDb(
-      ReviewDb db,
-      AccountGroup.Id groupId,
-      AccountGroup.NameKey originalName,
-      AccountGroup.NameKey updatedName)
-      throws OrmException {
-    try {
-      AccountGroupName id = new AccountGroupName(updatedName, groupId);
-      db.accountGroupNames().insert(ImmutableList.of(id));
-    } catch (OrmException e) {
-      AccountGroupName other = db.accountGroupNames().get(updatedName);
-      if (other != null) {
-        // If we are using this identity, don't report the exception.
-        if (other.getId().equals(groupId)) {
-          return;
-        }
-      }
-      throw e;
-    }
-    db.accountGroupNames().deleteKeys(ImmutableList.of(originalName));
-  }
-
-  private static Set<Account.Id> addGroupMembersInReviewDb(
-      ReviewDb db,
-      AccountGroup.Id groupId,
-      ImmutableSet<Account.Id> originalMembers,
-      ImmutableSet<Account.Id> updatedMembers)
-      throws OrmException {
-    Set<Account.Id> accountIds = Sets.difference(updatedMembers, originalMembers);
-    if (accountIds.isEmpty()) {
-      return accountIds;
-    }
-
-    ImmutableSet<AccountGroupMember> newMembers = toAccountGroupMembers(groupId, accountIds);
-    db.accountGroupMembers().insert(newMembers);
-    return accountIds;
-  }
-
-  private static Set<Account.Id> deleteGroupMembersInReviewDb(
-      ReviewDb db,
-      AccountGroup.Id groupId,
-      ImmutableSet<Account.Id> originalMembers,
-      ImmutableSet<Account.Id> updatedMembers)
-      throws OrmException {
-    Set<Account.Id> accountIds = Sets.difference(originalMembers, updatedMembers);
-    if (accountIds.isEmpty()) {
-      return accountIds;
-    }
-
-    ImmutableSet<AccountGroupMember> membersToRemove = toAccountGroupMembers(groupId, accountIds);
-    db.accountGroupMembers().delete(membersToRemove);
-    return accountIds;
-  }
-
-  private static ImmutableSet<AccountGroupMember> toAccountGroupMembers(
-      AccountGroup.Id groupId, Set<Account.Id> accountIds) {
-    return accountIds
-        .stream()
-        .map(accountId -> new AccountGroupMember.Key(accountId, groupId))
-        .map(AccountGroupMember::new)
-        .collect(toImmutableSet());
-  }
-
-  private static Set<AccountGroup.UUID> addSubgroupsInReviewDb(
-      ReviewDb db,
-      AccountGroup.Id parentGroupId,
-      ImmutableSet<AccountGroup.UUID> originalSubgroups,
-      ImmutableSet<AccountGroup.UUID> updatedSubgroups)
-      throws OrmException {
-    Set<AccountGroup.UUID> subgroupUuids = Sets.difference(updatedSubgroups, originalSubgroups);
-    if (subgroupUuids.isEmpty()) {
-      return subgroupUuids;
-    }
-
-    ImmutableSet<AccountGroupById> newSubgroups = toAccountGroupByIds(parentGroupId, subgroupUuids);
-    db.accountGroupById().insert(newSubgroups);
-    return subgroupUuids;
-  }
-
-  private static Set<AccountGroup.UUID> deleteSubgroupsInReviewDb(
-      ReviewDb db,
-      AccountGroup.Id parentGroupId,
-      ImmutableSet<AccountGroup.UUID> originalSubgroups,
-      ImmutableSet<AccountGroup.UUID> updatedSubgroups)
-      throws OrmException {
-    Set<AccountGroup.UUID> subgroupUuids = Sets.difference(originalSubgroups, updatedSubgroups);
-    if (subgroupUuids.isEmpty()) {
-      return subgroupUuids;
-    }
-
-    ImmutableSet<AccountGroupById> subgroupsToRemove =
-        toAccountGroupByIds(parentGroupId, subgroupUuids);
-    db.accountGroupById().delete(subgroupsToRemove);
-    return subgroupUuids;
-  }
-
-  private static ImmutableSet<AccountGroupById> toAccountGroupByIds(
-      AccountGroup.Id parentGroupId, Set<AccountGroup.UUID> subgroupUuids) {
-    return subgroupUuids
-        .stream()
-        .map(subgroupUuid -> new AccountGroupById.Key(parentGroupId, subgroupUuid))
-        .map(AccountGroupById::new)
-        .collect(toImmutableSet());
-  }
-
   private InternalGroup createGroupInNoteDbWithRetry(
       InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmException {
+      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
     try {
       return retryHelper.execute(
           RetryHelper.ActionType.GROUP_UPDATE,
@@ -514,14 +298,11 @@
 
   private Optional<UpdateResult> updateGroupInNoteDb(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
-      throws IOException, ConfigInvalidException, OrmDuplicateKeyException, NoSuchGroupException {
+      throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
     try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
       GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, groupUuid);
       groupConfig.setGroupUpdate(groupUpdate, auditLogFormatter);
       if (!groupConfig.getLoadedGroup().isPresent()) {
-        if (groupsMigration.readFromNoteDb()) {
-          throw new NoSuchGroupException(groupUuid);
-        }
         return Optional.empty();
       }
 
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 262c732..6dbad6a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -517,7 +517,6 @@
   /** Number of unresolved comments of the change. */
   public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
       intRange(ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT)
-          .stored()
           .build(ChangeData::unresolvedCommentCount);
 
   /** Whether the change is mergeable. */
@@ -536,13 +535,11 @@
   /** The number of inserted lines in this change. */
   public static final FieldDef<ChangeData, Integer> ADDED =
       intRange(ChangeQueryBuilder.FIELD_ADDED)
-          .stored()
           .build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null);
 
   /** The number of deleted lines in this change. */
   public static final FieldDef<ChangeData, Integer> DELETED =
       intRange(ChangeQueryBuilder.FIELD_DELETED)
-          .stored()
           .build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null);
 
   /** The total number of modified lines in this change. */
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index cf51197..1ea115a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -152,7 +152,7 @@
   }
 
   private static boolean autoReindexIfStale(Config cfg) {
-    return cfg.getBoolean("index", null, "autoReindexIfStale", true);
+    return cfg.getBoolean("index", null, "autoReindexIfStale", false);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index 47dad94..c90bece 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -24,14 +24,11 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.index.SiteIndexer;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.group.db.GroupsNoteDbConsistencyChecker;
 import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -52,18 +49,15 @@
 public class AllGroupsIndexer extends SiteIndexer<AccountGroup.UUID, InternalGroup, GroupIndex> {
   private static final Logger log = LoggerFactory.getLogger(AllGroupsIndexer.class);
 
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ListeningExecutorService executor;
   private final GroupCache groupCache;
   private final Groups groups;
 
   @Inject
   AllGroupsIndexer(
-      SchemaFactory<ReviewDb> schemaFactory,
       @IndexExecutor(BATCH) ListeningExecutorService executor,
       GroupCache groupCache,
       Groups groups) {
-    this.schemaFactory = schemaFactory;
     this.executor = executor;
     this.groupCache = groupCache;
     this.groups = groups;
@@ -77,7 +71,7 @@
     List<AccountGroup.UUID> uuids;
     try {
       uuids = collectGroups(progress);
-    } catch (OrmException | IOException | ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException e) {
       log.error("Error collecting groups", e);
       return new SiteIndexer.Result(sw, false, 0, 0);
     }
@@ -133,13 +127,10 @@
   }
 
   private List<AccountGroup.UUID> collectGroups(ProgressMonitor progress)
-      throws OrmException, IOException, ConfigInvalidException {
+      throws IOException, ConfigInvalidException {
     progress.beginTask("Collecting groups", ProgressMonitor.UNKNOWN);
-    try (ReviewDb db = schemaFactory.open()) {
-      return groups
-          .getAllGroupReferences(db)
-          .map(GroupReference::getUUID)
-          .collect(toImmutableList());
+    try {
+      return groups.getAllGroupReferences().map(GroupReference::getUUID).collect(toImmutableList());
     } finally {
       progress.endTask();
     }
diff --git a/java/com/google/gerrit/server/index/group/StalenessChecker.java b/java/com/google/gerrit/server/index/group/StalenessChecker.java
index 94e1be7..7900287 100644
--- a/java/com/google/gerrit/server/index/group/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/group/StalenessChecker.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -47,27 +46,20 @@
   private final GitRepositoryManager repoManager;
   private final IndexConfig indexConfig;
   private final AllUsersName allUsers;
-  private final GroupsMigration groupsMigration;
 
   @Inject
   StalenessChecker(
       GroupIndexCollection indexes,
       GitRepositoryManager repoManager,
       IndexConfig indexConfig,
-      AllUsersName allUsers,
-      GroupsMigration groupsMigration) {
+      AllUsersName allUsers) {
     this.indexes = indexes;
     this.repoManager = repoManager;
     this.indexConfig = indexConfig;
     this.allUsers = allUsers;
-    this.groupsMigration = groupsMigration;
   }
 
   public boolean isStale(AccountGroup.UUID uuid) throws IOException {
-    if (!groupsMigration.readFromNoteDb()) {
-      return false; // This class only treats staleness for groups in NoteDb.
-    }
-
     GroupIndex i = indexes.getSearchIndex();
     if (i == null) {
       return false; // No index; caller couldn't do anything if it is stale.
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index 10d14a8..a12e226 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -39,19 +38,16 @@
     CreateChangeSender create(Project.NameKey project, Change.Id id);
   }
 
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final PermissionBackend permissionBackend;
 
   @Inject
   public CreateChangeSender(
       EmailArguments ea,
-      IdentifiedUser.GenericFactory identifiedUserFactory,
       PermissionBackend permissionBackend,
       @Assisted Project.NameKey project,
       @Assisted Change.Id id)
       throws OrmException {
     super(ea, newChangeData(ea, project, id));
-    this.identifiedUserFactory = identifiedUserFactory;
     this.permissionBackend = permissionBackend;
   }
 
@@ -83,7 +79,7 @@
 
   private boolean isOwnerOfProjectOrBranch(Account.Id userId) {
     return permissionBackend
-        .user(identifiedUserFactory.create(userId))
+        .absentUser(userId)
         .ref(change.getDest())
         .testOrFalse(RefPermission.WRITE_CONFIG);
   }
diff --git a/java/com/google/gerrit/server/notedb/GroupsMigration.java b/java/com/google/gerrit/server/notedb/GroupsMigration.java
deleted file mode 100644
index 293f3c6..0000000
--- a/java/com/google/gerrit/server/notedb/GroupsMigration.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2017 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 com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
-import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.READ;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.Set;
-import org.eclipse.jgit.lib.Config;
-
-@Singleton
-public class GroupsMigration {
-  public static class Module extends AbstractModule {
-    @Override
-    public void configure() {
-      bind(GroupsMigration.class);
-    }
-  }
-
-  private final boolean writeToNoteDb;
-  private final boolean readFromNoteDb;
-  private final boolean disableGroupReviewDb;
-
-  @Inject
-  public GroupsMigration(@GerritServerConfig Config cfg) {
-    // TODO(aliceks): Remove these flags when all other necessary TODOs for writing groups to
-    // NoteDb have been addressed.
-    // Don't flip these flags in a production setting! We only added them to spread the
-    // implementation of groups in NoteDb among several changes which are gradually merged.
-    this(
-        cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, false),
-        cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false),
-        cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false));
-  }
-
-  public GroupsMigration(
-      boolean writeToNoteDb, boolean readFromNoteDb, boolean disableGroupReviewDb) {
-    this.writeToNoteDb = writeToNoteDb;
-    this.readFromNoteDb = readFromNoteDb;
-    this.disableGroupReviewDb = disableGroupReviewDb;
-  }
-
-  public boolean writeToNoteDb() {
-    return writeToNoteDb;
-  }
-
-  public boolean readFromNoteDb() {
-    return readFromNoteDb;
-  }
-
-  public boolean disableGroupReviewDb() {
-    return disableGroupReviewDb;
-  }
-
-  public void setConfigValuesIfNotSetYet(Config cfg) {
-    Set<String> subsections = cfg.getSubsections(SECTION_NOTE_DB);
-    if (!subsections.contains(GROUPS.key())) {
-      cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, writeToNoteDb());
-      cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, readFromNoteDb());
-      cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, disableGroupReviewDb());
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java b/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
new file mode 100644
index 0000000..0653192
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
@@ -0,0 +1,122 @@
+// 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.rebuild;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_GC_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_AUTO;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GarbageCollection;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class GcAllUsers {
+  private static final Logger log = LoggerFactory.getLogger(GcAllUsers.class);
+
+  private final AllUsersName allUsers;
+  private final GarbageCollection.Factory gcFactory;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  GcAllUsers(
+      AllUsersName allUsers,
+      GarbageCollection.Factory gcFactory,
+      GitRepositoryManager repoManager) {
+    this.allUsers = allUsers;
+    this.gcFactory = gcFactory;
+    this.repoManager = repoManager;
+  }
+
+  public void runWithLogger() {
+    // Print log messages using logger, and skip progress.
+    run(s -> log.info(s), null);
+  }
+
+  public void run(PrintWriter writer) {
+    // Print both log messages and progress to given writer.
+    run(checkNotNull(writer)::println, writer);
+  }
+
+  private void run(Consumer<String> logOneLine, @Nullable PrintWriter progressWriter) {
+    if (!(repoManager instanceof LocalDiskRepositoryManager)) {
+      logOneLine.accept("Skipping GC of " + allUsers + "; not a local disk repo");
+      return;
+    }
+    if (!enableAutoGc(logOneLine)) {
+      logOneLine.accept(
+          "Skipping GC of "
+              + allUsers
+              + " due to disabling "
+              + CONFIG_GC_SECTION
+              + "."
+              + CONFIG_KEY_AUTO);
+      logOneLine.accept(
+          "If loading accounts is slow after the NoteDb migration, run `git gc` on "
+              + allUsers
+              + " manually");
+      return;
+    }
+
+    if (progressWriter == null) {
+      // Mimic log line from GarbageCollection.
+      logOneLine.accept("collecting garbage for \"" + allUsers + "\":\n");
+    }
+    GarbageCollectionResult result =
+        gcFactory.create().run(ImmutableList.of(allUsers), progressWriter);
+    if (!result.hasErrors()) {
+      return;
+    }
+    for (GarbageCollectionResult.Error e : result.getErrors()) {
+      switch (e.getType()) {
+        case GC_ALREADY_SCHEDULED:
+          logOneLine.accept("GC already scheduled for " + e.getProjectName());
+          break;
+        case GC_FAILED:
+          logOneLine.accept("GC failed for " + e.getProjectName());
+          break;
+        case REPOSITORY_NOT_FOUND:
+          logOneLine.accept(e.getProjectName() + " repo not found");
+          break;
+        default:
+          logOneLine.accept("GC failed for " + e.getProjectName() + ": " + e.getType());
+          break;
+      }
+    }
+  }
+
+  private boolean enableAutoGc(Consumer<String> logOneLine) {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      return repo.getConfig().getInt(CONFIG_GC_SECTION, CONFIG_KEY_AUTO, -1) != 0;
+    } catch (IOException e) {
+      logOneLine.accept(
+          "Error reading config for " + allUsers + ":\n" + Throwables.getStackTraceAsString(e));
+      return false;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
index 535736d..65755ed 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
@@ -50,19 +50,22 @@
     }
   }
 
-  private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
+  private final GcAllUsers gcAllUsers;
   private final OnlineUpgrader indexUpgrader;
+  private final Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
   private final boolean upgradeIndex;
   private final boolean trial;
 
   @Inject
   OnlineNoteDbMigrator(
       @GerritServerConfig Config cfg,
-      Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
+      GcAllUsers gcAllUsers,
       OnlineUpgrader indexUpgrader,
+      Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
       @Named(TRIAL) boolean trial) {
-    this.migratorBuilderProvider = migratorBuilderProvider;
+    this.gcAllUsers = gcAllUsers;
     this.indexUpgrader = indexUpgrader;
+    this.migratorBuilderProvider = migratorBuilderProvider;
     this.upgradeIndex = VersionManager.getOnlineUpgrade(cfg);
     this.trial = trial || NoteDbMigrator.getTrialMode(cfg);
   }
@@ -88,6 +91,7 @@
     } catch (Exception e) {
       log.error("Error in online NoteDb migration", e);
     }
+    gcAllUsers.runWithLogger();
     log.info("Online NoteDb migration completed in {}s", sw.elapsed(TimeUnit.SECONDS));
 
     if (upgradeIndex) {
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 31a7e84..b0e5310 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -205,11 +205,7 @@
     PatchSet psEntityB = psb.get() == 0 ? new PatchSet(psb) : psUtil.get(db, notes, psb);
     if (psEntityA != null || psEntityB != null) {
       try {
-        permissionBackend
-            .user(userProvider)
-            .change(notes)
-            .database(db)
-            .check(ChangePermission.READ);
+        permissionBackend.currentUser().change(notes).database(db).check(ChangePermission.READ);
       } catch (AuthException e) {
         throw new NoSuchChangeException(changeId);
       }
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 47c8543..e49686a 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -138,19 +138,6 @@
         && !isPatchSetLocked(db);
   }
 
-  /** Can this user delete this change? */
-  private boolean canDelete(Change.Status status) {
-    switch (status) {
-      case NEW:
-      case ABANDONED:
-        return (isOwner() && refControl.canPerform(Permission.DELETE_OWN_CHANGES))
-            || getProjectControl().isAdmin();
-      case MERGED:
-      default:
-        return false;
-    }
-  }
-
   /** Can this user rebase this change? */
   private boolean canRebase(ReviewDb db) throws OrmException {
     return (isOwner() || refControl.canSubmit(isOwner()) || refControl.canRebase())
@@ -369,7 +356,8 @@
           case ABANDON:
             return canAbandon(db());
           case DELETE:
-            return canDelete(getChange().getStatus());
+            return (isOwner() && refControl.canPerform(Permission.DELETE_OWN_CHANGES))
+                || getProjectControl().isAdmin();
           case ADD_PATCH_SET:
             return canAddPatchSet(db());
           case EDIT_ASSIGNEE:
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index 800d877..2c8c951 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -23,15 +23,18 @@
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
@@ -44,14 +47,21 @@
 public class DefaultPermissionBackend extends PermissionBackend {
   private static final CurrentUser.PropertyKey<Boolean> IS_ADMIN = CurrentUser.PropertyKey.create();
 
+  private final Provider<CurrentUser> currentUser;
   private final ProjectCache projectCache;
   private final ProjectControl.Factory projectControlFactory;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
 
   @Inject
   DefaultPermissionBackend(
-      ProjectCache projectCache, ProjectControl.Factory projectControlFactory) {
+      Provider<CurrentUser> currentUser,
+      ProjectCache projectCache,
+      ProjectControl.Factory projectControlFactory,
+      IdentifiedUser.GenericFactory identifiedUserFactory) {
+    this.currentUser = currentUser;
     this.projectCache = projectCache;
     this.projectControlFactory = projectControlFactory;
+    this.identifiedUserFactory = identifiedUserFactory;
   }
 
   private CapabilityCollection capabilities() {
@@ -59,10 +69,21 @@
   }
 
   @Override
+  public WithUser currentUser() {
+    return new WithUserImpl(currentUser.get());
+  }
+
+  @Override
   public WithUser user(CurrentUser user) {
     return new WithUserImpl(checkNotNull(user, "user"));
   }
 
+  @Override
+  public WithUser absentUser(Account.Id user) {
+    IdentifiedUser identifiedUser = identifiedUserFactory.create(checkNotNull(user, "user"));
+    return new WithUserImpl(identifiedUser);
+  }
+
   class WithUserImpl extends WithUser {
     private final CurrentUser user;
     private Boolean admin;
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 84a3d87..5905800 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -105,8 +105,7 @@
         permissionBackend.user(user).database(db).project(projectState.getNameKey());
   }
 
-  Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
-      throws PermissionBackendException {
+  Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) {
     if (projectState.isAllUsers()) {
       refs = addUsersSelfSymref(refs);
     }
@@ -283,7 +282,10 @@
             .ref(visibleChanges.get(id).get())
             .check(RefPermission.READ_PRIVATE_CHANGES);
         return true;
-      } catch (PermissionBackendException | AuthException e) {
+      } catch (AuthException e) {
+        return false;
+      } catch (PermissionBackendException e) {
+        log.error("Failed to check permission for " + id + " in " + projectState.getName(), e);
         return false;
       }
     }
@@ -336,7 +338,7 @@
         return r.notes();
       }
     } catch (PermissionBackendException e) {
-      log.warn("Failed to check permission for " + r.id() + " in " + projectState.getName(), e);
+      log.error("Failed to check permission for " + r.id() + " in " + projectState.getName(), e);
     }
     return null;
   }
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 56c300d..ed10184 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -90,13 +91,26 @@
 public abstract class PermissionBackend {
   private static final Logger logger = LoggerFactory.getLogger(PermissionBackend.class);
 
-  /** @return lightweight factory scoped to answer for the specified user. */
+  /** Returns an instance scoped to the current user. */
+  public abstract WithUser currentUser();
+
+  /**
+   * Returns an instance scoped to the specified user. Should be used in cases where the user could
+   * either be the issuer of the current request or an impersonated user. PermissionBackends that do
+   * not support impersonation can fail with an {@code IllegalStateException}.
+   *
+   * <p>If an instance scoped to the current user is desired, use {@code currentUser()} instead.
+   */
   public abstract WithUser user(CurrentUser user);
 
-  /** @return lightweight factory scoped to answer for the specified user. */
-  public <U extends CurrentUser> WithUser user(Provider<U> user) {
-    return user(checkNotNull(user, "Provider<CurrentUser>").get());
-  }
+  /**
+   * Returns an instance scoped to the provided user. Should be used in cases where the caller wants
+   * to check the permissions of a user who is not the issuer of the current request and not the
+   * target of impersonation.
+   *
+   * <p>Usage should be very limited as this can expose a group-oracle.
+   */
+  public abstract WithUser absentUser(Account.Id user);
 
   /**
    * Bulk evaluate a set of {@link PermissionBackendCondition} for view handling.
@@ -135,18 +149,18 @@
 
   /** PermissionBackend scoped to a specific user. */
   public abstract static class WithUser extends AcceptsReviewDb<WithUser> {
-    /** @return user this instance is scoped to. */
+    /** Returns the user this instance is scoped to. */
     public abstract CurrentUser user();
 
-    /** @return instance scoped for the specified project. */
+    /** Returns an instance scoped for the specified project. */
     public abstract ForProject project(Project.NameKey project);
 
-    /** @return instance scoped for the {@code ref}, and its parent project. */
+    /** Returns an instance scoped for the {@code ref}, and its parent project. */
     public ForRef ref(Branch.NameKey ref) {
       return project(ref.getParentKey()).ref(ref.get()).database(db);
     }
 
-    /** @return instance scoped for the change, and its destination ref and project. */
+    /** Returns an instance scoped for the change, and its destination ref and project. */
     public ForChange change(ChangeData cd) {
       try {
         return ref(cd.change().getDest()).change(cd);
@@ -155,15 +169,15 @@
       }
     }
 
-    /** @return instance scoped for the change, and its destination ref and project. */
+    /** Returns an instance scoped for the change, and its destination ref and project. */
     public ForChange change(ChangeNotes notes) {
       return ref(notes.getChange().getDest()).change(notes);
     }
 
     /**
-     * @return instance scoped for the change loaded from index, and its destination ref and
-     *     project. This method should only be used when database access is harmful and potentially
-     *     stale data from the index is acceptable.
+     * Returns an instance scoped for the change loaded from index, and its destination ref and
+     * project. This method should only be used when database access is harmful and potentially
+     * stale data from the index is acceptable.
      */
     public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
       return ref(notes.getChange().getDest()).indexedChange(cd, notes);
@@ -250,19 +264,19 @@
 
   /** PermissionBackend scoped to a user and project. */
   public abstract static class ForProject extends AcceptsReviewDb<ForProject> {
-    /** @return user this instance is scoped to. */
+    /** Returns the user this instance is scoped to. */
     public abstract CurrentUser user();
 
-    /** @return fully qualified resource path that this instance is scoped to. */
+    /** Returns the fully qualified resource path that this instance is scoped to. */
     public abstract String resourcePath();
 
-    /** @return new instance rescoped to same project, but different {@code user}. */
+    /** Returns a new instance rescoped to same project, but different {@code user}. */
     public abstract ForProject user(CurrentUser user);
 
-    /** @return instance scoped for {@code ref} in this project. */
+    /** Returns an instance scoped for {@code ref} in this project. */
     public abstract ForRef ref(String ref);
 
-    /** @return instance scoped for the change, and its destination ref and project. */
+    /** Returns an instance scoped for the change, and its destination ref and project. */
     public ForChange change(ChangeData cd) {
       try {
         return ref(cd.change().getDest().get()).change(cd);
@@ -271,15 +285,15 @@
       }
     }
 
-    /** @return instance scoped for the change, and its destination ref and project. */
+    /** Returns an instance scoped for the change, and its destination ref and project. */
     public ForChange change(ChangeNotes notes) {
       return ref(notes.getChange().getDest().get()).change(notes);
     }
 
     /**
-     * @return instance scoped for the change loaded from index, and its destination ref and
-     *     project. This method should only be used when database access is harmful and potentially
-     *     stale data from the index is acceptable.
+     * Returns an instance scoped for the change loaded from index, and its destination ref and
+     * project. This method should only be used when database access is harmful and potentially
+     * stale data from the index is acceptable.
      */
     public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
       return ref(notes.getChange().getDest().get()).indexedChange(cd, notes);
@@ -311,8 +325,8 @@
     }
 
     /**
-     * @return a partition of the provided refs that are visible to the user that this instance is
-     *     scoped to.
+     * Returns a partition of the provided refs that are visible to the user that this instance is
+     * scoped to.
      */
     public abstract Map<String, Ref> filter(
         Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
@@ -352,19 +366,19 @@
 
   /** PermissionBackend scoped to a user, project and reference. */
   public abstract static class ForRef extends AcceptsReviewDb<ForRef> {
-    /** @return user this instance is scoped to. */
+    /** Returns the user this instance is scoped to. */
     public abstract CurrentUser user();
 
-    /** @return fully qualified resource path that this instance is scoped to. */
+    /** Returns a fully qualified resource path that this instance is scoped to. */
     public abstract String resourcePath();
 
-    /** @return new instance rescoped to same reference, but different {@code user}. */
+    /** Returns a new instance rescoped to same reference, but different {@code user}. */
     public abstract ForRef user(CurrentUser user);
 
-    /** @return instance scoped to change. */
+    /** Returns an instance scoped to change. */
     public abstract ForChange change(ChangeData cd);
 
-    /** @return instance scoped to change. */
+    /** Returns an instance scoped to change. */
     public abstract ForChange change(ChangeNotes notes);
 
     /**
@@ -410,13 +424,13 @@
 
   /** PermissionBackend scoped to a user, project, reference and change. */
   public abstract static class ForChange extends AcceptsReviewDb<ForChange> {
-    /** @return user this instance is scoped to. */
+    /** Returns the user this instance is scoped to. */
     public abstract CurrentUser user();
 
-    /** @return fully qualified resource path that this instance is scoped to. */
+    /** Returns the fully qualified resource path that this instance is scoped to. */
     public abstract String resourcePath();
 
-    /** @return new instance rescoped to same change, but different {@code user}. */
+    /** Returns a new instance rescoped to same change, but different {@code user}. */
     public abstract ForChange user(CurrentUser user);
 
     /** Verify scoped user can {@code perm}, throwing if denied. */
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 30ed180..dbd60ea 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -202,11 +202,6 @@
     return false;
   }
 
-  /** Returns whether the project is hidden. */
-  private boolean isHidden() {
-    return getProject().getState().equals(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
-  }
-
   private boolean canAddRefs() {
     return (canPerformOnAnyRef(Permission.CREATE) || isAdmin());
   }
@@ -400,8 +395,7 @@
     private boolean can(ProjectPermission perm) throws PermissionBackendException {
       switch (perm) {
         case ACCESS:
-          return (!isHidden() && (user.isInternalUser() || canPerformOnAnyRef(Permission.READ)))
-              || isOwner();
+          return user.isInternalUser() || isOwner() || canPerformOnAnyRef(Permission.READ);
 
         case READ:
           return allRefsAreVisible(Collections.emptySet());
diff --git a/java/com/google/gerrit/server/plugins/DisablePlugin.java b/java/com/google/gerrit/server/plugins/DisablePlugin.java
index 266350f..62eb993 100644
--- a/java/com/google/gerrit/server/plugins/DisablePlugin.java
+++ b/java/com/google/gerrit/server/plugins/DisablePlugin.java
@@ -19,33 +19,28 @@
 import com.google.gerrit.extensions.common.PluginInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class DisablePlugin implements RestModifyView<PluginResource, Input> {
 
   private final PluginLoader loader;
-  private final Provider<IdentifiedUser> user;
   private final PermissionBackend permissionBackend;
 
   @Inject
-  DisablePlugin(
-      PluginLoader loader, Provider<IdentifiedUser> user, PermissionBackend permissionBackend) {
+  DisablePlugin(PluginLoader loader, PermissionBackend permissionBackend) {
     this.loader = loader;
-    this.user = user;
     this.permissionBackend = permissionBackend;
   }
 
   @Override
   public PluginInfo apply(PluginResource resource, Input input) throws RestApiException {
     try {
-      permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     } catch (PermissionBackendException e) {
       throw new RestApiException("Could not check permission", e);
     }
diff --git a/java/com/google/gerrit/server/project/ChildProjects.java b/java/com/google/gerrit/server/project/ChildProjects.java
index 0b174f6..868d0af 100644
--- a/java/com/google/gerrit/server/project/ChildProjects.java
+++ b/java/com/google/gerrit/server/project/ChildProjects.java
@@ -20,13 +20,11 @@
 import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -38,7 +36,6 @@
 public class ChildProjects {
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final AllProjectsName allProjects;
   private final ProjectJson json;
 
@@ -46,32 +43,30 @@
   ChildProjects(
       ProjectCache projectCache,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       AllProjectsName allProjectsName,
       ProjectJson json) {
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.allProjects = allProjectsName;
     this.json = json;
   }
 
   /** Gets all child projects recursively. */
   public List<ProjectInfo> list(Project.NameKey parent) throws PermissionBackendException {
-    Map<Project.NameKey, Project> projects = readAllProjects();
+    Map<Project.NameKey, Project> projects = readAllReadableProjects();
     Multimap<Project.NameKey, Project.NameKey> children = parentToChildren(projects);
-    PermissionBackend.WithUser perm = permissionBackend.user(user);
+    PermissionBackend.WithUser perm = permissionBackend.currentUser();
 
     List<ProjectInfo> results = new ArrayList<>();
     depthFirstFormat(results, perm, projects, children, parent);
     return results;
   }
 
-  private Map<Project.NameKey, Project> readAllProjects() {
+  private Map<Project.NameKey, Project> readAllReadableProjects() {
     Map<Project.NameKey, Project> projects = new HashMap<>();
     for (Project.NameKey name : projectCache.all()) {
       ProjectState c = projectCache.get(name);
-      if (c != null) {
+      if (c != null && c.statePermitsRead()) {
         projects.put(c.getNameKey(), c.getProject());
       }
     }
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index 7d4797a..90a7455 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -75,10 +76,10 @@
     }
     ps.checkStatePermitsWrite();
 
-    PermissionBackend.ForRef perm = permissionBackend.user(user).ref(branch);
+    PermissionBackend.ForRef perm = permissionBackend.user(user.get()).ref(branch);
     if (object instanceof RevCommit) {
       perm.check(RefPermission.CREATE);
-      checkCreateCommit(repo, (RevCommit) object, ps, perm);
+      checkCreateCommit(repo, (RevCommit) object, ps.getNameKey(), perm);
     } else if (object instanceof RevTag) {
       RevTag tag = (RevTag) object;
       try (RevWalk rw = new RevWalk(repo)) {
@@ -98,14 +99,14 @@
 
       RevObject target = tag.getObject();
       if (target instanceof RevCommit) {
-        checkCreateCommit(repo, (RevCommit) target, ps, perm);
+        checkCreateCommit(repo, (RevCommit) target, ps.getNameKey(), perm);
       } else {
         checkCreateRef(user, repo, branch, target);
       }
 
       // If the tag has a PGP signature, allow a lower level of permission
       // than if it doesn't have a PGP signature.
-      PermissionBackend.ForRef forRef = permissionBackend.user(user).ref(branch);
+      PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(branch);
       if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
         forRef.check(RefPermission.CREATE_SIGNED_TAG);
       } else {
@@ -119,7 +120,7 @@
    * new commit to the repository.
    */
   private void checkCreateCommit(
-      Repository repo, RevCommit commit, ProjectState projectState, PermissionBackend.ForRef forRef)
+      Repository repo, RevCommit commit, Project.NameKey project, PermissionBackend.ForRef forRef)
       throws AuthException, PermissionBackendException {
     try {
       // If the user has update (push) permission, they can create the ref regardless
@@ -129,7 +130,7 @@
     } catch (AuthException denied) {
       // Fall through to check reachability.
     }
-    if (reachable.fromHeadsOrTags(projectState, repo, commit)) {
+    if (reachable.fromHeadsOrTags(project, repo, commit)) {
       // If the user has no push permissions, check whether the object is
       // merged into a branch or tag readable by this user. If so, they are
       // not effectively "pushing" more objects, so they can create the ref
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 2ef7891..e3140b5 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -67,6 +67,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Ref;
@@ -197,6 +198,24 @@
     return capabilities;
   }
 
+  /**
+   * Returns true if the Prolog engine is expected to run for this project, that is if this project
+   * or a parent possesses a rules.pl file.
+   */
+  public boolean hasPrologRules() {
+    // We check if this project has a rules.pl file
+    if (getConfig().getRulesId() != null) {
+      return true;
+    }
+
+    // If not, we check the parents.
+    return parents()
+        .stream()
+        .map(ProjectState::getConfig)
+        .map(ProjectConfig::getRulesId)
+        .anyMatch(Objects::nonNull);
+  }
+
   /** @return Construct a new PrologEnvironment for the calling thread. */
   public PrologEnvironment newPrologEnvironment() throws CompileException {
     PrologMachineCopy pmc = rulesMachine;
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 67d8f70..0196d92 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -16,13 +16,12 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
-import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.IncludedInResolver;
 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.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
@@ -45,37 +44,34 @@
   private static final Logger log = LoggerFactory.getLogger(Reachable.class);
 
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Inject
-  Reachable(PermissionBackend permissionBackend, Provider<CurrentUser> user) {
+  Reachable(PermissionBackend permissionBackend) {
     this.permissionBackend = permissionBackend;
-    this.user = user;
   }
 
   /** @return true if a commit is reachable from a given set of refs. */
   public boolean fromRefs(
-      ProjectState state, Repository repo, RevCommit commit, Map<String, Ref> refs) {
+      Project.NameKey project, Repository repo, RevCommit commit, Map<String, Ref> refs) {
     try (RevWalk rw = new RevWalk(repo)) {
-      // TODO(hiesel) Convert interface to Project.NameKey
       Map<String, Ref> filtered =
           permissionBackend
-              .user(user)
-              .project(state.getNameKey())
+              .currentUser()
+              .project(project)
               .filter(refs, repo, RefFilterOptions.builder().setFilterTagsSeparately(true).build());
       return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values());
     } catch (IOException | PermissionBackendException e) {
       log.error(
           String.format(
               "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), state.getNameKey()),
+              commit.name(), project),
           e);
       return false;
     }
   }
 
   /** @return true if a commit is reachable from a repo's branches and tags. */
-  boolean fromHeadsOrTags(ProjectState state, Repository repo, RevCommit commit) {
+  boolean fromHeadsOrTags(Project.NameKey project, Repository repo, RevCommit commit) {
     try {
       RefDatabase refdb = repo.getRefDatabase();
       Collection<Ref> heads = refdb.getRefs(Constants.R_HEADS).values();
@@ -84,12 +80,12 @@
       for (Ref r : Iterables.concat(heads, tags)) {
         refs.put(r.getName(), r);
       }
-      return fromRefs(state, repo, commit, refs);
+      return fromRefs(project, repo, commit, refs);
     } catch (IOException e) {
       log.error(
           String.format(
               "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), state.getProject().getNameKey()),
+              commit.name(), project),
           e);
       return false;
     }
diff --git a/java/com/google/gerrit/server/project/RefUtil.java b/java/com/google/gerrit/server/project/RefUtil.java
index 62e48be..e42a7df 100644
--- a/java/com/google/gerrit/server/project/RefUtil.java
+++ b/java/com/google/gerrit/server/project/RefUtil.java
@@ -39,6 +39,8 @@
 public class RefUtil {
   private static final Logger log = LoggerFactory.getLogger(RefUtil.class);
 
+  private RefUtil() {}
+
   public static ObjectId parseBaseRevision(
       Repository repo, Project.NameKey projectName, String baseRevision)
       throws InvalidRevisionException {
diff --git a/java/com/google/gerrit/server/project/SuggestParentCandidates.java b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
index 7c7f8a5..99833af 100644
--- a/java/com/google/gerrit/server/project/SuggestParentCandidates.java
+++ b/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -17,13 +17,11 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.HashSet;
 import java.util.List;
@@ -33,35 +31,30 @@
 public class SuggestParentCandidates {
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final AllProjectsName allProjects;
 
   @Inject
   SuggestParentCandidates(
-      ProjectCache projectCache,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
-      AllProjectsName allProjects) {
+      ProjectCache projectCache, PermissionBackend permissionBackend, AllProjectsName allProjects) {
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.allProjects = allProjects;
   }
 
   public List<Project.NameKey> getNameKeys() throws PermissionBackendException {
     return permissionBackend
-        .user(user)
-        .filter(ProjectPermission.ACCESS, parents())
+        .currentUser()
+        .filter(ProjectPermission.ACCESS, readableParents())
         .stream()
         .sorted()
         .collect(toList());
   }
 
-  private Set<Project.NameKey> parents() {
+  private Set<Project.NameKey> readableParents() {
     Set<Project.NameKey> parents = new HashSet<>();
     for (Project.NameKey p : projectCache.all()) {
       ProjectState ps = projectCache.get(p);
-      if (ps != null) {
+      if (ps != null && ps.statePermitsRead()) {
         Project.NameKey parent = ps.getProject().getParent();
         if (parent != null) {
           parents.add(parent);
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index acb963c..57a0dcc 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -126,8 +126,7 @@
 
   public static Predicate<AccountState> cansee(
       AccountQueryBuilder.Arguments args, ChangeNotes changeNotes) {
-    return new CanSeeChangePredicate(
-        args.db, args.permissionBackend, args.userFactory, changeNotes);
+    return new CanSeeChangePredicate(args.db, args.permissionBackend, changeNotes);
   }
 
   static class AccountPredicate extends IndexPredicate<AccountState>
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 8b6e1e4..f627ec8 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -63,7 +63,6 @@
   public static class Arguments {
     final Provider<ReviewDb> db;
     final ChangeFinder changeFinder;
-    final IdentifiedUser.GenericFactory userFactory;
     final PermissionBackend permissionBackend;
 
     private final Provider<CurrentUser> self;
@@ -75,13 +74,11 @@
         AccountIndexCollection indexes,
         Provider<ReviewDb> db,
         ChangeFinder changeFinder,
-        IdentifiedUser.GenericFactory userFactory,
         PermissionBackend permissionBackend) {
       this.self = self;
       this.indexes = indexes;
       this.db = db;
       this.changeFinder = changeFinder;
-      this.userFactory = userFactory;
       this.permissionBackend = permissionBackend;
     }
 
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index c436a45..b5e7b90 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -31,17 +30,12 @@
 public class CanSeeChangePredicate extends PostFilterPredicate<AccountState> {
   private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
-  private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeNotes changeNotes;
 
   CanSeeChangePredicate(
-      Provider<ReviewDb> db,
-      PermissionBackend permissionBackend,
-      IdentifiedUser.GenericFactory userFactory,
-      ChangeNotes changeNotes) {
+      Provider<ReviewDb> db, PermissionBackend permissionBackend, ChangeNotes changeNotes) {
     this.db = db;
     this.permissionBackend = permissionBackend;
-    this.userFactory = userFactory;
     this.changeNotes = changeNotes;
   }
 
@@ -49,7 +43,7 @@
   public boolean match(AccountState accountState) throws OrmException {
     try {
       return permissionBackend
-          .user(userFactory.create(accountState.getAccount().getId()))
+          .absentUser(accountState.getAccount().getId())
           .database(db)
           .change(changeNotes)
           .test(ChangePermission.READ);
@@ -65,7 +59,7 @@
 
   @Override
   public Predicate<AccountState> copy(Collection<? extends Predicate<AccountState>> children) {
-    return new CanSeeChangePredicate(db, permissionBackend, userFactory, changeNotes);
+    return new CanSeeChangePredicate(db, permissionBackend, changeNotes);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index f1be580..02386ae 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -17,11 +17,9 @@
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
-import com.google.common.base.Joiner;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.IndexConfig;
@@ -37,8 +35,6 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Query wrapper for the account index.
@@ -47,8 +43,6 @@
  * holding on to a single instance.
  */
 public class InternalAccountQuery extends InternalQuery<AccountState> {
-  private static final Logger log = LoggerFactory.getLogger(InternalAccountQuery.class);
-
   @Inject
   InternalAccountQuery(
       AccountQueryProcessor queryProcessor,
@@ -94,28 +88,6 @@
     return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
   }
 
-  public AccountState oneByExternalId(String externalId) throws OrmException {
-    return oneByExternalId(ExternalId.Key.parse(externalId));
-  }
-
-  public AccountState oneByExternalId(String scheme, String id) throws OrmException {
-    return oneByExternalId(ExternalId.Key.create(scheme, id));
-  }
-
-  public AccountState oneByExternalId(ExternalId.Key externalId) throws OrmException {
-    List<AccountState> accountStates = byExternalId(externalId);
-    if (accountStates.size() == 1) {
-      return accountStates.get(0);
-    } else if (accountStates.size() > 0) {
-      StringBuilder msg = new StringBuilder();
-      msg.append("Ambiguous external ID ").append(externalId).append(" for accounts: ");
-      Joiner.on(", ")
-          .appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
-      log.warn(msg.toString());
-    }
-    return null;
-  }
-
   public List<AccountState> byFullName(String fullName) throws OrmException {
     return query(AccountPredicates.fullName(fullName));
   }
diff --git a/java/com/google/gerrit/server/restapi/account/AddSshKey.java b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
index 4485f70..7539cf3 100644
--- a/java/com/google/gerrit/server/restapi/account/AddSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/AddSshKey.java
@@ -75,7 +75,7 @@
       throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
     return apply(rsrc.getUser(), input);
   }
diff --git a/java/com/google/gerrit/server/restapi/account/Capabilities.java b/java/com/google/gerrit/server/restapi/account/Capabilities.java
index e337662..2dd54a5 100644
--- a/java/com/google/gerrit/server/restapi/account/Capabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/Capabilities.java
@@ -62,7 +62,7 @@
       throws ResourceNotFoundException, AuthException, PermissionBackendException {
     IdentifiedUser target = parent.getUser();
     if (self.get() != target) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     GlobalOrPluginPermission perm = parse(id);
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 31bf93f..404b3d3 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountExternalIdCreator;
@@ -67,7 +66,6 @@
     CreateAccount create(String username);
   }
 
-  private final ReviewDb db;
   private final Sequences seq;
   private final GroupsCollection groupsCollection;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
@@ -81,7 +79,6 @@
 
   @Inject
   CreateAccount(
-      ReviewDb db,
       Sequences seq,
       GroupsCollection groupsCollection,
       VersionedAuthorizedKeys.Accessor authorizedKeys,
@@ -92,7 +89,6 @@
       @UserInitiated Provider<GroupsUpdate> groupsUpdate,
       OutgoingEmailValidator validator,
       @Assisted String username) {
-    this.db = db;
     this.seq = seq;
     this.groupsCollection = groupsCollection;
     this.authorizedKeys = authorizedKeys;
@@ -202,6 +198,6 @@
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, ImmutableSet.of(accountId)))
             .build();
-    groupsUpdate.get().updateGroup(db, groupUuid, groupUpdate);
+    groupsUpdate.get().updateGroup(groupUuid, groupUpdate);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index 8ec024e..2248e35 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -98,7 +98,7 @@
     }
 
     if (self.get() != rsrc.getUser() || input.noConfirmation) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
index d36dfe9..ad1b103 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteEmail.java
@@ -72,7 +72,7 @@
           MethodNotAllowedException, OrmException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
   }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index 61ba8cc..472756e 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -68,7 +68,7 @@
       throws RestApiException, IOException, OrmException, ConfigInvalidException,
           PermissionBackendException {
     if (self.get() != resource.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ACCESS_DATABASE);
+      permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
 
     if (extIds == null || extIds.size() == 0) {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index 2621a4a..f278df2 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -58,7 +58,7 @@
       throws AuthException, OrmException, RepositoryNotFoundException, IOException,
           ConfigInvalidException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     authorizedKeys.deleteKey(rsrc.getUser().getAccountId(), rsrc.getSshKey().getKey().get());
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
index 6242e1e..ce10b38 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteWatchedProjects.java
@@ -62,7 +62,7 @@
       throws AuthException, UnprocessableEntityException, OrmException, IOException,
           ConfigInvalidException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
     if (input == null) {
       return Response.none();
diff --git a/java/com/google/gerrit/server/restapi/account/EmailsCollection.java b/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
index d75a01a..c7a6bae 100644
--- a/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/EmailsCollection.java
@@ -65,7 +65,7 @@
   public AccountResource.Email parse(AccountResource rsrc, IdString id)
       throws ResourceNotFoundException, PermissionBackendException, AuthException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     if ("preferred".equals(id.get())) {
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 5260bef0..c623e3e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -76,7 +76,7 @@
 
   @Override
   public Object apply(AccountResource rsrc) throws AuthException, PermissionBackendException {
-    PermissionBackend.WithUser perm = permissionBackend.user(self);
+    PermissionBackend.WithUser perm = permissionBackend.currentUser();
     if (self.get() != rsrc.getUser()) {
       perm.check(GlobalPermission.ADMINISTRATE_SERVER);
       perm = permissionBackend.user(rsrc.getUser());
diff --git a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
index af87025..a8d14f6 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDiffPreferences.java
@@ -51,7 +51,7 @@
   public DiffPreferencesInfo apply(AccountResource rsrc)
       throws RestApiException, ConfigInvalidException, IOException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
diff --git a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
index 1ea5cec..f24991d 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEditPreferences.java
@@ -51,7 +51,7 @@
   public EditPreferencesInfo apply(AccountResource rsrc)
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index 640cc64..63d042c 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -45,7 +45,7 @@
   public List<EmailInfo> apply(AccountResource rsrc)
       throws AuthException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     List<EmailInfo> emails = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index 284c7f9..2f72ad7 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -62,7 +62,7 @@
   public List<AccountExternalIdInfo> apply(AccountResource resource)
       throws RestApiException, IOException, OrmException, PermissionBackendException {
     if (self.get() != resource.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ACCESS_DATABASE);
+      permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
     }
 
     Collection<ExternalId> ids = externalIds.byAccount(resource.getUser().getAccountId());
diff --git a/java/com/google/gerrit/server/restapi/account/GetPreferences.java b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
index efc2de1..e32d434 100644
--- a/java/com/google/gerrit/server/restapi/account/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/GetPreferences.java
@@ -49,7 +49,7 @@
   public GeneralPreferencesInfo apply(AccountResource rsrc)
       throws RestApiException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
index 15156dc..cd8dc09 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
@@ -58,7 +58,7 @@
       throws AuthException, OrmException, RepositoryNotFoundException, IOException,
           ConfigInvalidException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser());
   }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 389dc0e..3a6595c 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -62,7 +62,7 @@
       throws OrmException, AuthException, IOException, ConfigInvalidException,
           PermissionBackendException, ResourceNotFoundException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     Account.Id accountId = rsrc.getUser().getAccountId();
diff --git a/java/com/google/gerrit/server/restapi/account/Index.java b/java/com/google/gerrit/server/restapi/account/Index.java
index fc6b7b3..0d8171d 100644
--- a/java/com/google/gerrit/server/restapi/account/Index.java
+++ b/java/com/google/gerrit/server/restapi/account/Index.java
@@ -50,7 +50,7 @@
   public Response<?> apply(AccountResource rsrc, Input input)
       throws IOException, AuthException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     accountIndexer.get().index(rsrc.getUser().getAccountId());
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index bdc6dcb..6486d18 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -69,7 +69,7 @@
       throws OrmException, RestApiException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     Map<ProjectWatchKey, Set<NotifyType>> projectWatches = asMap(input);
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 468f285..7f7c2ae 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -76,7 +76,7 @@
       throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
           IOException, ConfigInvalidException, PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     if (input == null) {
@@ -91,7 +91,7 @@
       newPassword = null;
     } else {
       // Only administrators can explicitly set the password.
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
       newPassword = input.httpPassword;
     }
     return apply(rsrc.getUser(), newPassword);
diff --git a/java/com/google/gerrit/server/restapi/account/PutName.java b/java/com/google/gerrit/server/restapi/account/PutName.java
index 031249d..a982331 100644
--- a/java/com/google/gerrit/server/restapi/account/PutName.java
+++ b/java/com/google/gerrit/server/restapi/account/PutName.java
@@ -63,7 +63,7 @@
       throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
           IOException, PermissionBackendException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), input);
   }
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index aafdfc1..a30e074 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -57,7 +57,7 @@
       throws AuthException, ResourceNotFoundException, OrmException, IOException,
           PermissionBackendException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
   }
diff --git a/java/com/google/gerrit/server/restapi/account/PutStatus.java b/java/com/google/gerrit/server/restapi/account/PutStatus.java
index c9ae2a0..3c173a0 100644
--- a/java/com/google/gerrit/server/restapi/account/PutStatus.java
+++ b/java/com/google/gerrit/server/restapi/account/PutStatus.java
@@ -57,7 +57,7 @@
       throws AuthException, ResourceNotFoundException, OrmException, IOException,
           PermissionBackendException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), input);
   }
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 9893cf0..4024c10 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -75,7 +75,7 @@
           ResourceConflictException, OrmException, IOException, ConfigInvalidException,
           PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
     if (!realm.allowsEdit(AccountFieldName.USER_NAME)) {
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index fa4550d..516b485 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
@@ -42,7 +41,6 @@
 import com.google.gerrit.server.query.account.AccountQueryProcessor;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.LinkedHashMap;
@@ -55,7 +53,6 @@
 public class QueryAccounts implements RestReadView<TopLevelResource> {
   private static final int MAX_SUGGEST_RESULTS = 100;
 
-  private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
   private final AccountLoader.Factory accountLoaderFactory;
   private final AccountQueryBuilder queryBuilder;
@@ -125,13 +122,11 @@
 
   @Inject
   QueryAccounts(
-      Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
       AccountLoader.Factory accountLoaderFactory,
       AccountQueryBuilder queryBuilder,
       AccountQueryProcessor queryProcessor,
       @GerritServerConfig Config cfg) {
-    this.self = self;
     this.permissionBackend = permissionBackend;
     this.accountLoaderFactory = accountLoaderFactory;
     this.queryBuilder = queryBuilder;
@@ -170,7 +165,7 @@
     }
     boolean modifyAccountCapabilityChecked = false;
     if (options.contains(ListAccountsOption.ALL_EMAILS)) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
       modifyAccountCapabilityChecked = true;
       fillOptions.add(FillOptions.EMAIL);
       fillOptions.add(FillOptions.SECONDARY_EMAILS);
@@ -180,7 +175,7 @@
       fillOptions.add(FillOptions.EMAIL);
 
       if (modifyAccountCapabilityChecked
-          || permissionBackend.user(self).test(GlobalPermission.MODIFY_ACCOUNT)) {
+          || permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
         fillOptions.add(FillOptions.SECONDARY_EMAILS);
       }
     }
diff --git a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
index 1e300c7..6aa88de 100644
--- a/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetDiffPreferences.java
@@ -58,7 +58,7 @@
       throws RestApiException, ConfigInvalidException, RepositoryNotFoundException, IOException,
           PermissionBackendException, OrmException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     if (input == null) {
diff --git a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
index e06eaf3..dad6e0f 100644
--- a/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetEditPreferences.java
@@ -59,7 +59,7 @@
       throws RestApiException, RepositoryNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException, OrmException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     if (input == null) {
diff --git a/java/com/google/gerrit/server/restapi/account/SetPreferences.java b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
index dcc2695..11ecfdb 100644
--- a/java/com/google/gerrit/server/restapi/account/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/account/SetPreferences.java
@@ -64,7 +64,7 @@
       throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException,
           OrmException {
     if (self.get() != rsrc.getUser()) {
-      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     checkDownloadScheme(input.downloadScheme);
diff --git a/java/com/google/gerrit/server/restapi/account/SshKeys.java b/java/com/google/gerrit/server/restapi/account/SshKeys.java
index 52861ce..fa447c2 100644
--- a/java/com/google/gerrit/server/restapi/account/SshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/SshKeys.java
@@ -68,7 +68,7 @@
           PermissionBackendException {
     if (self.get() != rsrc.getUser()) {
       try {
-        permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+        permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
       } catch (AuthException e) {
         // If lacking MODIFY_ACCOUNT claim the resource does not exist.
         throw new ResourceNotFoundException();
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index e44896e..bf8bb1f 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -137,7 +137,7 @@
 
   private boolean canRead(ChangeNotes notes) throws PermissionBackendException, IOException {
     try {
-      permissionBackend.user(user).change(notes).database(db).check(ChangePermission.READ);
+      permissionBackend.currentUser().change(notes).database(db).check(ChangePermission.READ);
     } catch (AuthException e) {
       return false;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Check.java b/java/com/google/gerrit/server/restapi/change/Check.java
index fbf19be..f3e0077 100644
--- a/java/com/google/gerrit/server/restapi/change/Check.java
+++ b/java/com/google/gerrit/server/restapi/change/Check.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -32,7 +31,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import javax.inject.Singleton;
 
@@ -40,13 +38,11 @@
 public class Check
     implements RestReadView<ChangeResource>, RestModifyView<ChangeResource, FixInput> {
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final ChangeJson.Factory jsonFactory;
 
   @Inject
-  Check(PermissionBackend permissionBackend, Provider<CurrentUser> user, ChangeJson.Factory json) {
+  Check(PermissionBackend permissionBackend, ChangeJson.Factory json) {
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.jsonFactory = json;
   }
 
@@ -59,7 +55,7 @@
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
       throws RestApiException, OrmException, PermissionBackendException, NoSuchProjectException,
           IOException {
-    PermissionBackend.WithUser perm = permissionBackend.user(user);
+    PermissionBackend.WithUser perm = permissionBackend.currentUser();
     if (!rsrc.isUserOwner()) {
       try {
         perm.project(rsrc.getProject()).check(ProjectPermission.READ_CONFIG);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index 2de5ba46..26a85e1 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.IntegrationException;
@@ -44,7 +43,6 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -57,7 +55,6 @@
     implements UiAction<RevisionResource> {
   private static final Logger log = LoggerFactory.getLogger(CherryPick.class);
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
   private final ContributorAgreementsChecker contributorAgreements;
@@ -66,7 +63,6 @@
   @Inject
   CherryPick(
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       RetryHelper retryHelper,
       CherryPickChange cherryPickChange,
       ChangeJson.Factory json,
@@ -74,7 +70,6 @@
       ProjectCache projectCache) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
     this.contributorAgreements = contributorAgreements;
@@ -97,7 +92,7 @@
     contributorAgreements.check(rsrc.getProject(), rsrc.getUser());
 
     permissionBackend
-        .user(user)
+        .currentUser()
         .project(rsrc.getChange().getProject())
         .ref(refName)
         .check(RefPermission.CREATE_CHANGE);
@@ -134,7 +129,7 @@
             and(
                 rsrc.isCurrent() && projectStatePermitsWrite,
                 permissionBackend
-                    .user(user)
+                    .currentUser()
                     .project(rsrc.getProject())
                     .testCond(ProjectPermission.CREATE_CHANGE)));
   }
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index 7c10086..3221eaf 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -90,7 +90,7 @@
     String refName = RefNames.fullName(destination);
     contributorAgreements.check(projectName, user.get());
     permissionBackend
-        .user(user)
+        .currentUser()
         .project(projectName)
         .ref(refName)
         .check(RefPermission.CREATE_CHANGE);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 499cbb6..d8aa880 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -192,7 +192,11 @@
 
     Project.NameKey project = rsrc.getNameKey();
     String refName = RefNames.fullName(input.branch);
-    permissionBackend.user(user).project(project).ref(refName).check(RefPermission.CREATE_CHANGE);
+    permissionBackend
+        .currentUser()
+        .project(project)
+        .ref(refName)
+        .check(RefPermission.CREATE_CHANGE);
     rsrc.getProjectState().checkStatePermitsWrite();
 
     try (Repository git = gitManager.openRepository(project);
@@ -208,7 +212,7 @@
         }
         ChangeNotes change = Iterables.getOnlyElement(notes);
         try {
-          permissionBackend.user(user).change(change).database(db).check(ChangePermission.READ);
+          permissionBackend.currentUser().change(change).database(db).check(ChangePermission.READ);
         } catch (AuthException e) {
           throw new UnprocessableEntityException("Read not permitted for " + input.baseChange);
         }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index dcaba77..70a2251 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -210,7 +210,7 @@
     }
     ChangeNotes change = Iterables.getOnlyElement(notes);
     try {
-      permissionBackend.user(user).change(change).database(db).check(ChangePermission.READ);
+      permissionBackend.currentUser().change(change).database(db).check(ChangePermission.READ);
     } catch (AuthException e) {
       throw new UnprocessableEntityException("Read not permitted for " + baseChange);
     }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index e33b4a4..4bd10ed 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -56,7 +56,7 @@
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws RestApiException, UpdateException, PermissionBackendException {
-    if (rsrc.getChange().getStatus() == Change.Status.MERGED) {
+    if (!isChangeDeletable(rsrc.getChange().getStatus())) {
       throw new MethodNotAllowedException("delete not permitted");
     }
     rsrc.permissions().database(db).check(ChangePermission.DELETE);
@@ -78,20 +78,15 @@
     return new UiAction.Description()
         .setLabel("Delete")
         .setTitle("Delete change " + rsrc.getId())
-        .setVisible(and(couldDeleteWhenIn(status), perm.testCond(ChangePermission.DELETE)));
+        .setVisible(and(isChangeDeletable(status), perm.testCond(ChangePermission.DELETE)));
   }
 
-  private boolean couldDeleteWhenIn(Change.Status status) {
-    switch (status) {
-      case NEW:
-      case ABANDONED:
-        // New or abandoned changes can be deleted with the right permissions.
-        return true;
-
-      case MERGED:
-        // Merged changes should never be deleted.
-        return false;
+  private static boolean isChangeDeletable(Change.Status status) {
+    if (status == Change.Status.MERGED) {
+      // Merged changes should never be deleted.
+      return false;
     }
-    return false;
+    // New or abandoned changes can be deleted with the right permissions.
+    return true;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Index.java b/java/com/google/gerrit/server/restapi/change/Index.java
index 55f53a6..a5dd868 100644
--- a/java/com/google/gerrit/server/restapi/change/Index.java
+++ b/java/com/google/gerrit/server/restapi/change/Index.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -38,7 +37,6 @@
 
   private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final ChangeIndexer indexer;
 
   @Inject
@@ -46,12 +44,10 @@
       Provider<ReviewDb> db,
       RetryHelper retryHelper,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       ChangeIndexer indexer) {
     super(retryHelper);
     this.db = db;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.indexer = indexer;
   }
 
@@ -59,7 +55,7 @@
   protected Response<?> applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
       throws IOException, AuthException, OrmException, PermissionBackendException {
-    permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER);
+    permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER);
     indexer.index(db.get(), rsrc.getChange());
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index f9d7083..2049929 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -360,7 +360,7 @@
       ListMultimap<RecipientType, Account.Id> accountsToNotify)
       throws PermissionBackendException {
     if (!permissionBackend
-        .user(anonymousProvider)
+        .user(anonymousProvider.get())
         .change(rsrc.getNotes())
         .database(dbProvider)
         .test(ChangePermission.READ)) {
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index 303401c..fbdfb54 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -64,20 +63,17 @@
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
   private final ProjectCache projectCache;
-  private final Provider<CurrentUser> currentUserProvider;
 
   @Inject
   RelatedChangesSorter(
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
       Provider<ReviewDb> dbProvider,
-      ProjectCache projectCache,
-      Provider<CurrentUser> currentUserProvider) {
+      ProjectCache projectCache) {
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
     this.dbProvider = dbProvider;
     this.projectCache = projectCache;
-    this.currentUserProvider = currentUserProvider;
   }
 
   public List<PatchSetData> sort(List<ChangeData> in, PatchSet startPs)
@@ -239,8 +235,7 @@
   }
 
   private boolean isVisible(PatchSetData psd) throws PermissionBackendException, IOException {
-    PermissionBackend.WithUser perm =
-        permissionBackend.user(currentUserProvider).database(dbProvider);
+    PermissionBackend.WithUser perm = permissionBackend.currentUser().database(dbProvider);
     try {
       perm.change(psd.data()).check(ChangePermission.READ);
     } catch (AuthException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 5f6b088..78687cd 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.index.query.Predicate;
@@ -100,7 +101,7 @@
   }
 
   public List<Account.Id> suggestReviewers(
-      ChangeNotes changeNotes,
+      @Nullable ChangeNotes changeNotes,
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       List<Account.Id> candidateList)
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 38ed756..95557b5 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.common.GroupBaseInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -161,17 +162,25 @@
   }
 
   public List<SuggestedReviewerInfo> suggestReviewers(
-      ChangeNotes changeNotes,
+      @Nullable ChangeNotes changeNotes,
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       VisibilityControl visibilityControl,
       boolean excludeGroups)
       throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
     CurrentUser currentUser = self.get();
-    log.debug(
-        "Suggesting reviewers for change {} to user {}.",
-        changeNotes.getChangeId().get(),
-        currentUser.getLoggableName());
+    if (changeNotes != null) {
+      log.debug(
+          "Suggesting reviewers for change {} to user {}.",
+          changeNotes.getChangeId().get(),
+          currentUser.getLoggableName());
+    } else {
+      log.debug(
+          "Suggesting default reviewers for project {} to user {}.",
+          projectState.getName(),
+          currentUser.getLoggableName());
+    }
+
     String query = suggestReviewers.getQuery();
     log.debug("Query: {}", query);
     int limit = suggestReviewers.getLimit();
@@ -272,7 +281,7 @@
   }
 
   private List<Account.Id> recommendAccounts(
-      ChangeNotes changeNotes,
+      @Nullable ChangeNotes changeNotes,
       SuggestReviewers suggestReviewers,
       ProjectState projectState,
       List<Account.Id> candidateList)
@@ -286,7 +295,7 @@
   private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds)
       throws OrmException, PermissionBackendException {
     Set<FillOptions> fillOptions =
-        permissionBackend.user(self).test(GlobalPermission.MODIFY_ACCOUNT)
+        permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)
             ? EnumSet.of(FillOptions.SECONDARY_EMAILS)
             : EnumSet.noneOf(FillOptions.class);
     fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index f487c28..5298857 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -24,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
@@ -44,7 +46,6 @@
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
   private final Provider<ReviewDb> db;
-  private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
 
   @Inject
@@ -52,12 +53,10 @@
       RetryHelper retryHelper,
       WorkInProgressOp.Factory opFactory,
       Provider<ReviewDb> db,
-      Provider<CurrentUser> self,
       PermissionBackend permissionBackend) {
     super(retryHelper);
     this.opFactory = opFactory;
     this.db = db;
-    this.self = self;
     this.permissionBackend = permissionBackend;
   }
 
@@ -67,7 +66,7 @@
       throws RestApiException, UpdateException, PermissionBackendException {
     Change change = rsrc.getChange();
     if (!rsrc.isUserOwner()
-        && !permissionBackend.user(self).test(GlobalPermission.ADMINISTRATE_SERVER)) {
+        && !permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)) {
       throw new AuthException("not allowed to set ready for review");
     }
 
@@ -93,8 +92,12 @@
         .setLabel("Start Review")
         .setTitle("Set Ready For Review")
         .setVisible(
-            rsrc.isUserOwner()
-                && rsrc.getChange().getStatus() == Status.NEW
-                && rsrc.getChange().isWorkInProgress());
+            and(
+                rsrc.getChange().getStatus() == Status.NEW && rsrc.getChange().isWorkInProgress(),
+                or(
+                    rsrc.isUserOwner(),
+                    permissionBackend
+                        .currentUser()
+                        .testCond(GlobalPermission.ADMINISTRATE_SERVER))));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index 7fcf6a0..93568d5 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -24,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.change.WorkInProgressOp.Input;
@@ -44,7 +46,6 @@
     implements UiAction<ChangeResource> {
   private final WorkInProgressOp.Factory opFactory;
   private final Provider<ReviewDb> db;
-  private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
 
   @Inject
@@ -52,12 +53,10 @@
       WorkInProgressOp.Factory opFactory,
       RetryHelper retryHelper,
       Provider<ReviewDb> db,
-      Provider<CurrentUser> self,
       PermissionBackend permissionBackend) {
     super(retryHelper);
     this.opFactory = opFactory;
     this.db = db;
-    this.self = self;
     this.permissionBackend = permissionBackend;
   }
 
@@ -68,7 +67,7 @@
     Change change = rsrc.getChange();
 
     if (!rsrc.isUserOwner()
-        && !permissionBackend.user(self).test(GlobalPermission.ADMINISTRATE_SERVER)) {
+        && !permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)) {
       throw new AuthException("not allowed to set work in progress");
     }
 
@@ -94,8 +93,12 @@
         .setLabel("WIP")
         .setTitle("Set Work In Progress")
         .setVisible(
-            rsrc.isUserOwner()
-                && rsrc.getChange().getStatus() == Status.NEW
-                && !rsrc.getChange().isWorkInProgress());
+            and(
+                rsrc.getChange().getStatus() == Status.NEW && !rsrc.getChange().isWorkInProgress(),
+                or(
+                    rsrc.isUserOwner(),
+                    permissionBackend
+                        .currentUser()
+                        .testCond(GlobalPermission.ADMINISTRATE_SERVER))));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index 4033d10..2dac5ef 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -88,7 +88,7 @@
   private VisibilityControl getVisibility(ChangeResource rsrc) {
     // Use the destination reference, not the change, as private changes deny anyone who is not
     // already a reviewer.
-    PermissionBackend.ForRef perm = permissionBackend.user(self).ref(rsrc.getChange().getDest());
+    PermissionBackend.ForRef perm = permissionBackend.currentUser().ref(rsrc.getChange().getDest());
     return new VisibilityControl() {
       @Override
       public boolean isVisibleTo(Account.Id account) throws OrmException {
diff --git a/java/com/google/gerrit/server/restapi/config/CachesCollection.java b/java/com/google/gerrit/server/restapi/config/CachesCollection.java
index cfdc648..e3d9e3c 100644
--- a/java/com/google/gerrit/server/restapi/config/CachesCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/CachesCollection.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.CacheResource;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -45,7 +44,6 @@
   private final DynamicMap<RestView<CacheResource>> views;
   private final Provider<ListCaches> list;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> self;
   private final DynamicMap<Cache<?, ?>> cacheMap;
   private final PostCaches postCaches;
 
@@ -54,13 +52,11 @@
       DynamicMap<RestView<CacheResource>> views,
       Provider<ListCaches> list,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> self,
       DynamicMap<Cache<?, ?>> cacheMap,
       PostCaches postCaches) {
     this.views = views;
     this.list = list;
     this.permissionBackend = permissionBackend;
-    this.self = self;
     this.cacheMap = cacheMap;
     this.postCaches = postCaches;
   }
@@ -73,7 +69,7 @@
   @Override
   public CacheResource parse(ConfigResource parent, IdString id)
       throws AuthException, ResourceNotFoundException, PermissionBackendException {
-    permissionBackend.user(self).check(GlobalPermission.VIEW_CACHES);
+    permissionBackend.currentUser().check(GlobalPermission.VIEW_CACHES);
 
     String cacheName = id.get();
     String pluginName = "gerrit";
diff --git a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
index 95b20c2..a16736b 100644
--- a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
+++ b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountsConsistencyChecker;
 import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
 import com.google.gerrit.server.config.ConfigResource;
@@ -32,7 +31,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -40,7 +38,6 @@
 @Singleton
 public class CheckConsistency implements RestModifyView<ConfigResource, ConsistencyCheckInput> {
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final AccountsConsistencyChecker accountsConsistencyChecker;
   private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
   private final GroupsConsistencyChecker groupsConsistencyChecker;
@@ -48,12 +45,10 @@
   @Inject
   CheckConsistency(
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       AccountsConsistencyChecker accountsConsistencyChecker,
       ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
       GroupsConsistencyChecker groupsChecker) {
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.accountsConsistencyChecker = accountsConsistencyChecker;
     this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
     this.groupsConsistencyChecker = groupsChecker;
@@ -63,7 +58,7 @@
   public ConsistencyCheckInfo apply(ConfigResource resource, ConsistencyCheckInput input)
       throws RestApiException, IOException, OrmException, PermissionBackendException,
           ConfigInvalidException {
-    permissionBackend.user(user).check(GlobalPermission.ACCESS_DATABASE);
+    permissionBackend.currentUser().check(GlobalPermission.ACCESS_DATABASE);
 
     if (input == null
         || (input.checkAccounts == null
diff --git a/java/com/google/gerrit/server/restapi/config/FlushCache.java b/java/com/google/gerrit/server/restapi/config/FlushCache.java
index 55e9dc3..9ea9e33 100644
--- a/java/com/google/gerrit/server/restapi/config/FlushCache.java
+++ b/java/com/google/gerrit/server/restapi/config/FlushCache.java
@@ -22,13 +22,11 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.CacheResource;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @RequiresAnyCapability({FLUSH_CACHES, MAINTAIN_SERVER})
@@ -38,19 +36,17 @@
   public static final String WEB_SESSIONS = "web_sessions";
 
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> self;
 
   @Inject
-  public FlushCache(PermissionBackend permissionBackend, Provider<CurrentUser> self) {
+  public FlushCache(PermissionBackend permissionBackend) {
     this.permissionBackend = permissionBackend;
-    this.self = self;
   }
 
   @Override
   public Response<String> apply(CacheResource rsrc, Input input)
       throws AuthException, PermissionBackendException {
     if (WEB_SESSIONS.equals(rsrc.getName())) {
-      permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER);
+      permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER);
     }
 
     rsrc.getCache().invalidateAll();
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index 71ee5ad..fb2819c 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -28,6 +28,8 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 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.IdGenerator;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -46,13 +48,18 @@
   private final PermissionBackend permissionBackend;
   private final WorkQueue workQueue;
   private final Provider<CurrentUser> self;
+  private final ProjectCache projectCache;
 
   @Inject
   public ListTasks(
-      PermissionBackend permissionBackend, WorkQueue workQueue, Provider<CurrentUser> self) {
+      PermissionBackend permissionBackend,
+      WorkQueue workQueue,
+      Provider<CurrentUser> self,
+      ProjectCache projectCache) {
     this.permissionBackend = permissionBackend;
     this.workQueue = workQueue;
     this.self = self;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -77,14 +84,17 @@
       if (task.projectName != null) {
         Boolean visible = visibilityCache.get(task.projectName);
         if (visible == null) {
-          try {
-            permissionBackend
-                .user(user)
-                .project(new Project.NameKey(task.projectName))
-                .check(ProjectPermission.ACCESS);
-            visible = true;
-          } catch (AuthException e) {
+          Project.NameKey nameKey = new Project.NameKey(task.projectName);
+          ProjectState state = projectCache.get(nameKey);
+          if (state == null || !state.statePermitsRead()) {
             visible = false;
+          } else {
+            try {
+              permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
+              visible = true;
+            } catch (AuthException e) {
+              visible = false;
+            }
           }
           visibilityCache.put(task.projectName, visible);
         }
diff --git a/java/com/google/gerrit/server/restapi/config/TasksCollection.java b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
index f5b6e56..dda54a0 100644
--- a/java/com/google/gerrit/server/restapi/config/TasksCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
@@ -18,8 +18,10 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.TaskResource;
@@ -30,6 +32,8 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 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.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -41,6 +45,7 @@
   private final WorkQueue workQueue;
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
+  private final ProjectCache projectCache;
 
   @Inject
   TasksCollection(
@@ -48,12 +53,14 @@
       ListTasks list,
       WorkQueue workQueue,
       Provider<CurrentUser> self,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache) {
     this.views = views;
     this.list = list;
     this.workQueue = workQueue;
     this.self = self;
     this.permissionBackend = permissionBackend;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -63,7 +70,8 @@
 
   @Override
   public TaskResource parse(ConfigResource parent, IdString id)
-      throws ResourceNotFoundException, AuthException, PermissionBackendException {
+      throws ResourceNotFoundException, AuthException, PermissionBackendException,
+          ResourceConflictException {
     CurrentUser user = self.get();
     if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
@@ -78,11 +86,16 @@
 
     Task<?> task = workQueue.getTask(taskId);
     if (task instanceof ProjectTask) {
+      Project.NameKey nameKey = ((ProjectTask<?>) task).getProjectNameKey();
+      ProjectState state = projectCache.get(nameKey);
+      if (state == null) {
+        throw new ResourceNotFoundException(String.format("project %s not found", nameKey));
+      }
+
+      state.checkStatePermitsRead();
+
       try {
-        permissionBackend
-            .user(user)
-            .project(((ProjectTask<?>) task).getProjectNameKey())
-            .check(ProjectPermission.ACCESS);
+        permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
         return new TaskResource(task);
       } catch (AuthException e) {
         // Fall through and try view queue permission.
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index f65b29f..9ddafe3 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
@@ -92,7 +91,6 @@
   private final AccountResolver accountResolver;
   private final AccountCache accountCache;
   private final AccountLoader.Factory infoFactory;
-  private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Inject
@@ -103,7 +101,6 @@
       AccountResolver accountResolver,
       AccountCache accountCache,
       AccountLoader.Factory infoFactory,
-      Provider<ReviewDb> db,
       @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
     this.accountManager = accountManager;
     this.authType = authConfig.getAuthType();
@@ -111,7 +108,6 @@
     this.accountResolver = accountResolver;
     this.accountCache = accountCache;
     this.infoFactory = infoFactory;
-    this.db = db;
     this.groupsUpdateProvider = groupsUpdateProvider;
   }
 
@@ -186,7 +182,7 @@
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.union(memberIds, newMemberIds))
             .build();
-    groupsUpdateProvider.get().updateGroup(db.get(), groupUuid, groupUpdate);
+    groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
   }
 
   private Optional<Account> createAccountByLdap(String user) throws IOException {
diff --git a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
index e29bb7c..e11f389 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
@@ -75,18 +74,15 @@
   }
 
   private final GroupsCollection groupsCollection;
-  private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
   private final GroupJson json;
 
   @Inject
   public AddSubgroups(
       GroupsCollection groupsCollection,
-      Provider<ReviewDb> db,
       @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
       GroupJson json) {
     this.groupsCollection = groupsCollection;
-    this.db = db;
     this.groupsUpdateProvider = groupsUpdateProvider;
     this.json = json;
   }
@@ -128,7 +124,7 @@
         InternalGroupUpdate.builder()
             .setSubgroupModification(subgroupUuids -> Sets.union(subgroupUuids, newSubgroupUuids))
             .build();
-    groupsUpdateProvider.get().updateGroup(db.get(), parentGroupUuid, groupUpdate);
+    groupsUpdateProvider.get().updateGroup(parentGroupUuid, groupUpdate);
   }
 
   static class PutSubgroup implements RestModifyView<GroupResource, Input> {
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index a0c88f2..79f9688 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.Sequences;
@@ -75,7 +74,6 @@
 
   private final Provider<IdentifiedUser> self;
   private final PersonIdent serverIdent;
-  private final ReviewDb db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
   private final GroupCache groupCache;
   private final GroupsCollection groups;
@@ -91,7 +89,6 @@
   CreateGroup(
       Provider<IdentifiedUser> self,
       @GerritPersonIdent PersonIdent serverIdent,
-      ReviewDb db,
       @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
       GroupCache groupCache,
       GroupsCollection groups,
@@ -104,7 +101,6 @@
       Sequences sequences) {
     this.self = self;
     this.serverIdent = serverIdent;
-    this.db = db;
     this.groupsUpdateProvider = groupsUpdateProvider;
     this.groupCache = groupCache;
     this.groups = groups;
@@ -222,7 +218,7 @@
     groupUpdateBuilder.setMemberModification(
         members -> ImmutableSet.copyOf(createGroupArgs.initialMembers));
     try {
-      return groupsUpdateProvider.get().createGroup(db, groupCreation, groupUpdateBuilder.build());
+      return groupsUpdateProvider.get().createGroup(groupCreation, groupUpdateBuilder.build());
     } catch (OrmDuplicateKeyException e) {
       throw new ResourceConflictException(
           "group '" + createGroupArgs.getGroupName() + "' already exists");
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index d92c521..3685469 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
@@ -46,16 +45,12 @@
 @Singleton
 public class DeleteMembers implements RestModifyView<GroupResource, Input> {
   private final AccountsCollection accounts;
-  private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Inject
   DeleteMembers(
-      AccountsCollection accounts,
-      Provider<ReviewDb> db,
-      @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
+      AccountsCollection accounts, @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
     this.accounts = accounts;
-    this.db = db;
     this.groupsUpdateProvider = groupsUpdateProvider;
   }
 
@@ -93,7 +88,7 @@
         InternalGroupUpdate.builder()
             .setMemberModification(memberIds -> Sets.difference(memberIds, accountIds))
             .build();
-    groupsUpdateProvider.get().updateGroup(db.get(), groupUuid, groupUpdate);
+    groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
   }
 
   @Singleton
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
index 6a5ac5d..0eba8c7 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.GroupResource;
@@ -45,16 +44,13 @@
 @Singleton
 public class DeleteSubgroups implements RestModifyView<GroupResource, Input> {
   private final GroupsCollection groupsCollection;
-  private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Inject
   DeleteSubgroups(
       GroupsCollection groupsCollection,
-      Provider<ReviewDb> db,
       @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
     this.groupsCollection = groupsCollection;
-    this.db = db;
     this.groupsUpdateProvider = groupsUpdateProvider;
   }
 
@@ -96,7 +92,7 @@
             .setSubgroupModification(
                 subgroupUuids -> Sets.difference(subgroupUuids, removedSubgroupUuids))
             .build();
-    groupsUpdateProvider.get().updateGroup(db.get(), parentGroupUuid, groupUpdate);
+    groupsUpdateProvider.get().updateGroup(parentGroupUuid, groupUpdate);
   }
 
   @Singleton
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index 51fffbb..eb66a37 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
@@ -38,7 +37,6 @@
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -50,7 +48,6 @@
 
 @Singleton
 public class GetAuditLog implements RestReadView<GroupResource> {
-  private final Provider<ReviewDb> db;
   private final AccountLoader.Factory accountLoaderFactory;
   private final AllUsersName allUsers;
   private final GroupCache groupCache;
@@ -61,7 +58,6 @@
 
   @Inject
   public GetAuditLog(
-      Provider<ReviewDb> db,
       AccountLoader.Factory accountLoaderFactory,
       AllUsersName allUsers,
       GroupCache groupCache,
@@ -69,7 +65,6 @@
       GroupBackend groupBackend,
       Groups groups,
       GitRepositoryManager repoManager) {
-    this.db = db;
     this.accountLoaderFactory = accountLoaderFactory;
     this.allUsers = allUsers;
     this.groupCache = groupCache;
@@ -95,7 +90,7 @@
 
     try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
       for (AccountGroupMemberAudit auditEvent :
-          groups.getMembersAudit(db.get(), allUsersRepo, group.getGroupUUID())) {
+          groups.getMembersAudit(allUsersRepo, group.getGroupUUID())) {
         AccountInfo member = accountLoader.get(auditEvent.getMemberId());
 
         auditEvents.add(
@@ -110,7 +105,7 @@
       }
 
       for (AccountGroupByIdAud auditEvent :
-          groups.getSubgroupsAudit(db.get(), allUsersRepo, group.getGroupUUID())) {
+          groups.getSubgroupsAudit(allUsersRepo, group.getGroupUUID())) {
         AccountGroup.UUID includedGroupUUID = auditEvent.getIncludeUUID();
         Optional<InternalGroup> includedGroup = groupCache.get(includedGroupUUID);
         GroupInfo member;
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 91aee6d..0dbd7b6 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -83,7 +82,6 @@
   private final GroupBackend groupBackend;
   private final Groups groups;
   private final GroupsCollection groupsCollection;
-  private final Provider<ReviewDb> db;
 
   private EnumSet<ListGroupsOption> options = EnumSet.noneOf(ListGroupsOption.class);
   private boolean visibleToAll;
@@ -232,8 +230,7 @@
       final GroupsCollection groupsCollection,
       GroupJson json,
       GroupBackend groupBackend,
-      Groups groups,
-      Provider<ReviewDb> db) {
+      Groups groups) {
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
     this.genericGroupControlFactory = genericGroupControlFactory;
@@ -244,7 +241,6 @@
     this.groupBackend = groupBackend;
     this.groups = groups;
     this.groupsCollection = groupsCollection;
-    this.db = db;
   }
 
   public void setOptions(EnumSet<ListGroupsOption> options) {
@@ -316,8 +312,7 @@
     return groupInfos;
   }
 
-  private Stream<GroupReference> getAllExistingGroups()
-      throws OrmException, IOException, ConfigInvalidException {
+  private Stream<GroupReference> getAllExistingGroups() throws IOException, ConfigInvalidException {
     if (!projects.isEmpty()) {
       return projects
           .stream()
@@ -325,7 +320,7 @@
           .flatMap(Collection::stream)
           .distinct();
     }
-    return groups.getAllGroupReferences(db.get());
+    return groups.getAllGroupReferences();
   }
 
   private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException {
@@ -388,7 +383,7 @@
     Pattern pattern = getRegexPattern();
     Stream<? extends GroupDescription.Internal> foundGroups =
         groups
-            .getAllGroupReferences(db.get())
+            .getAllGroupReferences()
             .filter(group -> isRelevant(pattern, group))
             .map(this::loadGroup)
             .flatMap(Streams::stream)
diff --git a/java/com/google/gerrit/server/restapi/group/PutDescription.java b/java/com/google/gerrit/server/restapi/group/PutDescription.java
index 6a68c7a..d407f69 100644
--- a/java/com/google/gerrit/server/restapi/group/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/PutDescription.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -38,13 +37,10 @@
 
 @Singleton
 public class PutDescription implements RestModifyView<GroupResource, DescriptionInput> {
-  private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Inject
-  PutDescription(
-      Provider<ReviewDb> db, @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
-    this.db = db;
+  PutDescription(@UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
     this.groupsUpdateProvider = groupsUpdateProvider;
   }
 
@@ -69,7 +65,7 @@
       InternalGroupUpdate groupUpdate =
           InternalGroupUpdate.builder().setDescription(newDescription).build();
       try {
-        groupsUpdateProvider.get().updateGroup(db.get(), groupUuid, groupUpdate);
+        groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
       } catch (NoSuchGroupException e) {
         throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid));
       }
diff --git a/java/com/google/gerrit/server/restapi/group/PutName.java b/java/com/google/gerrit/server/restapi/group/PutName.java
index f1dd4f1..1f1968a 100644
--- a/java/com/google/gerrit/server/restapi/group/PutName.java
+++ b/java/com/google/gerrit/server/restapi/group/PutName.java
@@ -24,13 +24,11 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -39,19 +37,17 @@
 
 @Singleton
 public class PutName implements RestModifyView<GroupResource, NameInput> {
-  private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Inject
-  PutName(Provider<ReviewDb> db, @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
-    this.db = db;
+  PutName(@UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
     this.groupsUpdateProvider = groupsUpdateProvider;
   }
 
   @Override
   public String apply(GroupResource rsrc, NameInput input)
       throws NotInternalGroupException, AuthException, BadRequestException,
-          ResourceConflictException, ResourceNotFoundException, OrmException, IOException,
+          ResourceConflictException, ResourceNotFoundException, IOException,
           ConfigInvalidException {
     GroupDescription.Internal internalGroup =
         rsrc.asInternalGroup().orElseThrow(NotInternalGroupException::new);
@@ -74,13 +70,13 @@
   }
 
   private void renameGroup(GroupDescription.Internal group, String newName)
-      throws ResourceConflictException, ResourceNotFoundException, OrmException, IOException,
+      throws ResourceConflictException, ResourceNotFoundException, IOException,
           ConfigInvalidException {
     AccountGroup.UUID groupUuid = group.getGroupUUID();
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder().setName(new AccountGroup.NameKey(newName)).build();
     try {
-      groupsUpdateProvider.get().updateGroup(db.get(), groupUuid, groupUpdate);
+      groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid));
     } catch (OrmDuplicateKeyException e) {
diff --git a/java/com/google/gerrit/server/restapi/group/PutOptions.java b/java/com/google/gerrit/server/restapi/group/PutOptions.java
index ab2ae1a..29b87d2 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOptions.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOptions.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -36,12 +35,10 @@
 
 @Singleton
 public class PutOptions implements RestModifyView<GroupResource, GroupOptionsInfo> {
-  private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Inject
-  PutOptions(Provider<ReviewDb> db, @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
-    this.db = db;
+  PutOptions(@UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
     this.groupsUpdateProvider = groupsUpdateProvider;
   }
 
@@ -67,7 +64,7 @@
       InternalGroupUpdate groupUpdate =
           InternalGroupUpdate.builder().setVisibleToAll(input.visibleToAll).build();
       try {
-        groupsUpdateProvider.get().updateGroup(db.get(), groupUuid, groupUpdate);
+        groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
       } catch (NoSuchGroupException e) {
         throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid));
       }
diff --git a/java/com/google/gerrit/server/restapi/group/PutOwner.java b/java/com/google/gerrit/server/restapi/group/PutOwner.java
index 4d16e1e..5e7563e 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOwner.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -41,18 +40,15 @@
 public class PutOwner implements RestModifyView<GroupResource, OwnerInput> {
   private final GroupsCollection groupsCollection;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
-  private final Provider<ReviewDb> db;
   private final GroupJson json;
 
   @Inject
   PutOwner(
       GroupsCollection groupsCollection,
       @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
-      Provider<ReviewDb> db,
       GroupJson json) {
     this.groupsCollection = groupsCollection;
     this.groupsUpdateProvider = groupsUpdateProvider;
-    this.db = db;
     this.json = json;
   }
 
@@ -77,7 +73,7 @@
       InternalGroupUpdate groupUpdate =
           InternalGroupUpdate.builder().setOwnerGroupUUID(owner.getGroupUUID()).build();
       try {
-        groupsUpdateProvider.get().updateGroup(db.get(), groupUuid, groupUpdate);
+        groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
       } catch (NoSuchGroupException e) {
         throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid));
       }
diff --git a/java/com/google/gerrit/server/restapi/project/BranchesCollection.java b/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
index da5f058..f8ff7b9 100644
--- a/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/BranchesCollection.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -45,7 +44,6 @@
   private final DynamicMap<RestView<BranchResource>> views;
   private final Provider<ListBranches> list;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final GitRepositoryManager repoManager;
   private final CreateBranch.Factory createBranchFactory;
 
@@ -54,13 +52,11 @@
       DynamicMap<RestView<BranchResource>> views,
       Provider<ListBranches> list,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       GitRepositoryManager repoManager,
       CreateBranch.Factory createBranchFactory) {
     this.views = views;
     this.list = list;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.repoManager = repoManager;
     this.createBranchFactory = createBranchFactory;
   }
@@ -86,7 +82,7 @@
       // rights on the symbolic reference itself. This check prevents seeing a hidden
       // branch simply because the symbolic reference name was visible.
       permissionBackend
-          .user(user)
+          .currentUser()
           .project(project)
           .ref(ref.isSymbolic() ? ref.getTarget().getName() : ref.getName())
           .check(RefPermission.READ);
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index f98a96a..2c0653a 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -88,7 +88,7 @@
     IdentifiedUser user = userFactory.create(match.getId());
     try {
       permissionBackend.user(user).project(rsrc.getNameKey()).check(ProjectPermission.ACCESS);
-    } catch (AuthException | PermissionBackendException e) {
+    } catch (AuthException e) {
       info.message =
           String.format(
               "user %s (%s) cannot see project %s",
@@ -119,7 +119,7 @@
             .user(user)
             .ref(new Branch.NameKey(rsrc.getNameKey(), input.ref))
             .check(refPerm);
-      } catch (AuthException | PermissionBackendException e) {
+      } catch (AuthException e) {
         info.status = HttpServletResponse.SC_FORBIDDEN;
         info.message =
             String.format(
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index 9b9d008..87b5343 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -122,6 +122,6 @@
       }
     }
 
-    return reachable.fromRefs(state, repo, commit, repo.getAllRefs());
+    return reachable.fromRefs(project, repo, commit, repo.getAllRefs());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index c154e0e..91dd923 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -94,19 +93,18 @@
 
   @Override
   public Response<ChangeInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
-      throws PermissionBackendException, PermissionDeniedException, IOException,
-          ConfigInvalidException, OrmException, InvalidNameException, UpdateException,
-          RestApiException {
+      throws PermissionBackendException, AuthException, IOException, ConfigInvalidException,
+          OrmException, InvalidNameException, UpdateException, RestApiException {
     PermissionBackend.ForProject forProject =
         permissionBackend.user(rsrc.getUser()).project(rsrc.getNameKey());
     if (!check(forProject, ProjectPermission.READ_CONFIG)) {
-      throw new PermissionDeniedException(RefNames.REFS_CONFIG + " not visible");
+      throw new AuthException(RefNames.REFS_CONFIG + " not visible");
     }
     if (!check(forProject, ProjectPermission.WRITE_CONFIG)) {
       try {
         forProject.ref(RefNames.REFS_CONFIG).check(RefPermission.CREATE_CHANGE);
       } catch (AuthException denied) {
-        throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG);
+        throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG);
       }
     }
     projectCache.checkedGet(rsrc.getNameKey()).checkStatePermitsWrite();
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index b87e2f9..9f3c473 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -179,7 +179,7 @@
         info.ref = ref;
         info.revision = revid.getName();
         info.canDelete =
-            permissionBackend.user(identifiedUser).ref(name).testOrFalse(RefPermission.DELETE)
+            permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
                     && rsrc.getProjectState().statePermitsWrite()
                 ? true
                 : null;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index 2dc2c4a..2c3735f 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -39,7 +38,6 @@
 import com.google.gerrit.server.project.RefUtil;
 import com.google.gerrit.server.project.RefUtil.InvalidRevisionException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.TimeZone;
@@ -63,7 +61,6 @@
   }
 
   private final PermissionBackend permissionBackend;
-  private final Provider<IdentifiedUser> identifiedUser;
   private final GitRepositoryManager repoManager;
   private final TagCache tagCache;
   private final GitReferenceUpdated referenceUpdated;
@@ -73,14 +70,12 @@
   @Inject
   CreateTag(
       PermissionBackend permissionBackend,
-      Provider<IdentifiedUser> identifiedUser,
       GitRepositoryManager repoManager,
       TagCache tagCache,
       GitReferenceUpdated referenceUpdated,
       WebLinks webLinks,
       @Assisted String ref) {
     this.permissionBackend = permissionBackend;
-    this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.tagCache = tagCache;
     this.referenceUpdated = referenceUpdated;
@@ -103,7 +98,7 @@
 
     ref = RefUtil.normalizeTagRef(ref);
     PermissionBackend.ForRef perm =
-        permissionBackend.user(identifiedUser).project(resource.getNameKey()).ref(ref);
+        permissionBackend.currentUser().project(resource.getNameKey()).ref(ref);
 
     try (Repository repo = repoManager.openRepository(resource.getNameKey())) {
       ObjectId revid = RefUtil.parseBaseRevision(repo, resource.getNameKey(), input.revision);
@@ -134,7 +129,10 @@
         if (isAnnotated) {
           tag.setMessage(input.message)
               .setTagger(
-                  identifiedUser.get().newCommitterIdent(TimeUtil.nowTs(), TimeZone.getDefault()));
+                  resource
+                      .getUser()
+                      .asIdentifiedUser()
+                      .newCommitterIdent(TimeUtil.nowTs(), TimeZone.getDefault()));
         }
 
         Ref result = tag.call();
@@ -145,7 +143,7 @@
             ref,
             ObjectId.zeroId(),
             result.getObjectId(),
-            identifiedUser.get().state());
+            resource.getUser().asIdentifiedUser().state());
         try (RevWalk w = new RevWalk(repo)) {
           return ListTags.createTagInfo(perm, result, w, resource.getProjectState(), links);
         }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index 3114f8a..89213a0 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -38,25 +37,22 @@
 
   private final Provider<InternalChangeQuery> queryProvider;
   private final DeleteRef.Factory deleteRefFactory;
-  private final Provider<CurrentUser> user;
   private final PermissionBackend permissionBackend;
 
   @Inject
   DeleteBranch(
       Provider<InternalChangeQuery> queryProvider,
       DeleteRef.Factory deleteRefFactory,
-      Provider<CurrentUser> user,
       PermissionBackend permissionBackend) {
     this.queryProvider = queryProvider;
     this.deleteRefFactory = deleteRefFactory;
-    this.user = user;
     this.permissionBackend = permissionBackend;
   }
 
   @Override
   public Response<?> apply(BranchResource rsrc, Input input)
       throws RestApiException, OrmException, IOException, PermissionBackendException {
-    permissionBackend.user(user).ref(rsrc.getBranchKey()).check(RefPermission.DELETE);
+    permissionBackend.currentUser().ref(rsrc.getBranchKey()).check(RefPermission.DELETE);
     rsrc.getProjectState().checkStatePermitsWrite();
 
     if (!queryProvider.get().setLimit(1).byBranchOpen(rsrc.getBranchKey()).isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 56d68f9..c51fc56 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -222,7 +222,7 @@
 
     try {
       permissionBackend
-          .user(identifiedUser)
+          .currentUser()
           .project(project.getNameKey())
           .ref(refName)
           .check(RefPermission.DELETE);
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index f432129..a3886bc 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -26,7 +25,6 @@
 import com.google.gerrit.server.project.TagResource;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 
@@ -34,16 +32,11 @@
 public class DeleteTag implements RestModifyView<TagResource, Input> {
 
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final DeleteRef.Factory deleteRefFactory;
 
   @Inject
-  DeleteTag(
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
-      DeleteRef.Factory deleteRefFactory) {
+  DeleteTag(PermissionBackend permissionBackend, DeleteRef.Factory deleteRefFactory) {
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.deleteRefFactory = deleteRefFactory;
   }
 
@@ -52,7 +45,7 @@
       throws OrmException, RestApiException, IOException, PermissionBackendException {
     String tag = RefUtil.normalizeTagRef(resource.getTagInfo().ref);
     permissionBackend
-        .user(user)
+        .currentUser()
         .project(resource.getNameKey())
         .ref(tag)
         .check(RefPermission.DELETE);
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 21d6013..38eb20a7 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -137,7 +137,7 @@
     Project.NameKey projectName = rsrc.getNameKey();
     ProjectAccessInfo info = new ProjectAccessInfo();
     ProjectState projectState = projectCache.checkedGet(projectName);
-    PermissionBackend.ForProject perm = permissionBackend.user(user).project(projectName);
+    PermissionBackend.ForProject perm = permissionBackend.currentUser().project(projectName);
 
     ProjectConfig config;
     try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
@@ -160,12 +160,12 @@
         config.commit(md);
         projectCache.evict(config.getProject());
         projectState = projectCache.checkedGet(projectName);
-        perm = permissionBackend.user(user).project(projectName);
+        perm = permissionBackend.currentUser().project(projectName);
       } else if (config.getRevision() != null
           && !config.getRevision().equals(projectState.getConfig().getRevision())) {
         projectCache.evict(config.getProject());
         projectState = projectCache.checkedGet(projectName);
-        perm = permissionBackend.user(user).project(projectName);
+        perm = permissionBackend.currentUser().project(projectName);
       }
     } catch (ConfigInvalidException e) {
       throw new ResourceConflictException(e.getMessage());
@@ -239,7 +239,7 @@
     }
 
     if (info.ownerOf.isEmpty()
-        && permissionBackend.user(user).test(GlobalPermission.ADMINISTRATE_SERVER)) {
+        && permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)) {
       // Special case: If the section list is empty, this project has no current
       // access control information. Fall back to site administrators.
       info.ownerOf.add(AccessSection.ALL);
@@ -255,7 +255,7 @@
     }
 
     if (projectName.equals(allProjectsName)
-        && permissionBackend.user(user).testOrFalse(ADMINISTRATE_SERVER)) {
+        && permissionBackend.currentUser().testOrFalse(ADMINISTRATE_SERVER)) {
       info.ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index 05b6def..b6fa6d0 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefFilter;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -58,7 +57,6 @@
 public class ListBranches implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final DynamicMap<RestView<BranchResource>> branchViews;
   private final UiActions uiActions;
   private final WebLinks webLinks;
@@ -112,13 +110,11 @@
   public ListBranches(
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       DynamicMap<RestView<BranchResource>> branchViews,
       UiActions uiActions,
       WebLinks webLinks) {
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.branchViews = branchViews;
     this.uiActions = uiActions;
     this.webLinks = webLinks;
@@ -183,7 +179,7 @@
       }
     }
 
-    PermissionBackend.ForProject perm = permissionBackend.user(user).project(rsrc.getNameKey());
+    PermissionBackend.ForProject perm = permissionBackend.currentUser().project(rsrc.getNameKey());
     List<BranchInfo> branches = new ArrayList<>(refs.size());
     for (Ref ref : refs) {
       if (ref.isSymbolic()) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index 5727c6b..3067c89 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -31,7 +30,6 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -44,7 +42,6 @@
 
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final AllProjectsName allProjects;
   private final ProjectJson json;
   private final ChildProjects childProjects;
@@ -53,13 +50,11 @@
   ListChildProjects(
       ProjectCache projectCache,
       PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
       AllProjectsName allProjectsName,
       ProjectJson json,
       ChildProjects childProjects) {
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.allProjects = allProjectsName;
     this.json = json;
     this.childProjects = childProjects;
@@ -85,12 +80,14 @@
     Map<Project.NameKey, Project> children = new HashMap<>();
     for (Project.NameKey name : projectCache.all()) {
       ProjectState c = projectCache.get(name);
-      if (c != null && parent.equals(c.getProject().getParent(allProjects))) {
+      if (c != null
+          && parent.equals(c.getProject().getParent(allProjects))
+          && c.statePermitsRead()) {
         children.put(c.getNameKey(), c.getProject());
       }
     }
     return permissionBackend
-        .user(user)
+        .currentUser()
         .filter(ProjectPermission.ACCESS, children.keySet())
         .stream()
         .sorted()
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 2c8ea2e..882e922 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -29,7 +28,6 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -53,19 +51,14 @@
 
   private final GitRepositoryManager gitManager;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
 
   @Option(name = "--inherited", usage = "include inherited dashboards")
   private boolean inherited;
 
   @Inject
-  ListDashboards(
-      GitRepositoryManager gitManager,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user) {
+  ListDashboards(GitRepositoryManager gitManager, PermissionBackend permissionBackend) {
     this.gitManager = gitManager;
     this.permissionBackend = permissionBackend;
-    this.user = user;
   }
 
   @Override
@@ -95,16 +88,19 @@
   private Collection<ProjectState> tree(ProjectResource rsrc) throws PermissionBackendException {
     Map<Project.NameKey, ProjectState> tree = new LinkedHashMap<>();
     for (ProjectState ps : rsrc.getProjectState().tree()) {
-      tree.put(ps.getNameKey(), ps);
+      if (ps.statePermitsRead()) {
+        tree.put(ps.getNameKey(), ps);
+      }
     }
+
     tree.keySet()
-        .retainAll(permissionBackend.user(user).filter(ProjectPermission.ACCESS, tree.keySet()));
+        .retainAll(permissionBackend.currentUser().filter(ProjectPermission.ACCESS, tree.keySet()));
     return tree.values();
   }
 
   private List<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
       throws ResourceNotFoundException, IOException, PermissionBackendException {
-    PermissionBackend.ForProject perm = permissionBackend.user(user).project(state.getNameKey());
+    PermissionBackend.ForProject perm = permissionBackend.currentUser().project(state.getNameKey());
     try (Repository git = gitManager.openRepository(state.getNameKey());
         RevWalk rw = new RevWalk(git)) {
       List<DashboardInfo> all = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index a1572c6..9a8232e 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
@@ -521,11 +522,28 @@
     if (type == FilterType.PARENT_CANDIDATES) {
       matches = parentsOf(matches);
     }
-    // TODO(dborowitz): Streamified PermissionBackend#filter.
-    return perm.filter(ProjectPermission.ACCESS, matches.collect(toList()))
-        .stream()
-        .sorted()
-        .collect(toList());
+
+    List<Project.NameKey> results = new ArrayList<>();
+    List<Project.NameKey> projectNameKeys = matches.sorted().collect(toList());
+    for (Project.NameKey nameKey : projectNameKeys) {
+      ProjectState state = projectCache.get(nameKey);
+      checkNotNull(state, "Failed to load project %s", nameKey);
+
+      // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+      // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+      // be allowed for other users). Allowing project owners to access here will help them to view
+      // and update the config of hidden projects easily.
+      ProjectPermission permissionToCheck =
+          state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+      try {
+        perm.project(nameKey).check(permissionToCheck);
+        results.add(nameKey);
+      } catch (AuthException e) {
+        // Not added to results.
+      }
+    }
+
+    return results;
   }
 
   private Stream<Project.NameKey> parentsOf(Stream<Project.NameKey> matches) {
@@ -551,13 +569,19 @@
   }
 
   private boolean isParentAccessible(
-      Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState p)
+      Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState state)
       throws PermissionBackendException {
-    Project.NameKey name = p.getNameKey();
+    Project.NameKey name = state.getNameKey();
     Boolean b = checked.get(name);
     if (b == null) {
       try {
-        perm.project(name).check(ProjectPermission.ACCESS);
+        // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+        // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+        // be allowed for other users). Allowing project owners to access here will help them to view
+        // and update the config of hidden projects easily.
+        ProjectPermission permissionToCheck =
+            state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+        perm.project(name).check(permissionToCheck);
         b = true;
       } catch (AuthException denied) {
         b = false;
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index b0148c1..31ec7e1 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CommonConverters;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -35,7 +34,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefFilter;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -58,7 +56,6 @@
 public class ListTags implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
-  private final Provider<CurrentUser> user;
   private final WebLinks links;
 
   @Option(
@@ -108,13 +105,9 @@
 
   @Inject
   public ListTags(
-      GitRepositoryManager repoManager,
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
-      WebLinks webLinks) {
+      GitRepositoryManager repoManager, PermissionBackend permissionBackend, WebLinks webLinks) {
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
-    this.user = user;
     this.links = webLinks;
   }
 
@@ -133,7 +126,8 @@
 
     List<TagInfo> tags = new ArrayList<>();
 
-    PermissionBackend.ForProject perm = permissionBackend.user(user).project(resource.getNameKey());
+    PermissionBackend.ForProject perm =
+        permissionBackend.currentUser().project(resource.getNameKey());
     try (Repository repo = getRepository(resource.getNameKey());
         RevWalk rw = new RevWalk(repo)) {
       Map<String, Ref> all =
@@ -236,7 +230,7 @@
       Project.NameKey project, Repository repo, Map<String, Ref> tags)
       throws PermissionBackendException {
     return permissionBackend
-        .user(user)
+        .currentUser()
         .project(project)
         .filter(
             tags,
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index c14ebab3..3af8424 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -150,8 +150,14 @@
     }
 
     if (checkAccess) {
+      // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+      // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+      // be allowed for other users). Allowing project owners to access here will help them to view
+      // and update the config of hidden projects easily.
+      ProjectPermission permissionToCheck =
+          state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
       try {
-        permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
+        permissionBackend.currentUser().project(nameKey).check(permissionToCheck);
       } catch (AuthException e) {
         return null; // Pretend like not found on access denied.
       }
@@ -161,7 +167,7 @@
       // ACTIVE). Individual views should still check for checkStatePermitsRead() and this should
       // just serve as a safety net in case the individual check is forgotten.
       try {
-        permissionBackend.user(user).project(nameKey).check(ProjectPermission.WRITE_CONFIG);
+        permissionBackend.currentUser().project(nameKey).check(ProjectPermission.WRITE_CONFIG);
       } catch (AuthException e) {
         state.checkStatePermitsRead();
       }
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index 69c4c05..2cf407a 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -111,7 +111,10 @@
   @Override
   public ConfigInfo apply(ProjectResource rsrc, ConfigInput input)
       throws RestApiException, PermissionBackendException {
-    permissionBackend.user(user).project(rsrc.getNameKey()).check(ProjectPermission.WRITE_CONFIG);
+    permissionBackend
+        .currentUser()
+        .project(rsrc.getNameKey())
+        .check(ProjectPermission.WRITE_CONFIG);
     return apply(rsrc.getProjectState(), input);
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index 5a34522..80a07b5 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -97,12 +97,12 @@
         boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
         if (isGlobalCapabilities) {
           if (!checkedAdmin) {
-            permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER);
+            permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
             checkedAdmin = true;
           }
         } else {
           permissionBackend
-              .user(identifiedUser)
+              .currentUser()
               .project(rsrc.getNameKey())
               .ref(section.getName())
               .check(RefPermission.WRITE_CONFIG);
diff --git a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
new file mode 100644
index 0000000..a9482fe
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
@@ -0,0 +1,132 @@
+// 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.rules;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Java implementation of Gerrit's default pre-submit rules behavior: check if the labels have the
+ * correct values, according to the {@link LabelFunction} they are attached to.
+ *
+ * <p>As this behavior is also implemented by the Prolog rules system, we skip it if at least one
+ * project in the hierarchy has a {@code rules.pl} file.
+ */
+@Singleton
+public final class DefaultSubmitRule implements SubmitRule {
+  private static final Logger log = LoggerFactory.getLogger(DefaultSubmitRule.class);
+
+  public static class Module extends FactoryModule {
+    @Override
+    public void configure() {
+      bind(SubmitRule.class)
+          .annotatedWith(Exports.named("DefaultRules"))
+          .to(DefaultSubmitRule.class);
+    }
+  }
+
+  private final ProjectCache projectCache;
+
+  @Inject
+  DefaultSubmitRule(ProjectCache projectCache) {
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions options) {
+    ProjectState projectState = projectCache.get(cd.project());
+
+    // In case at least one project has a rules.pl file, we let Prolog handle it.
+    // The Prolog rules engine will also handle the labels for us.
+    if (projectState == null || projectState.hasPrologRules()) {
+      return Collections.emptyList();
+    }
+
+    SubmitRecord submitRecord = new SubmitRecord();
+    submitRecord.status = SubmitRecord.Status.OK;
+
+    List<LabelType> labelTypes;
+    List<PatchSetApproval> approvals;
+    try {
+      labelTypes = cd.getLabelTypes().getLabelTypes();
+      approvals = cd.currentApprovals();
+    } catch (OrmException e) {
+      log.error("Unable to fetch labels and approvals for change {}", cd.getId(), e);
+
+      submitRecord.errorMessage = "Unable to fetch labels and approvals for the change";
+      submitRecord.status = SubmitRecord.Status.RULE_ERROR;
+      return Collections.singletonList(submitRecord);
+    }
+
+    submitRecord.labels = new ArrayList<>(labelTypes.size());
+
+    for (LabelType t : labelTypes) {
+      LabelFunction labelFunction = t.getFunction();
+      if (labelFunction == null) {
+        log.error(
+            "Unable to find the LabelFunction for label {}, change {}", t.getName(), cd.getId());
+
+        submitRecord.errorMessage = "Unable to find the LabelFunction for label " + t.getName();
+        submitRecord.status = SubmitRecord.Status.RULE_ERROR;
+        return Collections.singletonList(submitRecord);
+      }
+
+      Collection<PatchSetApproval> approvalsForLabel = getApprovalsForLabel(approvals, t);
+      SubmitRecord.Label label = labelFunction.check(t, approvalsForLabel);
+      submitRecord.labels.add(label);
+
+      switch (label.status) {
+        case OK:
+        case MAY:
+          break;
+
+        case NEED:
+        case REJECT:
+        case IMPOSSIBLE:
+          submitRecord.status = SubmitRecord.Status.NOT_READY;
+          break;
+      }
+    }
+
+    return Collections.singletonList(submitRecord);
+  }
+
+  private static List<PatchSetApproval> getApprovalsForLabel(
+      List<PatchSetApproval> approvals, LabelType t) {
+    return approvals
+        .stream()
+        .filter(input -> input.getLabel().equals(t.getLabelId().get()))
+        .collect(toImmutableList());
+  }
+}
diff --git a/java/com/google/gerrit/server/rules/PrologRule.java b/java/com/google/gerrit/server/rules/PrologRule.java
index 44f18c5f..deddc36 100644
--- a/java/com/google/gerrit/server/rules/PrologRule.java
+++ b/java/com/google/gerrit/server/rules/PrologRule.java
@@ -16,25 +16,34 @@
 
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.rules.PrologRuleEvaluator.Factory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collection;
+import java.util.Collections;
 
 @Singleton
 public class PrologRule implements SubmitRule {
-
-  private final Factory factory;
+  private final PrologRuleEvaluator.Factory factory;
+  private final ProjectCache projectCache;
 
   @Inject
-  private PrologRule(PrologRuleEvaluator.Factory factory) {
+  private PrologRule(PrologRuleEvaluator.Factory factory, ProjectCache projectCache) {
     this.factory = factory;
+    this.projectCache = projectCache;
   }
 
   @Override
   public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions opts) {
+    ProjectState projectState = projectCache.get(cd.project());
+    // We only want to run the prolog engine if we have at least one rules.pl file to use.
+    if (projectState == null || !projectState.hasPrologRules()) {
+      return Collections.emptyList();
+    }
+
     return getEvaluator(cd, opts).evaluate();
   }
 
diff --git a/java/com/google/gerrit/server/schema/GroupBundle.java b/java/com/google/gerrit/server/schema/GroupBundle.java
index 302ea55..3c1c409 100644
--- a/java/com/google/gerrit/server/schema/GroupBundle.java
+++ b/java/com/google/gerrit/server/schema/GroupBundle.java
@@ -195,7 +195,7 @@
         Timestamp createdOn = rs.getTimestamp(3);
         String description = rs.getString(4);
         AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID(rs.getString(5));
-        boolean visibleToAll = rs.getBoolean(6);
+        boolean visibleToAll = "Y".equals(rs.getString(6));
 
         AccountGroup group = new AccountGroup(groupName, groupId, groupUuid, createdOn);
         group.setDescription(description);
diff --git a/java/com/google/gerrit/server/schema/NoGroupsReviewDbWrapper.java b/java/com/google/gerrit/server/schema/NoGroupsReviewDbWrapper.java
deleted file mode 100644
index 33c4d77..0000000
--- a/java/com/google/gerrit/server/schema/NoGroupsReviewDbWrapper.java
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright (C) 2017 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.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.AccountGroupAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupMemberAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-
-/**
- * Wrapper for ReviewDb that never calls the underlying groups tables.
- *
- * <p>See {@link NotesMigrationSchemaFactory} for discussion.
- */
-public class NoGroupsReviewDbWrapper extends ReviewDbWrapper {
-  private static <T> ResultSet<T> empty() {
-    return new ListResultSet<>(ImmutableList.of());
-  }
-
-  private final AccountGroupAccess groups;
-  private final AccountGroupNameAccess groupNames;
-  private final AccountGroupMemberAccess members;
-  private final AccountGroupMemberAuditAccess memberAudits;
-  private final AccountGroupByIdAccess byIds;
-  private final AccountGroupByIdAudAccess byIdAudits;
-
-  protected NoGroupsReviewDbWrapper(ReviewDb db) {
-    super(db);
-    this.groups = new Groups(this, delegate);
-    this.groupNames = new GroupNames(this, delegate);
-    this.members = new Members(this, delegate);
-    this.memberAudits = new MemberAudits(this, delegate);
-    this.byIds = new ByIds(this, delegate);
-    this.byIdAudits = new ByIdAudits(this, delegate);
-  }
-
-  @Override
-  public AccountGroupAccess accountGroups() {
-    return groups;
-  }
-
-  @Override
-  public AccountGroupNameAccess accountGroupNames() {
-    return groupNames;
-  }
-
-  @Override
-  public AccountGroupMemberAccess accountGroupMembers() {
-    return members;
-  }
-
-  @Override
-  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
-    return memberAudits;
-  }
-
-  @Override
-  public AccountGroupByIdAudAccess accountGroupByIdAud() {
-    return byIdAudits;
-  }
-
-  @Override
-  public AccountGroupByIdAccess accountGroupById() {
-    return byIds;
-  }
-
-  private static class Groups extends AbstractDisabledAccess<AccountGroup, AccountGroup.Id>
-      implements AccountGroupAccess {
-    private Groups(ReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.accountGroups());
-    }
-
-    @Override
-    public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<AccountGroup> all() throws OrmException {
-      return empty();
-    }
-  }
-
-  private static class GroupNames
-      extends AbstractDisabledAccess<AccountGroupName, AccountGroup.NameKey>
-      implements AccountGroupNameAccess {
-    private GroupNames(ReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.accountGroupNames());
-    }
-
-    @Override
-    public ResultSet<AccountGroupName> all() throws OrmException {
-      return empty();
-    }
-  }
-
-  private static class Members
-      extends AbstractDisabledAccess<AccountGroupMember, AccountGroupMember.Key>
-      implements AccountGroupMemberAccess {
-    private Members(ReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.accountGroupMembers());
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> byAccount(Account.Id id) throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) throws OrmException {
-      return empty();
-    }
-  }
-
-  private static class MemberAudits
-      extends AbstractDisabledAccess<AccountGroupMemberAudit, AccountGroupMemberAudit.Key>
-      implements AccountGroupMemberAuditAccess {
-    private MemberAudits(ReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.accountGroupMembersAudit());
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> byGroupAccount(
-        AccountGroup.Id groupId, com.google.gerrit.reviewdb.client.Account.Id accountId)
-        throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) throws OrmException {
-      return empty();
-    }
-  }
-
-  private static class ByIds extends AbstractDisabledAccess<AccountGroupById, AccountGroupById.Key>
-      implements AccountGroupByIdAccess {
-    private ByIds(ReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.accountGroupById());
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<AccountGroupById> all() throws OrmException {
-      return empty();
-    }
-  }
-
-  private static class ByIdAudits
-      extends AbstractDisabledAccess<AccountGroupByIdAud, AccountGroupByIdAud.Key>
-      implements AccountGroupByIdAudAccess {
-    private ByIdAudits(ReviewDbWrapper wrapper, ReviewDb db) {
-      super(wrapper, db.accountGroupByIdAud());
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> byGroupInclude(
-        AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) throws OrmException {
-      return empty();
-    }
-
-    @Override
-    public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) throws OrmException {
-      return empty();
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java b/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
index 9bc8b61..0d95610 100644
--- a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
+++ b/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
@@ -15,9 +15,7 @@
 package com.google.gerrit.server.schema;
 
 import com.google.gerrit.reviewdb.server.DisallowReadFromChangesReviewDbWrapper;
-import com.google.gerrit.reviewdb.server.DisallowReadFromGroupsReviewDbWrapper;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -28,16 +26,12 @@
 public class NotesMigrationSchemaFactory implements SchemaFactory<ReviewDb> {
   private final SchemaFactory<ReviewDb> delegate;
   private final NotesMigration migration;
-  private final GroupsMigration groupsMigration;
 
   @Inject
   NotesMigrationSchemaFactory(
-      @ReviewDbFactory SchemaFactory<ReviewDb> delegate,
-      NotesMigration migration,
-      GroupsMigration groupsMigration) {
+      @ReviewDbFactory SchemaFactory<ReviewDb> delegate, NotesMigration migration) {
     this.delegate = delegate;
     this.migration = migration;
-    this.groupsMigration = groupsMigration;
   }
 
   @Override
@@ -73,23 +67,12 @@
       db = new NoChangesReviewDbWrapper(db);
     }
 
-    if (groupsMigration.readFromNoteDb() && groupsMigration.disableGroupReviewDb()) {
-      // Disable writes to group tables in ReviewDb (ReviewDb access for groups are No-Ops).
-      db = new NoGroupsReviewDbWrapper(db);
-    }
-
     // Second create the wrappers which can be removed by ReviewDbUtil#unwrapDb(ReviewDb).
     if (migration.readChanges()) {
       // If reading changes from NoteDb is configured, changes should not be read from ReviewDb.
       // Make sure that any attempt to read a change from ReviewDb anyway fails with an exception.
       db = new DisallowReadFromChangesReviewDbWrapper(db);
     }
-
-    if (groupsMigration.readFromNoteDb()) {
-      // If reading groups from NoteDb is configured, groups should not be read from ReviewDb.
-      // Make sure that any attempt to read a group from ReviewDb anyway fails with an exception.
-      db = new DisallowReadFromGroupsReviewDbWrapper(db);
-    }
     return db;
   }
 }
diff --git a/java/com/google/gerrit/server/schema/SchemaCreator.java b/java/com/google/gerrit/server/schema/SchemaCreator.java
index ba68b2c..88b3e28 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,13 +14,11 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -40,12 +38,10 @@
 import com.google.gerrit.server.group.db.AuditLogFormatter;
 import com.google.gerrit.server.group.db.GroupConfig;
 import com.google.gerrit.server.group.db.GroupNameNotes;
-import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.group.db.InternalGroupCreation;
 import com.google.gerrit.server.group.db.InternalGroupUpdate;
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.update.RefUpdateUtil;
 import com.google.gwtorm.jdbc.JdbcExecutor;
@@ -73,7 +69,6 @@
   private final PersonIdent serverUser;
   private final DataSourceType dataSourceType;
   private final GroupIndexCollection indexCollection;
-  private final GroupsMigration groupsMigration;
   private final String serverId;
 
   private final Config config;
@@ -91,7 +86,6 @@
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst,
       GroupIndexCollection ic,
-      GroupsMigration gm,
       @GerritServerId String serverId,
       @GerritServerConfig Config config,
       MetricMaker metricMaker,
@@ -106,7 +100,6 @@
         au,
         dst,
         ic,
-        gm,
         serverId,
         config,
         metricMaker,
@@ -123,7 +116,6 @@
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst,
       GroupIndexCollection ic,
-      GroupsMigration gm,
       String serverId,
       Config config,
       MetricMaker metricMaker,
@@ -137,7 +129,6 @@
     serverUser = au;
     dataSourceType = dst;
     indexCollection = ic;
-    groupsMigration = gm;
     this.serverId = serverId;
 
     this.config = config;
@@ -176,25 +167,24 @@
             allUsersName,
             metricMaker);
     try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-      createAdminsGroup(db, seqs, allUsersRepo, admins);
-      createBatchUsersGroup(db, seqs, allUsersRepo, batchUsers, admins.getUUID());
+      createAdminsGroup(seqs, allUsersRepo, admins);
+      createBatchUsersGroup(seqs, allUsersRepo, batchUsers, admins.getUUID());
     }
 
     dataSourceType.getIndexScript().run(db);
   }
 
   private void createAdminsGroup(
-      ReviewDb db, Sequences seqs, Repository allUsersRepo, GroupReference groupReference)
+      Sequences seqs, Repository allUsersRepo, GroupReference groupReference)
       throws OrmException, IOException, ConfigInvalidException {
     InternalGroupCreation groupCreation = getGroupCreation(seqs, groupReference);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder().setDescription("Gerrit Site Administrators").build();
 
-    createGroup(db, allUsersRepo, groupCreation, groupUpdate);
+    createGroup(allUsersRepo, groupCreation, groupUpdate);
   }
 
   private void createBatchUsersGroup(
-      ReviewDb db,
       Sequences seqs,
       Repository allUsersRepo,
       GroupReference groupReference,
@@ -207,35 +197,16 @@
             .setOwnerGroupUUID(adminsGroupUuid)
             .build();
 
-    createGroup(db, allUsersRepo, groupCreation, groupUpdate);
+    createGroup(allUsersRepo, groupCreation, groupUpdate);
   }
 
   private void createGroup(
-      ReviewDb db,
-      Repository allUsersRepo,
-      InternalGroupCreation groupCreation,
-      InternalGroupUpdate groupUpdate)
+      Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
       throws OrmException, ConfigInvalidException, IOException {
-    InternalGroup groupInReviewDb = createGroupInReviewDb(db, groupCreation, groupUpdate);
-
-    if (!groupsMigration.writeToNoteDb()) {
-      index(groupInReviewDb);
-      return;
-    }
-
     InternalGroup createdGroup = createGroupInNoteDb(allUsersRepo, groupCreation, groupUpdate);
     index(createdGroup);
   }
 
-  private static InternalGroup createGroupInReviewDb(
-      ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
-      throws OrmException {
-    AccountGroup group = GroupsUpdate.createAccountGroup(groupCreation, groupUpdate);
-    db.accountGroupNames().insert(ImmutableList.of(new AccountGroupName(group)));
-    db.accountGroups().insert(ImmutableList.of(group));
-    return InternalGroup.create(group, ImmutableSet.of(), ImmutableSet.of());
-  }
-
   private InternalGroup createGroupInNoteDb(
       Repository allUsersRepo, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
       throws ConfigInvalidException, IOException, OrmDuplicateKeyException {
diff --git a/java/com/google/gerrit/server/schema/SchemaVersion.java b/java/com/google/gerrit/server/schema/SchemaVersion.java
index 4b8c13f..e8a59d0 100644
--- a/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_166> C = Schema_166.class;
+  public static final Class<Schema_168> C = Schema_168.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/java/com/google/gerrit/server/schema/Schema_167.java b/java/com/google/gerrit/server/schema/Schema_167.java
new file mode 100644
index 0000000..5e93b2c
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_167.java
@@ -0,0 +1,287 @@
+// 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 static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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;
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Migrate groups from ReviewDb to NoteDb. */
+public class Schema_167 extends SchemaVersion {
+  private static final Logger log = LoggerFactory.getLogger(Schema_167.class);
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final Config gerritConfig;
+  private final SitePaths sitePaths;
+  private final PersonIdent serverIdent;
+  private final SystemGroupBackend systemGroupBackend;
+
+  @Inject
+  protected Schema_167(
+      Provider<Schema_166> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      @GerritServerConfig Config gerritConfig,
+      SitePaths sitePaths,
+      @GerritPersonIdent PersonIdent serverIdent,
+      SystemGroupBackend systemGroupBackend) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.gerritConfig = gerritConfig;
+    this.sitePaths = sitePaths;
+    this.serverIdent = serverIdent;
+    this.systemGroupBackend = systemGroupBackend;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    if (gerritConfig.getBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false)) {
+      // Groups in ReviewDb have already been disabled, nothing to do.
+      return;
+    }
+
+    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+      List<GroupReference> allGroupReferences = readGroupReferencesFromReviewDb(db);
+
+      BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
+      writeAllGroupNamesToNoteDb(allUsersRepo, allGroupReferences, batchRefUpdate);
+
+      GroupRebuilder groupRebuilder = createGroupRebuilder(db, allUsersRepo);
+      for (GroupReference groupReference : allGroupReferences) {
+        migrateOneGroupToNoteDb(
+            db, allUsersRepo, groupRebuilder, groupReference.getUUID(), batchRefUpdate);
+      }
+
+      RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new OrmException(
+          String.format("Failed to migrate groups to NoteDb for %s", allUsersName.get()), e);
+    }
+  }
+
+  private List<GroupReference> readGroupReferencesFromReviewDb(ReviewDb db) throws SQLException {
+    try (Statement stmt = ReviewDbWrapper.unwrapJbdcSchema(db).getConnection().createStatement();
+        ResultSet rs = stmt.executeQuery("SELECT group_uuid, name FROM account_groups")) {
+      List<GroupReference> allGroupReferences = new ArrayList<>();
+      while (rs.next()) {
+        AccountGroup.UUID groupUuid = new AccountGroup.UUID(rs.getString(1));
+        String groupName = rs.getString(2);
+        allGroupReferences.add(new GroupReference(groupUuid, groupName));
+      }
+      return allGroupReferences;
+    }
+  }
+
+  private void writeAllGroupNamesToNoteDb(
+      Repository allUsersRepo,
+      List<GroupReference> allGroupReferences,
+      BatchRefUpdate batchRefUpdate)
+      throws IOException {
+    try (ObjectInserter inserter = allUsersRepo.newObjectInserter()) {
+      GroupNameNotes.updateAllGroups(
+          allUsersRepo, inserter, batchRefUpdate, allGroupReferences, serverIdent);
+      inserter.flush();
+    }
+  }
+
+  private GroupRebuilder createGroupRebuilder(ReviewDb db, Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    AuditLogFormatter auditLogFormatter =
+        createAuditLogFormatter(db, allUsersRepo, gerritConfig, sitePaths);
+    return new GroupRebuilder(serverIdent, allUsersName, auditLogFormatter);
+  }
+
+  private AuditLogFormatter createAuditLogFormatter(
+      ReviewDb db, Repository allUsersRepo, Config gerritConfig, SitePaths sitePaths)
+      throws IOException, ConfigInvalidException {
+    String serverId = new GerritServerIdProvider(gerritConfig, sitePaths).get();
+    SimpleInMemoryAccountCache accountCache = new SimpleInMemoryAccountCache(allUsersRepo);
+    SimpleInMemoryGroupCache groupCache = new SimpleInMemoryGroupCache(db);
+    return AuditLogFormatter.create(
+        accountCache::get,
+        uuid -> {
+          if (systemGroupBackend.handles(uuid)) {
+            return Optional.ofNullable(systemGroupBackend.get(uuid));
+          }
+          return groupCache.get(uuid);
+        },
+        serverId);
+  }
+
+  private static void migrateOneGroupToNoteDb(
+      ReviewDb db,
+      Repository allUsersRepo,
+      GroupRebuilder rebuilder,
+      AccountGroup.UUID uuid,
+      BatchRefUpdate batchRefUpdate)
+      throws ConfigInvalidException, IOException, OrmException {
+    GroupBundle reviewDbBundle = GroupBundle.Factory.fromReviewDb(db, uuid);
+    RefUpdateUtil.deleteChecked(allUsersRepo, RefNames.refsGroups(uuid));
+    rebuilder.rebuild(allUsersRepo, reviewDbBundle, batchRefUpdate);
+  }
+
+  // The regular account cache isn't available during init. -> Use a simple replacement which tries
+  // to load every account only once from disk.
+  private static class SimpleInMemoryAccountCache {
+    private final Repository allUsersRepo;
+    private Map<Account.Id, Optional<Account>> accounts = new HashMap<>();
+
+    public SimpleInMemoryAccountCache(Repository allUsersRepo) {
+      this.allUsersRepo = allUsersRepo;
+    }
+
+    public Optional<Account> get(Account.Id accountId) {
+      accounts.computeIfAbsent(accountId, this::load);
+      return accounts.get(accountId);
+    }
+
+    private Optional<Account> load(Account.Id accountId) {
+      try {
+        AccountConfig accountConfig = new AccountConfig(accountId, allUsersRepo).load();
+        return accountConfig.getLoadedAccount();
+      } catch (IOException | ConfigInvalidException ignored) {
+        log.warn(
+            "Failed to load account {}."
+                + " Cannot get account name for group audit log commit messages.",
+            accountId.get(),
+            ignored);
+        return Optional.empty();
+      }
+    }
+  }
+
+  // The regular GroupBackends (especially external GroupBackends) and our internal group cache
+  // aren't available during init. -> Use a simple replacement which tries to look up only internal
+  // groups and which loads every internal group only once from disc. (There's no way we can look up
+  // external groups during init. As we need those groups only for cosmetic aspects in
+  // AuditLogFormatter, it's safe to exclude them.)
+  private static class SimpleInMemoryGroupCache {
+    private final ReviewDb db;
+    private Map<AccountGroup.UUID, Optional<GroupDescription.Basic>> groups = new HashMap<>();
+
+    public SimpleInMemoryGroupCache(ReviewDb db) {
+      this.db = db;
+    }
+
+    public Optional<GroupDescription.Basic> get(AccountGroup.UUID groupUuid) {
+      groups.computeIfAbsent(groupUuid, this::load);
+      return groups.get(groupUuid);
+    }
+
+    private Optional<GroupDescription.Basic> load(AccountGroup.UUID groupUuid) {
+      if (!AccountGroup.isInternalGroup(groupUuid)) {
+        return Optional.empty();
+      }
+
+      List<GroupDescription.Basic> groupDescriptions = getGroupDescriptions(groupUuid);
+      if (groupDescriptions.size() == 1) {
+        return Optional.of(Iterables.getOnlyElement(groupDescriptions));
+      }
+      return Optional.empty();
+    }
+
+    private List<GroupDescription.Basic> getGroupDescriptions(AccountGroup.UUID groupUuid) {
+      try (Statement stmt = ReviewDbWrapper.unwrapJbdcSchema(db).getConnection().createStatement();
+          ResultSet rs =
+              stmt.executeQuery(
+                  "SELECT name FROM account_groups where group_uuid = '" + groupUuid + "'")) {
+        List<GroupDescription.Basic> groupDescriptions = new ArrayList<>();
+        while (rs.next()) {
+          String groupName = rs.getString(1);
+          groupDescriptions.add(toGroupDescription(groupUuid, groupName));
+        }
+        return groupDescriptions;
+      } catch (SQLException ignored) {
+        log.warn(
+            "Failed to load group {}."
+                + " Cannot get group name for group audit log commit messages.",
+            groupUuid.get(),
+            ignored);
+        return ImmutableList.of();
+      }
+    }
+
+    private static GroupDescription.Basic toGroupDescription(
+        AccountGroup.UUID groupUuid, String groupName) {
+      return new GroupDescription.Basic() {
+        @Override
+        public AccountGroup.UUID getGroupUUID() {
+          return groupUuid;
+        }
+
+        @Override
+        public String getName() {
+          return groupName;
+        }
+
+        @Nullable
+        @Override
+        public String getEmailAddress() {
+          return null;
+        }
+
+        @Nullable
+        @Override
+        public String getUrl() {
+          return null;
+        }
+      };
+    }
+  }
+}
diff --git a/java/com/google/gerrit/common/errors/PermissionDeniedException.java b/java/com/google/gerrit/server/schema/Schema_168.java
similarity index 63%
rename from java/com/google/gerrit/common/errors/PermissionDeniedException.java
rename to java/com/google/gerrit/server/schema/Schema_168.java
index 0faf498..3ea8468 100644
--- a/java/com/google/gerrit/common/errors/PermissionDeniedException.java
+++ b/java/com/google/gerrit/server/schema/Schema_168.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.errors;
+package com.google.gerrit.server.schema;
 
-/** Indicates the user cannot perform this task. */
-public class PermissionDeniedException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-  public PermissionDeniedException(String msg) {
-    super(msg);
+/** Drop group tables. */
+public class Schema_168 extends SchemaVersion {
+  @Inject
+  Schema_168(Provider<Schema_167> prior) {
+    super(prior);
   }
 }
diff --git a/java/com/google/gerrit/sshd/commands/KillCommand.java b/java/com/google/gerrit/sshd/commands/KillCommand.java
index a7e751a..ef12f5f 100644
--- a/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.TaskResource;
@@ -51,7 +52,10 @@
       try {
         TaskResource taskRsrc = tasksCollection.parse(cfgRsrc, IdString.fromDecoded(id));
         deleteTask.apply(taskRsrc, null);
-      } catch (AuthException | ResourceNotFoundException | PermissionBackendException e) {
+      } catch (AuthException
+          | ResourceNotFoundException
+          | ResourceConflictException
+          | PermissionBackendException e) {
         stderr.print("kill: " + id + ": No such task\n");
       }
     }
diff --git a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 9e334e6..cd9fbda 100644
--- a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.restapi.group.PutName;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -53,7 +52,7 @@
       NameInput input = new NameInput();
       input.name = newGroupName;
       putName.apply(rsrc, input);
-    } catch (RestApiException | OrmException | IOException | ConfigInvalidException e) {
+    } catch (RestApiException | IOException | ConfigInvalidException e) {
       throw die(e);
     }
   }
diff --git a/java/com/google/gerrit/testing/DisabledReviewDb.java b/java/com/google/gerrit/testing/DisabledReviewDb.java
index 998b893..d902e11 100644
--- a/java/com/google/gerrit/testing/DisabledReviewDb.java
+++ b/java/com/google/gerrit/testing/DisabledReviewDb.java
@@ -14,12 +14,6 @@
 
 package com.google.gerrit.testing;
 
-import com.google.gerrit.reviewdb.server.AccountGroupAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupMemberAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
 import com.google.gerrit.reviewdb.server.ChangeAccess;
 import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
@@ -82,26 +76,6 @@
   }
 
   @Override
-  public AccountGroupAccess accountGroups() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupNameAccess accountGroupNames() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupMemberAccess accountGroupMembers() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
-    throw new Disabled();
-  }
-
-  @Override
   public ChangeAccess changes() {
     throw new Disabled();
   }
@@ -127,16 +101,6 @@
   }
 
   @Override
-  public AccountGroupByIdAccess accountGroupById() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupByIdAudAccess accountGroupByIdAud() {
-    throw new Disabled();
-  }
-
-  @Override
   public int nextAccountId() {
     throw new Disabled();
   }
diff --git a/java/com/google/gerrit/testing/GroupNoteDbMode.java b/java/com/google/gerrit/testing/GroupNoteDbMode.java
deleted file mode 100644
index 86e92b8..0000000
--- a/java/com/google/gerrit/testing/GroupNoteDbMode.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2017 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 static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.base.Enums;
-import com.google.common.base.Strings;
-import com.google.gerrit.server.notedb.GroupsMigration;
-
-public enum GroupNoteDbMode {
-  /** NoteDb is disabled, groups are only in ReviewDb */
-  OFF(new GroupsMigration(false, false, false)),
-
-  /** Writing new groups to NoteDb is enabled. */
-  WRITE(new GroupsMigration(true, false, false)),
-
-  /**
-   * Reading/writing groups from/to NoteDb is enabled. Trying to read groups from ReviewDb throws an
-   * exception.
-   */
-  READ_WRITE(new GroupsMigration(true, true, false)),
-
-  /**
-   * All group tables in ReviewDb are entirely disabled. Trying to read groups from ReviewDb throws
-   * an exception. Reading groups through an unwrapped ReviewDb instance writing groups to ReviewDb
-   * is a No-Op.
-   */
-  ON(new GroupsMigration(true, true, true));
-
-  private static final String ENV_VAR = "GERRIT_NOTEDB_GROUPS";
-  private static final String SYS_PROP = "gerrit.notedb.groups";
-
-  public static GroupNoteDbMode get() {
-    String value = System.getenv(ENV_VAR);
-    if (Strings.isNullOrEmpty(value)) {
-      value = System.getProperty(SYS_PROP);
-    }
-    if (Strings.isNullOrEmpty(value)) {
-      return OFF;
-    }
-    value = value.toUpperCase().replace("-", "_");
-    GroupNoteDbMode mode = Enums.getIfPresent(GroupNoteDbMode.class, value).orNull();
-    if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
-      checkArgument(
-          mode != null, "Invalid value for env variable %s: %s", ENV_VAR, System.getenv(ENV_VAR));
-    } else {
-      checkArgument(
-          mode != null,
-          "Invalid value for system property %s: %s",
-          SYS_PROP,
-          System.getProperty(SYS_PROP));
-    }
-    return mode;
-  }
-
-  private final GroupsMigration groupsMigration;
-
-  private GroupNoteDbMode(GroupsMigration groupsMigration) {
-    this.groupsMigration = groupsMigration;
-  }
-
-  public GroupsMigration getGroupsMigration() {
-    return groupsMigration;
-  }
-}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index ea4b174..af7c72c 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.base.Strings;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.extensions.client.AuthType;
@@ -46,6 +47,7 @@
 import com.google.gerrit.server.config.GerritOptions;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.config.GerritServerIdProvider;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.config.TrackingFootersProvider;
@@ -183,7 +185,7 @@
     bind(String.class)
         .annotatedWith(AnonymousCowardName.class)
         .toProvider(AnonymousCowardNameProvider.class);
-    bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
+
     bind(AllProjectsName.class).toProvider(AllProjectsNameProvider.class);
     bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
     bind(GitRepositoryManager.class).to(InMemoryRepositoryManager.class);
@@ -269,6 +271,19 @@
 
   @Provides
   @Singleton
+  @GerritServerId
+  public String createServerId() {
+    String serverId =
+        cfg.getString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY);
+    if (!Strings.isNullOrEmpty(serverId)) {
+      return serverId;
+    }
+
+    return "gerrit";
+  }
+
+  @Provides
+  @Singleton
   InMemoryDatabase getInMemoryDatabase(SchemaCreator schemaCreator) throws OrmException {
     return new InMemoryDatabase(schemaCreator);
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
index 389efb4..7af057f 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
@@ -15,11 +15,6 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
-import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.READ;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
 import static com.google.gerrit.truth.ListSubject.assertThat;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
@@ -29,7 +24,6 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
@@ -48,27 +42,15 @@
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.Test;
 
 public class GroupIndexerIT {
-  private static Config createPureNoteDbConfig() {
-    Config config = new Config();
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, true);
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, true);
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, true);
-    return config;
-  }
-
-  @Rule
-  public InMemoryTestEnvironment testEnvironment =
-      new InMemoryTestEnvironment(GroupIndexerIT::createPureNoteDbConfig);
+  @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
 
   @Inject private GroupIndexer groupIndexer;
   @Inject private GerritApi gApi;
   @Inject private GroupCache groupCache;
-  @Inject private ReviewDb db;
   @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
   @Inject private Provider<InternalGroupQuery> groupQueryProvider;
 
@@ -176,7 +158,7 @@
   private void updateGroupWithoutCacheOrIndex(
       AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
       throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
-    groupsUpdate.updateGroupInDb(db, groupUuid, groupUpdate);
+    groupsUpdate.updateGroupInDb(groupUuid, groupUpdate);
   }
 
   private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index 4e9c37b..87a566e 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
@@ -15,9 +15,7 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -32,11 +30,7 @@
 import com.google.gerrit.server.group.db.GroupConfig;
 import com.google.gerrit.server.group.db.GroupNameNotes;
 import com.google.gerrit.server.group.db.testing.GroupTestUtil;
-import com.google.gerrit.server.notedb.GroupsMigration;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.testing.ConfigSuite;
 import java.util.List;
-import javax.inject.Inject;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.RefRename;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -53,17 +47,6 @@
 @Sandboxed
 @NoHttpd
 public class GroupsConsistencyIT extends AbstractDaemonTest {
-
-  @ConfigSuite.Config
-  public static Config noteDbConfig() {
-    Config config = new Config();
-    config.setBoolean(NotesMigration.SECTION_NOTE_DB, GROUPS.key(), NotesMigration.WRITE, true);
-    config.setBoolean(NotesMigration.SECTION_NOTE_DB, GROUPS.key(), NotesMigration.READ, true);
-    return config;
-  }
-
-  @Inject private GroupsMigration groupsMigration;
-
   private GroupInfo gAdmin;
   private GroupInfo g1;
   private GroupInfo g2;
@@ -72,7 +55,6 @@
 
   @Before
   public void basicSetup() throws Exception {
-    assume().that(groupsInNoteDb()).isTrue();
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
     String name1 = createGroup("g1");
@@ -87,10 +69,6 @@
     this.gAdmin = gApi.groups().id("Administrators").detail();
   }
 
-  private boolean groupsInNoteDb() {
-    return groupsMigration.writeToNoteDb();
-  }
-
   @Test
   public void allGood() throws Exception {
     assertThat(check()).isEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 0e5da12..c176acd 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -16,18 +16,12 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
-import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.READ;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.stream.Collectors.toList;
@@ -85,7 +79,6 @@
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.index.group.StalenessChecker;
 import com.google.gerrit.server.util.MagicBranch;
-import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -119,21 +112,6 @@
 
 @NoHttpd
 public class GroupsIT extends AbstractDaemonTest {
-  @ConfigSuite.Config
-  public static Config noteDbConfig() {
-    Config config = new Config();
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, true);
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, true);
-    return config;
-  }
-
-  @ConfigSuite.Config
-  public static Config disableReviewDb() {
-    Config config = noteDbConfig();
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, true);
-    return config;
-  }
-
   @Inject private Groups groups;
   @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
   @Inject private GroupIncludeCache groupIncludeCache;
@@ -732,7 +710,7 @@
   @Test
   public void listAllGroups() throws Exception {
     List<String> expectedGroups =
-        groups.getAllGroupReferences(db).map(GroupReference::getName).sorted().collect(toList());
+        groups.getAllGroupReferences().map(GroupReference::getName).sorted().collect(toList());
     assertThat(expectedGroups.size()).isAtLeast(2);
     assertThat(gApi.groups().list().getAsMap().keySet())
         .containsExactlyElementsIn(expectedGroups)
@@ -908,8 +886,6 @@
   @Sandboxed
   @IgnoreGroupInconsistencies
   public void getAuditLogAfterDeletingASubgroup() throws Exception {
-    assume().that(readGroupsFromNoteDb()).isTrue();
-
     GroupInfo parentGroup = gApi.groups().create(name("parent-group")).get();
 
     // Creates a subgroup and adds it to "parent-group" as a subgroup.
@@ -973,7 +949,6 @@
 
   @Test
   public void pushToGroupBranchIsRejectedForAllUsersRepo() throws Exception {
-    assume().that(groupsInNoteDb()).isTrue(); // branch only exists when groups are in NoteDb
     assertPushToGroupBranch(
         allUsers, RefNames.refsGroups(adminGroupUuid()), "group update not allowed");
   }
@@ -989,7 +964,6 @@
 
   @Test
   public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception {
-    assume().that(groupsInNoteDb()).isTrue(); // branch only exists when groups are in NoteDb
     // refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, "group update not allowed");
@@ -1114,8 +1088,6 @@
   @Test
   @IgnoreGroupInconsistencies
   public void cannotCreateGroupNamesBranch() throws Exception {
-    assume().that(groupsInNoteDb()).isTrue();
-
     // Use ProjectResetter to restore the group names ref
     try (ProjectResetter resetter =
         projectResetter
@@ -1155,7 +1127,6 @@
 
   @Test
   public void cannotDeleteGroupBranch() throws Exception {
-    assume().that(groupsInNoteDb()).isTrue();
     testCannotDeleteGroupBranch(RefNames.REFS_GROUPS + "*", RefNames.refsGroups(adminGroupUuid()));
   }
 
@@ -1168,8 +1139,6 @@
 
   @Test
   public void cannotDeleteGroupNamesBranch() throws Exception {
-    assume().that(groupsInNoteDb()).isTrue();
-
     // refs/meta/group-names is only visible with ACCESS_DATABASE
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
@@ -1199,8 +1168,6 @@
   @Test
   @IgnoreGroupInconsistencies
   public void stalenessChecker() throws Exception {
-    assume().that(readGroupsFromNoteDb()).isTrue();
-
     // Newly created group is not stale
     GroupInfo groupInfo = gApi.groups().create(name("foo")).get();
     AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupInfo.id);
@@ -1246,8 +1213,6 @@
   @Test
   @Sandboxed
   public void groupsOfUserCanBeListedInSlaveMode() throws Exception {
-    assume().that(readGroupsFromNoteDb()).isTrue();
-
     GroupInput groupInput = new GroupInput();
     groupInput.name = name("contributors");
     groupInput.members = ImmutableList.of(user.username);
@@ -1267,11 +1232,8 @@
   @GerritConfig(name = "index.autoReindexIfStale", value = "false")
   @IgnoreGroupInconsistencies
   public void reindexGroupsInSlaveMode() throws Exception {
-    assume().that(readGroupsFromNoteDb()).isTrue();
-    assume().that(cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false)).isTrue();
-
     List<AccountGroup.UUID> expectedGroups =
-        groups.getAllGroupReferences(db).map(GroupReference::getUUID).collect(toList());
+        groups.getAllGroupReferences().map(GroupReference::getUUID).collect(toList());
     assertThat(expectedGroups.size()).isAtLeast(2);
 
     // Restart the server as slave, on startup of the slave all groups are indexed.
@@ -1303,7 +1265,7 @@
       // Update a group without updating the cache or index,
       // then run the reindexer -> only the updated group is reindexed.
       groupsUpdate.updateGroupInDb(
-          db, groupUuid, InternalGroupUpdate.builder().setDescription("bar").build());
+          groupUuid, InternalGroupUpdate.builder().setDescription("bar").build());
       slaveGroupIndexer.run();
       groupIndexedCounter.assertReindexOf(groupUuid);
 
@@ -1328,10 +1290,8 @@
   @GerritConfig(name = "index.autoReindexIfStale", value = "false")
   @IgnoreGroupInconsistencies
   public void disabledReindexGroupsOnStartupSlaveMode() throws Exception {
-    assume().that(readGroupsFromNoteDb()).isTrue();
-
     List<AccountGroup.UUID> expectedGroups =
-        groups.getAllGroupReferences(db).map(GroupReference::getUUID).collect(toList());
+        groups.getAllGroupReferences().map(GroupReference::getUUID).collect(toList());
     assertThat(expectedGroups.size()).isAtLeast(2);
 
     restartAsSlave();
@@ -1360,8 +1320,6 @@
 
   private void pushToGroupBranchForReviewAndSubmit(
       Project.NameKey project, String groupRef, String expectedError) throws Exception {
-    assume().that(groupsInNoteDb()).isTrue(); // branch only exists when groups are in NoteDb
-
     grantLabel(
         "Code-Review", -2, 2, project, RefNames.REFS_GROUPS + "*", false, REGISTERED_USERS, false);
     grant(project, RefNames.REFS_GROUPS + "*", Permission.SUBMIT, false, REGISTERED_USERS);
@@ -1484,14 +1442,6 @@
     }
   }
 
-  private boolean groupsInNoteDb() {
-    return cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, false);
-  }
-
-  private boolean readGroupsFromNoteDb() {
-    return groupsInNoteDb() && cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false);
-  }
-
   @Target({METHOD})
   @Retention(RUNTIME)
   private @interface IgnoreGroupInconsistencies {}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
index e7ddcae..44be241 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
@@ -15,17 +15,11 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth8.assertThat;
-import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
-import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.READ;
-import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.group.db.GroupsUpdate;
@@ -39,27 +33,14 @@
 import java.util.Set;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.Test;
 
 public class GroupsUpdateIT {
-
-  private static Config createPureNoteDbConfig() {
-    Config config = new Config();
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, true);
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, true);
-    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, true);
-    return config;
-  }
-
-  @Rule
-  public InMemoryTestEnvironment testEnvironment =
-      new InMemoryTestEnvironment(GroupsUpdateIT::createPureNoteDbConfig);
+  @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
 
   @Inject @ServerInitiated private Provider<GroupsUpdate> groupsUpdateProvider;
   @Inject private Groups groups;
-  @Inject private ReviewDb reviewDb;
 
   @Test
   public void groupCreationIsRetriedWhenFailedDueToConcurrentNameModification() throws Exception {
@@ -100,17 +81,16 @@
 
   private void createGroup(InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
       throws OrmException, IOException, ConfigInvalidException {
-    groupsUpdateProvider.get().createGroup(reviewDb, groupCreation, groupUpdate);
+    groupsUpdateProvider.get().createGroup(groupCreation, groupUpdate);
   }
 
   private void updateGroup(AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
       throws Exception {
-    groupsUpdateProvider.get().updateGroup(reviewDb, groupUuid, groupUpdate);
+    groupsUpdateProvider.get().updateGroup(groupUuid, groupUpdate);
   }
 
-  private Stream<String> getAllGroupNames()
-      throws OrmException, IOException, ConfigInvalidException {
-    return groups.getAllGroupReferences(reviewDb).map(GroupReference::getName);
+  private Stream<String> getAllGroupNames() throws IOException, ConfigInvalidException {
+    return groups.getAllGroupReferences().map(GroupReference::getName);
   }
 
   private static InternalGroupCreation getGroupCreation(String groupName, String groupUuid) {
@@ -145,7 +125,7 @@
       InternalGroupCreation groupCreation = getGroupCreation(groupName, groupName + "-UUID");
       InternalGroupUpdate groupUpdate = InternalGroupUpdate.builder().build();
       try {
-        groupsUpdateProvider.get().createGroup(reviewDb, groupCreation, groupUpdate);
+        groupsUpdateProvider.get().createGroup(groupCreation, groupUpdate);
       } catch (OrmException | IOException | ConfigInvalidException e) {
         throw new IllegalStateException(e);
       }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 867ace4..0988dab 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
 import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.GitUtil.pushOne;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
@@ -30,6 +31,7 @@
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
 import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.testing.Util.category;
 import static com.google.gerrit.server.project.testing.Util.value;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -48,6 +50,7 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.DraftInput;
@@ -78,6 +81,7 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.receive.NoteDbPushOption;
 import com.google.gerrit.server.git.receive.ReceiveConstants;
 import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -2018,6 +2022,53 @@
         .endsWith("Pushing to refs/publish/* is deprecated, use refs/for/* instead.\n");
   }
 
+  @Test
+  public void pushNoteDbRef() throws Exception {
+    String ref = "refs/changes/34/1234/meta";
+    RevCommit c = testRepo.commit().message("Junk NoteDb commit").create();
+    PushResult pr = pushOne(testRepo, c.name(), ref, false, false, null);
+    assertThat(pr.getMessages()).doesNotContain(NoteDbPushOption.OPTION_NAME);
+    assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow");
+
+    pr = pushOne(testRepo, c.name(), ref, false, false, ImmutableList.of("notedb=foobar"));
+    assertThat(pr.getMessages()).contains("Invalid value in -o notedb=foobar");
+    assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow");
+
+    List<String> opts = ImmutableList.of("notedb=allow");
+    pr = pushOne(testRepo, c.name(), ref, false, false, opts);
+    assertPushRejected(pr, ref, "NoteDb update requires access database permission");
+
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    pr = pushOne(testRepo, c.name(), ref, false, false, opts);
+    assertPushRejected(pr, ref, "prohibited by Gerrit: create not permitted for " + ref);
+
+    grant(project, "refs/changes/*", Permission.CREATE);
+    grant(project, "refs/changes/*", Permission.PUSH);
+    grantSkipValidation(project, "refs/changes/*", REGISTERED_USERS);
+    pr = pushOne(testRepo, c.name(), ref, false, false, opts);
+    assertPushOk(pr, ref);
+  }
+
+  @Test
+  public void pushNoteDbRefWithoutOptionOnlyFailsThatCommand() throws Exception {
+    String ref = "refs/changes/34/1234/meta";
+    RevCommit noteDbCommit = testRepo.commit().message("Junk NoteDb commit").create();
+    RevCommit changeCommit =
+        testRepo.branch("HEAD").commit().message("A change").insertChangeId().create();
+    PushResult pr =
+        Iterables.getOnlyElement(
+            testRepo
+                .git()
+                .push()
+                .setRefSpecs(
+                    new RefSpec(noteDbCommit.name() + ":" + ref),
+                    new RefSpec(changeCommit.name() + ":refs/for/master"))
+                .call());
+
+    assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow");
+    assertPushOk(pr, "refs/for/master");
+  }
+
   private DraftInput newDraft(String path, int line, String message) {
     DraftInput d = new DraftInput();
     d.path = path;
diff --git a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
index 35fcc94..1bb23fb 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.pgm;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
@@ -28,7 +29,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
@@ -41,6 +44,11 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -72,6 +80,16 @@
     gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
     // Unlike in the running server, for tests, we don't stack notedb.config on gerrit.config.
     noteDbConfig = new FileBasedConfig(sitePaths.notedb_config.toFile(), FS.detect());
+
+    // Set gc.pruneExpire=now so GC prunes all unreachable objects from All-Users, which allows us
+    // to reliably test that it behaves as expected.
+    Path cfgPath = sitePaths.site_path.resolve("git").resolve("All-Users.git").resolve("config");
+    assertWithMessage("Expected All-Users config at %s", cfgPath)
+        .that(Files.isRegularFile(cfgPath))
+        .isTrue();
+    FileBasedConfig cfg = new FileBasedConfig(cfgPath.toFile(), FS.detect());
+    cfg.setString("gc", null, "pruneExpire", "now");
+    cfg.save();
   }
 
   @Test
@@ -114,11 +132,17 @@
     migrate();
     assertNotesMigrationState(NotesMigrationState.NOTE_DB);
 
+    File allUsersDir;
     try (ServerContext ctx = startServer()) {
       GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
       try (Repository repo = repoManager.openRepository(project)) {
         assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNotNull();
       }
+      assertThat(repoManager).isInstanceOf(LocalDiskRepositoryManager.class);
+      try (Repository repo =
+          repoManager.openRepository(ctx.getInjector().getInstance(AllUsersName.class))) {
+        allUsersDir = repo.getDirectory();
+      }
 
       try (ReviewDb db = openUnderlyingReviewDb(ctx)) {
         Change c = db.changes().get(changeId);
@@ -137,6 +161,15 @@
     }
     assertNoAutoMigrateConfig(gerritConfig);
     assertAutoMigrateConfig(noteDbConfig, false);
+
+    try (FileRepository repo = new FileRepository(allUsersDir)) {
+      try (Stream<Path> paths = Files.walk(repo.getObjectsDirectory().toPath())) {
+        assertThat(paths.filter(p -> !p.toString().contains("pack") && Files.isRegularFile(p)))
+            .named("loose object files in All-Users")
+            .isEmpty();
+      }
+      assertThat(repo.getObjectDatabase().getPacks()).named("packfiles in All-Users").hasSize(1);
+    }
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 7d60b8d..95b64e0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.account;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
@@ -31,6 +32,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
@@ -74,6 +76,7 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushResult;
@@ -779,6 +782,130 @@
     assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExternalIds);
   }
 
+  @Test
+  public void unsetEmail() throws Exception {
+    ExternalId extId = ExternalId.createWithEmail("x", "1", user.id, "x@example.com");
+    insertExtId(extId);
+
+    ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id);
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.upsert(extIdWithoutEmail);
+      extIdNotes.commit(md);
+
+      assertThat(extIdNotes.get(extId.key())).hasValue(extIdWithoutEmail);
+    }
+  }
+
+  @Test
+  public void unsetHttpPassword() throws Exception {
+    ExternalId extId =
+        ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id, null, "secret");
+    insertExtId(extId);
+
+    ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id);
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.upsert(extIdWithoutPassword);
+      extIdNotes.commit(md);
+
+      assertThat(extIdNotes.get(extId.key())).hasValue(extIdWithoutPassword);
+    }
+  }
+
+  @Test
+  public void footers() throws Exception {
+    // Insert external ID for different accounts
+    TestAccount user1 = accountCreator.create("user1");
+    TestAccount user2 = accountCreator.create("user2");
+    ExternalId extId1 = ExternalId.create("foo", "1", user1.id);
+    ExternalId extId2 = ExternalId.create("foo", "2", user1.id);
+    ExternalId extId3 = ExternalId.create("foo", "3", user2.id);
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.insert(ImmutableSet.of(extId1, extId2, extId3));
+      RevCommit c = extIdNotes.commit(md);
+      assertThat(getFooters(c))
+          .containsExactly("Account: " + user1.getId(), "Account: " + user2.getId())
+          .inOrder();
+    }
+
+    // Insert external ID with different emails
+    ExternalId extId4 = ExternalId.createWithEmail("foo", "4", user1.id, "foo4@example.com");
+    ExternalId extId5 = ExternalId.createWithEmail("foo", "5", user2.id, "foo5@example.com");
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.insert(ImmutableSet.of(extId4, extId5));
+      RevCommit c = extIdNotes.commit(md);
+      assertThat(getFooters(c))
+          .containsExactly(
+              "Account: " + user1.getId(),
+              "Account: " + user2.getId(),
+              "Email: foo4@example.com",
+              "Email: foo5@example.com")
+          .inOrder();
+    }
+
+    // Update external ID - Add Email
+    ExternalId extId1a = ExternalId.createWithEmail("foo", "1", user1.id, "foo1@example.com");
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.upsert(extId1a);
+      RevCommit c = extIdNotes.commit(md);
+      assertThat(getFooters(c))
+          .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+          .inOrder();
+    }
+
+    // Update external ID - Remove Email
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.upsert(extId1);
+      RevCommit c = extIdNotes.commit(md);
+      assertThat(getFooters(c))
+          .containsExactly("Account: " + user1.getId(), "Email: foo1@example.com")
+          .inOrder();
+    }
+
+    // Delete external IDs
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.delete(ImmutableSet.of(extId1, extId5));
+      RevCommit c = extIdNotes.commit(md);
+      assertThat(getFooters(c))
+          .containsExactly(
+              "Account: " + user1.getId(), "Account: " + user2.getId(), "Email: foo5@example.com")
+          .inOrder();
+    }
+
+    // Delete external ID by key without email
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.delete(extId2.accountId(), extId2.key());
+      RevCommit c = extIdNotes.commit(md);
+      assertThat(getFooters(c)).containsExactly("Account: " + user1.getId()).inOrder();
+    }
+
+    // Delete external ID by key with email
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.delete(extId4.accountId(), extId4.key());
+      RevCommit c = extIdNotes.commit(md);
+      assertThat(getFooters(c))
+          .containsExactly("Account: " + user1.getId(), "Email: foo4@example.com")
+          .inOrder();
+    }
+  }
+
   private void insertExtId(ExternalId extId) throws Exception {
     accountsUpdateProvider
         .get()
@@ -823,6 +950,10 @@
     }
   }
 
+  private List<String> getFooters(RevCommit c) {
+    return c.getFooterLines().stream().map(FooterLine::toString).collect(toList());
+  }
+
   private List<AccountExternalIdInfo> toExternalIdInfos(Collection<ExternalId> extIds) {
     return extIds.stream().map(this::toExternalIdInfo).collect(toList());
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index e7d7fc7..0c1be53 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -232,6 +232,23 @@
   }
 
   @Test
+  public void customLabelMaxWithBlock_MaxVoteNegativeVoteBlock() throws Exception {
+    label.setFunction(MAX_WITH_BLOCK);
+    saveLabelConfig();
+    PushOneCommit.Result r = createChange();
+    revision(r).review(new ReviewInput().label(label.getName(), 1));
+    revision(r).review(new ReviewInput().label(label.getName(), -1));
+    ChangeInfo c = getWithLabels(r);
+    LabelInfo q = c.labels.get(label.getName());
+    assertThat(q.all).hasSize(1);
+    assertThat(q.approved).isNull();
+    assertThat(q.recommended).isNull();
+    assertThat(q.disliked).isNull();
+    assertThat(q.rejected).isNotNull();
+    assertThat(q.blocking).isTrue();
+  }
+
+  @Test
   public void customLabel_DisallowPostSubmit() throws Exception {
     label.setFunction(NO_OP);
     label.setAllowPostSubmit(false);
diff --git a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
new file mode 100644
index 0000000..985f514
--- /dev/null
+++ b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
@@ -0,0 +1,148 @@
+// 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.common.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import java.sql.Date;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+
+public class LabelFunctionTest {
+  private static final String LABEL_NAME = "Verified";
+  private static final LabelId LABEL_ID = new LabelId(LABEL_NAME);
+  private static final Change.Id CHANGE_ID = new Change.Id(100);
+  private static final PatchSet.Id PS_ID = new PatchSet.Id(CHANGE_ID, 1);
+  private static final LabelType VERIFIED_LABEL = makeLabel();
+  private static final PatchSetApproval APPROVAL_2 = makeApproval(2);
+  private static final PatchSetApproval APPROVAL_1 = makeApproval(1);
+  private static final PatchSetApproval APPROVAL_0 = makeApproval(0);
+  private static final PatchSetApproval APPROVAL_M1 = makeApproval(-1);
+  private static final PatchSetApproval APPROVAL_M2 = makeApproval(-2);
+
+  @Test
+  public void checkLabelNameIsCorrect() {
+    for (LabelFunction function : LabelFunction.values()) {
+      SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, ImmutableList.of());
+      assertThat(myLabel.label).isEqualTo("Verified");
+    }
+  }
+
+  @Test
+  public void checkFunctionDoesNothing() {
+    checkNothingHappens(LabelFunction.NO_BLOCK);
+    checkNothingHappens(LabelFunction.NO_OP);
+    checkNothingHappens(LabelFunction.PATCH_SET_LOCK);
+    checkNothingHappens(LabelFunction.ANY_WITH_BLOCK);
+
+    checkLabelIsRequired(LabelFunction.MAX_WITH_BLOCK);
+    checkLabelIsRequired(LabelFunction.MAX_NO_BLOCK);
+  }
+
+  @Test
+  public void checkBlockWorks() {
+    checkBlockWorks(LabelFunction.ANY_WITH_BLOCK);
+    checkBlockWorks(LabelFunction.MAX_WITH_BLOCK);
+  }
+
+  @Test
+  public void checkMaxWorks() {
+    checkMaxIsEnforced(LabelFunction.MAX_NO_BLOCK);
+    checkMaxIsEnforced(LabelFunction.MAX_WITH_BLOCK);
+
+    checkMaxValidatesTheLabel(LabelFunction.MAX_NO_BLOCK);
+    checkMaxValidatesTheLabel(LabelFunction.MAX_WITH_BLOCK);
+  }
+
+  @Test
+  public void checkMaxNoBlockIgnoresMin() {
+    List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_M2, APPROVAL_2, APPROVAL_M2);
+
+    SubmitRecord.Label myLabel = LabelFunction.MAX_NO_BLOCK.check(VERIFIED_LABEL, approvals);
+
+    assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.OK);
+    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_2.getAccountId());
+  }
+
+  private static LabelType makeLabel() {
+    List<LabelValue> values = new ArrayList<>();
+    // The label text is irrelevant here, only the numerical value is used
+    values.add(new LabelValue((short) -2, "Great job, please fix compilation."));
+    values.add(new LabelValue((short) -1, "Really good, please make some minor changes."));
+    values.add(new LabelValue((short) 0, "No vote."));
+    values.add(new LabelValue((short) 1, "Closest thing perfection."));
+    values.add(new LabelValue((short) 2, "Perfect!"));
+    return new LabelType(LABEL_NAME, values);
+  }
+
+  private static PatchSetApproval makeApproval(int value) {
+    Account.Id accountId = new Account.Id(10000 + value);
+    PatchSetApproval.Key key = makeKey(PS_ID, accountId, LABEL_ID);
+    return new PatchSetApproval(key, (short) value, Date.from(Instant.now()));
+  }
+
+  private static PatchSetApproval.Key makeKey(Id psId, Account.Id accountId, LabelId labelId) {
+    return new PatchSetApproval.Key(psId, accountId, labelId);
+  }
+
+  private static void checkBlockWorks(LabelFunction function) {
+    List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_1, APPROVAL_M2, APPROVAL_2);
+
+    SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
+
+    assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.REJECT);
+    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_M2.getAccountId());
+  }
+
+  private static void checkNothingHappens(LabelFunction function) {
+    SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, ImmutableList.of());
+
+    assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.MAY);
+    assertThat(myLabel.appliedBy).isNull();
+  }
+
+  private static void checkLabelIsRequired(LabelFunction function) {
+    SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, ImmutableList.of());
+
+    assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.NEED);
+    assertThat(myLabel.appliedBy).isNull();
+  }
+
+  private static void checkMaxIsEnforced(LabelFunction function) {
+    List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_1, APPROVAL_0);
+
+    SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
+
+    assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.NEED);
+  }
+
+  private static void checkMaxValidatesTheLabel(LabelFunction function) {
+    List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_1, APPROVAL_2, APPROVAL_M1);
+
+    SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
+
+    assertThat(myLabel.status).isEqualTo(SubmitRecord.Label.Status.OK);
+    assertThat(myLabel.appliedBy).isEqualTo(APPROVAL_2.getAccountId());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 75f3b3e..24d2822 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -42,16 +42,19 @@
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/group/testing",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/truth",
         "//java/org/eclipse/jgit:server",
         "//lib:grappa",
         "//lib:gson",
         "//lib:guava-retrying",
         "//lib:gwtorm",
         "//lib:truth-java8-extension",
+        "//lib/auto:auto-value",
         "//lib/commons:codec",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index ac9775a..a3fbb5c 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -27,11 +27,14 @@
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Optional;
 import java.util.TimeZone;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -116,9 +119,9 @@
         getAccountName(id), getAccountEmail(id), ident.getWhen(), ident.getTimeZone());
   }
 
-  protected static AuditLogFormatter getAuditLogFormatter() {
+  protected AuditLogFormatter getAuditLogFormatter() {
     return AuditLogFormatter.create(
-        AbstractGroupTest::getAccount, AbstractGroupTest::getGroup, SERVER_ID);
+        AbstractGroupTest::getAccount, uuid -> getGroup(uuid), SERVER_ID);
   }
 
   private static Optional<Account> getAccount(Account.Id id) {
@@ -127,7 +130,7 @@
     return Optional.of(account);
   }
 
-  private static Optional<GroupDescription.Basic> getGroup(AccountGroup.UUID uuid) {
+  private Optional<GroupDescription.Basic> getGroup(AccountGroup.UUID uuid) {
     GroupDescription.Basic group =
         new GroupDescription.Basic() {
           @Override
@@ -137,7 +140,14 @@
 
           @Override
           public String getName() {
-            return "Group " + uuid;
+            try {
+              return GroupConfig.loadForGroup(allUsersRepo, uuid)
+                  .getLoadedGroup()
+                  .map(InternalGroup::getName)
+                  .orElse("Group " + uuid);
+            } catch (IOException | ConfigInvalidException e) {
+              return "Group " + uuid;
+            }
           }
 
           @Nullable
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index 4effa94..dbbfe3a 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -1350,7 +1350,7 @@
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
     assertThat(revCommit.getFullMessage())
-        .isEqualTo("Create group\n\nAdd: John <13@server-id>\nAdd: Jane <7@server-id>");
+        .isEqualTo("Create group\n\nAdd: Jane <7@server-id>\nAdd: John <13@server-id>");
   }
 
   @Test
@@ -1397,7 +1397,7 @@
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
     assertThat(revCommit.getFullMessage())
-        .isEqualTo("Update group\n\nAdd: John <13@GerritServer1>\nAdd: Jane <7@GerritServer1>");
+        .isEqualTo("Update group\n\nAdd: Jane <7@GerritServer1>\nAdd: John <13@GerritServer1>");
   }
 
   @Test
@@ -1531,11 +1531,11 @@
         .isEqualTo(
             "Update group\n"
                 + "\n"
-                + "Rename from Old name to New name\n"
-                + "Remove: Jane <7@serverId>\n"
+                + "Add-group: Bots <129403>\n"
                 + "Add: John <13@serverId>\n"
                 + "Remove-group: Verifiers <8903493>\n"
-                + "Add-group: Bots <129403>");
+                + "Remove: Jane <7@serverId>\n"
+                + "Rename from Old name to New name");
   }
 
   private static Timestamp toTimestamp(LocalDateTime localDateTime) {
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index ddda473..c3b5af9 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -228,8 +229,7 @@
             "Add Email",
             userId,
             u -> u.addExternalId(ExternalId.createEmail(userId, email)).setPreferredEmail(email));
-    user = userFactory.create(userId);
-    requestContext.setContext(newRequestContext(userId));
+    resetUser();
   }
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
@@ -247,6 +247,11 @@
     };
   }
 
+  protected void resetUser() {
+    user = userFactory.create(userId);
+    requestContext.setContext(newRequestContext(userId));
+  }
+
   @After
   public void tearDownInjector() {
     if (lifecycle != null) {
@@ -385,6 +390,20 @@
   }
 
   @Test
+  public void byStatusAbandoned() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
+    insert(repo, ins1);
+    ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
+    Change change1 = insert(repo, ins2);
+    insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+
+    assertQuery("status:abandoned", change1);
+    assertQuery("status:ABANDONED", change1);
+    assertQuery("is:abandoned", change1);
+  }
+
+  @Test
   public void byStatusPrefix() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
@@ -1502,6 +1521,8 @@
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChange(repo));
 
+    assertQuery("has:draft");
+
     DraftInput in = new DraftInput();
     in.line = 1;
     in.message = "nit: trailing whitespace";
@@ -1517,6 +1538,7 @@
     int user2 =
         accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId().get();
 
+    assertQuery("has:draft", change2, change1);
     assertQuery("draftby:" + userId.get(), change2, change1);
     assertQuery("draftby:" + user2);
   }
@@ -2156,6 +2178,35 @@
   }
 
   @Test
+  public void watched() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
+    Change change1 = insert(repo, ins1);
+
+    TestRepository<Repo> repo2 = createProject("repo2");
+
+    ChangeInserter ins2 = newChangeWithStatus(repo2, Change.Status.NEW);
+    insert(repo2, ins2);
+
+    assertQuery("is:watched");
+    assertQuery("watchedby:self");
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = "repo";
+    pwi.filter = null;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    resetUser();
+
+    assertQuery("is:watched", change1);
+    assertQuery("watchedby:self", change1);
+  }
+
+  @Test
   public void selfAndMe() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 2bff3f9..f1b65d4 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -359,7 +359,7 @@
     AccountGroup.UUID groupUuid = new AccountGroup.UUID(group1.id);
     InternalGroupUpdate groupUpdate =
         InternalGroupUpdate.builder().setDescription(newDescription).build();
-    groupsUpdateProvider.get().updateGroupInDb(db, groupUuid, groupUpdate);
+    groupsUpdateProvider.get().updateGroupInDb(groupUuid, groupUpdate);
 
     assertQuery("description:" + group1.description, group1);
     assertQuery("description:" + newDescription);
diff --git a/javatests/com/google/gerrit/server/schema/GroupRebuilderIT.java b/javatests/com/google/gerrit/server/schema/GroupRebuilderIT.java
deleted file mode 100644
index 709be8e..0000000
--- a/javatests/com/google/gerrit/server/schema/GroupRebuilderIT.java
+++ /dev/null
@@ -1,300 +0,0 @@
-// Copyright (C) 2017 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 static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.accounts.AccountInput;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ServerInitiated;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountsUpdate;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerId;
-import com.google.gerrit.server.config.GerritServerIdProvider;
-import com.google.gerrit.server.git.CommitUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.group.db.AuditLogFormatter;
-import com.google.gerrit.server.notedb.GroupsMigration;
-import com.google.gerrit.testing.GerritBaseTests;
-import com.google.gerrit.testing.InMemoryTestEnvironment;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.gerrit.testing.TestTimeUtil.TempClockStep;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class GroupRebuilderIT extends GerritBaseTests {
-
-  private static Config createConfigWithServerId() {
-    Config config = new Config();
-    config.setString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY, "1234567");
-    return config;
-  }
-
-  @Rule
-  public InMemoryTestEnvironment testEnv =
-      new InMemoryTestEnvironment(GroupRebuilderIT::createConfigWithServerId);
-
-  @Inject private GroupsMigration migration;
-  @Inject private GerritApi gApi;
-  @Inject private ReviewDb db;
-  @Inject private GitRepositoryManager repoManager;
-  @Inject private AllUsersName allUsersName;
-  @Inject private IdentifiedUser currentUser;
-  @Inject private @GerritServerId String serverId;
-  @Inject private AccountCache accountCache;
-  @Inject private @ServerInitiated AccountsUpdate accountsUpdate;
-  @Inject private GroupBackend groupBackend;
-  @Inject private GroupBundle.Factory bundleFactory;
-  @Inject private @GerritPersonIdent Provider<PersonIdent> serverIdent;
-
-  private GroupRebuilder rebuilder;
-
-  @Before
-  public void setup() throws Exception {
-    // This test is explicitly testing the migration from ReviewDb to NoteDb, and handles reading
-    // from NoteDb manually. It should work regardless of the value of noteDb.groups.write, however.
-    assume().that(migration.readFromNoteDb()).isFalse();
-
-    accountsUpdate.update(
-        "Set Name for CurrentUser", currentUser.getAccountId(), u -> u.setFullName("current"));
-
-    AuditLogFormatter auditLogFormatter =
-        AuditLogFormatter.createBackedBy(accountCache, groupBackend, serverId);
-    rebuilder = new GroupRebuilder(serverIdent.get(), allUsersName, auditLogFormatter);
-  }
-
-  @Before
-  public void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
-  @Test
-  public void basicGroupProperties() throws Exception {
-    GroupInfo createdGroup = gApi.groups().create("group").get();
-    GroupBundle reviewDbBundle =
-        GroupBundle.Factory.fromReviewDb(db, new AccountGroup.UUID(createdGroup.id));
-
-    deleteGroupRefs(reviewDbBundle);
-    assertMigratedCleanly(rebuild(reviewDbBundle), reviewDbBundle);
-  }
-
-  @Test
-  public void logFormat() throws Exception {
-    AccountInfo user1 = createAccount("user1");
-    AccountInfo user2 = createAccount("user2");
-    GroupInfo group1 = gApi.groups().create("group1").get();
-    GroupInfo group2 = gApi.groups().create("group2").get();
-
-    try (TempClockStep step = TestTimeUtil.freezeClock()) {
-      gApi.groups()
-          .id(group1.id)
-          .addMembers(Integer.toString(user1._accountId), Integer.toString(user2._accountId));
-    }
-    TimeUtil.nowTs();
-
-    try (TempClockStep step = TestTimeUtil.freezeClock()) {
-      gApi.groups().id(group1.id).addGroups(group2.id, SystemGroupBackend.REGISTERED_USERS.get());
-    }
-
-    GroupBundle reviewDbBundle =
-        GroupBundle.Factory.fromReviewDb(db, new AccountGroup.UUID(group1.id));
-    deleteGroupRefs(reviewDbBundle);
-
-    GroupBundle noteDbBundle = rebuild(reviewDbBundle);
-    assertMigratedCleanly(noteDbBundle, reviewDbBundle);
-
-    ImmutableList<CommitInfo> log = log(group1);
-    assertThat(log).hasSize(4);
-
-    assertThat(log.get(0)).message().isEqualTo("Create group");
-    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.get().getName());
-    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.get().getEmailAddress());
-    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
-    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.get().getTimeZoneOffset());
-    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
-
-    assertThat(log.get(1))
-        .message()
-        .isEqualTo(
-            "Update group\n\nAdd: "
-                + currentUser.getName()
-                + " <"
-                + currentUser.getAccountId()
-                + "@"
-                + serverId
-                + ">");
-    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
-
-    assertThat(log.get(2))
-        .message()
-        .isEqualTo(
-            "Update group\n"
-                + "\n"
-                + ("Add: user1 <" + user1._accountId + "@" + serverId + ">\n")
-                + ("Add: user2 <" + user2._accountId + "@" + serverId + ">"));
-    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
-
-    assertThat(log.get(3))
-        .message()
-        .isEqualTo(
-            "Update group\n"
-                + "\n"
-                + ("Add-group: " + group2.name + " <" + group2.id + ">\n")
-                + ("Add-group: Registered Users <global:Registered-Users>"));
-    assertThat(log.get(3)).author().name().isEqualTo(currentUser.getName());
-    assertThat(log.get(3)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
-    assertThat(log.get(3)).committer().hasSameDateAs(log.get(3).author);
-  }
-
-  @Test
-  public void unknownGroupUuid() throws Exception {
-    GroupInfo group = gApi.groups().create("group").get();
-
-    AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("mybackend:foo");
-
-    AccountGroupById byId =
-        new AccountGroupById(
-            new AccountGroupById.Key(new AccountGroup.Id(group.groupId), subgroupUuid));
-    assertThat(groupBackend.handles(byId.getIncludeUUID())).isFalse();
-    db.accountGroupById().insert(Collections.singleton(byId));
-
-    AccountGroupByIdAud audit =
-        new AccountGroupByIdAud(byId, currentUser.getAccountId(), TimeUtil.nowTs());
-    db.accountGroupByIdAud().insert(Collections.singleton(audit));
-
-    GroupBundle reviewDbBundle =
-        GroupBundle.Factory.fromReviewDb(db, new AccountGroup.UUID(group.id));
-    deleteGroupRefs(reviewDbBundle);
-
-    GroupBundle noteDbBundle = rebuild(reviewDbBundle);
-    assertMigratedCleanly(noteDbBundle, reviewDbBundle);
-
-    ImmutableList<CommitInfo> log = log(group);
-    assertThat(log).hasSize(3);
-
-    assertThat(log.get(0)).message().isEqualTo("Create group");
-    assertThat(log.get(1))
-        .message()
-        .isEqualTo(
-            "Update group\n\nAdd: "
-                + currentUser.getName()
-                + " <"
-                + currentUser.getAccountId()
-                + "@"
-                + serverId
-                + ">");
-    assertThat(log.get(2))
-        .message()
-        .isEqualTo("Update group\n\nAdd-group: mybackend:foo <mybackend:foo>");
-  }
-
-  private void deleteGroupRefs(GroupBundle bundle) throws Exception {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      String refName = RefNames.refsGroups(bundle.uuid());
-      RefUpdate ru = repo.updateRef(refName);
-      ru.setForceUpdate(true);
-      Ref oldRef = repo.exactRef(refName);
-      if (oldRef == null) {
-        return;
-      }
-      ru.setExpectedOldObjectId(oldRef.getObjectId());
-      ru.setNewObjectId(ObjectId.zeroId());
-      assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    }
-  }
-
-  private GroupBundle rebuild(GroupBundle reviewDbBundle) throws Exception {
-    try (Repository repo = repoManager.openRepository(allUsersName)) {
-      rebuilder.rebuild(repo, reviewDbBundle, null);
-      return bundleFactory.fromNoteDb(repo, reviewDbBundle.uuid());
-    }
-  }
-
-  private void assertMigratedCleanly(GroupBundle noteDbBundle, GroupBundle expectedReviewDbBundle) {
-    assertThat(GroupBundle.compareWithAudits(expectedReviewDbBundle, noteDbBundle)).isEmpty();
-  }
-
-  private AccountInfo createAccount(String name) throws RestApiException {
-    AccountInput accountInput = new AccountInput();
-    accountInput.username = name;
-    accountInput.name = name;
-    return gApi.accounts().create(accountInput).get();
-  }
-
-  private ImmutableList<CommitInfo> log(GroupInfo g) throws Exception {
-    ImmutableList.Builder<CommitInfo> result = ImmutableList.builder();
-    List<Date> commitDates = new ArrayList<>();
-    try (Repository repo = repoManager.openRepository(allUsersName);
-        RevWalk rw = new RevWalk(repo)) {
-      Ref ref = repo.exactRef(RefNames.refsGroups(new AccountGroup.UUID(g.id)));
-      if (ref != null) {
-        rw.sort(RevSort.REVERSE);
-        rw.setRetainBody(true);
-        rw.markStart(rw.parseCommit(ref.getObjectId()));
-        for (RevCommit c : rw) {
-          result.add(CommitUtil.toCommitInfo(c));
-          commitDates.add(c.getCommitterIdent().getWhen());
-        }
-      }
-    }
-    assertThat(commitDates).named("commit timestamps for %s", result).isOrdered();
-    return result.build();
-  }
-}
diff --git a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java b/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
index 6c670c0..a6178ac 100644
--- a/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
+++ b/javatests/com/google/gerrit/server/schema/GroupRebuilderTest.java
@@ -181,10 +181,10 @@
         log.get(1),
         "Update group\n"
             + "\n"
-            + "Add: Account 1 <1@server-id>\n"
-            + "Add: Account 2 <2@server-id>\n"
             + "Add-group: Group x <x>\n"
-            + "Add-group: Group y <y>");
+            + "Add-group: Group y <y>\n"
+            + "Add: Account 1 <1@server-id>\n"
+            + "Add: Account 2 <2@server-id>");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index 047b933..ed94c97 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.notedb.GroupsMigration;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testing.InMemoryDatabase;
 import com.google.gerrit.testing.InMemoryH2Type;
@@ -119,7 +118,6 @@
 
                     bind(SystemGroupBackend.class);
                     install(new NotesMigration.Module());
-                    install(new GroupsMigration.Module());
                     bind(MetricMaker.class).to(DisabledMetricMaker.class);
                   }
                 })
diff --git a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java b/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
index b817e71..42af2ca 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
@@ -18,25 +18,30 @@
 import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.api.groups.GroupInput;
-import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroup.Id;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.restapi.group.CreateGroup;
+import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.gerrit.testing.TestUpdateUI;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.sql.Statement;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -46,20 +51,58 @@
 
   @Rule public InMemoryTestEnvironment testEnv = new InMemoryTestEnvironment();
 
-  @Inject private CreateGroup.Factory createGroupFactory;
   @Inject private Schema_151 schema151;
   @Inject private ReviewDb db;
+  @Inject private IdentifiedUser currentUser;
+  @Inject private @GerritPersonIdent Provider<PersonIdent> serverIdent;
+  @Inject private Sequences seq;
 
   private Connection connection;
   private PreparedStatement createdOnRetrieval;
   private PreparedStatement createdOnUpdate;
   private PreparedStatement auditEntryDeletion;
+  private JdbcSchema jdbcSchema;
+
+  @Before
+  public void unwrapDb() {
+    jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
+  }
 
   @Before
   public void setUp() throws Exception {
     assume().that(db instanceof JdbcSchema).isTrue();
 
     connection = ((JdbcSchema) db).getConnection();
+
+    try (Statement stmt = connection.createStatement()) {
+      stmt.execute(
+          "CREATE TABLE account_groups ("
+              + " group_uuid varchar(255) DEFAULT '' NOT NULL,"
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " name varchar(255) DEFAULT '' NOT NULL,"
+              + " created_on TIMESTAMP,"
+              + " description CLOB,"
+              + " owner_group_uuid varchar(255) DEFAULT '' NOT NULL,"
+              + " visible_to_all CHAR(1) DEFAULT 'N' NOT NULL"
+              + ")");
+
+      stmt.execute(
+          "CREATE TABLE account_group_members ("
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " account_id INTEGER DEFAULT 0 NOT NULL"
+              + ")");
+
+      stmt.execute(
+          "CREATE TABLE account_group_members_audit ("
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " account_id INTEGER DEFAULT 0 NOT NULL,"
+              + " added_by INTEGER DEFAULT 0 NOT NULL,"
+              + " added_on TIMESTAMP,"
+              + " removed_by INTEGER,"
+              + " removed_on TIMESTAMP"
+              + ")");
+    }
+
     createdOnRetrieval =
         connection.prepareStatement("SELECT created_on FROM account_groups WHERE group_id = ?");
     createdOnUpdate =
@@ -87,7 +130,7 @@
   @Test
   public void createdOnIsPopulatedForGroupsCreatedAfterAudit() throws Exception {
     Timestamp testStartTime = TimeUtil.nowTs();
-    AccountGroup.Id groupId = createGroup("Group for schema migration");
+    AccountGroup.Id groupId = createGroupInReviewDb("Group for schema migration");
     setCreatedOnToVeryOldTimestamp(groupId);
 
     schema151.migrateData(db, new TestUpdateUI());
@@ -98,7 +141,7 @@
 
   @Test
   public void createdOnIsPopulatedForGroupsCreatedBeforeAudit() throws Exception {
-    AccountGroup.Id groupId = createGroup("Ancient group for schema migration");
+    AccountGroup.Id groupId = createGroupInReviewDb("Ancient group for schema migration");
     setCreatedOnToVeryOldTimestamp(groupId);
     removeAuditEntriesFor(groupId);
 
@@ -108,12 +151,16 @@
     assertThat(createdOn).isEqualTo(AccountGroup.auditCreationInstantTs());
   }
 
-  private AccountGroup.Id createGroup(String name) throws Exception {
-    GroupInput groupInput = new GroupInput();
-    groupInput.name = name;
-    GroupInfo groupInfo =
-        createGroupFactory.create(name).apply(TopLevelResource.INSTANCE, groupInput);
-    return new Id(groupInfo.groupId);
+  private AccountGroup.Id createGroupInReviewDb(String name) throws Exception {
+    AccountGroup group =
+        new AccountGroup(
+            new AccountGroup.NameKey(name),
+            new AccountGroup.Id(seq.nextGroupId()),
+            GroupUUID.make(name, serverIdent.get()),
+            TimeUtil.nowTs());
+    storeInReviewDb(group);
+    addMembersInReviewDb(group.getId(), currentUser.getAccountId());
+    return group.getId();
   }
 
   private Timestamp getCreatedOn(Id groupId) throws Exception {
@@ -138,4 +185,69 @@
     auditEntryDeletion.setInt(1, groupId.get());
     auditEntryDeletion.executeUpdate();
   }
+
+  private void storeInReviewDb(AccountGroup... groups) throws Exception {
+    try (PreparedStatement stmt =
+        jdbcSchema
+            .getConnection()
+            .prepareStatement(
+                "INSERT INTO account_groups"
+                    + " (group_uuid,"
+                    + " group_id,"
+                    + " name,"
+                    + " description,"
+                    + " created_on,"
+                    + " owner_group_uuid,"
+                    + " visible_to_all) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
+      for (AccountGroup group : groups) {
+        stmt.setString(1, group.getGroupUUID().get());
+        stmt.setInt(2, group.getId().get());
+        stmt.setString(3, group.getName());
+        stmt.setString(4, group.getDescription());
+        stmt.setTimestamp(5, group.getCreatedOn());
+        stmt.setString(6, group.getOwnerGroupUUID().get());
+        stmt.setString(7, group.isVisibleToAll() ? "Y" : "N");
+        stmt.addBatch();
+      }
+      stmt.executeBatch();
+    }
+  }
+
+  private void addMembersInReviewDb(AccountGroup.Id groupId, Account.Id... memberIds)
+      throws Exception {
+    try (PreparedStatement addMemberStmt =
+            jdbcSchema
+                .getConnection()
+                .prepareStatement(
+                    "INSERT INTO account_group_members"
+                        + " (group_id,"
+                        + " account_id) VALUES ("
+                        + groupId.get()
+                        + ", ?)");
+        PreparedStatement addMemberAuditStmt =
+            jdbcSchema
+                .getConnection()
+                .prepareStatement(
+                    "INSERT INTO account_group_members_audit"
+                        + " (group_id,"
+                        + " account_id,"
+                        + " added_by,"
+                        + " added_on) VALUES ("
+                        + groupId.get()
+                        + ", ?, "
+                        + currentUser.getAccountId().get()
+                        + ", ?)")) {
+      Timestamp addedOn = TimeUtil.nowTs();
+      for (Account.Id memberId : memberIds) {
+        addMemberStmt.setInt(1, memberId.get());
+        addMemberStmt.addBatch();
+
+        addMemberAuditStmt.setInt(1, memberId.get());
+        addMemberAuditStmt.setTimestamp(2, addedOn);
+        addMemberAuditStmt.addBatch();
+      }
+      addMemberStmt.executeBatch();
+      addMemberAuditStmt.executeBatch();
+    }
+  }
 }
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
new file mode 100644
index 0000000..57689b3
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInNoteDbTest.java
@@ -0,0 +1,227 @@
+// 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 static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.db.GroupNameNotes;
+import com.google.gerrit.server.group.db.GroupsUpdate;
+import com.google.gerrit.server.group.db.InternalGroupCreation;
+import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.testing.InMemoryTestEnvironment;
+import com.google.gerrit.testing.TestUpdateUI;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.sql.PreparedStatement;
+import java.sql.Statement;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class Schema_166_to_167_WithGroupsInNoteDbTest {
+  private static Config createConfig() {
+    Config config = new Config();
+    config.setString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY, "1234567");
+
+    // Disable groups in ReviewDb. This means the primary storage for groups is NoteDb.
+    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, true);
+
+    return config;
+  }
+
+  @Rule
+  public InMemoryTestEnvironment testEnv =
+      new InMemoryTestEnvironment(Schema_166_to_167_WithGroupsInNoteDbTest::createConfig);
+
+  @Inject private Schema_167 schema167;
+  @Inject private ReviewDb db;
+  @Inject private GitRepositoryManager gitRepoManager;
+  @Inject private AllUsersName allUsersName;
+  @Inject private @ServerInitiated GroupsUpdate groupsUpdate;
+  @Inject private Sequences seq;
+
+  private JdbcSchema jdbcSchema;
+
+  @Before
+  public void initDb() throws Exception {
+    jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
+
+    try (Statement stmt = jdbcSchema.getConnection().createStatement()) {
+      stmt.execute(
+          "CREATE TABLE account_groups ("
+              + " group_uuid varchar(255) DEFAULT '' NOT NULL,"
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " name varchar(255) DEFAULT '' NOT NULL,"
+              + " created_on TIMESTAMP,"
+              + " description CLOB,"
+              + " owner_group_uuid varchar(255) DEFAULT '' NOT NULL,"
+              + " visible_to_all CHAR(1) DEFAULT 'N' NOT NULL"
+              + ")");
+    }
+  }
+
+  @Test
+  public void migrationIsSkipped() throws Exception {
+    // Create a group in NoteDb (doesn't create the group in ReviewDb since
+    // disableReviewDb == true)
+    InternalGroup internalGroup =
+        groupsUpdate.createGroup(
+            InternalGroupCreation.builder()
+                .setNameKey(new AccountGroup.NameKey("users"))
+                .setGroupUUID(new AccountGroup.UUID("users"))
+                .setId(new AccountGroup.Id(seq.nextGroupId()))
+                .build(),
+            InternalGroupUpdate.builder().setDescription("description").build());
+
+    // Insert the group into ReviewDb
+    AccountGroup group1 =
+        newGroup()
+            .setName(internalGroup.getName())
+            .setGroupUuid(internalGroup.getGroupUUID())
+            .setId(internalGroup.getId())
+            .setCreatedOn(internalGroup.getCreatedOn())
+            .setDescription(internalGroup.getDescription())
+            .setGroupUuid(internalGroup.getGroupUUID())
+            .setVisibleToAll(internalGroup.isVisibleToAll())
+            .build();
+    storeInReviewDb(group1);
+
+    // Update the group description in ReviewDb so that the group state differs between ReviewDb and
+    // NoteDb
+    group1.setDescription("outdated");
+    updateInReviewDb(group1);
+
+    // Create a group that only exists in ReviewDb
+    AccountGroup group2 = newGroup().setName("reviewDbOnlyGroup").build();
+    storeInReviewDb(group2);
+
+    // Remember the SHA1 of the group ref in NoteDb
+    ObjectId groupSha1 = getGroupSha1(group1.getGroupUUID());
+
+    executeSchemaMigration(schema167);
+
+    // Verify the groups in NoteDb: "users" should still exist, "reviewDbOnlyGroup" should not have
+    // been created
+    ImmutableList<GroupReference> groupReferences = getAllGroupsFromNoteDb();
+    ImmutableList<String> groupNames =
+        groupReferences.stream().map(GroupReference::getName).collect(toImmutableList());
+    assertThat(groupNames).contains("users");
+    assertThat(groupNames).doesNotContain("reviewDbOnlyGroup");
+
+    // Verify that the group refs in NoteDb were not touched.
+    assertThat(getGroupSha1(group1.getGroupUUID())).isEqualTo(groupSha1);
+    assertThat(getGroupSha1(group2.getGroupUUID())).isNull();
+  }
+
+  private static TestGroup.Builder newGroup() {
+    return TestGroup.builder();
+  }
+
+  private void storeInReviewDb(AccountGroup... groups) throws Exception {
+    try (PreparedStatement stmt =
+        jdbcSchema
+            .getConnection()
+            .prepareStatement(
+                "INSERT INTO account_groups"
+                    + " (group_uuid,"
+                    + " group_id,"
+                    + " name,"
+                    + " description,"
+                    + " created_on,"
+                    + " owner_group_uuid,"
+                    + " visible_to_all) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
+      for (AccountGroup group : groups) {
+        stmt.setString(1, group.getGroupUUID().get());
+        stmt.setInt(2, group.getId().get());
+        stmt.setString(3, group.getName());
+        stmt.setString(4, group.getDescription());
+        stmt.setTimestamp(5, group.getCreatedOn());
+        stmt.setString(6, group.getOwnerGroupUUID().get());
+        stmt.setString(7, group.isVisibleToAll() ? "Y" : "N");
+        stmt.addBatch();
+      }
+      stmt.executeBatch();
+    }
+  }
+
+  private void updateInReviewDb(AccountGroup... groups) throws Exception {
+    try (PreparedStatement stmt =
+        jdbcSchema
+            .getConnection()
+            .prepareStatement(
+                "UPDATE account_groups SET"
+                    + " group_uuid = ?,"
+                    + " name = ?,"
+                    + " description = ?,"
+                    + " created_on = ?,"
+                    + " owner_group_uuid = ?,"
+                    + " visible_to_all = ?"
+                    + " WHERE group_id = ?")) {
+      for (AccountGroup group : groups) {
+        stmt.setString(1, group.getGroupUUID().get());
+        stmt.setString(2, group.getName());
+        stmt.setString(3, group.getDescription());
+        stmt.setTimestamp(4, group.getCreatedOn());
+        stmt.setString(5, group.getOwnerGroupUUID().get());
+        stmt.setString(6, group.isVisibleToAll() ? "Y" : "N");
+        stmt.setInt(7, group.getId().get());
+        stmt.addBatch();
+      }
+      stmt.executeBatch();
+    }
+  }
+
+  private void executeSchemaMigration(SchemaVersion schema) throws Exception {
+    schema.migrateData(db, new TestUpdateUI());
+  }
+
+  private ImmutableList<GroupReference> getAllGroupsFromNoteDb()
+      throws IOException, ConfigInvalidException {
+    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
+      return GroupNameNotes.loadAllGroups(allUsersRepo);
+    }
+  }
+
+  @Nullable
+  private ObjectId getGroupSha1(AccountGroup.UUID groupUuid) throws IOException {
+    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
+      Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(groupUuid));
+      return ref != null ? ref.getObjectId() : null;
+    }
+  }
+}
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
new file mode 100644
index 0000000..9cd57e0
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
@@ -0,0 +1,1204 @@
+// 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 static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+import static com.google.gerrit.truth.OptionalSubject.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescription.Basic;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo.GroupMemberAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo.Type;
+import com.google.gerrit.extensions.common.GroupAuditEventInfo.UserMemberAuditEventInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.group.db.GroupConfig;
+import com.google.gerrit.server.group.db.GroupNameNotes;
+import com.google.gerrit.server.group.db.GroupsConsistencyChecker;
+import com.google.gerrit.server.group.testing.InternalGroupSubject;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.testing.InMemoryTestEnvironment;
+import com.google.gerrit.testing.TestTimeUtil;
+import com.google.gerrit.testing.TestTimeUtil.TempClockStep;
+import com.google.gerrit.testing.TestUpdateUI;
+import com.google.gerrit.truth.OptionalSubject;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.Month;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class Schema_166_to_167_WithGroupsInReviewDbTest {
+  private static Config createConfig() {
+    Config config = new Config();
+    config.setString(GerritServerIdProvider.SECTION, null, GerritServerIdProvider.KEY, "1234567");
+
+    // Enable groups in ReviewDb. This means the primary storage for groups is ReviewDb.
+    config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false);
+
+    return config;
+  }
+
+  @Rule
+  public InMemoryTestEnvironment testEnv =
+      new InMemoryTestEnvironment(Schema_166_to_167_WithGroupsInReviewDbTest::createConfig);
+
+  @Inject private GerritApi gApi;
+  @Inject private Schema_167 schema167;
+  @Inject private ReviewDb db;
+  @Inject private GitRepositoryManager gitRepoManager;
+  @Inject private AllUsersName allUsersName;
+  @Inject private GroupsConsistencyChecker consistencyChecker;
+  @Inject private IdentifiedUser currentUser;
+  @Inject private @GerritServerId String serverId;
+  @Inject private @GerritPersonIdent PersonIdent serverIdent;
+  @Inject private GroupBundle.Factory groupBundleFactory;
+  @Inject private GroupBackend groupBackend;
+  @Inject private DynamicSet<GroupBackend> backends;
+  @Inject private Sequences seq;
+
+  private JdbcSchema jdbcSchema;
+
+  @Before
+  public void initDb() throws Exception {
+    jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
+
+    try (Statement stmt = jdbcSchema.getConnection().createStatement()) {
+      stmt.execute(
+          "CREATE TABLE account_groups ("
+              + " group_uuid varchar(255) DEFAULT '' NOT NULL,"
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " name varchar(255) DEFAULT '' NOT NULL,"
+              + " created_on TIMESTAMP,"
+              + " description CLOB,"
+              + " owner_group_uuid varchar(255) DEFAULT '' NOT NULL,"
+              + " visible_to_all CHAR(1) DEFAULT 'N' NOT NULL"
+              + ")");
+
+      stmt.execute(
+          "CREATE TABLE account_group_members ("
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " account_id INTEGER DEFAULT 0 NOT NULL"
+              + ")");
+
+      stmt.execute(
+          "CREATE TABLE account_group_members_audit ("
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " account_id INTEGER DEFAULT 0 NOT NULL,"
+              + " added_by INTEGER DEFAULT 0 NOT NULL,"
+              + " added_on TIMESTAMP,"
+              + " removed_by INTEGER,"
+              + " removed_on TIMESTAMP"
+              + ")");
+
+      stmt.execute(
+          "CREATE TABLE account_group_by_id ("
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " include_uuid VARCHAR(255) DEFAULT '' NOT NULL"
+              + ")");
+
+      stmt.execute(
+          "CREATE TABLE account_group_by_id_aud ("
+              + " group_id INTEGER DEFAULT 0 NOT NULL,"
+              + " include_uuid VARCHAR(255) DEFAULT '' NOT NULL,"
+              + " added_by INTEGER DEFAULT 0 NOT NULL,"
+              + " added_on TIMESTAMP,"
+              + " removed_by INTEGER,"
+              + " removed_on TIMESTAMP"
+              + ")");
+    }
+  }
+
+  @Before
+  public void setTimeForTesting() {
+    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
+  @Test
+  public void reviewDbOnlyGroupsAreMigratedToNoteDb() throws Exception {
+    // Create groups only in ReviewDb
+    AccountGroup group1 = newGroup().setName("verifiers").build();
+    AccountGroup group2 = newGroup().setName("contributors").build();
+    storeInReviewDb(group1, group2);
+
+    executeSchemaMigration(schema167, group1, group2);
+
+    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
+    ImmutableList<String> groupNames =
+        groups.stream().map(GroupReference::getName).collect(toImmutableList());
+    assertThat(groupNames).containsAllOf("verifiers", "contributors");
+  }
+
+  @Test
+  public void alreadyExistingGroupsAreMigratedToNoteDb() throws Exception {
+    // Create group in NoteDb and ReviewDb
+    GroupInput groupInput = new GroupInput();
+    groupInput.name = "verifiers";
+    groupInput.description = "old";
+    GroupInfo group1 = gApi.groups().create(groupInput).get();
+    storeInReviewDb(group1);
+
+    // Update group only in ReviewDb
+    AccountGroup group1InReviewDb = getFromReviewDb(new AccountGroup.Id(group1.groupId));
+    group1InReviewDb.setDescription("new");
+    updateInReviewDb(group1InReviewDb);
+
+    // Create a second group in NoteDb and ReviewDb
+    GroupInfo group2 = gApi.groups().create("contributors").get();
+    storeInReviewDb(group2);
+
+    executeSchemaMigration(schema167, group1, group2);
+
+    // Verify that both groups are present in NoteDb
+    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
+    ImmutableList<String> groupNames =
+        groups.stream().map(GroupReference::getName).collect(toImmutableList());
+    assertThat(groupNames).containsAllOf("verifiers", "contributors");
+
+    // Verify that group1 has the description from ReviewDb
+    Optional<InternalGroup> group1InNoteDb = getGroupFromNoteDb(new AccountGroup.UUID(group1.id));
+    assertThatGroup(group1InNoteDb).value().description().isEqualTo("new");
+  }
+
+  @Test
+  public void adminGroupIsMigratedToNoteDb() throws Exception {
+    // Administrators group is automatically created for all Gerrit servers (NoteDb only).
+    GroupInfo adminGroup = gApi.groups().id("Administrators").get();
+    storeInReviewDb(adminGroup);
+
+    executeSchemaMigration(schema167, adminGroup);
+
+    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
+    ImmutableList<String> groupNames =
+        groups.stream().map(GroupReference::getName).collect(toImmutableList());
+    assertThat(groupNames).contains("Administrators");
+  }
+
+  @Test
+  public void nonInteractiveUsersGroupIsMigratedToNoteDb() throws Exception {
+    // 'Non-Interactive Users' group is automatically created for all Gerrit servers (NoteDb only).
+    GroupInfo nonInteractiveUsersGroup = gApi.groups().id("Non-Interactive Users").get();
+    storeInReviewDb(nonInteractiveUsersGroup);
+
+    executeSchemaMigration(schema167, nonInteractiveUsersGroup);
+
+    ImmutableList<GroupReference> groups = getAllGroupsFromNoteDb();
+    ImmutableList<String> groupNames =
+        groups.stream().map(GroupReference::getName).collect(toImmutableList());
+    assertThat(groupNames).contains("Non-Interactive Users");
+  }
+
+  @Test
+  public void groupsAreConsistentAfterMigrationToNoteDb() throws Exception {
+    // Administrators group are automatically created for all Gerrit servers (NoteDb only).
+    GroupInfo adminGroup = gApi.groups().id("Administrators").get();
+    GroupInfo nonInteractiveUsersGroup = gApi.groups().id("Non-Interactive Users").get();
+    storeInReviewDb(adminGroup, nonInteractiveUsersGroup);
+
+    AccountGroup group1 = newGroup().setName("verifiers").build();
+    AccountGroup group2 = newGroup().setName("contributors").build();
+    storeInReviewDb(group1, group2);
+
+    executeSchemaMigration(schema167, group1, group2);
+
+    List<ConsistencyCheckInfo.ConsistencyProblemInfo> consistencyProblems =
+        consistencyChecker.check();
+    assertThat(consistencyProblems).isEmpty();
+  }
+
+  @Test
+  public void nameIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup group = newGroup().setName("verifiers").build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().name().isEqualTo("verifiers");
+  }
+
+  @Test
+  public void emptyNameIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup group = newGroup().setName("").build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().name().isEqualTo("");
+  }
+
+  @Test
+  public void uuidIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup.UUID groupUuid = new AccountGroup.UUID("ABCDEF");
+    AccountGroup group = newGroup().setGroupUuid(groupUuid).build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(groupUuid);
+    assertThatGroup(groupInNoteDb).value().groupUuid().isEqualTo(groupUuid);
+  }
+
+  @Test
+  public void idIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup.Id id = new AccountGroup.Id(12345);
+    AccountGroup group = newGroup().setId(id).build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().id().isEqualTo(id);
+  }
+
+  @Test
+  public void createdOnIsKeptDuringMigrationToNoteDb() throws Exception {
+    Timestamp createdOn =
+        Timestamp.from(
+            LocalDate.of(2018, Month.FEBRUARY, 20)
+                .atTime(18, 2, 56)
+                .atZone(ZoneOffset.UTC)
+                .toInstant());
+    AccountGroup group = newGroup().setCreatedOn(createdOn).build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().createdOn().isEqualTo(createdOn);
+  }
+
+  @Test
+  public void ownerUuidIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID("UVWXYZ");
+    AccountGroup group = newGroup().setOwnerGroupUuid(ownerGroupUuid).build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().ownerGroupUuid().isEqualTo(ownerGroupUuid);
+  }
+
+  @Test
+  public void descriptionIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup group = newGroup().setDescription("A test group").build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().description().isEqualTo("A test group");
+  }
+
+  @Test
+  public void absentDescriptionIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup group = newGroup().build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().description().isNull();
+  }
+
+  @Test
+  public void visibleToAllIsKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup group = newGroup().setVisibleToAll(true).build();
+    storeInReviewDb(group);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().visibleToAll().isTrue();
+  }
+
+  @Test
+  public void membersAreKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup group = newGroup().build();
+    storeInReviewDb(group);
+    Account.Id member1 = new Account.Id(23456);
+    Account.Id member2 = new Account.Id(93483);
+    addMembersInReviewDb(group.getId(), member1, member2);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().members().containsExactly(member1, member2);
+  }
+
+  @Test
+  public void subgroupsAreKeptDuringMigrationToNoteDb() throws Exception {
+    AccountGroup group = newGroup().build();
+    storeInReviewDb(group);
+    AccountGroup.UUID subgroup1 = new AccountGroup.UUID("FGHIKL");
+    AccountGroup.UUID subgroup2 = new AccountGroup.UUID("MNOPQR");
+    addSubgroupsInReviewDb(group.getId(), subgroup1, subgroup2);
+
+    executeSchemaMigration(schema167, group);
+
+    Optional<InternalGroup> groupInNoteDb = getGroupFromNoteDb(group.getGroupUUID());
+    assertThatGroup(groupInNoteDb).value().subgroups().containsExactly(subgroup1, subgroup2);
+  }
+
+  @Test
+  public void logFormatWithAccountsAndGerritGroups() throws Exception {
+    AccountInfo user1 = createAccount("user1");
+    AccountInfo user2 = createAccount("user2");
+
+    AccountGroup group1 = createInReviewDb("group1");
+    AccountGroup group2 = createInReviewDb("group2");
+    AccountGroup group3 = createInReviewDb("group3");
+
+    // Add some accounts
+    try (TempClockStep step = TestTimeUtil.freezeClock()) {
+      addMembersInReviewDb(
+          group1.getId(), new Account.Id(user1._accountId), new Account.Id(user2._accountId));
+    }
+    TimeUtil.nowTs();
+
+    // Add some Gerrit groups
+    try (TempClockStep step = TestTimeUtil.freezeClock()) {
+      addSubgroupsInReviewDb(group1.getId(), group2.getGroupUUID(), group3.getGroupUUID());
+    }
+
+    executeSchemaMigration(schema167, group1, group2, group3);
+
+    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group1.getGroupUUID());
+
+    ImmutableList<CommitInfo> log = log(group1);
+    assertThat(log).hasSize(4);
+
+    // Verify commit that created the group
+    assertThat(log.get(0)).message().isEqualTo("Create group");
+    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
+    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
+    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
+    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
+    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
+
+    // Verify commit that the group creator as member
+    assertThat(log.get(1))
+        .message()
+        .isEqualTo(
+            "Update group\n\nAdd: "
+                + currentUser.getName()
+                + " <"
+                + currentUser.getAccountId()
+                + "@"
+                + serverId
+                + ">");
+    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
+
+    // Verify commit that added members
+    assertThat(log.get(2))
+        .message()
+        .isEqualTo(
+            "Update group\n"
+                + "\n"
+                + ("Add: user1 <" + user1._accountId + "@" + serverId + ">\n")
+                + ("Add: user2 <" + user2._accountId + "@" + serverId + ">"));
+    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
+
+    // Verify commit that added Gerrit groups
+    assertThat(log.get(3))
+        .message()
+        .isEqualTo(
+            "Update group\n"
+                + "\n"
+                + ("Add-group: " + group2.getName() + " <" + group2.getGroupUUID().get() + ">\n")
+                + ("Add-group: " + group3.getName() + " <" + group3.getGroupUUID().get() + ">"));
+    assertThat(log.get(3)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(3)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(3)).committer().hasSameDateAs(log.get(3).author);
+
+    // Verify that audit log is correctly read by Gerrit
+    List<? extends GroupAuditEventInfo> auditEvents =
+        gApi.groups().id(group1.getGroupUUID().get()).auditLog();
+    assertThat(auditEvents).hasSize(5);
+    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
+    assertMemberAuditEvent(
+        auditEvents.get(4), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
+    assertMemberAuditEvents(
+        auditEvents.get(3),
+        auditEvents.get(2),
+        Type.ADD_USER,
+        currentUser.getAccountId(),
+        user1,
+        user2);
+    assertSubgroupAuditEvents(
+        auditEvents.get(1),
+        auditEvents.get(0),
+        Type.ADD_GROUP,
+        currentUser.getAccountId(),
+        toGroupInfo(group2),
+        toGroupInfo(group3));
+  }
+
+  @Test
+  public void logFormatWithSystemGroups() throws Exception {
+    AccountGroup group = createInReviewDb("group");
+
+    try (TempClockStep step = TestTimeUtil.freezeClock()) {
+      addSubgroupsInReviewDb(
+          group.getId(), SystemGroupBackend.ANONYMOUS_USERS, SystemGroupBackend.REGISTERED_USERS);
+    }
+
+    executeSchemaMigration(schema167, group);
+
+    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group.getGroupUUID());
+
+    ImmutableList<CommitInfo> log = log(group);
+    assertThat(log).hasSize(3);
+
+    // Verify commit that created the group
+    assertThat(log.get(0)).message().isEqualTo("Create group");
+    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
+    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
+    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
+    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
+    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
+
+    // Verify commit that the group creator as member
+    assertThat(log.get(1))
+        .message()
+        .isEqualTo(
+            "Update group\n\nAdd: "
+                + currentUser.getName()
+                + " <"
+                + currentUser.getAccountId()
+                + "@"
+                + serverId
+                + ">");
+    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
+
+    // Verify commit that added system groups
+    assertThat(log.get(2))
+        .message()
+        .isEqualTo(
+            "Update group\n"
+                + "\n"
+                + "Add-group: Anonymous Users <global:Anonymous-Users>\n"
+                + "Add-group: Registered Users <global:Registered-Users>");
+    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
+
+    // Verify that audit log is correctly read by Gerrit
+    List<? extends GroupAuditEventInfo> auditEvents =
+        gApi.groups().id(group.getGroupUUID().get()).auditLog();
+    assertThat(auditEvents).hasSize(3);
+    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
+    assertMemberAuditEvent(
+        auditEvents.get(2), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
+    assertSubgroupAuditEvents(
+        auditEvents.get(1),
+        auditEvents.get(0),
+        Type.ADD_GROUP,
+        currentUser.getAccountId(),
+        groupInfoForExternalGroup(SystemGroupBackend.ANONYMOUS_USERS),
+        groupInfoForExternalGroup(SystemGroupBackend.REGISTERED_USERS));
+  }
+
+  @Test
+  public void logFormatWithExternalGroup() throws Exception {
+    AccountGroup group = createInReviewDb("group");
+
+    backends.add(new TestGroupBackend());
+    AccountGroup.UUID subgroupUuid = TestGroupBackend.createUuuid("foo");
+
+    assertThat(groupBackend.handles(subgroupUuid)).isTrue();
+    addSubgroupsInReviewDb(group.getId(), subgroupUuid);
+
+    executeSchemaMigration(schema167, group);
+
+    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group.getGroupUUID());
+
+    ImmutableList<CommitInfo> log = log(group);
+    assertThat(log).hasSize(3);
+
+    // Verify commit that created the group
+    assertThat(log.get(0)).message().isEqualTo("Create group");
+    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
+    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
+    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
+    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
+    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
+
+    // Verify commit that the group creator as member
+    assertThat(log.get(1))
+        .message()
+        .isEqualTo(
+            "Update group\n\nAdd: "
+                + currentUser.getName()
+                + " <"
+                + currentUser.getAccountId()
+                + "@"
+                + serverId
+                + ">");
+    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
+
+    // Verify commit that added system groups
+    // Note: The schema migration can only resolve names of Gerrit groups, not of external groups
+    // and system groups, hence the UUID shows up in commit messages where we would otherwise
+    // expect the group name.
+    assertThat(log.get(2))
+        .message()
+        .isEqualTo(
+            "Update group\n"
+                + "\n"
+                + "Add-group: "
+                + TestGroupBackend.PREFIX
+                + "foo <"
+                + TestGroupBackend.PREFIX
+                + "foo>");
+    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
+
+    // Verify that audit log is correctly read by Gerrit
+    List<? extends GroupAuditEventInfo> auditEvents =
+        gApi.groups().id(group.getGroupUUID().get()).auditLog();
+    assertThat(auditEvents).hasSize(2);
+    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
+    assertMemberAuditEvent(
+        auditEvents.get(1), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
+    assertSubgroupAuditEvent(
+        auditEvents.get(0),
+        Type.ADD_GROUP,
+        currentUser.getAccountId(),
+        groupInfoForExternalGroup(subgroupUuid));
+  }
+
+  @Test
+  public void logFormatWithNonExistingExternalGroup() throws Exception {
+    AccountGroup group = createInReviewDb("group");
+
+    AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("notExisting:foo");
+
+    assertThat(groupBackend.handles(subgroupUuid)).isFalse();
+    addSubgroupsInReviewDb(group.getId(), subgroupUuid);
+
+    executeSchemaMigration(schema167, group);
+
+    GroupBundle noteDbBundle = readGroupBundleFromNoteDb(group.getGroupUUID());
+
+    ImmutableList<CommitInfo> log = log(group);
+    assertThat(log).hasSize(3);
+
+    // Verify commit that created the group
+    assertThat(log.get(0)).message().isEqualTo("Create group");
+    assertThat(log.get(0)).author().name().isEqualTo(serverIdent.getName());
+    assertThat(log.get(0)).author().email().isEqualTo(serverIdent.getEmailAddress());
+    assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
+    assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.getTimeZoneOffset());
+    assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
+
+    // Verify commit that the group creator as member
+    assertThat(log.get(1))
+        .message()
+        .isEqualTo(
+            "Update group\n\nAdd: "
+                + currentUser.getName()
+                + " <"
+                + currentUser.getAccountId()
+                + "@"
+                + serverId
+                + ">");
+    assertThat(log.get(1)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(1)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
+
+    // Verify commit that added system groups
+    // Note: The schema migration can only resolve names of Gerrit groups, not of external groups
+    // and system groups, hence the UUID shows up in commit messages where we would otherwise
+    // expect the group name.
+    assertThat(log.get(2))
+        .message()
+        .isEqualTo("Update group\n" + "\n" + "Add-group: notExisting:foo <notExisting:foo>");
+    assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
+    assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
+    assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
+
+    // Verify that audit log is correctly read by Gerrit
+    List<? extends GroupAuditEventInfo> auditEvents =
+        gApi.groups().id(group.getGroupUUID().get()).auditLog();
+    assertThat(auditEvents).hasSize(2);
+    AccountInfo currentUserInfo = gApi.accounts().id(currentUser.getAccountId().get()).get();
+    assertMemberAuditEvent(
+        auditEvents.get(1), Type.ADD_USER, currentUser.getAccountId(), currentUserInfo);
+    assertSubgroupAuditEvent(
+        auditEvents.get(0),
+        Type.ADD_GROUP,
+        currentUser.getAccountId(),
+        groupInfoForExternalGroup(subgroupUuid));
+  }
+
+  private static TestGroup.Builder newGroup() {
+    return TestGroup.builder();
+  }
+
+  private AccountGroup createInReviewDb(String groupName) throws Exception {
+    AccountGroup group =
+        new AccountGroup(
+            new AccountGroup.NameKey(groupName),
+            new AccountGroup.Id(seq.nextGroupId()),
+            GroupUUID.make(groupName, serverIdent),
+            TimeUtil.nowTs());
+    storeInReviewDb(group);
+    addMembersInReviewDb(group.getId(), currentUser.getAccountId());
+    return group;
+  }
+
+  private void storeInReviewDb(GroupInfo... groups) throws Exception {
+    storeInReviewDb(
+        Arrays.stream(groups)
+            .map(Schema_166_to_167_WithGroupsInReviewDbTest::toAccountGroup)
+            .toArray(AccountGroup[]::new));
+  }
+
+  private void storeInReviewDb(AccountGroup... groups) throws Exception {
+    try (PreparedStatement stmt =
+        jdbcSchema
+            .getConnection()
+            .prepareStatement(
+                "INSERT INTO account_groups"
+                    + " (group_uuid,"
+                    + " group_id,"
+                    + " name,"
+                    + " description,"
+                    + " created_on,"
+                    + " owner_group_uuid,"
+                    + " visible_to_all) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
+      for (AccountGroup group : groups) {
+        stmt.setString(1, group.getGroupUUID().get());
+        stmt.setInt(2, group.getId().get());
+        stmt.setString(3, group.getName());
+        stmt.setString(4, group.getDescription());
+        stmt.setTimestamp(5, group.getCreatedOn());
+        stmt.setString(6, group.getOwnerGroupUUID().get());
+        stmt.setString(7, group.isVisibleToAll() ? "Y" : "N");
+        stmt.addBatch();
+      }
+      stmt.executeBatch();
+    }
+  }
+
+  private void updateInReviewDb(AccountGroup... groups) throws Exception {
+    try (PreparedStatement stmt =
+        jdbcSchema
+            .getConnection()
+            .prepareStatement(
+                "UPDATE account_groups SET"
+                    + " group_uuid = ?,"
+                    + " name = ?,"
+                    + " description = ?,"
+                    + " created_on = ?,"
+                    + " owner_group_uuid = ?,"
+                    + " visible_to_all = ?"
+                    + " WHERE group_id = ?")) {
+      for (AccountGroup group : groups) {
+        stmt.setString(1, group.getGroupUUID().get());
+        stmt.setString(2, group.getName());
+        stmt.setString(3, group.getDescription());
+        stmt.setTimestamp(4, group.getCreatedOn());
+        stmt.setString(5, group.getOwnerGroupUUID().get());
+        stmt.setString(6, group.isVisibleToAll() ? "Y" : "N");
+        stmt.setInt(7, group.getId().get());
+        stmt.addBatch();
+      }
+      stmt.executeBatch();
+    }
+  }
+
+  private AccountGroup getFromReviewDb(AccountGroup.Id groupId) throws Exception {
+    try (Statement stmt = jdbcSchema.getConnection().createStatement();
+        ResultSet rs =
+            stmt.executeQuery(
+                "SELECT group_uuid,"
+                    + " name,"
+                    + " description,"
+                    + " created_on,"
+                    + " owner_group_uuid,"
+                    + " visible_to_all"
+                    + " FROM account_groups"
+                    + " WHERE group_id = "
+                    + groupId.get())) {
+      if (!rs.next()) {
+        throw new OrmException(String.format("Group %s not found", groupId.get()));
+      }
+
+      AccountGroup.UUID groupUuid = new AccountGroup.UUID(rs.getString(1));
+      AccountGroup.NameKey groupName = new AccountGroup.NameKey(rs.getString(2));
+      String description = rs.getString(3);
+      Timestamp createdOn = rs.getTimestamp(4);
+      AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID(rs.getString(5));
+      boolean visibleToAll = "Y".equals(rs.getString(6));
+
+      AccountGroup group = new AccountGroup(groupName, groupId, groupUuid, createdOn);
+      group.setDescription(description);
+      group.setOwnerGroupUUID(ownerGroupUuid);
+      group.setVisibleToAll(visibleToAll);
+
+      if (rs.next()) {
+        throw new OrmException(String.format("Group ID %s is ambiguous", groupId.get()));
+      }
+
+      return group;
+    }
+  }
+
+  private void addMembersInReviewDb(AccountGroup.Id groupId, Account.Id... memberIds)
+      throws Exception {
+    try (PreparedStatement addMemberStmt =
+            jdbcSchema
+                .getConnection()
+                .prepareStatement(
+                    "INSERT INTO account_group_members"
+                        + " (group_id,"
+                        + " account_id) VALUES ("
+                        + groupId.get()
+                        + ", ?)");
+        PreparedStatement addMemberAuditStmt =
+            jdbcSchema
+                .getConnection()
+                .prepareStatement(
+                    "INSERT INTO account_group_members_audit"
+                        + " (group_id,"
+                        + " account_id,"
+                        + " added_by,"
+                        + " added_on) VALUES ("
+                        + groupId.get()
+                        + ", ?, "
+                        + currentUser.getAccountId().get()
+                        + ", ?)")) {
+      Timestamp addedOn = TimeUtil.nowTs();
+      for (Account.Id memberId : memberIds) {
+        addMemberStmt.setInt(1, memberId.get());
+        addMemberStmt.addBatch();
+
+        addMemberAuditStmt.setInt(1, memberId.get());
+        addMemberAuditStmt.setTimestamp(2, addedOn);
+        addMemberAuditStmt.addBatch();
+      }
+      addMemberStmt.executeBatch();
+      addMemberAuditStmt.executeBatch();
+    }
+  }
+
+  private void addSubgroupsInReviewDb(AccountGroup.Id groupId, AccountGroup.UUID... subgroupUuids)
+      throws Exception {
+    try (PreparedStatement addSubGroupStmt =
+            jdbcSchema
+                .getConnection()
+                .prepareStatement(
+                    "INSERT INTO account_group_by_id"
+                        + " (group_id,"
+                        + " include_uuid) VALUES ("
+                        + groupId.get()
+                        + ", ?)");
+        PreparedStatement addSubGroupAuditStmt =
+            jdbcSchema
+                .getConnection()
+                .prepareStatement(
+                    "INSERT INTO account_group_by_id_aud"
+                        + " (group_id,"
+                        + " include_uuid,"
+                        + " added_by,"
+                        + " added_on) VALUES ("
+                        + groupId.get()
+                        + ", ?, "
+                        + currentUser.getAccountId().get()
+                        + ", ?)")) {
+      Timestamp addedOn = TimeUtil.nowTs();
+      for (AccountGroup.UUID subgroupUuid : subgroupUuids) {
+        addSubGroupStmt.setString(1, subgroupUuid.get());
+        addSubGroupStmt.addBatch();
+
+        addSubGroupAuditStmt.setString(1, subgroupUuid.get());
+        addSubGroupAuditStmt.setTimestamp(2, addedOn);
+        addSubGroupAuditStmt.addBatch();
+      }
+      addSubGroupStmt.executeBatch();
+      addSubGroupAuditStmt.executeBatch();
+    }
+  }
+
+  private AccountInfo createAccount(String name) throws RestApiException {
+    AccountInput accountInput = new AccountInput();
+    accountInput.username = name;
+    accountInput.name = name;
+    return gApi.accounts().create(accountInput).get();
+  }
+
+  private GroupBundle readGroupBundleFromNoteDb(AccountGroup.UUID groupUuid) throws Exception {
+    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
+      return groupBundleFactory.fromNoteDb(allUsersRepo, groupUuid);
+    }
+  }
+
+  private void executeSchemaMigration(SchemaVersion schema, AccountGroup... groupsToVerify)
+      throws Exception {
+    executeSchemaMigration(
+        schema,
+        Arrays.stream(groupsToVerify)
+            .map(AccountGroup::getGroupUUID)
+            .toArray(AccountGroup.UUID[]::new));
+  }
+
+  private void executeSchemaMigration(SchemaVersion schema, GroupInfo... groupsToVerify)
+      throws Exception {
+    executeSchemaMigration(
+        schema,
+        Arrays.stream(groupsToVerify)
+            .map(i -> new AccountGroup.UUID(i.id))
+            .toArray(AccountGroup.UUID[]::new));
+  }
+
+  private void executeSchemaMigration(SchemaVersion schema, AccountGroup.UUID... groupsToVerify)
+      throws Exception {
+    List<GroupBundle> reviewDbBundles = new ArrayList<>();
+    for (AccountGroup.UUID groupUuid : groupsToVerify) {
+      reviewDbBundles.add(GroupBundle.Factory.fromReviewDb(db, groupUuid));
+    }
+
+    schema.migrateData(db, new TestUpdateUI());
+
+    for (GroupBundle reviewDbBundle : reviewDbBundles) {
+      assertMigratedCleanly(readGroupBundleFromNoteDb(reviewDbBundle.uuid()), reviewDbBundle);
+    }
+  }
+
+  private void assertMigratedCleanly(GroupBundle noteDbBundle, GroupBundle expectedReviewDbBundle) {
+    assertThat(GroupBundle.compareWithAudits(expectedReviewDbBundle, noteDbBundle)).isEmpty();
+  }
+
+  private ImmutableList<CommitInfo> log(AccountGroup group) throws Exception {
+    ImmutableList.Builder<CommitInfo> result = ImmutableList.builder();
+    List<Date> commitDates = new ArrayList<>();
+    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(allUsersRepo)) {
+      Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(group.getGroupUUID()));
+      if (ref != null) {
+        rw.sort(RevSort.REVERSE);
+        rw.setRetainBody(true);
+        rw.markStart(rw.parseCommit(ref.getObjectId()));
+        for (RevCommit c : rw) {
+          result.add(CommitUtil.toCommitInfo(c));
+          commitDates.add(c.getCommitterIdent().getWhen());
+        }
+      }
+    }
+    assertThat(commitDates).named("commit timestamps for %s", result).isOrdered();
+    return result.build();
+  }
+
+  private ImmutableList<GroupReference> getAllGroupsFromNoteDb()
+      throws IOException, ConfigInvalidException {
+    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
+      return GroupNameNotes.loadAllGroups(allUsersRepo);
+    }
+  }
+
+  private Optional<InternalGroup> getGroupFromNoteDb(AccountGroup.UUID groupUuid) throws Exception {
+    try (Repository allUsersRepo = gitRepoManager.openRepository(allUsersName)) {
+      return GroupConfig.loadForGroup(allUsersRepo, groupUuid).getLoadedGroup();
+    }
+  }
+
+  private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
+      Optional<InternalGroup> group) {
+    return assertThat(group, InternalGroupSubject::assertThat).named("group");
+  }
+
+  private void assertMemberAuditEvent(
+      GroupAuditEventInfo info,
+      Type expectedType,
+      Account.Id expectedUser,
+      AccountInfo expectedMember) {
+    assertThat(info.user._accountId).isEqualTo(expectedUser.get());
+    assertThat(info.type).isEqualTo(expectedType);
+    assertThat(info).isInstanceOf(UserMemberAuditEventInfo.class);
+    assertAccount(((UserMemberAuditEventInfo) info).member, expectedMember);
+  }
+
+  private void assertMemberAuditEvents(
+      GroupAuditEventInfo info1,
+      GroupAuditEventInfo info2,
+      Type expectedType,
+      Account.Id expectedUser,
+      AccountInfo expectedMember1,
+      AccountInfo expectedMember2) {
+    assertThat(info1).isInstanceOf(UserMemberAuditEventInfo.class);
+    assertThat(info2).isInstanceOf(UserMemberAuditEventInfo.class);
+
+    UserMemberAuditEventInfo event1 = (UserMemberAuditEventInfo) info1;
+    UserMemberAuditEventInfo event2 = (UserMemberAuditEventInfo) info2;
+
+    assertThat(event1.member._accountId)
+        .isAnyOf(expectedMember1._accountId, expectedMember2._accountId);
+    assertThat(event2.member._accountId)
+        .isAnyOf(expectedMember1._accountId, expectedMember2._accountId);
+    assertThat(event1.member._accountId).isNotEqualTo(event2.member._accountId);
+
+    if (event1.member._accountId == expectedMember1._accountId) {
+      assertMemberAuditEvent(info1, expectedType, expectedUser, expectedMember1);
+      assertMemberAuditEvent(info2, expectedType, expectedUser, expectedMember2);
+    } else {
+      assertMemberAuditEvent(info1, expectedType, expectedUser, expectedMember2);
+      assertMemberAuditEvent(info2, expectedType, expectedUser, expectedMember1);
+    }
+  }
+
+  private void assertSubgroupAuditEvent(
+      GroupAuditEventInfo info,
+      Type expectedType,
+      Account.Id expectedUser,
+      GroupInfo expectedSubGroup) {
+    assertThat(info.user._accountId).isEqualTo(expectedUser.get());
+    assertThat(info.type).isEqualTo(expectedType);
+    assertThat(info).isInstanceOf(GroupMemberAuditEventInfo.class);
+    assertGroup(((GroupMemberAuditEventInfo) info).member, expectedSubGroup);
+  }
+
+  private void assertSubgroupAuditEvents(
+      GroupAuditEventInfo info1,
+      GroupAuditEventInfo info2,
+      Type expectedType,
+      Account.Id expectedUser,
+      GroupInfo expectedSubGroup1,
+      GroupInfo expectedSubGroup2) {
+    assertThat(info1).isInstanceOf(GroupMemberAuditEventInfo.class);
+    assertThat(info2).isInstanceOf(GroupMemberAuditEventInfo.class);
+
+    GroupMemberAuditEventInfo event1 = (GroupMemberAuditEventInfo) info1;
+    GroupMemberAuditEventInfo event2 = (GroupMemberAuditEventInfo) info2;
+
+    assertThat(event1.member.id).isAnyOf(expectedSubGroup1.id, expectedSubGroup2.id);
+    assertThat(event2.member.id).isAnyOf(expectedSubGroup1.id, expectedSubGroup2.id);
+    assertThat(event1.member.id).isNotEqualTo(event2.member.id);
+
+    if (event1.member.id.equals(expectedSubGroup1.id)) {
+      assertSubgroupAuditEvent(info1, expectedType, expectedUser, expectedSubGroup1);
+      assertSubgroupAuditEvent(info2, expectedType, expectedUser, expectedSubGroup2);
+    } else {
+      assertSubgroupAuditEvent(info1, expectedType, expectedUser, expectedSubGroup2);
+      assertSubgroupAuditEvent(info2, expectedType, expectedUser, expectedSubGroup1);
+    }
+  }
+
+  private void assertAccount(AccountInfo actual, AccountInfo expected) {
+    assertThat(actual._accountId).isEqualTo(expected._accountId);
+    assertThat(actual.name).isEqualTo(expected.name);
+    assertThat(actual.email).isEqualTo(expected.email);
+    assertThat(actual.username).isEqualTo(expected.username);
+  }
+
+  private void assertGroup(GroupInfo actual, GroupInfo expected) {
+    assertThat(actual.id).isEqualTo(expected.id);
+    assertThat(actual.name).isEqualTo(expected.name);
+    assertThat(actual.groupId).isEqualTo(expected.groupId);
+  }
+
+  private GroupInfo groupInfoForExternalGroup(AccountGroup.UUID groupUuid) {
+    GroupInfo groupInfo = new GroupInfo();
+    groupInfo.id = IdString.fromDecoded(groupUuid.get()).encoded();
+
+    if (groupBackend.handles(groupUuid)) {
+      groupInfo.name = groupBackend.get(groupUuid).getName();
+    }
+
+    return groupInfo;
+  }
+
+  private static AccountGroup toAccountGroup(GroupInfo info) {
+    AccountGroup group =
+        new AccountGroup(
+            new AccountGroup.NameKey(info.name),
+            new AccountGroup.Id(info.groupId),
+            new AccountGroup.UUID(info.id),
+            info.createdOn);
+    group.setDescription(info.description);
+    if (info.ownerId != null) {
+      group.setOwnerGroupUUID(new AccountGroup.UUID(info.ownerId));
+    }
+    group.setVisibleToAll(
+        info.options != null && info.options.visibleToAll != null && info.options.visibleToAll);
+    return group;
+  }
+
+  private static GroupInfo toGroupInfo(AccountGroup group) {
+    GroupInfo groupInfo = new GroupInfo();
+    groupInfo.id = group.getGroupUUID().get();
+    groupInfo.groupId = group.getId().get();
+    groupInfo.name = group.getName();
+    groupInfo.createdOn = group.getCreatedOn();
+    groupInfo.description = group.getDescription();
+    groupInfo.owner = group.getOwnerGroupUUID().get();
+    groupInfo.options = new GroupOptionsInfo();
+    groupInfo.options.visibleToAll = group.isVisibleToAll() ? true : null;
+    return groupInfo;
+  }
+
+  private static class TestGroupBackend implements GroupBackend {
+    static final String PREFIX = "testbackend:";
+
+    static AccountGroup.UUID createUuuid(String name) {
+      return new AccountGroup.UUID(PREFIX + name);
+    }
+
+    @Override
+    public Collection<GroupReference> suggest(String name, ProjectState project) {
+      return ImmutableSet.of();
+    }
+
+    @Override
+    public GroupMembership membershipsOf(IdentifiedUser user) {
+      return new GroupMembership() {
+        @Override
+        public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
+          return ImmutableSet.of();
+        }
+
+        @Override
+        public Set<AccountGroup.UUID> getKnownGroups() {
+          return ImmutableSet.of();
+        }
+
+        @Override
+        public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
+          return false;
+        }
+
+        @Override
+        public boolean contains(AccountGroup.UUID groupId) {
+          return false;
+        }
+      };
+    }
+
+    @Override
+    public boolean isVisibleToAll(AccountGroup.UUID uuid) {
+      return false;
+    }
+
+    @Override
+    public boolean handles(AccountGroup.UUID uuid) {
+      return uuid.get().startsWith(PREFIX);
+    }
+
+    @Override
+    public Basic get(AccountGroup.UUID uuid) {
+      return new GroupDescription.Basic() {
+        @Override
+        public AccountGroup.UUID getGroupUUID() {
+          return uuid;
+        }
+
+        @Override
+        public String getName() {
+          return uuid.get().substring(PREFIX.length());
+        }
+
+        @Override
+        public String getEmailAddress() {
+          return null;
+        }
+
+        @Override
+        public String getUrl() {
+          return null;
+        }
+      };
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/TestGroup.java b/javatests/com/google/gerrit/server/schema/TestGroup.java
new file mode 100644
index 0000000..49cf028
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/TestGroup.java
@@ -0,0 +1,81 @@
+// 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.auto.value.AutoValue;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.sql.Timestamp;
+import java.util.Optional;
+import org.junit.Ignore;
+
+@AutoValue
+@Ignore
+public abstract class TestGroup {
+  abstract Optional<AccountGroup.NameKey> getNameKey();
+
+  abstract Optional<AccountGroup.UUID> getGroupUuid();
+
+  abstract Optional<AccountGroup.Id> getId();
+
+  abstract Optional<Timestamp> getCreatedOn();
+
+  abstract Optional<AccountGroup.UUID> getOwnerGroupUuid();
+
+  abstract Optional<String> getDescription();
+
+  abstract boolean isVisibleToAll();
+
+  public static Builder builder() {
+    return new AutoValue_TestGroup.Builder().setVisibleToAll(false);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder setNameKey(AccountGroup.NameKey nameKey);
+
+    public Builder setName(String name) {
+      return setNameKey(new AccountGroup.NameKey(name));
+    }
+
+    public abstract Builder setGroupUuid(AccountGroup.UUID uuid);
+
+    public abstract Builder setId(AccountGroup.Id id);
+
+    public abstract Builder setCreatedOn(Timestamp createdOn);
+
+    public abstract Builder setOwnerGroupUuid(AccountGroup.UUID ownerGroupUuid);
+
+    public abstract Builder setDescription(String description);
+
+    public abstract Builder setVisibleToAll(boolean visibleToAll);
+
+    public abstract TestGroup autoBuild();
+
+    public AccountGroup build() {
+      TestGroup testGroup = autoBuild();
+      AccountGroup.NameKey name = testGroup.getNameKey().orElse(new AccountGroup.NameKey("users"));
+      AccountGroup.Id id = testGroup.getId().orElse(new AccountGroup.Id(Math.abs(name.hashCode())));
+      AccountGroup.UUID uuid =
+          testGroup.getGroupUuid().orElse(new AccountGroup.UUID(name + "-UUID"));
+      Timestamp createdOn = testGroup.getCreatedOn().orElseGet(TimeUtil::nowTs);
+      AccountGroup accountGroup = new AccountGroup(name, id, uuid, createdOn);
+      testGroup.getOwnerGroupUuid().ifPresent(accountGroup::setOwnerGroupUUID);
+      testGroup.getDescription().ifPresent(accountGroup::setDescription);
+      accountGroup.setVisibleToAll(testGroup.isVisibleToAll());
+      return accountGroup;
+    }
+  }
+}
diff --git a/lib/LICENSE-ba-linkify b/lib/LICENSE-ba-linkify
new file mode 100644
index 0000000..93672f9
--- /dev/null
+++ b/lib/LICENSE-ba-linkify
@@ -0,0 +1,22 @@
+Copyright (c) 2009 "Cowboy" Ben Alman
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/ba-linkify/BUILD b/lib/ba-linkify/BUILD
new file mode 100644
index 0000000..9a8b442
--- /dev/null
+++ b/lib/ba-linkify/BUILD
@@ -0,0 +1 @@
+exports_files(["ba-linkify.js"])
diff --git a/lib/ba-linkify/README.md b/lib/ba-linkify/README.md
new file mode 100644
index 0000000..8c3e9a4
--- /dev/null
+++ b/lib/ba-linkify/README.md
@@ -0,0 +1,6 @@
+This is the latest version of ba-linkify.js from:
+https://github.com/cowboy/javascript-linkify/blob/178ffc271f89cef403faf73cabd74dda0a79af62/ba-linkify.js
+
+The file was modified manually to include a @license JSDoc tag. The file hasn't
+been updated since 2009, but on the off chance you need to update it, please
+make sure you include a @license.
diff --git a/lib/ba-linkify/ba-linkify.js b/lib/ba-linkify/ba-linkify.js
new file mode 100644
index 0000000..32fbea3
--- /dev/null
+++ b/lib/ba-linkify/ba-linkify.js
@@ -0,0 +1,239 @@
+/**
+ * @license
+ * Copyright (c) 2009 "Cowboy" Ben Alman
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+/*!
+ * JavaScript Linkify - v0.3 - 6/27/2009
+ * http://benalman.com/projects/javascript-linkify/
+ * 
+ * Copyright (c) 2009 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ * 
+ * Some regexps adapted from http://userscripts.org/scripts/review/7122
+ */
+
+// Script: JavaScript Linkify: Process links in text!
+//
+// *Version: 0.3, Last updated: 6/27/2009*
+// 
+// Project Home - http://benalman.com/projects/javascript-linkify/
+// GitHub       - http://github.com/cowboy/javascript-linkify/
+// Source       - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.js
+// (Minified)   - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.min.js (2.8kb)
+// 
+// About: License
+// 
+// Copyright (c) 2009 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+// 
+// About: Examples
+// 
+// This working example, complete with fully commented code, illustrates one way
+// in which this code can be used.
+// 
+// Linkify - http://benalman.com/code/projects/javascript-linkify/examples/linkify/
+// 
+// About: Support and Testing
+// 
+// Information about what browsers this code has been tested in.
+// 
+// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.7, Safari 3-4, Chrome, Opera 9.6-10.
+// 
+// About: Release History
+// 
+// 0.3 - (6/27/2009) Initial release
+
+// Function: linkify
+// 
+// Turn text into linkified html.
+// 
+// Usage:
+// 
+//  > var html = linkify( text [, options ] );
+// 
+// Arguments:
+// 
+//  text - (String) Non-HTML text containing links to be parsed.
+//  options - (Object) An optional object containing linkify parse options.
+// 
+// Options:
+// 
+//  callback (Function) - If specified, this will be called once for each link-
+//    or non-link-chunk with two arguments, text and href. If the chunk is
+//    non-link, href will be omitted. If unspecified, the default linkification
+//    callback is used.
+//  punct_regexp (RegExp) - A RegExp that will be used to trim trailing
+//    punctuation from links, instead of the default. If set to null, trailing
+//    punctuation will not be trimmed.
+// 
+// Returns:
+// 
+//  (String) An HTML string containing links.
+
+window.linkify = (function(){
+  var
+    SCHEME = "[a-z\\d.-]+://",
+    IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",
+    HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",
+    TLD = "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)",
+    HOST_OR_IP = "(?:" + HOSTNAME + TLD + "|" + IPV4 + ")",
+    PATH = "(?:[;/][^#?<>\\s]*)?",
+    QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",
+    URI1 = "\\b" + SCHEME + "[^<>\\s]+",
+    URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)",
+    
+    MAILTO = "mailto:",
+    EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)",
+    
+    URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ),
+    SCHEME_RE = new RegExp( "^" + SCHEME, "i" ),
+    
+    quotes = {
+      "'": "`",
+      '>': '<',
+      ')': '(',
+      ']': '[',
+      '}': '{',
+      '»': '«',
+      '›': '‹'
+    },
+    
+    default_options = {
+      callback: function( text, href ) {
+        return href ? '<a href="' + href + '" title="' + href + '">' + text + '</a>' : text;
+      },
+      punct_regexp: /(?:[!?.,:;'"]|(?:&|&amp;)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/
+    };
+  
+  return function( txt, options ) {
+    options = options || {};
+    
+    // Temp variables.
+    var arr,
+      i,
+      link,
+      href,
+      
+      // Output HTML.
+      html = '',
+      
+      // Store text / link parts, in order, for re-combination.
+      parts = [],
+      
+      // Used for keeping track of indices in the text.
+      idx_prev,
+      idx_last,
+      idx,
+      link_last,
+      
+      // Used for trimming trailing punctuation and quotes from links.
+      matches_begin,
+      matches_end,
+      quote_begin,
+      quote_end;
+    
+    // Initialize options.
+    for ( i in default_options ) {
+      if ( options[ i ] === undefined ) {
+        options[ i ] = default_options[ i ];
+      }
+    }
+    
+    // Find links.
+    while ( arr = URI_RE.exec( txt ) ) {
+      
+      link = arr[0];
+      idx_last = URI_RE.lastIndex;
+      idx = idx_last - link.length;
+      
+      // Not a link if preceded by certain characters.
+      if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) {
+        continue;
+      }
+      
+      // Trim trailing punctuation.
+      do {
+        // If no changes are made, we don't want to loop forever!
+        link_last = link;
+        
+        quote_end = link.substr( -1 )
+        quote_begin = quotes[ quote_end ];
+        
+        // Ending quote character?
+        if ( quote_begin ) {
+          matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) );
+          matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) );
+          
+          // If quotes are unbalanced, remove trailing quote character.
+          if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) {
+            link = link.substr( 0, link.length - 1 );
+            idx_last--;
+          }
+        }
+        
+        // Ending non-quote punctuation character?
+        if ( options.punct_regexp ) {
+          link = link.replace( options.punct_regexp, function(a){
+            idx_last -= a.length;
+            return '';
+          });
+        }
+      } while ( link.length && link !== link_last );
+      
+      href = link;
+      
+      // Add appropriate protocol to naked links.
+      if ( !SCHEME_RE.test( href ) ) {
+        href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO )
+          : !href.indexOf( 'irc.' ) ? 'irc://'
+          : !href.indexOf( 'ftp.' ) ? 'ftp://'
+          : 'http://' )
+          + href;
+      }
+      
+      // Push preceding non-link text onto the array.
+      if ( idx_prev != idx ) {
+        parts.push([ txt.slice( idx_prev, idx ) ]);
+        idx_prev = idx_last;
+      }
+      
+      // Push massaged link onto the array
+      parts.push([ link, href ]);
+    };
+    
+    // Push remaining non-link text onto the array.
+    parts.push([ txt.substr( idx_prev ) ]);
+    
+    // Process the array items.
+    for ( i = 0; i < parts.length; i++ ) {
+      html += options.callback.apply( window, parts[i] );
+    }
+    
+    // In case of catastrophic failure, return the original text;
+    return html || txt;
+  };
+  
+})();
diff --git a/lib/js/BUILD b/lib/js/BUILD
index 0bb7b0c..8a7986e 100644
--- a/lib/js/BUILD
+++ b/lib/js/BUILD
@@ -24,18 +24,18 @@
 
 define_bower_components()
 
-js_component(
-    name = "highlightjs",
-    srcs = ["//lib/highlightjs:highlight.min.js"],
-    license = "//lib:LICENSE-highlightjs",
-)
-
 filegroup(
     name = "highlightjs_files",
     srcs = ["//lib/highlightjs:highlight.min.js"],
     data = ["//lib:LICENSE-highlightjs"],
 )
 
+js_component(
+    name = "ba-linkify",
+    srcs = ["//lib/ba-linkify:ba-linkify.js"],
+    license = "//lib:LICENSE-ba-linkify",
+)
+
 bower_component(
     name = "codemirror-minified",
     license = "//lib:LICENSE-codemirror-minified",
diff --git a/plugins/replication b/plugins/replication
index d8f5bce..a62d1c6 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit d8f5bcec21492cc21e2499c332298a94ff8defc6
+Subproject commit a62d1c601e6c7fb669c847a0e1843e6f60cd1cb2
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 7487ad5..b338cbf 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -8,10 +8,9 @@
 bower_component_bundle(
     name = "polygerrit_components.bower_components",
     deps = [
+        "//lib/js:ba-linkify",
         "//lib/js:es6-promise",
         "//lib/js:fetch",
-        # TODO(hanwen): this is inserted separately in the UI zip. Do we need this here?
-        "//lib/js:highlightjs",
         "//lib/js:iron-a11y-keys-behavior",
         "//lib/js:iron-autogrow-textarea",
         "//lib/js:iron-dropdown",
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
index c15df90..47f47ec 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
@@ -32,44 +32,60 @@
 <dom-module id="gr-repo-access">
   <template>
     <style include="shared-styles">
-      gr-button {
+      gr-button,
+      #inheritsFrom,
+      #editInheritFromInput,
+      .editing #inheritFromName,
+      .weblinks,
+      #loadingText,
+      .loading {
         display: none;
       }
-      .admin gr-button.visible {
-        display: inline-block;
-        margin: 1em 0;
+      #inheritsFrom.show {
+        display: flex;
+        min-height: 2em;
+        align-items: center;
       }
       .weblink {
         margin-right: .2em;
       }
-      .weblinks {
-        display: none;
-      }
-      .weblinks.show {
+      .weblinks.show,
+      #loadingText.loading,
+      .referenceContainer {
         display: block;
       }
-      .loading {
-        display: none;
+      .rightsText {
+        margin-right: .3rem;
       }
-      #loading.loading {
-        display: block;
+
+      .editing gr-button,
+      .admin #editBtn {
+        display: inline-block;
+        margin: 1em 0;
       }
-      #loading:not(.loading) {
-        display: none;
+      .editing #editInheritFromInput {
+        display: inline-block;
       }
     </style>
     <style include="gr-menu-page-styles"></style>
-    <main class$="[[_computeAdminClass(_isAdmin, _canUpload)]]">
-      <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
+    <main class$="[[_computeMainClass(_isAdmin, _canUpload, _editing)]]">
+      <div id="loadingText" class$="[[_computeLoadingClass(_loading)]]">
         Loading...
       </div>
       <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-        <template is="dom-if" if="[[_inheritsFrom]]">
-          <h3 id="inheritsFrom">Rights Inherit From
-            <a href$="[[_computeParentHref(_inheritsFrom.name)]]" rel="noopener">
-                [[_inheritsFrom.name]]</a>
-          </h3>
-        </template>
+        <h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]">
+          <span class="rightsText">Rights Inherit From</span>
+          <a
+              href$="[[_computeParentHref(_inheritsFrom.name)]]"
+              rel="noopener"
+              id="inheritFromName">
+            [[_inheritsFrom.name]]</a>
+          <gr-autocomplete
+              id="editInheritFromInput"
+              text="{{_inheritFromFilter}}"
+              query="[[_query]]"
+              on-commit="_handleUpdateInheritFrom"></gr-autocomplete>
+        </h3>
         <div class$="weblinks [[_computeWebLinkClass(_weblinks)]]">
           History:
           <template is="dom-repeat" items="[[_weblinks]]" as="link">
@@ -79,11 +95,9 @@
           </template>
         </div>
         <gr-button id="editBtn"
-            class="visible"
             on-tap="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
         <gr-button id="saveBtn"
             primary
-            class$="[[_computeShowSaveClass(_editing)]]"
             on-tap="_handleSaveForReview"
             disabled$="[[!_modified]]">
               Save for review</gr-button>
@@ -98,9 +112,10 @@
               editing="[[_editing]]"
               groups="[[_groups]]"></gr-access-section>
         </template>
-        <gr-button id="addReferenceBtn"
-            class$="[[_computeShowSaveClass(_editing)]]"
-            on-tap="_handleCreateSection">Add Reference</gr-button>
+        <div class="referenceContainer">
+          <gr-button id="addReferenceBtn"
+              on-tap="_handleCreateSection">Add Reference</gr-button>
+        </div>
       </div>
     </main>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index a8d21e2..1982b9a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -21,6 +21,8 @@
 
   const NOTHING_TO_SAVE = 'No changes to save.';
 
+  const MAX_AUTOCOMPLETE_RESULTS = 20;
+
   /**
    * Fired when save is a no-op
    *
@@ -85,6 +87,13 @@
         type: Boolean,
         value: false,
       },
+      _inheritFromFilter: String,
+      _query: {
+        type: Function,
+        value() {
+          return this._getInheritFromSuggestions.bind(this);
+        },
+      },
       _ownerOf: Array,
       _capabilities: Object,
       _groups: Object,
@@ -142,7 +151,18 @@
           .then(res => {
             if (!res) { return Promise.resolve(); }
 
-            this._inheritsFrom = res.inherits_from;
+            // Keep a copy of the original inherit from values separate from
+            // the ones data bound to gr-autocomplete, so the original value
+            // can be restored if the user cancels.
+            this._inheritsFrom = res.inherits_from ? Object.assign({},
+                res.inherits_from) : null;
+            this._originalInheritsFrom = res.inherits_from ? Object.assign({},
+                res.inherits_from) : null;
+            // Initialize the filter value so when the user clicks edit, the
+            // current value appears. If there is no parent repo, it is
+            // initialized as an empty string.
+            this._inheritFromFilter = res.inherits_from ?
+                this._inheritsFrom.name : '';
             this._local = res.local;
             this._groups = res.groups;
             this._weblinks = res.config_web_links || [];
@@ -176,6 +196,33 @@
       });
     },
 
+    _handleUpdateInheritFrom(e) {
+      const projectId = decodeURIComponent(e.detail.value);
+      if (!this._inheritsFrom) {
+        this._inheritsFrom = {};
+      }
+      this._inheritsFrom.id = projectId;
+      this._inheritsFrom.name = this._inheritFromFilter;
+      this._handleAccessModified();
+    },
+
+    _getInheritFromSuggestions() {
+      return this.$.restAPI.getRepos(
+          this._inheritFromFilter,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(response => {
+            const projects = [];
+            for (const key in response) {
+              if (!response.hasOwnProperty(key)) { continue; }
+              projects.push({
+                name: key,
+                value: response[key].id,
+              });
+            }
+            return projects;
+          });
+    },
+
     _computeLoadingClass(loading) {
       return loading ? 'loading' : '';
     },
@@ -192,11 +239,22 @@
       return weblinks.length ? 'show' : '';
     },
 
+    _computeShowInherit(inheritsFrom) {
+      return inheritsFrom ? 'show' : '';
+    },
+
     _handleEditingChanged(editing, editingOld) {
       // Ignore when editing gets set initially.
       if (!editingOld || editing) { return; }
       // Remove any unsaved but added refs.
-      this._sections = this._sections.filter(p => !p.value.added);
+      if (this._sections) {
+        this._sections = this._sections.filter(p => !p.value.added);
+      }
+      // Restore inheritFrom.
+      if (this._inheritsFrom) {
+        this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
+        this._inheritFromFilter = this._inheritsFrom.name;
+      }
       for (const key of Object.keys(this._local)) {
         if (this._local[key].added) {
           delete this._local[key];
@@ -297,7 +355,19 @@
         remove: {},
       };
 
+      const inheritFromChanged =
+          // Inherit from changed
+          (this._originalInheritsFrom &&
+          this._originalInheritsFrom.id !== this._inheritsFrom.id) ||
+          // Inherit froma dded (did not have one initially);
+          (!this._originalInheritsFrom && this._inheritsFrom
+              && this._inheritsFrom.id);
+
       this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
+
+      if (inheritFromChanged) {
+        addRemoveObj.parent = this._inheritsFrom.id;
+      }
       return addRemoveObj;
     },
 
@@ -318,20 +388,25 @@
 
     _handleSaveForReview() {
       const addRemoveObj = this._computeAddAndRemove();
-
       // If there are no changes, don't actually save.
       if (!Object.keys(addRemoveObj.add).length &&
-          !Object.keys(addRemoveObj.remove).length) {
+          !Object.keys(addRemoveObj.remove).length &&
+          !addRemoveObj.parent) {
         this.dispatchEvent(new CustomEvent('show-alert',
             {detail: {message: NOTHING_TO_SAVE}, bubbles: true}));
         return;
       }
-      return this.$.restAPI.setProjectAccessRightsForReview(this.repo, {
+      const obj = {
         add: addRemoveObj.add,
         remove: addRemoveObj.remove,
-      }).then(change => {
-        Gerrit.Nav.navigateToChange(change);
-      });
+      };
+      if (addRemoveObj.parent) {
+        obj.parent = addRemoveObj.parent;
+      }
+      return this.$.restAPI.setProjectAccessRightsForReview(this.repo, obj)
+          .then(change => {
+            Gerrit.Nav.navigateToChange(change);
+          });
     },
 
     _computeShowSaveClass(editing) {
@@ -339,8 +414,19 @@
       return 'visible';
     },
 
-    _computeAdminClass(isAdmin, canUpload) {
-      return isAdmin || canUpload ? 'admin' : '';
+    _computeEditingClass(editing) {
+      return editing ? 'editing': '';
+    },
+
+    _computeMainClass(isAdmin, canUpload, editing) {
+      const classList = [];
+      if (isAdmin || canUpload) {
+        classList.push('admin');
+      }
+      if (editing) {
+        classList.push('editing');
+      }
+      return classList.join(' ');
     },
 
     _computeParentHref(repoName) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
index 988fc7c..66fece3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -194,23 +194,48 @@
           '/admin/repos/test-repo,access');
     });
 
-    test('_computeAdminClass', () => {
+    test('_computeMainClass', () => {
       let isAdmin = true;
-      assert.equal(element._computeAdminClass(isAdmin), 'admin');
+      const editing = true;
+      const canUpload = false;
+      assert.equal(element._computeMainClass(isAdmin, canUpload), 'admin');
+      assert.equal(element._computeMainClass(isAdmin, canUpload, editing),
+          'admin editing');
       isAdmin = false;
-      assert.equal(element._computeAdminClass(isAdmin), '');
+      assert.equal(element._computeMainClass(isAdmin, canUpload), '');
+      assert.equal(element._computeMainClass(isAdmin, canUpload, editing),
+          'editing');
     });
 
     test('inherit section', () => {
+      element._local = {};
       sandbox.stub(element, '_computeParentHref');
-      assert.isNotOk(Polymer.dom(element.root).querySelector('#inheritsFrom'));
+      // Nothing should appear when no inherit from and not in edit mode.
+      assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
+      // The autocomplete should be hidden, and the link should be  displayed.
       assert.isFalse(element._computeParentHref.called);
+      // When it edit mode, the autocomplete should appear.
+      element._editing = true;
+      // When editing, the autocomplete should still not be shown.
+      assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
+      element._editing = false;
       element._inheritsFrom = {
         name: 'another-repo',
       };
+      // When there is a parent project, the link should be displayed.
       flushAsynchronousOperations();
-      assert.isOk(Polymer.dom(element.root).querySelector('#inheritsFrom'));
+      assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
+      assert.notEqual(getComputedStyle(element.$.inheritFromName).display,
+          'none');
+      assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
+          'none');
       assert.isTrue(element._computeParentHref.called);
+      element._editing = true;
+      // When editing, the autocomplete should be shown.
+      assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
+      assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none');
+      assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display,
+          'none');
     });
 
     test('_computeLoadingClass', () => {
@@ -240,14 +265,25 @@
         assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
         assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
         assert.equal(element.$.editBtn.innerText, 'EDIT');
+        assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
+            'none');
+        element._inheritsFrom = {
+          id: 'test-project',
+        };
+        flushAsynchronousOperations();
+        assert.equal(getComputedStyle(element.$$('#editInheritFromInput'))
+            .display, 'none');
 
         MockInteractions.tap(element.$.editBtn);
+        flushAsynchronousOperations();
 
         // Edit button changes to Cancel button, and Save button is visible but
         // disabled.
         assert.equal(element.$.editBtn.innerText, 'CANCEL');
         assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none');
         assert.isTrue(element.$.saveBtn.disabled);
+        assert.notEqual(getComputedStyle(element.$$('#editInheritFromInput'))
+            .display, 'none');
 
         // Save button should be enabled after access is modified
         element.fire('access-modified');
@@ -286,7 +322,18 @@
         assert.isTrue(element._handleAccessModified.called);
       });
 
-      test('_handleAccessModified called with event fired', () => {
+      test('_handleAccessModified called when parent changes', () => {
+        element._inheritsFrom = {
+          id: 'test-project',
+        };
+        flushAsynchronousOperations();
+        element.$$('#editInheritFromInput').fire('commit');
+        sandbox.spy(element, '_handleAccessModified');
+        element.fire('access-modified');
+        assert.isTrue(element._handleAccessModified.called);
+      });
+
+      test('_handleSaveForReview', () => {
         const saveStub =
             sandbox.stub(element.$.restAPI, 'setProjectAccessRightsForReview');
         sandbox.stub(element, '_computeAddAndRemove').returns({
@@ -335,6 +382,18 @@
         assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
       });
 
+      test('_handleSaveForReview parent change', () => {
+        element._inheritsFrom = {
+          id: 'test-project',
+        };
+        element._originalInheritsFrom = {
+          id: 'test-project-original',
+        };
+        assert.deepEqual(element._computeAddAndRemove(), {
+          parent: 'test-project', add: {}, remove: {},
+        });
+      });
+
       test('_handleSaveForReview rules', () => {
         // Delete a rule.
         element._local['refs/*'].permissions.owner.rules[123].deleted = true;
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
index 025ff79..c94a716 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
@@ -37,9 +37,9 @@
         threshold="[[suggestFrom]]"
         query="[[query]]"
         allow-non-suggested-values="[[allowAnyInput]]"
+        no-debounce
         on-commit="_handleInputCommit"
         clear-on-commit
-        no-debounce
         warn-uncommitted
         text="{{_inputText}}">
     </gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index d097d01..d54bbc3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -57,6 +57,10 @@
         align-items: center;
         color: #777;
       }
+      #confirmSubmitDialog .changeSubject {
+        margin: 1em;
+        text-align: center;
+      }
       @media screen and (max-width: 50em) {
         #mainContent,
         section,
@@ -207,6 +211,25 @@
           Do you really want to delete the edit?
         </div>
       </gr-confirm-dialog>
+      <gr-confirm-dialog
+          id="confirmSubmitDialog"
+          class="confirmDialog"
+          confirm-label="Submit"
+          confirm-on-enter
+          on-cancel="_handleConfirmDialogCancel"
+          on-confirm="_handleSubmitConfirm">
+        <div class="header" slot="header">
+          Submit Change
+        </div>
+        <div class="main" slot="main">
+          <p>
+            Are you sure you want to to <strong>submit</strong> this change?
+          </p>
+          <p class="changeSubject">
+            <strong>[[change.subject]]</strong>
+          </p>
+        </div>
+      </gr-confirm-dialog>
     </gr-overlay>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 5c27757..6d8108f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -203,6 +203,7 @@
        *    branch: string,
        *    id: string,
        *    project: string,
+       *    subject: string,
        *  }}
        */
       change: Object,
@@ -909,10 +910,9 @@
           this._handleDownloadTap();
           break;
         case RevisionActions.SUBMIT:
-          if (!this._canSubmitChange()) {
-            return;
-          }
-        // eslint-disable-next-line no-fallthrough
+          if (!this._canSubmitChange()) { return; }
+          this._showActionDialog(this.$.confirmSubmitDialog);
+          break;
         default:
           this._fireAction(this._prependSlash(key),
               this.revisionActions[key], true);
@@ -1035,6 +1035,12 @@
       this._fireAction('/edit', this.actions.deleteEdit, false);
     },
 
+    _handleSubmitConfirm() {
+      if (!this._canSubmitChange()) { return; }
+      this._hideAllDialogs();
+      this._fireAction('/submit', this.revisionActions.submit, true);
+    },
+
     _getActionOverflowIndex(type, key) {
       return this._overflowActions.findIndex(action => {
         return action.type === type && action.key === key;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index ca6337a..f09acd4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -254,6 +254,7 @@
           .returns(Promise.resolve('test'));
       sandbox.stub(element, 'fetchChangeUpdates',
           () => { return Promise.resolve({isLatest: true}); });
+      const showDialogStub = sandbox.stub(element, '_showActionDialog');
       element.change = {
         revisions: {
           rev1: {_number: 1},
@@ -267,13 +268,29 @@
         assert.ok(submitButton);
         MockInteractions.tap(submitButton);
 
-        // Upon success it should fire the reload-change event.
-        element.addEventListener('reload-change', () => {
-          done();
-        });
+        assert.isTrue(showDialogStub.calledOnce);
+        assert.equal(showDialogStub.lastCall.args[0],
+            element.$.confirmSubmitDialog);
+        done();
       });
     });
 
+    test('_handleSubmitConfirm', () => {
+      const fireStub = sandbox.stub(element, '_fireAction');
+      sandbox.stub(element, '_canSubmitChange').returns(true);
+      element._handleSubmitConfirm();
+      assert.isTrue(fireStub.calledOnce);
+      assert.deepEqual(fireStub.lastCall.args,
+          ['/submit', element.revisionActions.submit, true]);
+    });
+
+    test('_handleSubmitConfirm when not able to submit', () => {
+      const fireStub = sandbox.stub(element, '_fireAction');
+      sandbox.stub(element, '_canSubmitChange').returns(false);
+      element._handleSubmitConfirm();
+      assert.isFalse(fireStub.called);
+    });
+
     test('submit change with plugin hook', done => {
       sandbox.stub(element, '_canSubmitChange',
           () => { return false; });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 42404aa..ff4562b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -611,6 +611,7 @@
           on-send="_handleReplySent"
           on-cancel="_handleReplyCancel"
           on-autogrow="_handleReplyAutogrow"
+          on-send-disabled-changed="_resetReplyOverlayFocusStops"
           hidden$="[[!_loggedIn]]">
       </gr-reply-dialog>
     </gr-overlay>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 9be7db4..5c6859d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -999,7 +999,7 @@
      */
     _openReplyDialog(opt_section) {
       this.$.replyOverlay.open().then(() => {
-        this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+        this._resetReplyOverlayFocusStops();
         this.$.replyDialog.open(opt_section);
         Polymer.dom.flush();
         this.$.replyOverlay.center();
@@ -1554,5 +1554,9 @@
     _handleStopEditTap() {
       Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum);
     },
+
+    _resetReplyOverlayFocusStops() {
+      this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 8e179cc..5d69499 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -105,6 +105,7 @@
           <gr-autocomplete
               id="parentInput"
               query="[[_query]]"
+              no-debounce
               text="{{_inputText}}"
               on-tap="_handleEnterChangeNumberTap"
               on-commit="_handleBaseSelected"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index 24815db..b92fe1d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -166,7 +166,8 @@
 
       test('input text change triggers function', () => {
         sandbox.spy(element, '_getRecentChanges');
-        element._inputText = '1';
+        element.$.parentInput.noDebounce = true;
+        element.$.parentInput.text = '1';
         assert.isTrue(element._getRecentChanges.calledOnce);
         element._inputText = '12';
         assert.isTrue(element._getRecentChanges.calledTwice);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index 0cfae94..af3acc4 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -115,7 +115,7 @@
       sandbox.restore();
     });
 
-    test('send blocked when invalid email is supplied to ccs', () => {
+    test('_submit blocked when invalid email is supplied to ccs', () => {
       const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
       // Stub the below function to avoid side effects from the send promise
       // resolving.
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 088875f..8225dac 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -64,6 +64,8 @@
         display: flex;
         justify-content: space-between;
         position: sticky;
+        /* @see Issue 8602 */
+        z-index: 1;
       }
       .actions .right gr-button {
         margin-left: 1em;
@@ -291,9 +293,10 @@
               class="action cancel"
               on-tap="_cancelTapHandler">Cancel</gr-button>
           <gr-button
+              id="sendButton"
               link
               primary
-              disabled="[[_computeSendButtonDisabled(_sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged, _includeComments)]]"
+              disabled="[[_sendDisabled]]"
               class="action send"
               has-tooltip
               title$="[[_computeSendButtonTooltip(canBeStarted)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 3875e25..e8c720a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -52,6 +52,8 @@
   // googlesource.com.
   const START_REVIEW_MESSAGE = 'This change is ready for review.';
 
+  const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
+
   Polymer({
     is: 'gr-reply-dialog',
 
@@ -87,6 +89,12 @@
      * @event comment-refresh
      */
 
+     /**
+      * Fires when the state of the send button (enabled/disabled) changes.
+      *
+      * @event send-disabled-changed
+      */
+
     properties: {
       /**
        * @type {{ _number: number, removable_reviewers: Array }}
@@ -206,6 +214,12 @@
         type: String,
         value: '',
       },
+      _sendDisabled: {
+        type: Boolean,
+        computed: '_computeSendButtonDisabled(_sendButtonLabel, diffDrafts, ' +
+            'draft, _reviewersMutated, _labelsChanged, _includeComments)',
+        observer: '_sendDisabledChanged',
+      },
     },
 
     FocusTarget,
@@ -273,9 +287,10 @@
     },
 
     getFocusStops() {
+      const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
       return {
         start: this.$.reviewers.focusStart,
-        end: this.$.sendButton,
+        end,
       };
     },
 
@@ -726,6 +741,13 @@
         // the text field of the CC entry.
         return;
       }
+      if (this._sendDisabled) {
+        this.dispatchEvent(new CustomEvent('show-alert', {
+          bubbles: true,
+          detail: {message: EMPTY_REPLY_MESSAGE},
+        }));
+        return;
+      }
       return this.send(this._includeComments, this.canBeStarted)
           .then(keepReviewers => {
             this._purgeReviewersPendingRemove(false, keepReviewers);
@@ -856,5 +878,9 @@
     setPluginMessage(message) {
       this._pluginMessage = message;
     },
+
+    _sendDisabledChanged(sendDisabled) {
+      this.dispatchEvent(new CustomEvent('send-disabled-changed'));
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index b9eebdc..41d7b49 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -1100,6 +1100,33 @@
       assert.isFalse(fn('Send', {}, '', false, true, false));
     });
 
+    test('_submit blocked when no mutations exist', () => {
+      const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+      // Stub the below function to avoid side effects from the send promise
+      // resolving.
+      sandbox.stub(element, '_purgeReviewersPendingRemove');
+      element.diffDrafts = {};
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.$$('gr-button.send'));
+      assert.isFalse(sendStub.called);
+
+      element.diffDrafts = {test: true};
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.$$('gr-button.send'));
+      assert.isTrue(sendStub.called);
+    });
+
+    test('getFocusStops', () => {
+      // Setting diffDrafts to an empty object causes _sendDisabled to be
+      // computed to false.
+      element.diffDrafts = {};
+      assert.equal(element.getFocusStops().end, element.$.cancelButton);
+      element.diffDrafts = {test: true};
+      assert.equal(element.getFocusStops().end, element.$.sendButton);
+    });
+
     test('setPluginMessage', () => {
       element.setPluginMessage('foo');
       assert.equal(element.$.pluginMessage.textContent, 'foo');
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 169fd85..fdd730d 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -48,7 +48,6 @@
           id="searchInput"
           text="{{_inputVal}}"
           query="[[query]]"
-          debounce-wait="200"
           on-commit="_handleInputCommit"
           allow-non-suggested-values
           multi
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 017f2a3..997e1ba 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -288,6 +288,7 @@
 
     const button = this._createElement('gr-button', 'showContext');
     button.setAttribute('link', true);
+    button.setAttribute('no-uppercase', true);
 
     let text;
     const groups = []; // The groups that replace this one if tapped.
@@ -306,7 +307,7 @@
           [0, contextLines.length - context]);
     }
 
-    button.textContent = text;
+    Polymer.dom(button).textContent = text;
 
     button.addEventListener('tap', e => {
       e.detail = {
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 78efae7..129bff1 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
@@ -95,7 +95,7 @@
       let buttons = td.querySelectorAll('gr-button.showContext');
 
       assert.equal(buttons.length, 1);
-      assert.equal(buttons[0].textContent, 'Show 10 common lines');
+      assert.equal(Polymer.dom(buttons[0]).textContent, 'Show 10 common lines');
 
       // Add another line.
       line.contextGroup.lines.push('lorem upsum');
@@ -105,9 +105,9 @@
       buttons = td.querySelectorAll('gr-button.showContext');
 
       assert.equal(buttons.length, 3);
-      assert.equal(buttons[0].textContent, '+10↑');
-      assert.equal(buttons[1].textContent, 'Show 11 common lines');
-      assert.equal(buttons[2].textContent, '+10↓');
+      assert.equal(Polymer.dom(buttons[0]).textContent, '+10↑');
+      assert.equal(Polymer.dom(buttons[1]).textContent, 'Show 11 common lines');
+      assert.equal(Polymer.dom(buttons[2]).textContent, '+10↓');
     });
 
     test('newlines 1', () => {
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 99cfb03..fc23837 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -160,12 +160,15 @@
       .contextControl {
         background-color: #fff7d4;
         border: 1px solid #f6e6a5;
-        color: rgba(0,0,0.54);
       }
       .contextControl gr-button {
         display: inline-block;
-        font-family: var(--monospace-font-family);
         text-decoration: none;
+        --gr-button-color: rgba(0,0,0,.54);
+        --gr-button: {
+          font-family: var(--monospace-font-family);
+          padding: .2em;
+        }
       }
       .contextControl td:not(.lineNum) {
         text-align: center;
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 9841564..f280c33 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -321,7 +321,7 @@
     },
 
     _handleTap(e) {
-      const el = Polymer.dom(e).rootTarget;
+      const el = Polymer.dom(e).localTarget;
 
       if (el.classList.contains('showContext')) {
         this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index 9d85d37..c67a2af 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -61,12 +61,14 @@
 
   suite('edit button CUJ', () => {
     let navStubs;
+    let openAutoCcmplete;
 
     setup(() => {
       navStubs = [
         sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'),
         sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'),
       ];
+      openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
     });
 
     test('_isValidPath', () => {
@@ -84,8 +86,8 @@
         assert.isTrue(element._hideAllDialogs.called);
         assert.isTrue(element.$.openDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.openDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        openAutoCcmplete.noDebounce = true;
+        openAutoCcmplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isFalse(element.$.openDialog.disabled);
         MockInteractions.tap(element.$.openDialog.$$('gr-button[primary]'));
@@ -100,8 +102,8 @@
       MockInteractions.tap(element.$$('#open'));
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.openDialog.disabled);
-        element.$.openDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        openAutoCcmplete.noDebounce = true;
+        openAutoCcmplete.text = 'src/test.cpp';
         assert.isFalse(element.$.openDialog.disabled);
         MockInteractions.tap(element.$.openDialog.$$('gr-button'));
         for (const stub of navStubs) { assert.isFalse(stub.called); }
@@ -114,10 +116,13 @@
   suite('delete button CUJ', () => {
     let navStub;
     let deleteStub;
+    let deleteAutocomplete;
 
     setup(() => {
       navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
       deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
+      deleteAutocomplete =
+          element.$.deleteDialog.querySelector('gr-autocomplete');
     });
 
     test('delete', () => {
@@ -126,8 +131,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.deleteDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.deleteDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        deleteAutocomplete.noDebounce = true;
+        deleteAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isFalse(element.$.deleteDialog.disabled);
         MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
@@ -149,8 +154,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.deleteDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.deleteDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        deleteAutocomplete.noDebounce = true;
+        deleteAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isFalse(element.$.deleteDialog.disabled);
         MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
@@ -183,10 +188,13 @@
   suite('rename button CUJ', () => {
     let navStub;
     let renameStub;
+    let renameAutocomplete;
 
     setup(() => {
       navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
       renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+      renameAutocomplete =
+          element.$.renameDialog.querySelector('gr-autocomplete');
     });
 
     test('rename', () => {
@@ -195,8 +203,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.renameDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.renameDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        renameAutocomplete.noDebounce = true;
+        renameAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isTrue(element.$.renameDialog.disabled);
 
@@ -223,8 +231,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.renameDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.renameDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        renameAutocomplete.noDebounce = true;
+        renameAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isTrue(element.$.renameDialog.disabled);
 
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
index 31715c1..963d2e3 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.html
@@ -56,7 +56,8 @@
         margin: 0 2em;
         padding: .5em;
       }
-      .alreadySubmittedText.hide {
+      .alreadySubmittedText.hide,
+      .hideAgreementsTextBox {
         display: none;
       }
       main {
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index 0b8c331..558140f 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -96,7 +96,6 @@
             <th>
               <gr-autocomplete
                   id="newProject"
-                  debounce-wait="200"
                   query="[[_query]]"
                   threshold="1"
                   placeholder="Project"></gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 287ccdd..1eaad4e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -18,6 +18,7 @@
   'use strict';
 
   const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+  const DEBOUNCE_WAIT_MS = 200;
 
   Polymer({
     is: 'gr-autocomplete',
@@ -88,7 +89,6 @@
       text: {
         type: String,
         value: '',
-        observer: '_updateSuggestions',
         notify: true,
       },
 
@@ -133,12 +133,11 @@
       },
 
       /**
-       * The number of milliseconds to use as the debounce wait time. If null,
-       * no debouncing is used.
+       * When true, querying for suggestions is not debounced w/r/t keypresses
        */
-      debounceWait: {
-        type: Number,
-        value: null,
+      noDebounce: {
+        type: Boolean,
+        value: false,
       },
 
       /** @type {?} */
@@ -168,6 +167,7 @@
 
     observers: [
       '_maybeOpenDropdown(_suggestions, _focused)',
+      '_updateSuggestions(text, threshold, noDebounce)',
     ],
 
     attached() {
@@ -176,6 +176,7 @@
 
     detached() {
       this.unlisten(document.body, 'tap', '_handleBodyTap');
+      this.cancelDebouncer('update-suggestions');
     },
 
     get focusStart() {
@@ -219,7 +220,7 @@
 
     _onInputFocus() {
       this._focused = true;
-      this._updateSuggestions();
+      this._updateSuggestions(this.text, this.threshold, this.noDebounce);
       this.$.input.classList.remove('warnUncommitted');
       // Needed so that --paper-input-container-input updated style is applied.
       this.updateStyles();
@@ -232,14 +233,13 @@
       this.updateStyles();
     },
 
-    _updateSuggestions() {
+    _updateSuggestions(text, threshold, noDebounce) {
       if (this._disableSuggestions) { return; }
-      if (this.text === undefined || this.text.length < this.threshold) {
+      if (text === undefined || text.length < threshold) {
         this._suggestions = [];
         this.value = '';
         return;
       }
-      const text = this.text;
 
       const update = () => {
         this.query(text).then(suggestions => {
@@ -258,10 +258,10 @@
         });
       };
 
-      if (this.debounceWait) {
-        this.debounce('update-suggestions', update, this.debounceWait);
-      } else {
+      if (noDebounce) {
         update();
+      } else {
+        this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
       }
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index dfb0a0d..872f79f 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -28,7 +28,7 @@
 
 <test-fixture id="basic">
   <template>
-    <gr-autocomplete></gr-autocomplete>
+    <gr-autocomplete no-debounce></gr-autocomplete>
   </template>
 </test-fixture>
 
@@ -236,7 +236,7 @@
       assert.isTrue(queryStub.called);
     });
 
-    test('debounceWait debounces the query', () => {
+    test('noDebounce=false debounces the query', () => {
       const queryStub = sandbox.spy(() => {
         return Promise.resolve([]);
       });
@@ -244,11 +244,11 @@
       const debounceStub = sandbox.stub(element, 'debounce',
           (name, cb) => { callback = cb; });
       element.query = queryStub;
-      element.debounceWait = 100;
+      element.noDebounce = false;
       element.text = 'a';
       assert.isFalse(queryStub.called);
       assert.isTrue(debounceStub.called);
-      assert.equal(debounceStub.lastCall.args[2], 100);
+      assert.equal(debounceStub.lastCall.args[2], 200);
       assert.isFunction(callback);
       callback();
       assert.isTrue(queryStub.called);
@@ -261,9 +261,7 @@
     });
 
     test('undefined or empty text results in no suggestions', () => {
-      sandbox.spy(element, '_updateSuggestions');
-      element.text = undefined;
-      assert(element._updateSuggestions.calledOnce);
+      element._updateSuggestions(undefined, 0, null);
       assert.equal(element._suggestions.length, 0);
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 8867b9d..0119a5e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -338,6 +338,13 @@
       sandbox.stub(Gerrit, '_pluginInstalled');
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
+
+      // testplugin has already been installed once (in setup).
+      assert.isFalse(Gerrit._pluginInstalled.called);
+
+      // testplugin2 plugin has not yet been installed.
+      Gerrit.install(p => { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin2/static/test.js');
       assert.isTrue(Gerrit._pluginInstalled.calledOnce);
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index efb88401..372f899 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -430,14 +430,23 @@
     const src = opt_src || (document.currentScript &&
          (document.currentScript.src || document.currentScript.baseURI));
     const name = getPluginNameFromUrl(new URL(src));
-    const plugin = plugins[name] || new Plugin(src);
+    const existingPlugin = plugins[name];
+    const plugin = existingPlugin || new Plugin(src);
     try {
       callback(plugin);
       plugins[name] = plugin;
     } catch (e) {
       console.warn(`${name} install failed: ${e.name}: ${e.message}`);
     }
-    Gerrit._pluginInstalled();
+    // Don't double count plugins that may have an html and js install.
+    // TODO(beckysiegel) remove name check once name issue is resolved.
+    // If there isn't a name, it's due to an issue with the polyfill for
+    // html imports in Safari/Firefox. In this case, other plugin related
+    // features may still be broken, but still make sure to call.
+    // _pluginInstalled.
+    if (!name || !existingPlugin) {
+      Gerrit._pluginInstalled();
+    }
   };
 
   Gerrit.getLoggedIn = function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js b/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
deleted file mode 100644
index 26dacd6..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/*!
- * JavaScript Linkify - v0.3 - 6/27/2009
- * http://benalman.com/projects/javascript-linkify/
- *
- * Copyright (c) 2009 "Cowboy" Ben Alman
- * Dual licensed under the MIT and GPL licenses.
- * http://benalman.com/about/license/
- *
- * Some regexps adapted from http://userscripts.org/scripts/review/7122
- */
-
-// Script: JavaScript Linkify: Process links in text!
-//
-// *Version: 0.3, Last updated: 6/27/2009*
-//
-// Project Home - http://benalman.com/projects/javascript-linkify/
-// GitHub       - http://github.com/cowboy/javascript-linkify/
-// Source       - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.js
-// (Minified)   - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.min.js (2.8kb)
-//
-// About: License
-//
-// Copyright (c) 2009 "Cowboy" Ben Alman,
-// Dual licensed under the MIT and GPL licenses.
-// http://benalman.com/about/license/
-//
-// Permission is hereby granted, free of charge, to any person
-// obtaining a copy of this software and associated documentation
-// files (the "Software"), to deal in the Software without
-// restriction, including without limitation the rights to use,
-// copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following
-// conditions:
-
-// The above copyright notice and this permission notice shall be
-// included in all copies or substantial portions of the Software.
-
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-// OTHER DEALINGS IN THE SOFTWARE.
-
-window.linkify = (function(){
-  var
-  SCHEME = "[a-z\\d.-]+://",
-  IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",
-  HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",
-  TLD = "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)",
-  HOST_OR_IP = "(?:" + HOSTNAME + TLD + "|" + IPV4 + ")",
-  PATH = "(?:[;/][^#?<>\\s]*)?",
-  QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",
-  URI1 = "\\b" + SCHEME + "[^<>\\s]+",
-  URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)",
-
-  MAILTO = "mailto:",
-  EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)",
-
-  URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ),
-  SCHEME_RE = new RegExp( "^" + SCHEME, "i" ),
-
-  quotes = {
-    "'": "`",
-    '>': '<',
-    ')': '(',
-    ']': '[',
-    '}': '{',
-    '»': '«',
-    '›': '‹'
-  },
-
-  default_options = {
-    callback: function( text, href ) {
-      return href ? '<a href="' + href + '" title="' + href + '">' + text + '</a>' : text;
-    },
-    punct_regexp: /(?:[!?.,:;'"]|(?:&|&amp;)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/
-  };
-
-  return function( txt, options ) {
-    options = options || {};
-
-    // Temp variables.
-    var arr,
-    i,
-    link,
-    href,
-
-      // Output HTML.
-      html = '',
-
-      // Store text / link parts, in order, for re-combination.
-      parts = [],
-
-      // Used for keeping track of indices in the text.
-      idx_prev,
-      idx_last,
-      idx,
-      link_last,
-
-      // Used for trimming trailing punctuation and quotes from links.
-      matches_begin,
-      matches_end,
-      quote_begin,
-      quote_end;
-
-    // Initialize options.
-    for ( i in default_options ) {
-      if ( options[ i ] === undefined ) {
-        options[ i ] = default_options[ i ];
-      }
-    }
-
-    // Find links.
-    while ( arr = URI_RE.exec( txt ) ) {
-
-      link = arr[0];
-      idx_last = URI_RE.lastIndex;
-      idx = idx_last - link.length;
-
-      // Not a link if preceded by certain characters.
-      if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) {
-        continue;
-      }
-
-      // Trim trailing punctuation.
-      do {
-        // If no changes are made, we don't want to loop forever!
-        link_last = link;
-
-        quote_end = link.substr( -1 )
-        quote_begin = quotes[ quote_end ];
-
-        // Ending quote character?
-        if ( quote_begin ) {
-          matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) );
-          matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) );
-
-          // If quotes are unbalanced, remove trailing quote character.
-          if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) {
-            link = link.substr( 0, link.length - 1 );
-            idx_last--;
-          }
-        }
-
-        // Ending non-quote punctuation character?
-        if ( options.punct_regexp ) {
-          link = link.replace( options.punct_regexp, function(a){
-            idx_last -= a.length;
-            return '';
-          });
-        }
-      } while ( link.length && link !== link_last );
-
-      href = link;
-
-      // Add appropriate protocol to naked links.
-      if ( !SCHEME_RE.test( href ) ) {
-        href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO )
-          : !href.indexOf( 'irc.' ) ? 'irc://'
-          : !href.indexOf( 'ftp.' ) ? 'ftp://'
-          : 'http://' )
-        + href;
-      }
-
-      // Push preceding non-link text onto the array.
-      if ( idx_prev != idx ) {
-        parts.push([ txt.slice( idx_prev, idx ) ]);
-        idx_prev = idx_last;
-      }
-
-      // Push massaged link onto the array
-      parts.push([ link, href ]);
-    };
-
-    // Push remaining non-link text onto the array.
-    parts.push([ txt.substr( idx_prev ) ]);
-
-    // Process the array items.
-    for ( i = 0; i < parts.length; i++ ) {
-      html += options.callback.apply( window, parts[i] );
-    }
-
-    // In case of catastrophic failure, return the original text;
-    return html || txt;
-  };
-
-})();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
index 13443ee..ec589fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -18,7 +18,7 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
-<script src="ba-linkify.js"></script>
+<script src="../../../bower_components/ba-linkify/ba-linkify.js"></script>
 <script src="link-text-parser.js"></script>
 <dom-module id="gr-linked-text">
   <template>
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 80b01ce..8917fd7 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -101,6 +101,9 @@
         --paper-toggle-button-checked-bar-color: var(--color-link);
         --paper-toggle-button-checked-button-color: var(--color-link);
       }
+      strong {
+        font-family: var(--font-family-bold);
+      }
     </style>
   </template>
 </dom-module>
diff --git a/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 8f87503..c58edb7 100644
--- a/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -5,20 +5,6 @@
 --
 
 -- *********************************************************************
--- AccountGroupMemberAccess
---    @PrimaryKey covers: byAccount
-CREATE INDEX account_group_members_byGroup
-ON account_group_members (group_id);
-
-
--- *********************************************************************
--- AccountGroupByIdAccess
---    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_id_byInclude
-ON account_group_by_id (include_uuid);
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
diff --git a/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index 57b1a4a..7f0f1bd 100644
--- a/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -6,21 +6,6 @@
 --
 
 -- *********************************************************************
--- AccountGroupMemberAccess
---    @PrimaryKey covers: byAccount
-CREATE INDEX account_group_members_byGroup
-ON account_group_members (group_id)
-#
-
--- *********************************************************************
--- AccountGroupIncludeByUuidAccess
---    @PrimaryKey covers: byGroup
-CREATE INDEX acc_gr_incl_by_uuid_byInclude
-ON account_group_by_id (include_uuid)
-#
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing
 
diff --git a/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index e1d88ef..f2f24e1 100644
--- a/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -52,20 +52,6 @@
 --
 
 -- *********************************************************************
--- AccountGroupMemberAccess
---    @PrimaryKey covers: byAccount
-CREATE INDEX account_group_members_byGroup
-ON account_group_members (group_id);
-
-
--- *********************************************************************
--- AccountGroupByIdAccess
---    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_id_byInclude
-ON account_group_by_id (include_uuid);
-
-
--- *********************************************************************
 -- ApprovalCategoryAccess
 --    too small to bother indexing