Merge "Document library upgrade process"
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index d0f998c..00051b1 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
-import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gerrit.testutil.SshMode;
 import com.google.gwtorm.server.SchemaFactory;
@@ -59,7 +58,6 @@
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
   private final AccountByEmailCache byEmailCache;
-  private final AccountIndexer indexer;
   private final ExternalIdsUpdate.Server externalIdsUpdate;
 
   @Inject
@@ -71,7 +69,6 @@
       SshKeyCache sshKeyCache,
       AccountCache accountCache,
       AccountByEmailCache byEmailCache,
-      AccountIndexer indexer,
       ExternalIdsUpdate.Server externalIdsUpdate) {
     accounts = new HashMap<>();
     reviewDbProvider = schema;
@@ -81,7 +78,6 @@
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
     this.byEmailCache = byEmailCache;
-    this.indexer = indexer;
     this.externalIdsUpdate = externalIdsUpdate;
   }
 
@@ -123,6 +119,7 @@
           checkArgument(g != null, "group not found: %s", n);
           AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, g.getId()));
           db.accountGroupMembers().insert(Collections.singleton(m));
+          accountCache.evict(id);
         }
       }
 
@@ -138,8 +135,6 @@
       }
       byEmailCache.evict(email);
 
-      indexer.index(id);
-
       account = new TestAccount(id, username, email, fullName, sshKey, httpPass);
       if (username != null) {
         accounts.put(username, account);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
index 44323e4..fe0c628 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
@@ -34,13 +34,9 @@
    */
   String name();
 
-  /**
-   * Single value. Takes precedence over values specified in {@code values}.
-   */
+  /** Single value. Takes precedence over values specified in {@code values}. */
   String value() default "";
 
-  /**
-   * Multiple values (list). Ignored if {@code value} is specified.
-   */
+  /** Multiple values (list). Ignored if {@code value} is specified. */
   String[] values() default "";
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
index 3337f68..6edb3f0 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
@@ -25,23 +25,15 @@
 @Retention(RUNTIME)
 @Repeatable(GlobalPluginConfigs.class)
 public @interface GlobalPluginConfig {
-  /**
-   * Name of the plugin, corresponding to {@code $site/etc/@pluginName.comfig}.
-   */
+  /** Name of the plugin, corresponding to {@code $site/etc/@pluginName.comfig}. */
   String pluginName();
 
-  /**
-   * @see GerritConfig#name()
-   */
+  /** @see GerritConfig#name() */
   String name();
 
-  /**
-   * @see GerritConfig#value()
-   */
+  /** @see GerritConfig#value() */
   String value() default "";
 
-  /**
-   * @see GerritConfig#values()
-   */
+  /** @see GerritConfig#values() */
   String[] values() default "";
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 6ca8384..fe1d0b7 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -16,9 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -29,15 +31,19 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.testutil.TestNotesMigration;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
 import org.eclipse.jgit.api.TagCommand;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -138,6 +144,7 @@
   private final ChangeNotes.Factory notesFactory;
   private final ApprovalsUtil approvalsUtil;
   private final Provider<InternalChangeQuery> queryProvider;
+  private final TestNotesMigration notesMigration;
   private final ReviewDb db;
   private final TestRepository<?> testRepo;
 
@@ -155,6 +162,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
+      TestNotesMigration notesMigration,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo)
@@ -163,6 +171,7 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
+        notesMigration,
         db,
         i,
         testRepo,
@@ -176,6 +185,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
+      TestNotesMigration notesMigration,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
@@ -185,6 +195,7 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
+        notesMigration,
         db,
         i,
         testRepo,
@@ -199,6 +210,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
+      TestNotesMigration notesMigration,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
@@ -210,6 +222,7 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
+        notesMigration,
         db,
         i,
         testRepo,
@@ -224,13 +237,24 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
+      TestNotesMigration notesMigration,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
       @Assisted String subject,
       @Assisted Map<String, String> files)
       throws Exception {
-    this(notesFactory, approvalsUtil, queryProvider, db, i, testRepo, subject, files, null);
+    this(
+        notesFactory,
+        approvalsUtil,
+        queryProvider,
+        notesMigration,
+        db,
+        i,
+        testRepo,
+        subject,
+        files,
+        null);
   }
 
   @AssistedInject
@@ -238,6 +262,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
+      TestNotesMigration notesMigration,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
@@ -250,6 +275,7 @@
         notesFactory,
         approvalsUtil,
         queryProvider,
+        notesMigration,
         db,
         i,
         testRepo,
@@ -262,6 +288,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
+      TestNotesMigration notesMigration,
       ReviewDb db,
       PersonIdent i,
       TestRepository<?> testRepo,
@@ -274,6 +301,7 @@
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.queryProvider = queryProvider;
+    this.notesMigration = notesMigration;
     this.subject = subject;
     this.files = files;
     this.changeId = changeId;
@@ -392,16 +420,36 @@
     public void assertChange(
         Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers)
         throws OrmException {
+      assertChange(
+          expectedStatus, expectedTopic, Arrays.asList(expectedReviewers), ImmutableList.of());
+    }
+
+    public void assertChange(
+        Change.Status expectedStatus,
+        String expectedTopic,
+        List<TestAccount> expectedReviewers,
+        List<TestAccount> expectedCcs)
+        throws OrmException {
       Change c = getChange().change();
       assertThat(c.getSubject()).isEqualTo(resSubj);
       assertThat(c.getStatus()).isEqualTo(expectedStatus);
       assertThat(Strings.emptyToNull(c.getTopic())).isEqualTo(expectedTopic);
-      assertReviewers(c, expectedReviewers);
+      if (notesMigration.readChanges()) {
+        assertReviewers(c, ReviewerStateInternal.REVIEWER, expectedReviewers);
+        assertReviewers(c, ReviewerStateInternal.CC, expectedCcs);
+      } else {
+        assertReviewers(
+            c,
+            ReviewerStateInternal.REVIEWER,
+            Stream.concat(expectedReviewers.stream(), expectedCcs.stream()).collect(toList()));
+      }
     }
 
-    private void assertReviewers(Change c, TestAccount... expectedReviewers) throws OrmException {
+    private void assertReviewers(
+        Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers)
+        throws OrmException {
       Iterable<Account.Id> actualIds =
-          approvalsUtil.getReviewers(db, notesFactory.createChecked(db, c)).all();
+          approvalsUtil.getReviewers(db, notesFactory.createChecked(db, c)).byState(state);
       assertThat(actualIds)
           .containsExactlyElementsIn(Sets.newHashSet(TestAccount.ids(expectedReviewers)));
     }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index 3ab4a88..7acb135 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -32,10 +32,6 @@
     return accounts.stream().map(a -> a.id).collect(toList());
   }
 
-  public static List<Account.Id> ids(TestAccount... accounts) {
-    return ids(Arrays.asList(accounts));
-  }
-
   public static List<String> names(List<TestAccount> accounts) {
     return accounts.stream().map(a -> a.fullName).collect(toList());
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
index 451af0f..53f1839 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/annotation/UseGerritConfigAnnotationTest.java
@@ -74,8 +74,6 @@
     values = {"value-2", "value-3"}
   )
   public void valueHasPrecedenceOverValues() {
-    assertThat(cfg.getStringList("section", null, "name"))
-        .asList()
-        .containsExactly("value-1");
+    assertThat(cfg.getStringList("section", null, "name")).asList().containsExactly("value-1");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 9ac5a70..89d54e1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -172,8 +172,6 @@
       externalIdsUpdate.delete(getExternalIds(user));
       externalIdsUpdate.insert(savedExternalIds);
     }
-    accountCache.evict(admin.getId());
-    accountCache.evict(user.getId());
   }
 
   @After
@@ -213,9 +211,10 @@
     AccountInfo info = gApi.accounts().id(foo.id.get()).get();
     assertThat(info.username).isEqualTo("foo");
     if (SshMode.useSsh()) {
-      accountIndexedCounter.assertReindexOf(foo, 2); // account creation + adding SSH keys
+      accountIndexedCounter.assertReindexOf(
+          foo, 3); // account creation + external ID creation + adding SSH keys
     } else {
-      accountIndexedCounter.assertReindexOf(foo, 1); // account creation
+      accountIndexedCounter.assertReindexOf(foo, 2); // account creation + external ID creation
     }
 
     // check user branch
@@ -534,7 +533,6 @@
             ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email),
             ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email));
     externalIdsUpdateFactory.create().insert(extIds);
-    accountCache.evict(admin.id);
     accountIndexedCounter.assertReindexOf(admin);
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
@@ -589,7 +587,6 @@
     externalIdsUpdateFactory
         .create()
         .insert(ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email));
-    accountCache.evict(admin.id);
     assertEmail(byEmailCache.get(email), admin);
 
     // wrong case doesn't match
@@ -750,6 +747,79 @@
 
   @Test
   @Sandboxed
+  public void cannotCreateUserBranch() throws Exception {
+    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
+    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
+
+    String userRef = RefNames.refsUsers(new Account.Id(db.nextAccountId()));
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef);
+    r.assertErrorStatus();
+    assertThat(r.getMessage()).contains("Not allowed to create user branch.");
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNull();
+    }
+  }
+
+  @Test
+  @Sandboxed
+  public void createUserBranchWithAccessDatabaseCapability() throws Exception {
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
+    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
+
+    String userRef = RefNames.refsUsers(new Account.Id(db.nextAccountId()));
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef).assertOkStatus();
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNotNull();
+    }
+  }
+
+  @Test
+  @Sandboxed
+  public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability()
+      throws Exception {
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
+    grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
+
+    String userRef = RefNames.REFS_USERS + "foo";
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef);
+    r.assertErrorStatus();
+    assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNull();
+    }
+  }
+
+  @Test
+  @Sandboxed
+  public void createDefaultUserBranch() throws Exception {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNull();
+    }
+
+    grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.CREATE);
+    grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.PUSH);
+
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    pushFactory
+        .create(db, admin.getIdent(), allUsersRepo)
+        .to(RefNames.REFS_USERS_DEFAULT)
+        .assertOkStatus();
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNotNull();
+    }
+  }
+
+  @Test
+  @Sandboxed
   public void cannotDeleteUserBranch() throws Exception {
     grant(
         allUsers,
@@ -831,7 +901,6 @@
     // Both users have a matching external ID for this key.
     addExternalIdEmail(admin, "test5@example.com");
     externalIdsUpdate.insert(ExternalId.create("foo", "myId", user.getId()));
-    accountCache.evict(user.getId());
     accountIndexedCounter.assertReindexOf(user);
 
     TestKey key = validKeyWithSecondUserId();
@@ -1073,8 +1142,6 @@
     checkNotNull(email);
     externalIdsUpdate.insert(
         ExternalId.createWithEmail(name("test"), email, account.getId(), email));
-    // Clear saved AccountState and ExternalIds.
-    accountCache.evict(account.getId());
     accountIndexedCounter.assertReindexOf(account);
     setApiUser(account);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index dbe4a6c..5a2cb2a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -448,6 +448,40 @@
   }
 
   @Test
+  public void revertPreservesReviewersAndCcs() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    ReviewInput in = ReviewInput.approve();
+    in.reviewer(user.email);
+    in.reviewer(accounts.user2().email, ReviewerState.CC, true);
+    // Add user as reviewer that will create the revert
+    in.reviewer(accounts.admin2().email);
+
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    // expect both the original reviewers and CCs to be preserved
+    // original owner should be added as reviewer, user requesting the revert (new owner) removed
+    setApiUser(accounts.admin2());
+    Map<ReviewerState, Collection<AccountInfo>> result =
+        gApi.changes().id(r.getChangeId()).revert().get().reviewers;
+    assertThat(result).containsKey(ReviewerState.REVIEWER);
+
+    List<Integer> reviewers =
+        result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
+    if (notesMigration.readChanges()) {
+      assertThat(result).containsKey(ReviewerState.CC);
+      List<Integer> ccs =
+          result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
+      assertThat(ccs).containsExactly(accounts.user2().id.get());
+      assertThat(reviewers).containsExactly(user.id.get(), admin.id.get());
+    } else {
+      assertThat(reviewers)
+          .containsExactly(user.id.get(), admin.id.get(), accounts.user2().id.get());
+    }
+  }
+
+  @Test
   @TestProjectInput(createEmptyCommit = false)
   public void revertInitialCommit() throws Exception {
     PushOneCommit.Result r = createChange();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index b52136e..cdb8fbb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -323,10 +323,13 @@
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
     r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic);
+    r.assertChange(
+        Change.Status.NEW,
+        topic,
+        ImmutableList.of(),
+        ImmutableList.of(user));
 
     // cc several users
-    TestAccount user2 = accounts.create("another-user", "another.user@example.com", "Another User");
     r =
         pushTo(
             "refs/for/master/"
@@ -336,9 +339,14 @@
                 + ",cc="
                 + user.email
                 + ",cc="
-                + user2.email);
+                + accounts.user2().email);
     r.assertOkStatus();
-    r.assertChange(Change.Status.NEW, topic);
+    // Check that admin isn't CC'd as they own the change
+    r.assertChange(
+        Change.Status.NEW,
+        topic,
+        ImmutableList.of(),
+        ImmutableList.of(user, accounts.user2()));
 
     // cc non-existing user
     String nonExistingEmail = "non.existing@example.com";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 5f1f487..1976edc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -671,6 +671,7 @@
     ExternalIdsUpdate update =
         new ExternalIdsUpdate(
             repoManager,
+            accountCache,
             allUsers,
             metricMaker,
             externalIds,
@@ -706,6 +707,7 @@
     ExternalIdsUpdate update =
         new ExternalIdsUpdate(
             repoManager,
+            accountCache,
             allUsers,
             metricMaker,
             externalIds,
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
index 4c38d83..d6ad269 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -132,9 +132,9 @@
         + "> \n";
   }
 
-  protected static String textFooterForChange(String changeId, String timestamp) {
-    return "Gerrit-Change-Id: "
-        + changeId
+  protected static String textFooterForChange(int changeNumber, String timestamp) {
+    return "Gerrit-Change-Number: "
+        + changeNumber
         + "\n"
         + "Gerrit-PatchSet: 1\n"
         + "Gerrit-MessageType: comment\n"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
index 1dcdd97..a96c6ec 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
@@ -113,7 +113,7 @@
             null,
             null,
             null);
-    b.textContent(txt + textFooterForChange(changeId, ts));
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     mailProcessor.process(b.build());
     return changeInfo;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index ada222a..7cef8e7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -67,7 +67,8 @@
 
     Map<String, Object> expectedHeaders = new HashMap<>();
     expectedHeaders.put("Gerrit-PatchSet", "1");
-    expectedHeaders.put("Gerrit-Change-Id", newChange.getChangeId());
+    expectedHeaders.put(
+        "Gerrit-Change-Number", String.valueOf(newChange.getChange().getId().get()));
     expectedHeaders.put("Gerrit-MessageType", "newchange");
     expectedHeaders.put("Gerrit-Commit", newChange.getCommit().getId().name());
     expectedHeaders.put("Gerrit-ChangeURL", changeURL);
@@ -102,7 +103,8 @@
     String changeURL = "<" + canonicalWebUrl.get() + newChange.getChange().getId().get() + ">";
     Map<String, Object> expectedHeaders = new HashMap<>();
     expectedHeaders.put("Gerrit-PatchSet", "1");
-    expectedHeaders.put("Gerrit-Change-Id", newChange.getChangeId());
+    expectedHeaders.put(
+        "Gerrit-Change-Number", String.valueOf(newChange.getChange().getId().get()));
     expectedHeaders.put("Gerrit-MessageType", "comment");
     expectedHeaders.put("Gerrit-Commit", newChange.getCommit().getId().name());
     expectedHeaders.put("Gerrit-ChangeURL", changeURL);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index e7a0cda..9de4797 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -51,7 +51,7 @@
             null,
             null,
             null);
-    b.textContent(txt + textFooterForChange(changeId, ts));
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     mailProcessor.process(b.build());
 
@@ -79,7 +79,7 @@
             "Some Inline Comment",
             null,
             null);
-    b.textContent(txt + textFooterForChange(changeId, ts));
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     mailProcessor.process(b.build());
 
@@ -115,7 +115,7 @@
             null,
             "Some Comment on File 1",
             null);
-    b.textContent(txt + textFooterForChange(changeId, ts));
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     mailProcessor.process(b.build());
 
@@ -152,7 +152,7 @@
             "Some Inline Comment",
             null,
             null);
-    b.textContent(txt + textFooterForChange(changeId, ts));
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     mailProcessor.process(b.build());
     comments = gApi.changes().id(changeId).current().commentsAsList();
@@ -183,7 +183,7 @@
             "Some Inline Comment",
             null,
             null);
-    b.textContent(txt + textFooterForChange(changeId, ts));
+    b.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     // Set account state to inactive
     gApi.accounts().id("user").setActive(false);
@@ -219,7 +219,7 @@
     MailMessage.Builder b =
         messageBuilderWithDefaultFields()
             .from(user.emailAddress)
-            .textContent(txt + textFooterForChange(changeId, ts));
+            .textContent(txt + textFooterForChange(changeInfo._number, ts));
 
     sender.clear();
     mailProcessor.process(b.build());
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java
index f97abd9..1e3a2c8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java
@@ -33,4 +33,7 @@
 
   /** Scope of the named capabilities. */
   CapabilityScope scope() default CapabilityScope.CONTEXT;
+
+  /** Fall back to admin credentials. Only applies to plugin capability check. */
+  boolean fallBackToAdmin() default true;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
index 7717c84..b9ef7e0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
@@ -32,4 +32,7 @@
 
   /** Scope of the named capability. */
   CapabilityScope scope() default CapabilityScope.CONTEXT;
+
+  /** Fall back to admin credentials. Only applies to plugin capability check. */
+  boolean fallBackToAdmin() default true;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PluginPermission.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PluginPermission.java
index 33a85cd..7a467b8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PluginPermission.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PluginPermission.java
@@ -22,10 +22,16 @@
 public class PluginPermission implements GlobalOrPluginPermission {
   private final String pluginName;
   private final String capability;
+  private final boolean fallBackToAdmin;
 
   public PluginPermission(String pluginName, String capability) {
+    this(pluginName, capability, true);
+  }
+
+  public PluginPermission(String pluginName, String capability, boolean fallBackToAdmin) {
     this.pluginName = checkNotNull(pluginName, "pluginName");
     this.capability = checkNotNull(capability, "capability");
+    this.fallBackToAdmin = fallBackToAdmin;
   }
 
   public String pluginName() {
@@ -36,6 +42,10 @@
     return capability;
   }
 
+  public boolean fallBackToAdmin() {
+    return fallBackToAdmin;
+  }
+
   @Override
   public String permissionName() {
     return pluginName + '-' + capability;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index 4e6ea66..779d5d4 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.server.DeleteGpgKey.Input;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -43,18 +42,15 @@
 
   private final Provider<PersonIdent> serverIdent;
   private final Provider<PublicKeyStore> storeProvider;
-  private final AccountCache accountCache;
   private final ExternalIdsUpdate.User externalIdsUpdateFactory;
 
   @Inject
   DeleteGpgKey(
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       Provider<PublicKeyStore> storeProvider,
-      AccountCache accountCache,
       ExternalIdsUpdate.User externalIdsUpdateFactory) {
     this.serverIdent = serverIdent;
     this.storeProvider = storeProvider;
-    this.accountCache = accountCache;
     this.externalIdsUpdateFactory = externalIdsUpdateFactory;
   }
 
@@ -69,7 +65,6 @@
             rsrc.getUser().getAccountId(),
             ExternalId.Key.create(
                 SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
-    accountCache.evict(rsrc.getUser().getAccountId());
 
     try (PublicKeyStore store = storeProvider.get()) {
       store.remove(rsrc.getKeyRing().getPublicKey().getFingerprint());
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index af4d6bb..41fcd04 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -43,7 +43,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -88,7 +87,6 @@
   private final Provider<PublicKeyStore> storeProvider;
   private final GerritPublicKeyChecker.Factory checkerFactory;
   private final AddKeySender.Factory addKeyFactory;
-  private final AccountCache accountCache;
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final ExternalIds externalIds;
   private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@@ -100,7 +98,6 @@
       Provider<PublicKeyStore> storeProvider,
       GerritPublicKeyChecker.Factory checkerFactory,
       AddKeySender.Factory addKeyFactory,
-      AccountCache accountCache,
       Provider<InternalAccountQuery> accountQueryProvider,
       ExternalIds externalIds,
       ExternalIdsUpdate.User externalIdsUpdateFactory) {
@@ -109,7 +106,6 @@
     this.storeProvider = storeProvider;
     this.checkerFactory = checkerFactory;
     this.addKeyFactory = addKeyFactory;
-    this.accountCache = accountCache;
     this.accountQueryProvider = accountQueryProvider;
     this.externalIds = externalIds;
     this.externalIdsUpdateFactory = externalIdsUpdateFactory;
@@ -148,7 +144,6 @@
       externalIdsUpdateFactory
           .create()
           .replace(rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
-      accountCache.evict(rsrc.getUser().getAccountId());
       return toJson(newKeys, toRemove, store, rsrc.getUser());
     }
   }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index deb0dc4..64311e0 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -36,8 +36,9 @@
 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.AccountCache;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
@@ -52,7 +53,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -71,7 +71,9 @@
 
 /** Unit tests for {@link GerritPublicKeyChecker}. */
 public class GerritPublicKeyCheckerTest {
-  @Inject private AccountCache accountCache;
+  @Inject private Accounts accounts;
+
+  @Inject private AccountsUpdate.Server accountsUpdate;
 
   @Inject private AccountManager accountManager;
 
@@ -115,10 +117,10 @@
     db = schemaFactory.open();
     schemaCreator.create(db);
     userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
-    Account userAccount = db.accounts().get(userId);
+    Account userAccount = accounts.get(db, userId);
     // Note: does not match any key in TestKeys.
     userAccount.setPreferredEmail("user@example.com");
-    db.accounts().update(ImmutableList.of(userAccount));
+    accountsUpdate.create().update(db, userAccount);
     user = reloadUser();
 
     requestContext.setContext(
@@ -150,8 +152,7 @@
     return userFactory.create(id);
   }
 
-  private IdentifiedUser reloadUser() throws IOException {
-    accountCache.evict(userId);
+  private IdentifiedUser reloadUser() {
     user = userFactory.create(userId);
     return user;
   }
@@ -407,7 +408,6 @@
     assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
 
     externalIdsUpdateFactory.create().insert(newExtIds);
-    accountCache.evict(user.getAccountId());
   }
 
   private TestKey add(TestKey k, IdentifiedUser user) throws Exception {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 7f6255a..d0adc4b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -25,16 +25,17 @@
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -54,8 +55,10 @@
 @SuppressWarnings("serial")
 @Singleton
 class BecomeAnyAccountLoginServlet extends HttpServlet {
-  private final SchemaFactory<ReviewDb> schema;
   private final DynamicItem<WebSession> webSession;
+  private final SchemaFactory<ReviewDb> schema;
+  private final Accounts accounts;
+  private final AccountCache accountCache;
   private final AccountManager accountManager;
   private final SiteHeaderFooter headers;
   private final InternalAccountQuery accountQuery;
@@ -64,11 +67,15 @@
   BecomeAnyAccountLoginServlet(
       DynamicItem<WebSession> ws,
       SchemaFactory<ReviewDb> sf,
+      Accounts a,
+      AccountCache ac,
       AccountManager am,
       SiteHeaderFooter shf,
       InternalAccountQuery aq) {
     webSession = ws;
     schema = sf;
+    accounts = a;
+    accountCache = ac;
     accountManager = am;
     headers = shf;
     accountQuery = aq;
@@ -149,8 +156,8 @@
 
     Element userlistElement = HtmlDomUtil.find(doc, "userlist");
     try (ReviewDb db = schema.open()) {
-      ResultSet<Account> accounts = db.accounts().firstNById(100);
-      for (Account a : accounts) {
+      for (Account.Id accountId : accounts.firstNIds(100)) {
+        Account a = accountCache.get(accountId).getAccount();
         String displayName;
         if (a.getUserName() != null) {
           displayName = a.getUserName();
@@ -159,7 +166,7 @@
         } else if (a.getPreferredEmail() != null) {
           displayName = a.getPreferredEmail();
         } else {
-          displayName = a.getId().toString();
+          displayName = accountId.toString();
         }
 
         Element linkElement = doc.createElement("a");
@@ -223,7 +230,7 @@
       return null;
     }
     try (ReviewDb db = schema.open()) {
-      return auth(db.accounts().get(id));
+      return auth(accounts.get(db, id));
     } catch (OrmException e) {
       getServletContext().log("cannot query database", e);
       return null;
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java
index 0c3e1be..d106eec 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -22,6 +22,8 @@
 
 public class IndexServletTest {
   class TestIndexServlet extends IndexServlet {
+    private static final long serialVersionUID = 1L;
+
     TestIndexServlet(String canonicalURL, String cdnPath) throws URISyntaxException {
       super(canonicalURL, cdnPath);
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index 09626d7..ee939d6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtorm.server.OrmException;
@@ -60,6 +61,17 @@
     }
   }
 
+  public boolean hasAnyAccount() throws IOException {
+    File path = getPath();
+    if (path == null) {
+      return false;
+    }
+
+    try (Repository repo = new FileRepository(path)) {
+      return Accounts.hasAnyAccount(repo);
+    }
+  }
+
   private File getPath() {
     Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
     checkArgument(basePath != null, "gerrit.basePath must be configured");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 466404b..7162eab 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -88,7 +88,7 @@
     }
 
     try (ReviewDb db = dbFactory.open()) {
-      if (db.accounts().anyAccounts().toList().isEmpty()) {
+      if (!accounts.hasAnyAccount()) {
         ui.header("Gerrit Administrator");
         if (ui.yesno(true, "Create administrator user")) {
           Account.Id id = new Account.Id(db.nextAccountId());
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
index b015af8..db74caa 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -31,22 +31,6 @@
   @Query("WHERE preferredEmail = ? LIMIT 2")
   ResultSet<Account> byPreferredEmail(String email) throws OrmException;
 
-  @Query("WHERE fullName = ? LIMIT 2")
-  ResultSet<Account> byFullName(String name) throws OrmException;
-
-  @Query("WHERE fullName >= ? AND fullName <= ? ORDER BY fullName LIMIT ?")
-  ResultSet<Account> suggestByFullName(String nameA, String nameB, int limit) throws OrmException;
-
-  @Query("WHERE preferredEmail >= ? AND preferredEmail <= ? ORDER BY preferredEmail LIMIT ?")
-  ResultSet<Account> suggestByPreferredEmail(String nameA, String nameB, int limit)
-      throws OrmException;
-
-  @Query("LIMIT 1")
-  ResultSet<Account> anyAccounts() throws OrmException;
-
-  @Query("ORDER BY accountId LIMIT ?")
-  ResultSet<Account> firstNById(int n) throws OrmException;
-
   @Query("ORDER BY accountId")
   ResultSet<Account> all() throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 871ed20..5a83dc4 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -6,14 +6,10 @@
 
 -- *********************************************************************
 -- AccountAccess
---    covers:             byPreferredEmail, suggestByPreferredEmail
+--    covers:             byPreferredEmail
 CREATE INDEX accounts_byPreferredEmail
 ON accounts (preferred_email);
 
---    covers:             suggestByFullName
-CREATE INDEX accounts_byFullName
-ON accounts (full_name);
-
 
 -- *********************************************************************
 -- AccountGroupMemberAccess
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index c349241..98f05ca 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -7,16 +7,11 @@
 
 -- *********************************************************************
 -- AccountAccess
---    covers:             byPreferredEmail, suggestByPreferredEmail
+--    covers:             byPreferredEmail
 CREATE INDEX accounts_byPreferredEmail
 ON accounts (preferred_email)
 #
 
---    covers:             suggestByFullName
-CREATE INDEX accounts_byFullName
-ON accounts (full_name)
-#
-
 
 -- *********************************************************************
 -- AccountGroupMemberAccess
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index da99fef..dde86a4 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -53,14 +53,10 @@
 
 -- *********************************************************************
 -- AccountAccess
---    covers:             byPreferredEmail, suggestByPreferredEmail
+--    covers:             byPreferredEmail
 CREATE INDEX accounts_byPreferredEmail
 ON accounts (preferred_email);
 
---    covers:             suggestByFullName
-CREATE INDEX accounts_byFullName
-ON accounts (full_name);
-
 
 -- *********************************************************************
 -- AccountGroupMemberAccess
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 12fc938..1600982 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -154,6 +154,7 @@
 
   static class ByIdLoader extends CacheLoader<Account.Id, Optional<AccountState>> {
     private final SchemaFactory<ReviewDb> schema;
+    private final Accounts accounts;
     private final GroupCache groupCache;
     private final GeneralPreferencesLoader loader;
     private final LoadingCache<String, Optional<Account.Id>> byName;
@@ -163,11 +164,13 @@
     @Inject
     ByIdLoader(
         SchemaFactory<ReviewDb> sf,
+        Accounts accounts,
         GroupCache groupCache,
         GeneralPreferencesLoader loader,
         @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername,
         Provider<WatchConfig.Accessor> watchConfig,
         ExternalIds externalIds) {
+      this.accounts = accounts;
       this.schema = sf;
       this.groupCache = groupCache;
       this.loader = loader;
@@ -193,7 +196,7 @@
 
     private Optional<AccountState> load(final ReviewDb db, final Account.Id who)
         throws OrmException, IOException, ConfigInvalidException {
-      Account account = db.accounts().get(who);
+      Account account = accounts.get(db, who);
       if (account == null) {
         return Optional.empty();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 49a20fa..9f2b869 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -52,6 +52,7 @@
   private static final Logger log = LoggerFactory.getLogger(AccountManager.class);
 
   private final SchemaFactory<ReviewDb> schema;
+  private final Accounts accounts;
   private final AccountsUpdate.Server accountsUpdateFactory;
   private final AccountCache byIdCache;
   private final AccountByEmailCache byEmailCache;
@@ -68,6 +69,7 @@
   @Inject
   AccountManager(
       SchemaFactory<ReviewDb> schema,
+      Accounts accounts,
       AccountsUpdate.Server accountsUpdateFactory,
       AccountCache byIdCache,
       AccountByEmailCache byEmailCache,
@@ -80,6 +82,7 @@
       ExternalIds externalIds,
       ExternalIdsUpdate.Server externalIdsUpdateFactory) {
     this.schema = schema;
+    this.accounts = accounts;
     this.accountsUpdateFactory = accountsUpdateFactory;
     this.byIdCache = byIdCache;
     this.byEmailCache = byEmailCache;
@@ -190,21 +193,18 @@
     }
 
     if (toUpdate != null) {
-      db.accounts().update(Collections.singleton(toUpdate));
+      accountsUpdateFactory.create().update(db, toUpdate);
     }
 
     if (newEmail != null && !newEmail.equals(oldEmail)) {
       byEmailCache.evict(oldEmail);
       byEmailCache.evict(newEmail);
     }
-    if (toUpdate != null) {
-      byIdCache.evict(toUpdate.getId());
-    }
   }
 
   private Account load(Account toUpdate, Account.Id accountId, ReviewDb db) throws OrmException {
     if (toUpdate == null) {
-      toUpdate = db.accounts().get(accountId);
+      toUpdate = accounts.get(db, accountId);
       if (toUpdate == null) {
         throw new OrmException("Account " + accountId + " has been deleted");
       }
@@ -226,8 +226,7 @@
     account.setFullName(who.getDisplayName());
     account.setPreferredEmail(extId.email());
 
-    boolean isFirstAccount =
-        awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty();
+    boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false) && !accounts.hasAnyAccount();
 
     try {
       AccountsUpdate accountsUpdate = accountsUpdateFactory.create();
@@ -301,7 +300,6 @@
     }
 
     byEmailCache.evict(account.getPreferredEmail());
-    byIdCache.evict(account.getId());
     realm.onCreateAccount(who, account);
     return new AuthResult(newId, extId.key(), true);
   }
@@ -374,17 +372,13 @@
             .insert(ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
 
         if (who.getEmailAddress() != null) {
-          Account a = db.accounts().get(to);
+          Account a = accounts.get(db, to);
           if (a.getPreferredEmail() == null) {
             a.setPreferredEmail(who.getEmailAddress());
-            db.accounts().update(Collections.singleton(a));
+            accountsUpdateFactory.create().update(db, a);
           }
-        }
-
-        if (who.getEmailAddress() != null) {
           byEmailCache.evict(who.getEmailAddress());
         }
-        byIdCache.evict(to);
       }
 
       return new AuthResult(to, who.getExternalIdKey(), false);
@@ -418,7 +412,6 @@
                 .isPresent())) {
       externalIdsUpdateFactory.create().delete(filteredExtIdsByScheme);
     }
-    byIdCache.evict(to);
     return link(to, who);
   }
 
@@ -443,14 +436,13 @@
         externalIdsUpdateFactory.create().delete(extId);
 
         if (who.getEmailAddress() != null) {
-          Account a = db.accounts().get(from);
+          Account a = accounts.get(db, from);
           if (a.getPreferredEmail() != null
               && a.getPreferredEmail().equals(who.getEmailAddress())) {
             a.setPreferredEmail(null);
-            db.accounts().update(Collections.singleton(a));
+            accountsUpdateFactory.create().update(db, a);
           }
           byEmailCache.evict(who.getEmailAddress());
-          byIdCache.evict(from);
         }
 
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 9803143..8ce5f4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -33,6 +33,7 @@
 @Singleton
 public class AccountResolver {
   private final Realm realm;
+  private final Accounts accounts;
   private final AccountByEmailCache byEmail;
   private final AccountCache byId;
   private final Provider<InternalAccountQuery> accountQueryProvider;
@@ -40,10 +41,12 @@
   @Inject
   AccountResolver(
       Realm realm,
+      Accounts accounts,
       AccountByEmailCache byEmail,
       AccountCache byId,
       Provider<InternalAccountQuery> accountQueryProvider) {
     this.realm = realm;
+    this.accounts = accounts;
     this.byEmail = byEmail;
     this.byId = byId;
     this.accountQueryProvider = accountQueryProvider;
@@ -116,7 +119,7 @@
   }
 
   private boolean exists(ReviewDb db, Account.Id id) throws OrmException {
-    return db.accounts().get(id) != null;
+    return accounts.get(db, id) != null;
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
new file mode 100644
index 0000000..4c23745
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
@@ -0,0 +1,100 @@
+// 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.account;
+
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.gerrit.reviewdb.client.Account;
+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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.Repository;
+
+/** Class to access accounts. */
+@Singleton
+public class Accounts {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  Accounts(GitRepositoryManager repoManager, AllUsersName allUsersName) {
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+  }
+
+  public Account get(ReviewDb db, Account.Id accountId) throws OrmException {
+    return db.accounts().get(accountId);
+  }
+
+  /**
+   * Returns all account IDs.
+   *
+   * @return all account IDs
+   */
+  public Set<Account.Id> allIds() throws IOException {
+    return readUserRefs().collect(toSet());
+  }
+
+  /**
+   * Returns the first n account IDs.
+   *
+   * @param n the number of account IDs that should be returned
+   * @return first n account IDs
+   */
+  public List<Account.Id> firstNIds(int n) throws IOException {
+    return readUserRefs().sorted(comparing(id -> id.get())).limit(n).collect(toList());
+  }
+
+  /**
+   * Checks if any account exists.
+   *
+   * @return {@code true} if at least one account exists, otherwise {@code false}
+   */
+  public boolean hasAnyAccount() throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsersName)) {
+      return hasAnyAccount(repo);
+    }
+  }
+
+  public static boolean hasAnyAccount(Repository repo) throws IOException {
+    return readUserRefs(repo).findAny().isPresent();
+  }
+
+  private Stream<Account.Id> readUserRefs() throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsersName)) {
+      return readUserRefs(repo);
+    }
+  }
+
+  private static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
+    return repo.getRefDatabase()
+        .getRefs(RefNames.REFS_USERS)
+        .values()
+        .stream()
+        .map(r -> Account.Id.fromRef(r.getName()))
+        .filter(Objects::nonNull);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
index de87fc1..069380f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -31,6 +31,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.function.Consumer;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -49,26 +50,32 @@
    *
    * <p>The Gerrit server identity will be used as author and committer for all commits that update
    * the accounts.
+   *
+   * <p>On updating accounts this class takes care to evict them from the account cache and thus
+   * triggers reindex for them.
    */
   @Singleton
   public static class Server {
     private final GitRepositoryManager repoManager;
+    private final AccountCache accountCache;
     private final AllUsersName allUsersName;
     private final Provider<PersonIdent> serverIdent;
 
     @Inject
     public Server(
         GitRepositoryManager repoManager,
+        AccountCache accountCache,
         AllUsersName allUsersName,
         @GerritPersonIdent Provider<PersonIdent> serverIdent) {
       this.repoManager = repoManager;
+      this.accountCache = accountCache;
       this.allUsersName = allUsersName;
       this.serverIdent = serverIdent;
     }
 
     public AccountsUpdate create() {
       PersonIdent i = serverIdent.get();
-      return new AccountsUpdate(repoManager, allUsersName, i, i);
+      return new AccountsUpdate(repoManager, accountCache, allUsersName, i, i);
     }
   }
 
@@ -81,6 +88,7 @@
   @Singleton
   public static class User {
     private final GitRepositoryManager repoManager;
+    private final AccountCache accountCache;
     private final AllUsersName allUsersName;
     private final Provider<PersonIdent> serverIdent;
     private final Provider<IdentifiedUser> identifiedUser;
@@ -88,10 +96,12 @@
     @Inject
     public User(
         GitRepositoryManager repoManager,
+        AccountCache accountCache,
         AllUsersName allUsersName,
         @GerritPersonIdent Provider<PersonIdent> serverIdent,
         Provider<IdentifiedUser> identifiedUser) {
       this.repoManager = repoManager;
+      this.accountCache = accountCache;
       this.allUsersName = allUsersName;
       this.serverIdent = serverIdent;
       this.identifiedUser = identifiedUser;
@@ -100,7 +110,7 @@
     public AccountsUpdate create() {
       PersonIdent i = serverIdent.get();
       return new AccountsUpdate(
-          repoManager, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
+          repoManager, accountCache, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
     }
 
     private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -109,16 +119,19 @@
   }
 
   private final GitRepositoryManager repoManager;
+  private final AccountCache accountCache;
   private final AllUsersName allUsersName;
   private final PersonIdent committerIdent;
   private final PersonIdent authorIdent;
 
   private AccountsUpdate(
       GitRepositoryManager repoManager,
+      AccountCache accountCache,
       AllUsersName allUsersName,
       PersonIdent committerIdent,
       PersonIdent authorIdent) {
     this.repoManager = checkNotNull(repoManager, "repoManager");
+    this.accountCache = checkNotNull(accountCache, "accountCache");
     this.allUsersName = checkNotNull(allUsersName, "allUsersName");
     this.committerIdent = checkNotNull(committerIdent, "committerIdent");
     this.authorIdent = checkNotNull(authorIdent, "authorIdent");
@@ -133,6 +146,7 @@
   public void insert(ReviewDb db, Account account) throws OrmException, IOException {
     db.accounts().insert(ImmutableSet.of(account));
     createUserBranch(account);
+    accountCache.evict(account.getId());
   }
 
   /**
@@ -143,18 +157,50 @@
   public void upsert(ReviewDb db, Account account) throws OrmException, IOException {
     db.accounts().upsert(ImmutableSet.of(account));
     createUserBranchIfNeeded(account);
+    accountCache.evict(account.getId());
+  }
+
+  /** Updates the account. */
+  public void update(ReviewDb db, Account account) throws OrmException, IOException {
+    db.accounts().update(ImmutableSet.of(account));
+    accountCache.evict(account.getId());
+  }
+
+  /**
+   * Gets the account and updates it atomically.
+   *
+   * @param db ReviewDb
+   * @param accountId ID of the account
+   * @param consumer consumer to update the account, only invoked if the account exists
+   * @return the updated account, {@code null} if the account doesn't exist
+   * @throws OrmException if updating the account fails
+   */
+  public Account atomicUpdate(ReviewDb db, Account.Id accountId, Consumer<Account> consumer)
+      throws OrmException, IOException {
+    Account account =
+        db.accounts()
+            .atomicUpdate(
+                accountId,
+                a -> {
+                  consumer.accept(a);
+                  return a;
+                });
+    accountCache.evict(accountId);
+    return account;
   }
 
   /** Deletes the account. */
   public void delete(ReviewDb db, Account account) throws OrmException, IOException {
     db.accounts().delete(ImmutableSet.of(account));
     deleteUserBranch(account.getId());
+    accountCache.evict(account.getId());
   }
 
   /** Deletes the account. */
   public void deleteByKey(ReviewDb db, Account.Id accountId) throws OrmException, IOException {
     db.accounts().deleteKeys(ImmutableSet.of(accountId));
     deleteUserBranch(accountId);
+    accountCache.evict(accountId);
   }
 
   private void createUserBranch(Account account) throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 01e16d7..fa4c35b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -238,7 +238,9 @@
     if (perm instanceof GlobalPermission) {
       return can((GlobalPermission) perm);
     } else if (perm instanceof PluginPermission) {
-      return canPerform(perm.permissionName()) || isAdmin_DoNotUse();
+      PluginPermission pluginPermission = (PluginPermission) perm;
+      return canPerform(pluginPermission.permissionName())
+          || (pluginPermission.fallBackToAdmin() && isAdmin_DoNotUse());
     }
     throw new PermissionBackendException(perm + " unsupported");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index d2a9610..230e516 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -116,7 +116,6 @@
       accountCache.evictByUsername(extId.key().id());
     }
 
-    accountCache.evict(user.getAccountId());
     accountCache.evictByUsername(newUsername);
     sshKeyCache.evict(newUsername);
     return VoidResult.INSTANCE;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 5ea5e96..d617365 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
 import com.google.gerrit.server.group.GroupsCollection;
-import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -70,7 +69,6 @@
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
   private final AccountsUpdate.User accountsUpdate;
-  private final AccountIndexer indexer;
   private final AccountByEmailCache byEmailCache;
   private final AccountLoader.Factory infoLoader;
   private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
@@ -89,7 +87,6 @@
       SshKeyCache sshKeyCache,
       AccountCache accountCache,
       AccountsUpdate.User accountsUpdate,
-      AccountIndexer indexer,
       AccountByEmailCache byEmailCache,
       AccountLoader.Factory infoLoader,
       DynamicSet<AccountExternalIdCreator> externalIdCreators,
@@ -105,7 +102,6 @@
     this.sshKeyCache = sshKeyCache;
     this.accountCache = accountCache;
     this.accountsUpdate = accountsUpdate;
-    this.indexer = indexer;
     this.byEmailCache = byEmailCache;
     this.infoLoader = infoLoader;
     this.externalIdCreators = externalIdCreators;
@@ -198,7 +194,6 @@
 
     accountCache.evictByUsername(username);
     byEmailCache.evict(input.email);
-    indexer.index(id);
 
     AccountLoader loader = infoLoader.create(true);
     AccountInfo info = loader.get(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index d013120..3a996f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.DeleteActive.Input;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -39,14 +38,16 @@
   public static class Input {}
 
   private final Provider<ReviewDb> dbProvider;
-  private final AccountCache byIdCache;
+  private final AccountsUpdate.Server accountsUpdate;
   private final Provider<IdentifiedUser> self;
 
   @Inject
   DeleteActive(
-      Provider<ReviewDb> dbProvider, AccountCache byIdCache, Provider<IdentifiedUser> self) {
+      Provider<ReviewDb> dbProvider,
+      AccountsUpdate.Server accountsUpdate,
+      Provider<IdentifiedUser> self) {
     this.dbProvider = dbProvider;
-    this.byIdCache = byIdCache;
+    this.accountsUpdate = accountsUpdate;
     this.self = self;
   }
 
@@ -58,30 +59,25 @@
     }
 
     AtomicBoolean alreadyInactive = new AtomicBoolean(false);
-    Account a =
-        dbProvider
-            .get()
-            .accounts()
+    Account account =
+        accountsUpdate
+            .create()
             .atomicUpdate(
+                dbProvider.get(),
                 rsrc.getUser().getAccountId(),
-                new AtomicUpdate<Account>() {
-                  @Override
-                  public Account update(Account a) {
-                    if (!a.isActive()) {
-                      alreadyInactive.set(true);
-                    } else {
-                      a.setActive(false);
-                    }
-                    return a;
+                a -> {
+                  if (!a.isActive()) {
+                    alreadyInactive.set(true);
+                  } else {
+                    a.setActive(false);
                   }
                 });
-    if (a == null) {
+    if (account == null) {
       throw new ResourceNotFoundException("account not found");
     }
     if (alreadyInactive.get()) {
       throw new ResourceConflictException("account not active");
     }
-    byIdCache.evict(a.getId());
     return Response.none();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index 4c525c2..bc7c1d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.PutActive.Input;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -36,39 +35,34 @@
   public static class Input {}
 
   private final Provider<ReviewDb> dbProvider;
-  private final AccountCache byIdCache;
+  private final AccountsUpdate.Server accountsUpdate;
 
   @Inject
-  PutActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+  PutActive(Provider<ReviewDb> dbProvider, AccountsUpdate.Server accountsUpdate) {
     this.dbProvider = dbProvider;
-    this.byIdCache = byIdCache;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
       throws ResourceNotFoundException, OrmException, IOException {
     AtomicBoolean alreadyActive = new AtomicBoolean(false);
-    Account a =
-        dbProvider
-            .get()
-            .accounts()
+    Account account =
+        accountsUpdate
+            .create()
             .atomicUpdate(
+                dbProvider.get(),
                 rsrc.getUser().getAccountId(),
-                new AtomicUpdate<Account>() {
-                  @Override
-                  public Account update(Account a) {
-                    if (a.isActive()) {
-                      alreadyActive.set(true);
-                    } else {
-                      a.setActive(true);
-                    }
-                    return a;
+                a -> {
+                  if (a.isActive()) {
+                    alreadyActive.set(true);
+                  } else {
+                    a.setActive(true);
                   }
                 });
-    if (a == null) {
+    if (account == null) {
       throw new ResourceNotFoundException("account not found");
     }
-    byIdCache.evict(a.getId());
     return alreadyActive.get() ? Response.ok("") : Response.created("");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index 395f078..e00f6b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -59,7 +59,6 @@
 
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
-  private final AccountCache accountCache;
   private final ExternalIds externalIds;
   private final ExternalIdsUpdate.User externalIdsUpdate;
 
@@ -67,12 +66,10 @@
   PutHttpPassword(
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
-      AccountCache accountCache,
       ExternalIds externalIds,
       ExternalIdsUpdate.User externalIdsUpdate) {
     this.self = self;
     this.permissionBackend = permissionBackend;
-    this.accountCache = accountCache;
     this.externalIds = externalIds;
     this.externalIdsUpdate = externalIdsUpdate;
   }
@@ -117,7 +114,6 @@
     ExternalId newExtId =
         ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword);
     externalIdsUpdate.create().upsert(newExtId);
-    accountCache.evict(user.getAccountId());
 
     return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 7a2868e..0911820 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,7 +46,7 @@
   private final Realm realm;
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
-  private final AccountCache byIdCache;
+  private final AccountsUpdate.Server accountsUpdate;
 
   @Inject
   PutName(
@@ -55,12 +54,12 @@
       Realm realm,
       PermissionBackend permissionBackend,
       Provider<ReviewDb> dbProvider,
-      AccountCache byIdCache) {
+      AccountsUpdate.Server accountsUpdate) {
     this.self = self;
     this.realm = realm;
     this.permissionBackend = permissionBackend;
     this.dbProvider = dbProvider;
-    this.byIdCache = byIdCache;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
@@ -84,25 +83,15 @@
     }
 
     String newName = input.name;
-    Account a =
-        dbProvider
-            .get()
-            .accounts()
-            .atomicUpdate(
-                user.getAccountId(),
-                new AtomicUpdate<Account>() {
-                  @Override
-                  public Account update(Account a) {
-                    a.setFullName(newName);
-                    return a;
-                  }
-                });
-    if (a == null) {
+    Account account =
+        accountsUpdate
+            .create()
+            .atomicUpdate(dbProvider.get(), user.getAccountId(), a -> a.setFullName(newName));
+    if (account == null) {
       throw new ResourceNotFoundException("account not found");
     }
-    byIdCache.evict(a.getId());
-    return Strings.isNullOrEmpty(a.getFullName())
-        ? Response.<String>none()
-        : Response.ok(a.getFullName());
+    return Strings.isNullOrEmpty(account.getFullName())
+        ? Response.none()
+        : Response.ok(account.getFullName());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index 4941cc8..d473d53 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -41,18 +40,18 @@
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
-  private final AccountCache byIdCache;
+  private final AccountsUpdate.Server accountsUpdate;
 
   @Inject
   PutPreferred(
       Provider<CurrentUser> self,
       Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
-      AccountCache byIdCache) {
+      AccountsUpdate.Server accountsUpdate) {
     this.self = self;
     this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
-    this.byIdCache = byIdCache;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
@@ -68,27 +67,22 @@
   public Response<String> apply(IdentifiedUser user, String email)
       throws ResourceNotFoundException, OrmException, IOException {
     AtomicBoolean alreadyPreferred = new AtomicBoolean(false);
-    Account a =
-        dbProvider
-            .get()
-            .accounts()
+    Account account =
+        accountsUpdate
+            .create()
             .atomicUpdate(
+                dbProvider.get(),
                 user.getAccountId(),
-                new AtomicUpdate<Account>() {
-                  @Override
-                  public Account update(Account a) {
-                    if (email.equals(a.getPreferredEmail())) {
-                      alreadyPreferred.set(true);
-                    } else {
-                      a.setPreferredEmail(email);
-                    }
-                    return a;
+                a -> {
+                  if (email.equals(a.getPreferredEmail())) {
+                    alreadyPreferred.set(true);
+                  } else {
+                    a.setPreferredEmail(email);
                   }
                 });
-    if (a == null) {
+    if (account == null) {
       throw new ResourceNotFoundException("account not found");
     }
-    byIdCache.evict(a.getId());
     return alreadyPreferred.get() ? Response.ok("") : Response.created("");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
index 73a720b..81c0694 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -50,18 +49,18 @@
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
-  private final AccountCache byIdCache;
+  private final AccountsUpdate.Server accountsUpdate;
 
   @Inject
   PutStatus(
       Provider<CurrentUser> self,
       Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
-      AccountCache byIdCache) {
+      AccountsUpdate.Server accountsUpdate) {
     this.self = self;
     this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
-    this.byIdCache = byIdCache;
+    this.accountsUpdate = accountsUpdate;
   }
 
   @Override
@@ -81,23 +80,18 @@
     }
 
     String newStatus = input.status;
-    Account a =
-        dbProvider
-            .get()
-            .accounts()
+    Account account =
+        accountsUpdate
+            .create()
             .atomicUpdate(
+                dbProvider.get(),
                 user.getAccountId(),
-                new AtomicUpdate<Account>() {
-                  @Override
-                  public Account update(Account a) {
-                    a.setStatus(Strings.nullToEmpty(newStatus));
-                    return a;
-                  }
-                });
-    if (a == null) {
+                a -> a.setStatus(Strings.nullToEmpty(newStatus)));
+    if (account == null) {
       throw new ResourceNotFoundException("account not found");
     }
-    byIdCache.evict(a.getId());
-    return Strings.isNullOrEmpty(a.getStatus()) ? Response.none() : Response.ok(a.getStatus());
+    return Strings.isNullOrEmpty(account.getStatus())
+        ? Response.none()
+        : Response.ok(account.getStatus());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
index c21ff95..9beae0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
@@ -33,6 +33,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
 import com.google.common.util.concurrent.Runnables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.metrics.Counter0;
@@ -42,6 +43,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LockFailureException;
@@ -53,6 +55,8 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -83,6 +87,9 @@
  * </pre>
  *
  * For NoteDb each method call results in one commit on refs/meta/external-ids branch.
+ *
+ * <p>On updating external IDs this class takes care to evict affected accounts from the account
+ * cache and thus triggers reindex for them.
  */
 public class ExternalIdsUpdate {
   private static final String COMMIT_MSG = "Update external IDs";
@@ -96,6 +103,7 @@
   @Singleton
   public static class Server {
     private final GitRepositoryManager repoManager;
+    private final AccountCache accountCache;
     private final AllUsersName allUsersName;
     private final MetricMaker metricMaker;
     private final ExternalIds externalIds;
@@ -105,12 +113,14 @@
     @Inject
     public Server(
         GitRepositoryManager repoManager,
+        AccountCache accountCache,
         AllUsersName allUsersName,
         MetricMaker metricMaker,
         ExternalIds externalIds,
         ExternalIdCache externalIdCache,
         @GerritPersonIdent Provider<PersonIdent> serverIdent) {
       this.repoManager = repoManager;
+      this.accountCache = accountCache;
       this.allUsersName = allUsersName;
       this.metricMaker = metricMaker;
       this.externalIds = externalIds;
@@ -121,7 +131,7 @@
     public ExternalIdsUpdate create() {
       PersonIdent i = serverIdent.get();
       return new ExternalIdsUpdate(
-          repoManager, allUsersName, metricMaker, externalIds, externalIdCache, i, i);
+          repoManager, accountCache, allUsersName, metricMaker, externalIds, externalIdCache, i, i);
     }
   }
 
@@ -134,6 +144,7 @@
   @Singleton
   public static class User {
     private final GitRepositoryManager repoManager;
+    private final AccountCache accountCache;
     private final AllUsersName allUsersName;
     private final MetricMaker metricMaker;
     private final ExternalIds externalIds;
@@ -144,6 +155,7 @@
     @Inject
     public User(
         GitRepositoryManager repoManager,
+        AccountCache accountCache,
         AllUsersName allUsersName,
         MetricMaker metricMaker,
         ExternalIds externalIds,
@@ -151,6 +163,7 @@
         @GerritPersonIdent Provider<PersonIdent> serverIdent,
         Provider<IdentifiedUser> identifiedUser) {
       this.repoManager = repoManager;
+      this.accountCache = accountCache;
       this.allUsersName = allUsersName;
       this.metricMaker = metricMaker;
       this.externalIds = externalIds;
@@ -163,6 +176,7 @@
       PersonIdent i = serverIdent.get();
       return new ExternalIdsUpdate(
           repoManager,
+          accountCache,
           allUsersName,
           metricMaker,
           externalIds,
@@ -190,6 +204,7 @@
   private static final Retryer<RefsMetaExternalIdsUpdate> RETRYER = retryerBuilder().build();
 
   private final GitRepositoryManager repoManager;
+  private final AccountCache accountCache;
   private final AllUsersName allUsersName;
   private final ExternalIds externalIds;
   private final ExternalIdCache externalIdCache;
@@ -201,6 +216,7 @@
 
   private ExternalIdsUpdate(
       GitRepositoryManager repoManager,
+      AccountCache accountCache,
       AllUsersName allUsersName,
       MetricMaker metricMaker,
       ExternalIds externalIds,
@@ -209,6 +225,7 @@
       PersonIdent authorIdent) {
     this(
         repoManager,
+        accountCache,
         allUsersName,
         metricMaker,
         externalIds,
@@ -222,6 +239,7 @@
   @VisibleForTesting
   public ExternalIdsUpdate(
       GitRepositoryManager repoManager,
+      AccountCache accountCache,
       AllUsersName allUsersName,
       MetricMaker metricMaker,
       ExternalIds externalIds,
@@ -231,6 +249,7 @@
       Runnable afterReadRevision,
       Retryer<RefsMetaExternalIdsUpdate> retryer) {
     this.repoManager = checkNotNull(repoManager, "repoManager");
+    this.accountCache = accountCache;
     this.allUsersName = checkNotNull(allUsersName, "allUsersName");
     this.committerIdent = checkNotNull(committerIdent, "committerIdent");
     this.externalIds = checkNotNull(externalIds, "externalIds");
@@ -269,6 +288,7 @@
               }
             });
     externalIdCache.onCreate(u.oldRev(), u.newRev(), extIds);
+    evictAccounts(extIds);
   }
 
   /**
@@ -295,6 +315,7 @@
               }
             });
     externalIdCache.onUpdate(u.oldRev(), u.newRev(), extIds);
+    evictAccounts(extIds);
   }
 
   /**
@@ -324,6 +345,7 @@
               }
             });
     externalIdCache.onRemove(u.oldRev(), u.newRev(), extIds);
+    evictAccounts(extIds);
   }
 
   /**
@@ -353,6 +375,7 @@
               }
             });
     externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), accountId, extIdKeys);
+    accountCache.evict(accountId);
   }
 
   /**
@@ -362,14 +385,19 @@
    */
   public void deleteByKeys(Collection<ExternalId.Key> extIdKeys)
       throws IOException, ConfigInvalidException, OrmException {
+    Set<ExternalId> deletedExtIds = new HashSet<>();
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
               for (ExternalId.Key extIdKey : extIdKeys) {
-                remove(o.rw(), o.noteMap(), extIdKey, null);
+                ExternalId extId = remove(o.rw(), o.noteMap(), extIdKey, null);
+                if (extId != null) {
+                  deletedExtIds.add(extId);
+                }
               }
             });
     externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), extIdKeys);
+    evictAccounts(deletedExtIds);
   }
 
   /** Deletes all external IDs of the specified account. */
@@ -406,6 +434,7 @@
               }
             });
     externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), accountId, toDelete, toAdd);
+    accountCache.evict(accountId);
   }
 
   /**
@@ -420,11 +449,15 @@
    */
   public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
       throws IOException, ConfigInvalidException, OrmException {
+    Set<ExternalId> deletedExtIds = new HashSet<>();
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
               for (ExternalId.Key extIdKey : toDelete) {
-                remove(o.rw(), o.noteMap(), extIdKey, null);
+                ExternalId extId = remove(o.rw(), o.noteMap(), extIdKey, null);
+                if (extId != null) {
+                  deletedExtIds.add(extId);
+                }
               }
 
               for (ExternalId extId : toAdd) {
@@ -432,6 +465,7 @@
               }
             });
     externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), toDelete, toAdd);
+    evictAccounts(Streams.concat(deletedExtIds.stream(), toAdd.stream()).collect(toSet()));
   }
 
   /**
@@ -567,13 +601,15 @@
    *
    * @throws IllegalStateException is thrown if an expected account ID is provided and an external
    *     ID with the specified key exists, but belongs to another account.
+   * @return the external ID that was removed, {@code null} if no external ID with the specified key
+   *     exists
    */
-  private static void remove(
+  private static ExternalId remove(
       RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extIdKey.sha1();
     if (!noteMap.contains(noteId)) {
-      return;
+      return null;
     }
 
     byte[] raw =
@@ -589,6 +625,7 @@
           extId.accountId().get());
     }
     noteMap.remove(noteId);
+    return extId;
   }
 
   private RefsMetaExternalIdsUpdate updateNoteMap(MyConsumer<OpenRepo> update)
@@ -689,6 +726,12 @@
     return ins.insert(OBJ_TREE, new byte[] {});
   }
 
+  private void evictAccounts(Collection<ExternalId> extIds) throws IOException {
+    for (Account.Id id : extIds.stream().map(ExternalId::accountId).collect(toSet())) {
+      accountCache.evict(id);
+    }
+  }
+
   @FunctionalInterface
   private static interface MyConsumer<T> {
     void accept(T t) throws IOException, ConfigInvalidException, OrmException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 5391635..80d644f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.server.mail.send.CreateChangeSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -68,6 +69,7 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -100,6 +102,7 @@
   private final CommitValidators.Factory commitValidatorsFactory;
   private final RevisionCreated revisionCreated;
   private final CommentAdded commentAdded;
+  private final NotesMigration migration;
 
   private final Change.Id changeId;
   private final PatchSet.Id psId;
@@ -147,6 +150,7 @@
       CommitValidators.Factory commitValidatorsFactory,
       CommentAdded commentAdded,
       RevisionCreated revisionCreated,
+      NotesMigration migration,
       @Assisted Change.Id changeId,
       @Assisted ObjectId commitId,
       @Assisted String refName) {
@@ -162,6 +166,7 @@
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.revisionCreated = revisionCreated;
     this.commentAdded = commentAdded;
+    this.migration = migration;
 
     this.changeId = changeId;
     this.psId = new PatchSet.Id(changeId, INITIAL_PATCH_SET_ID);
@@ -408,6 +413,14 @@
      */
     update.fixStatus(change.getStatus());
 
+    Set<Account.Id> reviewersToAdd = new HashSet<>(reviewers);
+    if (migration.readChanges()) {
+      approvalsUtil.addCcs(
+          ctx.getNotes(), update, filterOnChangeVisibility(db, ctx.getNotes(), extraCC));
+    } else {
+      reviewersToAdd.addAll(extraCC);
+    }
+
     LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
     approvalsUtil.addReviewers(
         db,
@@ -416,7 +429,7 @@
         change,
         patchSet,
         patchSetInfo,
-        filterOnChangeVisibility(db, ctx.getNotes(), reviewers),
+        filterOnChangeVisibility(db, ctx.getNotes(), reviewersToAdd),
         Collections.<Account.Id>emptySet());
     approvalsUtil.addApprovalsForNewPatchSet(
         db, update, labelTypes, patchSet, ctx.getControl(), approvals);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index e1ccff9..ca70b0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.PatchSetState;
@@ -105,6 +106,7 @@
 
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeNotes.Factory notesFactory;
+  private final Accounts accounts;
   private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
   private final GitRepositoryManager repoManager;
   private final PatchSetInfoFactory patchSetInfoFactory;
@@ -134,6 +136,7 @@
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       ChangeControl.GenericFactory changeControlFactory,
       ChangeNotes.Factory notesFactory,
+      Accounts accounts,
       DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
       GitRepositoryManager repoManager,
       PatchSetInfoFactory patchSetInfoFactory,
@@ -142,6 +145,7 @@
       Provider<CurrentUser> user,
       Provider<ReviewDb> db,
       RetryHelper retryHelper) {
+    this.accounts = accounts;
     this.accountPatchReviewStore = accountPatchReviewStore;
     this.changeControlFactory = changeControlFactory;
     this.db = db;
@@ -219,7 +223,7 @@
 
   private void checkOwner() {
     try {
-      if (db.get().accounts().get(change().getOwner()) == null) {
+      if (accounts.get(db.get(), change().getOwner()) == null) {
         problem("Missing change owner: " + change().getOwner());
       }
     } catch (OrmException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 039aa9e..56d54ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -37,10 +37,12 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.extensions.events.ChangeReverted;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.send.RevertedSender;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -206,12 +208,18 @@
               .setTopic(changeToRevert.getTopic());
       ins.setMessage("Uploaded patch set 1.");
 
+      ReviewerSet reviewerSet = approvalsUtil.getReviewers(db.get(), ctl.getNotes());
+
       Set<Account.Id> reviewers = new HashSet<>();
       reviewers.add(changeToRevert.getOwner());
-      reviewers.addAll(approvalsUtil.getReviewers(db.get(), ctl.getNotes()).all());
+      reviewers.addAll(reviewerSet.byState(ReviewerStateInternal.REVIEWER));
       reviewers.remove(user.getAccountId());
       ins.setReviewers(reviewers);
 
+      Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
+      ccs.remove(user.getAccountId());
+      ins.setExtraCC(ccs);
+
       try (BatchUpdate bu = updateFactory.create(db.get(), project, user, now)) {
         bu.setRepository(git, revWalk, oi);
         bu.insertChange(ins);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 2956a4e..b7c017c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -82,8 +82,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.SetHashtagsOp;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -299,6 +300,8 @@
   private final Sequences seq;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeNotes.Factory notesFactory;
+  private final Accounts accounts;
+  private final AccountsUpdate.Server accountsUpdate;
   private final AccountResolver accountResolver;
   private final PermissionBackend permissionBackend;
   private final PermissionBackend.ForProject permissions;
@@ -310,7 +313,6 @@
   private final CommitValidators.Factory commitValidatorsFactory;
   private final RefOperationValidators.Factory refValidatorsFactory;
   private final TagCache tagCache;
-  private final AccountCache accountCache;
   private final ChangeInserter.Factory changeInserterFactory;
   private final RequestScopePropagator requestScopePropagator;
   private final SshInfo sshInfo;
@@ -373,6 +375,8 @@
       Sequences seq,
       Provider<InternalChangeQuery> queryProvider,
       ChangeNotes.Factory notesFactory,
+      Accounts accounts,
+      AccountsUpdate.Server accountsUpdate,
       AccountResolver accountResolver,
       PermissionBackend permissionBackend,
       CmdLineParser.Factory optionParserFactory,
@@ -380,7 +384,6 @@
       PatchSetUtil psUtil,
       ProjectCache projectCache,
       TagCache tagCache,
-      AccountCache accountCache,
       @Nullable SearchingChangeCacheImpl changeCache,
       ChangeInserter.Factory changeInserterFactory,
       CommitValidators.Factory commitValidatorsFactory,
@@ -412,6 +415,8 @@
     this.seq = seq;
     this.queryProvider = queryProvider;
     this.notesFactory = notesFactory;
+    this.accounts = accounts;
+    this.accountsUpdate = accountsUpdate;
     this.accountResolver = accountResolver;
     this.permissionBackend = permissionBackend;
     this.optionParserFactory = optionParserFactory;
@@ -420,7 +425,6 @@
     this.projectCache = projectCache;
     this.canonicalWebUrl = canonicalWebUrl;
     this.tagCache = tagCache;
-    this.accountCache = accountCache;
     this.changeInserterFactory = changeInserterFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.refValidatorsFactory = refValidatorsFactory;
@@ -1334,7 +1338,7 @@
       if (!hashtag.isEmpty()) {
         hashtags.add(hashtag);
       }
-      //TODO(dpursehouse): validate hashtags
+      // TODO(dpursehouse): validate hashtags
     }
 
     MagicBranchInput(
@@ -2737,12 +2741,11 @@
 
         if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
           try {
-            Account a = db.accounts().get(user.getAccountId());
+            Account a = accounts.get(db, user.getAccountId());
             if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
               a.setFullName(c.getCommitterIdent().getName());
-              db.accounts().update(Collections.singleton(a));
+              accountsUpdate.create().update(db, a);
               user.getAccount().setFullName(a.getFullName());
-              accountCache.evict(a.getId());
             }
           } catch (OrmException e) {
             logWarn("Cannot default full_name", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 0c4eb89..6d5824f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
@@ -68,7 +69,7 @@
     List<ValidationMessage> messages = new ArrayList<>();
     boolean withException = false;
     List<RefOperationValidationListener> listeners = new ArrayList<>();
-    listeners.add(new DisallowDeletionOfUserBranches(allUsersName));
+    listeners.add(new DisallowCreationAndDeletionOfUserBranches(allUsersName));
     refOperationValidationListeners.forEach(l -> listeners.add(l));
     try {
       for (RefOperationValidationListener listener : listeners) {
@@ -104,10 +105,11 @@
     }
   }
 
-  private static class DisallowDeletionOfUserBranches implements RefOperationValidationListener {
+  private static class DisallowCreationAndDeletionOfUserBranches
+      implements RefOperationValidationListener {
     private final AllUsersName allUsersName;
 
-    DisallowDeletionOfUserBranches(AllUsersName allUsersName) {
+    DisallowCreationAndDeletionOfUserBranches(AllUsersName allUsersName) {
       this.allUsersName = allUsersName;
     }
 
@@ -116,10 +118,20 @@
         throws ValidationException {
       if (refEvent.project.getNameKey().equals(allUsersName)
           && (refEvent.command.getRefName().startsWith(RefNames.REFS_USERS)
-              && !refEvent.command.getRefName().equals(RefNames.REFS_USERS_DEFAULT))
-          && refEvent.command.getType().equals(ReceiveCommand.Type.DELETE)) {
-        if (!refEvent.user.getCapabilities().canAccessDatabase()) {
-          throw new ValidationException("Not allowed to delete user branch.");
+              && !refEvent.command.getRefName().equals(RefNames.REFS_USERS_DEFAULT))) {
+        if (refEvent.command.getType().equals(ReceiveCommand.Type.CREATE)) {
+          if (!refEvent.user.getCapabilities().canAccessDatabase()) {
+            throw new ValidationException("Not allowed to create user branch.");
+          }
+          if (Account.Id.fromRef(refEvent.command.getRefName()) == null) {
+            throw new ValidationException(
+                String.format(
+                    "Not allowed to create non-user branch under %s.", RefNames.REFS_USERS));
+          }
+        } else if (refEvent.command.getType().equals(ReceiveCommand.Type.DELETE)) {
+          if (!refEvent.user.getCapabilities().canAccessDatabase()) {
+            throw new ValidationException("Not allowed to delete user branch.");
+          }
         }
       }
       return ImmutableList.of();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
index 36a409a..47a8cad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -21,15 +21,14 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.Account;
-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.Accounts;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.SiteIndexer;
-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;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -45,17 +44,17 @@
 public class AllAccountsIndexer extends SiteIndexer<Account.Id, AccountState, AccountIndex> {
   private static final Logger log = LoggerFactory.getLogger(AllAccountsIndexer.class);
 
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ListeningExecutorService executor;
+  private final Accounts accounts;
   private final AccountCache accountCache;
 
   @Inject
   AllAccountsIndexer(
-      SchemaFactory<ReviewDb> schemaFactory,
       @IndexExecutor(BATCH) ListeningExecutorService executor,
+      Accounts accounts,
       AccountCache accountCache) {
-    this.schemaFactory = schemaFactory;
     this.executor = executor;
+    this.accounts = accounts;
     this.accountCache = accountCache;
   }
 
@@ -67,7 +66,7 @@
     List<Account.Id> ids;
     try {
       ids = collectAccounts(progress);
-    } catch (OrmException e) {
+    } catch (IOException e) {
       log.error("Error collecting accounts", e);
       return new SiteIndexer.Result(sw, false, 0, 0);
     }
@@ -113,13 +112,12 @@
     return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get());
   }
 
-  private List<Account.Id> collectAccounts(ProgressMonitor progress) throws OrmException {
+  private List<Account.Id> collectAccounts(ProgressMonitor progress) throws IOException {
     progress.beginTask("Collecting accounts", ProgressMonitor.UNKNOWN);
     List<Account.Id> ids = new ArrayList<>();
-    try (ReviewDb db = schemaFactory.open()) {
-      for (Account account : db.accounts().all()) {
-        ids.add(account.getId());
-      }
+    for (Account.Id accountId : accounts.allIds()) {
+      ids.add(accountId);
+      progress.update(1);
     }
     progress.endTask();
     return ids;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MetadataName.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MetadataName.java
index 3db55c0..3080e4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MetadataName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MetadataName.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail;
 
 public final class MetadataName {
-  public static final String CHANGE_ID = "Gerrit-Change-Id";
+  public static final String CHANGE_NUMBER = "Gerrit-Change-Number";
   public static final String PATCH_SET = "Gerrit-PatchSet";
   public static final String MESSAGE_TYPE = "Gerrit-MessageType";
   public static final String TIMESTAMP = "Gerrit-Comment-Date";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMetadata.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMetadata.java
index f85291c..04c2add 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMetadata.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMetadata.java
@@ -19,14 +19,14 @@
 
 /** MailMetadata represents metadata parsed from inbound email. */
 public class MailMetadata {
-  public String changeId;
+  public Integer changeNumber;
   public Integer patchSet;
   public String author; // Author of the email
   public Timestamp timestamp;
   public String messageType; // we expect comment here
 
   public boolean hasRequiredFields() {
-    return changeId != null
+    return changeNumber != null
         && patchSet != null
         && author != null
         && timestamp != null
@@ -36,7 +36,7 @@
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
-        .add("Change-Id", changeId)
+        .add("Change-Number", changeNumber)
         .add("Patch-Set", patchSet)
         .add("Author", author)
         .add("Timestamp", timestamp)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 0c2cf04..7ad282b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -169,12 +169,12 @@
 
     try (ManualRequestContext ctx = oneOffRequestContext.openAs(account)) {
       List<ChangeData> changeDataList =
-          queryProvider.get().byKey(Change.Key.parse(metadata.changeId));
+          queryProvider.get().byLegacyChangeId(new Change.Id(metadata.changeNumber));
       if (changeDataList.size() != 1) {
         log.error(
             String.format(
                 "Message %s references unique change %s, but there are %d matching changes in the index. Will delete message.",
-                message.id(), metadata.changeId, changeDataList.size()));
+                message.id(), metadata.changeNumber, changeDataList.size()));
         return;
       }
       ChangeData cd = changeDataList.get(0);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java
index a18da68..7085051 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java
@@ -38,9 +38,9 @@
 
     // Check email headers for X-Gerrit-<Name>
     for (String header : m.additionalHeaders()) {
-      if (header.startsWith(toHeaderWithDelimiter(MetadataName.CHANGE_ID))) {
-        metadata.changeId =
-            header.substring(toHeaderWithDelimiter(MetadataName.CHANGE_ID).length());
+      if (header.startsWith(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER))) {
+        String num = header.substring(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER).length());
+        metadata.changeNumber = Ints.tryParse(num);
       } else if (header.startsWith(toHeaderWithDelimiter(MetadataName.PATCH_SET))) {
         String ps = header.substring(toHeaderWithDelimiter(MetadataName.PATCH_SET).length());
         metadata.patchSet = Ints.tryParse(ps);
@@ -84,8 +84,9 @@
 
   private static void extractFooters(String[] lines, MailMetadata metadata, MailMessage m) {
     for (String line : lines) {
-      if (metadata.changeId == null && line.contains(MetadataName.CHANGE_ID)) {
-        metadata.changeId = extractFooter(toFooterWithDelimiter(MetadataName.CHANGE_ID), line);
+      if (metadata.changeNumber == null && line.contains(MetadataName.CHANGE_NUMBER)) {
+        metadata.changeNumber =
+            Ints.tryParse(extractFooter(toFooterWithDelimiter(MetadataName.CHANGE_NUMBER), line));
       } else if (metadata.patchSet == null && line.contains(MetadataName.PATCH_SET)) {
         metadata.patchSet =
             Ints.tryParse(extractFooter(toFooterWithDelimiter(MetadataName.PATCH_SET), line));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
index 926057b..4d293c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -91,11 +91,24 @@
       throw new PermissionBackendException("cannot extract permission");
     } else if (rc != null) {
       return Collections.singleton(
-          resolve(pluginName, rc.value(), rc.scope(), clazz, RequiresCapability.class));
+          resolve(
+              pluginName,
+              rc.value(),
+              rc.scope(),
+              rc.fallBackToAdmin(),
+              clazz,
+              RequiresCapability.class));
     } else if (rac != null) {
       Set<GlobalOrPluginPermission> r = new LinkedHashSet<>();
       for (String capability : rac.value()) {
-        r.add(resolve(pluginName, capability, rac.scope(), clazz, RequiresAnyCapability.class));
+        r.add(
+            resolve(
+                pluginName,
+                capability,
+                rac.scope(),
+                rac.fallBackToAdmin(),
+                clazz,
+                RequiresAnyCapability.class));
       }
       return Collections.unmodifiableSet(r);
     } else {
@@ -129,13 +142,14 @@
       @Nullable String pluginName,
       String capability,
       CapabilityScope scope,
+      boolean fallBackToAdmin,
       Class<?> clazz,
       Class<?> annotationClass)
       throws PermissionBackendException {
     if (pluginName != null
         && !"gerrit".equals(pluginName)
         && (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) {
-      return new PluginPermission(pluginName, capability);
+      return new PluginPermission(pluginName, capability, fallBackToAdmin);
     }
 
     if (scope == CapabilityScope.PLUGIN) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index b68c81a..7fe89cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/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_151> C = Schema_151.class;
+  public static final Class<Schema_152> C = Schema_152.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_152.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_152.java
new file mode 100644
index 0000000..2ed7273
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_152.java
@@ -0,0 +1,41 @@
+// 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.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.sql.SQLException;
+
+/** Drop unused indexes from accounts table. */
+public class Schema_152 extends SchemaVersion {
+  @Inject
+  Schema_152(Provider<Schema_151> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    JdbcSchema schema = (JdbcSchema) db;
+    SqlDialect dialect = schema.getDialect();
+    try (StatementExecutor e = newExecutor(db)) {
+      dialect.dropIndex(e, "accounts", "accounts_byFullName");
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
index 0c4507e..84bae96 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
@@ -34,7 +34,7 @@
     b.dateReceived(new DateTime());
     b.subject("");
 
-    b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.CHANGE_ID) + "cid");
+    b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER) + "123");
     b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.PATCH_SET) + "1");
     b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment");
     b.addAdditionalHeader(
@@ -45,7 +45,7 @@
 
     MailMetadata meta = MetadataParser.parse(b.build());
     assertThat(meta.author).isEqualTo(author.getEmail());
-    assertThat(meta.changeId).isEqualTo("cid");
+    assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
     assertThat(meta.timestamp.getTime())
@@ -62,7 +62,7 @@
     b.subject("");
 
     StringBuilder stringBuilder = new StringBuilder();
-    stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid\r\n");
+    stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_NUMBER) + "123\r\n");
     stringBuilder.append("> " + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1\n");
     stringBuilder.append(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment\n");
     stringBuilder.append(
@@ -74,7 +74,7 @@
 
     MailMetadata meta = MetadataParser.parse(b.build());
     assertThat(meta.author).isEqualTo(author.getEmail());
-    assertThat(meta.changeId).isEqualTo("cid");
+    assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
     assertThat(meta.timestamp.getTime())
@@ -92,7 +92,7 @@
 
     StringBuilder stringBuilder = new StringBuilder();
     stringBuilder.append(
-        "<div id\"someid\">" + toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid</div>");
+        "<div id\"someid\">" + toFooterWithDelimiter(MetadataName.CHANGE_NUMBER) + "123</div>");
     stringBuilder.append("<div>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1</div>");
     stringBuilder.append(
         "<div>" + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment</div>");
@@ -108,7 +108,7 @@
 
     MailMetadata meta = MetadataParser.parse(b.build());
     assertThat(meta.author).isEqualTo(author.getEmail());
-    assertThat(meta.changeId).isEqualTo("cid");
+    assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
     assertThat(meta.timestamp.getTime())
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index a83db4a..042865e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -18,7 +18,7 @@
 import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.fail;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest;
@@ -37,6 +37,8 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.schema.SchemaCreator;
@@ -53,7 +55,6 @@
 import com.google.inject.util.Providers;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
@@ -71,6 +72,10 @@
     return cfg;
   }
 
+  @Inject protected Accounts accounts;
+
+  @Inject protected AccountsUpdate.Server accountsUpdate;
+
   @Inject protected AccountCache accountCache;
 
   @Inject protected AccountManager accountManager;
@@ -380,9 +385,9 @@
 
     // update account in the database so that account index is stale
     String newName = "Test User";
-    Account account = db.accounts().get(new Account.Id(user1._accountId));
+    Account account = accounts.get(db, new Account.Id(user1._accountId));
     account.setFullName(newName);
-    db.accounts().update(Collections.singleton(account));
+    db.accounts().update(ImmutableSet.of(account));
 
     assertQuery("name:" + quote(user1.name), user1);
     assertQuery("name:" + quote(newName));
@@ -464,12 +469,11 @@
       if (email != null) {
         accountManager.link(id, AuthRequest.forEmail(email));
       }
-      Account a = db.accounts().get(id);
+      Account a = accounts.get(db, id);
       a.setFullName(fullName);
       a.setPreferredEmail(email);
       a.setActive(active);
-      db.accounts().update(ImmutableList.of(a));
-      accountCache.evict(id);
+      accountsUpdate.create().update(db, a);
       return id;
     }
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index c58162f..7853b2d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -70,6 +70,8 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
@@ -147,7 +149,9 @@
     return cfg;
   }
 
+  @Inject protected Accounts accounts;
   @Inject protected AccountCache accountCache;
+  @Inject protected AccountsUpdate.Server accountsUpdate;
   @Inject protected AccountManager accountManager;
   @Inject protected AllUsersName allUsersName;
   @Inject protected BatchUpdate.Factory updateFactory;
@@ -204,11 +208,11 @@
     db = schemaFactory.open();
 
     userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
-    Account userAccount = db.accounts().get(userId);
+    Account userAccount = accounts.get(db, userId);
     String email = "user@example.com";
     externalIdsUpdate.create().insert(ExternalId.createEmail(userId, email));
     userAccount.setPreferredEmail(email);
-    db.accounts().update(ImmutableList.of(userAccount));
+    accountsUpdate.create().update(db, userAccount);
     user = userFactory.create(userId);
     requestContext.setContext(newRequestContext(userAccount.getId()));
   }
@@ -2304,12 +2308,11 @@
       if (email != null) {
         accountManager.link(id, AuthRequest.forEmail(email));
       }
-      Account a = db.accounts().get(id);
+      Account a = accounts.get(db, id);
       a.setFullName(fullName);
       a.setPreferredEmail(email);
       a.setActive(active);
-      db.accounts().update(ImmutableList.of(a));
-      accountCache.evict(id);
+      accountsUpdate.create().update(db, a);
       return id;
     }
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index b508f8b..fe2a8f3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -18,7 +18,6 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.groups.Groups.QueryRequest;
@@ -34,6 +33,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -70,6 +71,10 @@
     return cfg;
   }
 
+  @Inject protected Accounts accounts;
+
+  @Inject protected AccountsUpdate.Server accountsUpdate;
+
   @Inject protected AccountCache accountCache;
 
   @Inject protected AccountManager accountManager;
@@ -314,12 +319,11 @@
       if (email != null) {
         accountManager.link(id, AuthRequest.forEmail(email));
       }
-      Account a = db.accounts().get(id);
+      Account a = accounts.get(db, id);
       a.setFullName(fullName);
       a.setPreferredEmail(email);
       a.setActive(active);
-      db.accounts().update(ImmutableList.of(a));
-      accountCache.evict(id);
+      accountsUpdate.create().update(db, a);
       return id;
     }
   }
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 358340d..976d980 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
@@ -1196,9 +1196,8 @@
      */
     _updateRelatedChangeMaxHeight() {
       // Takes into account approximate height for the expand button and
-      // bottom margin
-      const extraHeight = 24;
-      let maxExistingHeight;
+      // bottom margin.
+      const EXTRA_HEIGHT = 30;
       let newHeight;
       const hasCommitToggle =
           !this._computeCommitToggleHidden(this._latestCommitMessage);
@@ -1216,7 +1215,7 @@
         // Note: extraHeight is to take into account margin/padding.
         const medRelatedHeight = Math.max(
             this._getOffsetHeight(this.$.mainChangeInfo) -
-            this._getOffsetHeight(this.$.commitMessage) - 2 * extraHeight,
+            this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT,
             MINIMUM_RELATED_MAX_HEIGHT);
         newHeight = medRelatedHeight;
       } else {
@@ -1224,29 +1223,30 @@
           // Make sure the content is lined up if both areas have buttons. If
           // the commit message is not collapsed, instead use the change info
           // height.
-          maxExistingHeight = this._getOffsetHeight(this.$.commitMessage);
+          newHeight = this._getOffsetHeight(this.$.commitMessage);
         } else {
-          maxExistingHeight = this._getOffsetHeight(this.$.mainChangeInfo) -
-              extraHeight;
-        }
-        // Get the line height of related changes, and convert it to the nearest
-        // integer.
-        const lineHeight = this._getLineHeight(this.$.relatedChanges);
-
-        // Figure out a new height that is divisible by the rounded line height.
-        const remainder = maxExistingHeight % lineHeight;
-        newHeight = maxExistingHeight - remainder;
-
-        // Update the max-height of the relation chain to this new height;
-        if (hasCommitToggle) {
-          this.customStyle['--related-change-btn-top-padding'] =
-            remainder + 'px';
+          newHeight = this._getOffsetHeight(this.$.commitAndRelated) -
+              EXTRA_HEIGHT;
         }
       }
       if (this.$.relatedChanges.hidden) {
         this.customStyle['--commit-message-max-width'] = 'none';
       }
+      // Get the line height of related changes, and convert it to the nearest
+      // integer.
+      const lineHeight = this._getLineHeight(this.$.relatedChanges);
+
+      // Figure out a new height that is divisible by the rounded line height.
+      const remainder = newHeight % lineHeight;
+      newHeight = newHeight - remainder;
+
       this.customStyle['--relation-chain-max-height'] = newHeight + 'px';
+
+      // Update the max-height of the relation chain to this new height.
+      if (hasCommitToggle) {
+        this.customStyle['--related-change-btn-top-padding'] =
+          remainder + 'px';
+      }
       this.updateStyles();
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index e91f372..0cc9994 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -1203,13 +1203,13 @@
         sandbox.stub(element, '_getLineHeight', () => 12);
         sandbox.stub(window, 'matchMedia', () => ({matches: false}));
 
-        // 50 (existing height) - 24 (extra height) = 26 (adjusted height).
-        // 50 (existing height)  % 12 (line height) = 2 (remainder).
-        // 26 (adjusted height) - 2 (remainder) = 24 (max height to set).
+        // 50 (existing height) - 30 (extra height) = 20 (adjusted height).
+        // 20 (max existing height)  % 12 (line height) = 6 (remainder).
+        // 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
 
         element._updateRelatedChangeMaxHeight();
         assert.equal(element.customStyle['--relation-chain-max-height'],
-            '24px');
+            '12px');
         assert.equal(element.customStyle['--related-change-btn-top-padding'],
             undefined);
       });
@@ -1237,8 +1237,12 @@
         sandbox.stub(window, 'matchMedia', () => ({matches: true}));
 
         element._updateRelatedChangeMaxHeight();
+
+        // 400 (new height) % 12 (line height) = 4 (remainder).
+        // 400 (new height) - 4 (remainder) = 396.
+
         assert.equal(element.customStyle['--relation-chain-max-height'],
-            '400px');
+            '396px');
       });
 
       test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
@@ -1253,9 +1257,11 @@
           }
         });
 
+        // 100 (new height) % 12 (line height) = 4 (remainder).
+        // 100 (new height) - 4 (remainder) = 96.
         element._updateRelatedChangeMaxHeight();
         assert.equal(element.customStyle['--relation-chain-max-height'],
-            '100px');
+            '96px');
       });
 
 
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index aa9c5fd..9094933 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index ab2ad11..d8cdb02 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -23,6 +23,7 @@
     behaviors: [
       Gerrit.BaseUrlBehavior,
       Gerrit.PathListBehavior,
+      Gerrit.URLEncodingBehavior,
     ],
 
     properties: {
@@ -38,7 +39,8 @@
     },
 
     _computeFileDiffURL(file, changeNum, patchNum) {
-      return `${this.getBaseUrl()}/c/${changeNum}/${patchNum}/${file}`;
+      return this.getBaseUrl() + '/c/' + changeNum + '/' + patchNum + '/' +
+          this.encodeURL(file);
     },
 
     _computeFileDisplayName(path) {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index 4c8b443..d25664e 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -61,9 +61,8 @@
     });
 
     test('_computeFileDiffURL', () => {
-      const expected = '/c/<change>/<patch>/<file>';
-      const actual =
-          element._computeFileDiffURL('<file>', '<change>', '<patch>');
+      const expected = '/c/change/patch/file';
+      const actual = element._computeFileDiffURL('file', 'change', 'patch');
       assert.equal(actual, expected);
     });
 
@@ -78,17 +77,23 @@
 
     test('_computeDiffLineURL', () => {
       const comment = {line: 123, side: 'REVISION', patch_set: 10};
-      let expected = '/c/<change>/<patch>/<file>#123';
-      let actual = element._computeDiffLineURL('<file>', '<change>', '<patch>',
+      let expected = '/c/change/patch/file#123';
+      let actual = element._computeDiffLineURL('file', 'change', 'patch',
           comment);
       assert.equal(actual, expected);
 
       comment.line = 321;
       comment.side = 'PARENT';
 
-      expected = '/c/<change>/<patch>/<file>#b321';
-      actual = element._computeDiffLineURL('<file>', '<change>', '<patch>',
-          comment);
+      expected = '/c/change/patch/file#b321';
+      actual = element._computeDiffLineURL('file', 'change', 'patch', comment);
+    });
+
+    test('_computeDiffLineURL encoding', () => {
+      const comment = {line: 123, side: 'REVISION', patch_set: 10};
+      const expected = '/c/123/2/x%252By.md#123';
+      const actual = element._computeDiffLineURL('x+y.md', '123', '2', comment);
+      assert.equal(actual, expected);
     });
 
     test('_computePatchDisplayName', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 81135ae..c0371ee 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -246,7 +246,7 @@
           as="file"
           initial-count="[[fileListIncrement]]"
           target-framerate="1">
-        <div class="file-row row" data-path$="[[file.__path]]">
+        <div class="file-row row" data-path$="[[file.__path]]" tabindex="-1">
           <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
             <input type="checkbox" checked="[[file.isReviewed]]"
                 class="reviewed" aria-label="Reviewed checkbox">
@@ -401,6 +401,7 @@
     <gr-cursor-manager
         id="fileCursor"
         scroll-behavior="keep-visible"
+        focus-on-move
         cursor-target-class="selected"></gr-cursor-manager>
     <gr-reporting id="reporting"></gr-reporting>
   </template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 7621a9d..0bcd9fd 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -135,7 +135,7 @@
       'c': '_handleCKey',
       '[': '_handleLeftBracketKey',
       ']': '_handleRightBracketKey',
-      'o enter': '_handleEnterKey',
+      'o': '_handleOKey',
       'n': '_handleNKey',
       'p': '_handlePKey',
       'shift+a': '_handleCapitalAKey',
@@ -537,14 +537,10 @@
       this._openSelectedFile(0);
     },
 
-    _handleEnterKey(e) {
+    _handleOKey(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e)) { return; }
 
-      // Use native handling if an anchor is selected. @see Issue 5754
-      if (e.detail && e.detail.keyboardEvent && e.detail.keyboardEvent.target &&
-          e.detail.keyboardEvent.target.tagName === 'A') { return; }
-
       e.preventDefault();
       if (this._showInlineDiffs) {
         this._openCursorFile();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index f0f5403..4577a05 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -331,9 +331,7 @@
         const showStub = sandbox.stub(page, 'show');
         assert.equal(element.$.fileCursor.index, 2);
         assert.equal(element.selectedIndex, 2);
-        MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
-        assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
-            'Should navigate to /c/42/2/myfile.txt');
+
         // k with a modifier should not move the cursor.
         MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
         assert.equal(element.$.fileCursor.index, 2);
@@ -384,7 +382,7 @@
         }
       });
 
-      suite('_handleEnterKey', () => {
+      suite('_handleOKey', () => {
         let interact;
 
         setup(() => {
@@ -402,7 +400,7 @@
 
             const e = new CustomEvent('fake-keyboard-event', opt_payload);
             sinon.stub(e, 'preventDefault');
-            element._handleEnterKey(e);
+            element._handleOKey(e);
             assert.isTrue(e.preventDefault.called);
             const result = {};
             if (openCursorStub.called) {
@@ -440,14 +438,6 @@
           element._userPrefs.expand_inline_diffs = true;
           assert.deepEqual(interact(), {expanded: true});
         });
-
-        test('noop when anchor focused', () => {
-          const e = new CustomEvent('fake-keyboard-event',
-              {detail: {keyboardEvent: {target: document.createElement('a')}}});
-          sinon.stub(e, 'preventDefault');
-          element._handleEnterKey(e);
-          assert.isFalse(e.preventDefault.called);
-        });
       });
     });
 
@@ -1242,4 +1232,6 @@
       assert.isFalse(element._displayLine);
     });
   });
+
+  a11ySuite('basic');
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 1396a2b..fef0f87 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -231,7 +231,7 @@
             <td>Select previous file</td>
           </tr>
           <tr>
-            <td><span class="key">Enter</span> or <span class="key">o</span></td>
+            <td><span class="key">o</span></td>
             <td>Show selected file</td>
           </tr>
           <tr>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
index 6f47f9b..9695b18 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -66,6 +66,7 @@
     row.classList.add('diff-row', 'side-by-side');
     row.setAttribute('left-type', leftLine.type);
     row.setAttribute('right-type', rightLine.type);
+    row.tabIndex = -1;
 
     this._appendPair(section, row, leftLine, leftLine.beforeNumber,
         GrDiffBuilder.Side.LEFT);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index 64c8f95..b1dd789 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -67,6 +67,7 @@
     lineEl.classList.add('right');
     row.appendChild(lineEl);
     row.classList.add('diff-row', 'unified');
+    row.tabIndex = -1;
 
     const action = this._createContextControl(section, line);
     if (action) {
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 bb23572..3cb1a39 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
@@ -385,6 +385,13 @@
     if (opt_class) {
       td.classList.add(opt_class);
     }
+
+    if (line.type === GrDiffLine.Type.REMOVE) {
+      td.setAttribute('aria-label', `${number} removed`);
+    } else if (line.type === GrDiffLine.Type.ADD) {
+      td.setAttribute('aria-label', `${number} added`);
+    }
+
     if (line.type === GrDiffLine.Type.BLANK) {
       return td;
     } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
index 2d0786a..3ae3159 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -23,6 +23,7 @@
         id="cursorManager"
         scroll-behavior="[[_scrollBehavior]]"
         cursor-target-class="target-row"
+        focus-on-move
         target="{{diffRow}}"></gr-cursor-manager>
   </template>
   <script src="gr-diff-cursor.js"></script>
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 d101fea..a61b9ca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -77,6 +77,9 @@
         font-family: var(--font-family);
         font-style: italic;
       }
+      .diff-row {
+        outline: none;
+      }
       .diff-row.target-row.target-side-left .lineNum.left,
       .diff-row.target-row.target-side-right .lineNum.right,
       .diff-row.target-row.unified .lineNum {
@@ -210,7 +213,10 @@
               is-image-diff="[[isImageDiff]]"
               base-image="[[_baseImage]]"
               revision-image="[[_revisionImage]]">
-            <table id="diffTable" class$="[[_diffTableClass]]"></table>
+            <table
+                id="diffTable"
+                class$="[[_diffTableClass]]"
+                role="presentation"></table>
           </gr-diff-builder>
         </gr-diff-highlight>
       </gr-diff-selection>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
new file mode 100644
index 0000000..64e5439
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
@@ -0,0 +1,64 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-copy-clipboard">
+  <template>
+    <style include="shared-styles">
+      .text {
+        display: flex;
+        flex-wrap: wrap;
+        margin-bottom: .5em;
+        width: 60em;
+      }
+      .text label {
+        flex: 0 0 100%;
+      }
+      .copyText {
+        flex-grow: 1;
+        margin-right: .3em;
+      }
+      .hideInput {
+        display: none;
+      }
+      input {
+        font-family: var(--monospace-font-family);
+        font-size: inherit;
+      }
+    </style>
+    <div class="text">
+        <label>[[title]]</label>
+        <input id="input" is="iron-input"
+            class$="copyText [[_computeInputClass(hideInput)]]"
+            type="text"
+            bind-value="[[text]]"
+            on-tap="_handleInputTap"
+            readonly>
+        <gr-button id="button"
+            class="copyToClipboard"
+            on-tap="_copyToClipboard">
+          copy
+        </gr-button>
+      </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-copy-clipboard.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
new file mode 100644
index 0000000..a371374
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -0,0 +1,52 @@
+// 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.
+(function() {
+  'use strict';
+
+  const COPY_TIMEOUT_MS = 1000;
+
+  Polymer({
+    is: 'gr-copy-clipboard',
+
+    properties: {
+      text: String,
+      title: String,
+      hideInput: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    focusOnCopy() {
+      this.$.button.focus();
+    },
+
+    _computeInputClass(hideInput) {
+      return hideInput ? 'hideInput' : '';
+    },
+
+    _handleInputTap(e) {
+      e.preventDefault();
+      Polymer.dom(e).rootTarget.select();
+    },
+
+    _copyToClipboard(e) {
+      this.$.input.select();
+      document.execCommand('copy');
+      window.getSelection().removeAllRanges();
+      e.target.textContent = 'done';
+      this.async(() => { e.target.textContent = 'copy'; }, COPY_TIMEOUT_MS);
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
new file mode 100644
index 0000000..7310629
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-copy-clipboard</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-copy-clipboard.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-copy-clipboard></gr-copy-clipboard>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-copy-clipboard tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        saveChangeStarred() { return Promise.resolve({ok: true}); },
+      });
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      element.title = 'Checkout';
+      element.text = `git fetch http://gerrit@localhost:8080/a/test-project
+          refs/changes/05/5/1 && git checkout FETCH_HEAD`;
+      flushAsynchronousOperations();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('copy to clipboard', () => {
+      const clipboardSpy = sandbox.spy(element, '_copyToClipboard');
+      const copyBtn = element.$$('.copyToClipboard');
+      MockInteractions.tap(copyBtn);
+      assert.isTrue(clipboardSpy.called);
+    });
+
+    test('focusOnCopy', () => {
+      element.focusOnCopy();
+      assert.deepEqual(Polymer.dom(element.root).activeElement,
+          element.$$('.copyToClipboard'));
+    });
+
+    test('_handleInputTap', () => {
+      const inputElement = element.$$('input');
+      MockInteractions.tap(inputElement);
+      assert.equal(inputElement.selectionStart, 0);
+      assert.equal(inputElement.selectionEnd, element.text.length - 1);
+    });
+
+    test('hideInput', () => {
+      assert.notEqual(getComputedStyle(element.$.input).display, 'none');
+      element.hideInput = true;
+      flushAsynchronousOperations();
+      assert.equal(getComputedStyle(element.$.input).display, 'none');
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
index 0e68899..c79e838 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
@@ -51,35 +51,35 @@
         display: flex;
         flex-direction: column;
       }
-      ::content tr.table {
-        border-bottom: 1px solid #eee;
-      }
       ::content #list {
         border-collapse: collapse;
         width: 100%;
       }
-      ::content td {
+      ::content #list tr.table {
+        border-bottom: 1px solid #eee;
+      }
+      ::content #list td {
         flex-shrink: 0;
         padding: .3em .5em;
       }
-      ::content th {
+      ::content #list th {
         background-color: #ddd;
         border-bottom: 1px solid #eee;
         font-weight: bold;
         padding: .3em .5em;
         text-align: left;
       }
-      ::content a {
+      ::content #list a {
         color: var(--default-text-color);
         text-decoration: none;
       }
-      ::content a:hover {
+      ::content #list a:hover {
         text-decoration: underline;
       }
-      ::content .description {
+      ::content #list .description {
         width: 70%;
       }
-      ::content .loading {
+      ::content #list .loading {
         color: #666;
         padding: 1em var(--default-horizontal-margin);
       }
diff --git a/polygerrit-ui/app/embed/change-diff-views.html b/polygerrit-ui/app/embed/change-diff-views.html
new file mode 100644
index 0000000..b4f521b
--- /dev/null
+++ b/polygerrit-ui/app/embed/change-diff-views.html
@@ -0,0 +1,18 @@
+<!--
+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.
+-->
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../elements/change/gr-change-view/gr-change-view.html">
+<link rel="import" href="../elements/diff/gr-diff-view/gr-diff-view.html">
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 5396679..cc1dabd 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -23,15 +23,18 @@
       sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form,
       label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article,
       aside, canvas, details, embed, figure, figcaption, footer, header, hgroup,
-      menu, nav, output, ruby, section, summary, time, mark, audio, video {
+      main, menu, nav, output, ruby, section, summary, time, mark, audio, video {
         border: 0;
+        box-sizing: border-box;
         font-size: 100%;
         font: inherit;
         margin: 0;
         padding: 0;
         vertical-align: baseline;
       }
-      input {
+      input,
+      iron-autogrow-textarea {
+        box-sizing: border-box;
         margin: 0;
         padding: 0;
       }
@@ -53,7 +56,6 @@
         border-collapse: collapse;
         border-spacing: 0;
       }
-
       /* Other Shared Styles*/
       h1 {
         font-size: 2em;
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 0f552a00..bd82da0 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -101,6 +101,7 @@
     'shared/gr-button/gr-button_test.html',
     'shared/gr-change-star/gr-change-star_test.html',
     'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
+    'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',