Merge changes from topic "add-members-subgroups-to-group-index"

* changes:
  Add fields for members and subgroups to the group index
  Use the term 'subgroup' instead of 'includes' where possible
  Avoid unnecessary loading of members and subgroups
  Cache instances of type InternalGroup instead of AccountGroup by UUID
  Base group index on new class InternalGroup
diff --git a/Documentation/prolog-change-facts.txt b/Documentation/prolog-change-facts.txt
index 11d17b8..01a1878 100644
--- a/Documentation/prolog-change-facts.txt
+++ b/Documentation/prolog-change-facts.txt
@@ -58,6 +58,10 @@
                       |`current_user(user(peer_daemon)).`
                       |`current_user(user(replication)).`
 
+|`pure_revert/1`     |`pure_revert(1).`
+    |link:rest-api-changes.html#get-pure-revert[Pure revert] as integer atom (1 if
+        the change is a pure revert, 0 otherwise)
+
 |`uploader/1`     |`uploader(user(1000000)).`
     |Uploader as `user(ID)` term. ID is the numeric account ID
 
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 1c114e2..40906bd 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -1048,6 +1048,56 @@
 indicate to the user that all the comments have to be resolved for the
 change to become submittable.
 
+=== Example 17: Make change submittable if it is a pure revert
+In this example we will use the `pure_revert` fact about a
+change. Our goal is to block the submission of any change that is not a
+pure revert. Basically, it can be achieved by the following rules:
+
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(R)) :-
+    gerrit:pure_revert(1),
+    !,
+    gerrit:commit_author(A),
+    R = label('Is-Pure-Revert', ok(A)).
+
+submit_rule(submit(R)) :-
+    gerrit:pure_revert(U),
+    U /= 1,
+    R = label('Is-Pure-Revert', need(_)).
+----
+
+Suppose currently a change is submittable if it gets `+2` for `Code-Review`
+and `+1` for `Verified`. It can be extended to support the above rules as
+follows:
+
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(CR, V, R)) :-
+    base(CR, V),
+    gerrit:pure_revert(1),
+    !,
+    gerrit:commit_author(A),
+    R = label('Is-Pure-Revert', ok(A)).
+
+submit_rule(submit(CR, V, R)) :-
+    base(CR, V),
+    gerrit:pure_revert(U),
+    U /= 1,
+    R = label('Is-Pure-Revert', need(_)).
+
+base(CR, V) :-
+    gerrit:max_with_block(-2, 2, 'Code-Review', CR),
+    gerrit:max_with_block(-1, 1, 'Verified', V).
+----
+
+Note that a new label as `Is-Pure-Revert` should not be configured.
+It's only used to show `'Needs Is-Pure-Revert'` in the UI to clearly
+indicate to the user that all the comments have to be resolved for the
+change to become submittable.
+
 == Examples - Submit Type
 The following examples show how to implement own submit type rules.
 
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index ede83f3..dca2fb0 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
 import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -30,7 +31,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
 import com.google.common.jimfs.Jimfs;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
@@ -103,7 +103,6 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.MutableNotesMigration;
 import com.google.gerrit.server.notedb.PatchSetState;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -254,7 +253,6 @@
   @Inject private InProcessProtocol inProcessProtocol;
   @Inject private Provider<AnonymousUser> anonymousUser;
   @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
-  @Inject private ChangeControl.GenericFactory changeControlFactory;
 
   private List<Repository> toClose;
 
@@ -645,11 +643,7 @@
 
   private List<Boolean> getPatchSetDraftStatuses(Change.Id id) throws Exception {
     Collection<RevisionInfo> revisionInfos =
-        gApi.changes()
-            .id(id.get())
-            .get(EnumSet.of(ListChangesOption.ALL_REVISIONS))
-            .revisions
-            .values();
+        gApi.changes().id(id.get()).get(ALL_REVISIONS).revisions.values();
     return revisionInfos.stream().map(revisionInfo -> revisionInfo.draft).collect(toList());
   }
 
@@ -806,9 +800,7 @@
   }
 
   protected ChangeInfo get(String id, ListChangesOption... options) throws RestApiException {
-    return gApi.changes()
-        .id(id)
-        .get(Sets.newEnumSet(Arrays.asList(options), ListChangesOption.class));
+    return gApi.changes().id(id).get(options);
   }
 
   protected List<ChangeInfo> query(String q) throws RestApiException {
@@ -1120,8 +1112,7 @@
   protected ChangeResource parseChangeResource(String changeId) throws Exception {
     List<ChangeNotes> notes = changeFinder.find(changeId);
     assertThat(notes).hasSize(1);
-    return changeResourceFactory.create(
-        changeControlFactory.controlFor(notes.get(0), atrScope.get().getUser()));
+    return changeResourceFactory.create(notes.get(0), atrScope.get().getUser());
   }
 
   protected String createGroup(String name) throws Exception {
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 8041e9f..c11474f 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
@@ -22,6 +22,20 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
+import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
+import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
+import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
 import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
@@ -49,7 +63,6 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.FooterConstants;
@@ -74,7 +87,6 @@
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -129,7 +141,6 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -703,17 +714,11 @@
     rebase.call(changeId);
 
     // Second change should have 2 patch sets and an approval
-    ChangeInfo c2 =
-        gApi.changes()
-            .id(changeId)
-            .get(EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.DETAILED_LABELS));
+    ChangeInfo c2 = gApi.changes().id(changeId).get(CURRENT_REVISION, DETAILED_LABELS);
     assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2);
 
     // ...and the committer and description should be correct
-    ChangeInfo info =
-        gApi.changes()
-            .id(changeId)
-            .get(EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT));
+    ChangeInfo info = gApi.changes().id(changeId).get(CURRENT_REVISION, CURRENT_COMMIT);
     GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
     assertThat(committer.name).isEqualTo(admin.fullName);
     assertThat(committer.email).isEqualTo(admin.email);
@@ -2059,15 +2064,13 @@
     assertThat(result.actions).isNull();
     assertThat(result.revisions).isNull();
 
-    EnumSet<ListChangesOption> options =
-        EnumSet.of(
-            ListChangesOption.ALL_REVISIONS,
-            ListChangesOption.CHANGE_ACTIONS,
-            ListChangesOption.CURRENT_ACTIONS,
-            ListChangesOption.DETAILED_LABELS,
-            ListChangesOption.MESSAGES);
     result =
-        Iterables.getOnlyElement(gApi.changes().query(r.getChangeId()).withOptions(options).get());
+        Iterables.getOnlyElement(
+            gApi.changes()
+                .query(r.getChangeId())
+                .withOptions(
+                    ALL_REVISIONS, CHANGE_ACTIONS, CURRENT_ACTIONS, DETAILED_LABELS, MESSAGES)
+                .get());
     assertThat(Iterables.getOnlyElement(result.labels.keySet())).isEqualTo("Code-Review");
     assertThat(result.messages).hasSize(1);
     assertThat(result.actions).isNotEmpty();
@@ -2193,8 +2196,7 @@
   public void check() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).get().problems).isNull();
-    assertThat(gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.CHECK)).problems)
-        .isEmpty();
+    assertThat(gApi.changes().id(r.getChangeId()).get(CHECK).problems).isEmpty();
   }
 
   @Test
@@ -2232,9 +2234,7 @@
     in.label("Custom2", 1);
     gApi.changes().id(r2.getChangeId()).current().review(in);
 
-    EnumSet<ListChangesOption> options =
-        EnumSet.of(ListChangesOption.ALL_REVISIONS, ListChangesOption.COMMIT_FOOTERS);
-    ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(options);
+    ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS);
     assertThat(actual.revisions).hasSize(2);
 
     // No footers except on latest patch set.
@@ -2277,9 +2277,7 @@
             });
     ChangeInfo actual;
     try {
-      EnumSet<ListChangesOption> options =
-          EnumSet.of(ListChangesOption.ALL_REVISIONS, ListChangesOption.COMMIT_FOOTERS);
-      actual = gApi.changes().id(change.getChangeId()).get(options);
+      actual = gApi.changes().id(change.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS);
     } finally {
       handle.remove();
     }
@@ -2320,9 +2318,9 @@
                   .query()
                   .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
                   // Options should match defaults in AccountDashboardScreen.
-                  .withOption(ListChangesOption.LABELS)
-                  .withOption(ListChangesOption.DETAILED_ACCOUNTS)
-                  .withOption(ListChangesOption.REVIEWED)
+                  .withOption(LABELS)
+                  .withOption(DETAILED_ACCOUNTS)
+                  .withOption(REVIEWED)
                   .get())
           .hasSize(2);
     } finally {
@@ -2335,7 +2333,7 @@
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
     gApi.changes().id(triplet).addReviewer(user.username);
-    ChangeInfo c = gApi.changes().id(triplet).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
@@ -2345,7 +2343,7 @@
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     Util.blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*");
     saveProjectConfig(project, cfg);
-    c = gApi.changes().id(triplet).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     approval = codeReview.all.get(0);
@@ -2360,10 +2358,7 @@
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 = amendChange(r1.getChangeId());
 
-    ChangeInfo info =
-        gApi.changes()
-            .id(r1.getChangeId())
-            .get(EnumSet.of(ListChangesOption.ALL_REVISIONS, ListChangesOption.PUSH_CERTIFICATES));
+    ChangeInfo info = gApi.changes().id(r1.getChangeId()).get(ALL_REVISIONS, PUSH_CERTIFICATES);
 
     RevisionInfo rev1 = info.revisions.get(r1.getCommit().name());
     assertThat(rev1).isNotNull();
@@ -2675,13 +2670,7 @@
     in.subject = "update change by merge ps2";
     gApi.changes().id(changeId).createMergePatchSet(in);
     ChangeInfo changeInfo =
-        gApi.changes()
-            .id(changeId)
-            .get(
-                EnumSet.of(
-                    ListChangesOption.ALL_REVISIONS,
-                    ListChangesOption.CURRENT_COMMIT,
-                    ListChangesOption.CURRENT_REVISION));
+        gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
     assertThat(changeInfo.revisions.size()).isEqualTo(2);
     assertThat(changeInfo.subject).isEqualTo(in.subject);
     assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
@@ -2718,13 +2707,7 @@
     in.inheritParent = true;
     gApi.changes().id(changeId).createMergePatchSet(in);
     ChangeInfo changeInfo =
-        gApi.changes()
-            .id(changeId)
-            .get(
-                EnumSet.of(
-                    ListChangesOption.ALL_REVISIONS,
-                    ListChangesOption.CURRENT_COMMIT,
-                    ListChangesOption.CURRENT_REVISION));
+        gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
 
     assertThat(changeInfo.revisions.size()).isEqualTo(2);
     assertThat(changeInfo.subject).isEqualTo(in.subject);
@@ -2930,7 +2913,7 @@
 
     gApi.changes().id(triplet).addReviewer(user.username);
 
-    ChangeInfo c = gApi.changes().id(triplet).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
@@ -2950,7 +2933,7 @@
         heads);
     saveProjectConfig(project, cfg);
 
-    c = gApi.changes().id(triplet).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     approval = codeReview.all.get(0);
@@ -2971,7 +2954,7 @@
 
     gApi.changes().id(triplet).addReviewer(user.username);
 
-    ChangeInfo c = gApi.changes().id(triplet).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
     LabelInfo codeReview = c.labels.get("Code-Review");
     assertThat(codeReview.all).hasSize(1);
     ApprovalInfo approval = codeReview.all.get(0);
@@ -2979,33 +2962,20 @@
     assertThat(approval.permittedVotingRange).isNull();
   }
 
-  @Sandboxed
   @Test
   public void unresolvedCommentsBlocked() throws Exception {
-    RevCommit oldHead = getRemoteHead();
-    GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
-    testRepo.reset("config");
-    PushOneCommit push =
-        pushFactory.create(
-            db,
-            admin.getIdent(),
-            testRepo,
-            "Configure",
-            "rules.pl",
-            "submit_rule(submit(R)) :- \n"
-                + "gerrit:unresolved_comments_count(0), \n"
-                + "!,"
-                + "gerrit:commit_author(A), \n"
-                + "R = label('All-Comments-Resolved', ok(A)).\n"
-                + "submit_rule(submit(R)) :- \n"
-                + "gerrit:unresolved_comments_count(U), \n"
-                + "U > 0,"
-                + "R = label('All-Comments-Resolved', need(_)). \n\n");
+    modifySubmitRules(
+        "submit_rule(submit(R)) :- \n"
+            + "gerrit:unresolved_comments_count(0), \n"
+            + "!,"
+            + "gerrit:commit_author(A), \n"
+            + "R = label('All-Comments-Resolved', ok(A)).\n"
+            + "submit_rule(submit(R)) :- \n"
+            + "gerrit:unresolved_comments_count(U), \n"
+            + "U > 0,"
+            + "R = label('All-Comments-Resolved', need(_)). \n\n");
 
-    push.to(RefNames.REFS_CONFIG);
-    testRepo.reset(oldHead);
-
-    oldHead = getRemoteHead();
+    String oldHead = getRemoteHead().name();
     PushOneCommit.Result result1 =
         pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
     testRepo.reset(oldHead);
@@ -3018,13 +2988,61 @@
     gApi.changes().id(result1.getChangeId()).current().submit();
 
     exception.expect(ResourceConflictException.class);
-    exception.expectMessage(
-        "Failed to submit 1 change due to the following problems:\n"
-            + "Change 2: needs All-Comments-Resolved");
+    exception.expectMessage("Failed to submit 1 change due to the following problems");
+    exception.expectMessage("needs All-Comments-Resolved");
     gApi.changes().id(result2.getChangeId()).current().submit();
   }
 
   @Test
+  public void pureRevertFactBlocksSubmissionOfNonReverts() throws Exception {
+    addPureRevertSubmitRule();
+
+    // Create a change that is not a revert of another change
+    PushOneCommit.Result r1 =
+        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+    approve(r1.getChangeId());
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Failed to submit 1 change due to the following problems");
+    exception.expectMessage("needs Is-Pure-Revert");
+    gApi.changes().id(r1.getChangeId()).current().submit();
+  }
+
+  @Test
+  public void pureRevertFactBlocksSubmissionOfNonPureReverts() throws Exception {
+    PushOneCommit.Result r1 =
+        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+    merge(r1);
+
+    addPureRevertSubmitRule();
+
+    // Create a revert and push a content change
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    amendChange(revertId);
+    approve(revertId);
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Failed to submit 1 change due to the following problems");
+    exception.expectMessage("needs Is-Pure-Revert");
+    gApi.changes().id(revertId).current().submit();
+  }
+
+  @Test
+  public void pureRevertFactAllowsSubmissionOfPureReverts() throws Exception {
+    // Create a change that we can later revert
+    PushOneCommit.Result r1 =
+        pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
+    merge(r1);
+
+    addPureRevertSubmitRule();
+
+    // Create a revert and submit it
+    String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+    approve(revertId);
+    gApi.changes().id(revertId).current().submit();
+  }
+
+  @Test
   public void changeCommitMessage() throws Exception {
     // Tests mutating the commit message as both the owner of the change and a regular user with
     // addPatchSet permission. Asserts that both cases succeed.
@@ -3155,14 +3173,7 @@
     ri.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(ci));
     gApi.changes().id(id).current().review(ri);
 
-    ChangeInfo info =
-        gApi.changes()
-            .id(id)
-            .get(
-                EnumSet.of(
-                    ListChangesOption.MESSAGES,
-                    ListChangesOption.CURRENT_COMMIT,
-                    ListChangesOption.CURRENT_REVISION));
+    ChangeInfo info = gApi.changes().id(id).get(MESSAGES, CURRENT_COMMIT, CURRENT_REVISION);
     assertThat(info.subject).isEqualTo(subject);
     assertThat(Iterables.getLast(info.messages).message).endsWith(ri.message);
     assertThat(Iterables.getOnlyElement(info.revisions.values()).commit.message)
@@ -3308,7 +3319,7 @@
 
   private Optional<ReviewerState> getReviewerState(String changeId, Account.Id accountId)
       throws Exception {
-    ChangeInfo c = gApi.changes().id(changeId).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    ChangeInfo c = gApi.changes().id(changeId).get(DETAILED_LABELS);
     Set<ReviewerState> states =
         c.reviewers
             .entrySet()
@@ -3353,6 +3364,33 @@
     }
   }
 
+  private void addPureRevertSubmitRule() throws Exception {
+    modifySubmitRules(
+        "submit_rule(submit(R)) :- \n"
+            + "gerrit:pure_revert(1), \n"
+            + "!,"
+            + "gerrit:commit_author(A), \n"
+            + "R = label('Is-Pure-Revert', ok(A)).\n"
+            + "submit_rule(submit(R)) :- \n"
+            + "gerrit:pure_revert(U), \n"
+            + "U \\= 1,"
+            + "R = label('Is-Pure-Revert', need(_)). \n\n");
+  }
+
+  private void modifySubmitRules(String newContent) throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      TestRepository<?> testRepo = new TestRepository<>((InMemoryRepository) repo);
+      testRepo
+          .branch(RefNames.REFS_CONFIG)
+          .commit()
+          .author(admin.getIdent())
+          .committer(admin.getIdent())
+          .add("rules.pl", newContent)
+          .message("Modify rules.pl")
+          .create();
+    }
+  }
+
   @Test
   @GerritConfig(name = "trackingid.jira-bug.footer", value = "Bug:")
   @GerritConfig(name = "trackingid.jira-bug.match", value = "JRA\\d{2,8}")
@@ -3369,8 +3407,7 @@
     PushOneCommit.Result result = push.to("refs/for/master");
     result.assertOkStatus();
 
-    ChangeInfo change =
-        gApi.changes().id(result.getChangeId()).get(EnumSet.of(ListChangesOption.TRACKING_IDS));
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get(TRACKING_IDS);
     Collection<TrackingIdInfo> trackingIds = change.trackingIds;
     assertThat(trackingIds).isNotNull();
     assertThat(trackingIds).hasSize(1);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 94f8494..28933ad 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -20,6 +20,9 @@
 import static com.google.gerrit.extensions.client.ChangeKind.NO_CODE_CHANGE;
 import static com.google.gerrit.extensions.client.ChangeKind.REWORK;
 import static com.google.gerrit.extensions.client.ChangeKind.TRIVIAL_REBASE;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
@@ -38,7 +41,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.client.ChangeKind;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -336,13 +338,7 @@
   }
 
   private ChangeInfo detailedChange(String changeId) throws Exception {
-    return gApi.changes()
-        .id(changeId)
-        .get(
-            EnumSet.of(
-                ListChangesOption.DETAILED_LABELS,
-                ListChangesOption.CURRENT_REVISION,
-                ListChangesOption.CURRENT_COMMIT));
+    return gApi.changes().id(changeId).get(DETAILED_LABELS, CURRENT_REVISION, CURRENT_COMMIT);
   }
 
   private void assertNotSticky(Set<ChangeKind> changeKinds) throws Exception {
@@ -533,7 +529,7 @@
   }
 
   private ChangeKind getChangeKind(String changeId) throws Exception {
-    ChangeInfo c = gApi.changes().id(changeId).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
+    ChangeInfo c = gApi.changes().id(changeId).get(CURRENT_REVISION);
     return c.revisions.get(c.currentRevision).kind;
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 33bbe6b..bc89e22 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -53,7 +53,6 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -92,7 +91,6 @@
 import java.text.SimpleDateFormat;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -162,8 +160,7 @@
     approval = getApproval(changeId, label);
     assertThat(approval.value).isEqualTo(1);
     assertThat(approval.postSubmit).isNull();
-    assertPermitted(
-        gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)), "Code-Review", 1, 2);
+    assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 1, 2);
 
     // Repeating the current label is allowed. Does not flip the postSubmit bit
     // due to deduplication codepath.
@@ -190,7 +187,7 @@
     approval = getApproval(changeId, label);
     assertThat(approval.value).isEqualTo(2);
     assertThat(approval.postSubmit).isTrue();
-    assertPermitted(gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)), "Code-Review", 2);
+    assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 2);
 
     // Decreasing to previous post-submit vote is still not allowed.
     try {
@@ -241,7 +238,7 @@
     ApprovalInfo cr =
         gApi.changes()
             .id(changeId)
-            .get(EnumSet.of(ListChangesOption.DETAILED_LABELS))
+            .get(DETAILED_LABELS)
             .labels
             .get("Code-Review")
             .all
@@ -1359,7 +1356,7 @@
   }
 
   private ApprovalInfo getApproval(String changeId, String label) throws Exception {
-    ChangeInfo info = gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS));
+    ChangeInfo info = gApi.changes().id(changeId).get(DETAILED_LABELS);
     LabelInfo li = info.labels.get(label);
     assertThat(li).isNotNull();
     int accountId = atrScope.get().getUser().getAccountId().get();
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 930b996..c6c4f3e 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
@@ -22,6 +22,8 @@
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.common.EditInfoSubject.assertThat;
 import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
@@ -1640,8 +1642,7 @@
     String ref = "refs/for/master%merged";
     assertPushOk(pushHead(testRepo, ref, false), ref);
 
-    EnumSet<ListChangesOption> opts = EnumSet.of(ListChangesOption.ALL_REVISIONS);
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get(opts);
+    ChangeInfo info = gApi.changes().id(r.getChangeId()).get(ALL_REVISIONS);
     assertThat(info.currentRevision).isEqualTo(c2.name());
     assertThat(info.revisions.keySet()).containsExactly(c1.name(), c2.name());
     // TODO(dborowitz): Fix ReceiveCommits to also auto-close the change.
@@ -1849,12 +1850,7 @@
 
   private String getLastMessage(String changeId) throws Exception {
     return Streams.findLast(
-            gApi.changes()
-                .id(changeId)
-                .get(EnumSet.of(ListChangesOption.MESSAGES))
-                .messages
-                .stream()
-                .map(m -> m.message))
+            gApi.changes().id(changeId).get(MESSAGES).messages.stream().map(m -> m.message))
         .get();
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 05e5f99..36843a5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
@@ -39,7 +40,6 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -64,7 +64,6 @@
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
-import java.util.EnumSet;
 import org.apache.http.Header;
 import org.apache.http.message.BasicHeader;
 import org.junit.After;
@@ -545,8 +544,7 @@
     setApiUser(accountCreator.user2());
     gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in);
 
-    ChangeInfo info =
-        gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.MESSAGES));
+    ChangeInfo info = gApi.changes().id(r.getChangeId()).get(MESSAGES);
     assertThat(info.messages).hasSize(2);
 
     ChangeMessageInfo changeMessageInfo = Iterables.getLast(info.messages);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 239c296..0061103 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 
@@ -316,7 +317,7 @@
   @Test
   public void changeActionVisitor() throws Exception {
     String id = createChange().getChangeId();
-    ChangeInfo origChange = gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS));
+    ChangeInfo origChange = gApi.changes().id(id).get(CHANGE_ACTIONS);
 
     class Visitor implements ActionVisitor {
       @Override
@@ -362,7 +363,7 @@
   public void currentRevisionActionVisitor() throws Exception {
     String id = createChange().getChangeId();
     amendChange(id);
-    ChangeInfo origChange = gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS));
+    ChangeInfo origChange = gApi.changes().id(id).get(CHANGE_ACTIONS);
     Change.Id changeId = new Change.Id(origChange._number);
 
     class Visitor implements ActionVisitor {
@@ -429,7 +430,7 @@
   public void oldRevisionActionVisitor() throws Exception {
     String id = createChange().getChangeId();
     amendChange(id);
-    ChangeInfo origChange = gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS));
+    ChangeInfo origChange = gApi.changes().id(id).get(CHANGE_ACTIONS);
 
     class Visitor implements ActionVisitor {
       @Override
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 6762263..93bb3fa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -28,13 +29,11 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
-import java.util.EnumSet;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
@@ -62,8 +61,7 @@
       input.state = state;
       gApi.changes().id(r.getChangeId()).addReviewer(input);
 
-      ChangeInfo info =
-          gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+      ChangeInfo info = gApi.changes().id(r.getChangeId()).get(DETAILED_LABELS);
       assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc)));
       // All reviewers added by email should be removable
       assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(acc));
@@ -89,8 +87,7 @@
       inputById.state = state;
       gApi.changes().id(r.getChangeId()).addReviewer(inputById);
 
-      ChangeInfo info =
-          gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+      ChangeInfo info = gApi.changes().id(r.getChangeId()).get(DETAILED_LABELS);
       assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(byId, byEmail)));
       // All reviewers (both by id and by email) should be removable
       assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(byId, byEmail));
@@ -112,8 +109,7 @@
 
       gApi.changes().id(r.getChangeId()).reviewer(acc.email).remove();
 
-      ChangeInfo info =
-          gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+      ChangeInfo info = gApi.changes().id(r.getChangeId()).get(DETAILED_LABELS);
       assertThat(info.reviewers).isEmpty();
     }
   }
@@ -135,8 +131,7 @@
     modifyInput.state = ReviewerState.REVIEWER;
     gApi.changes().id(r.getChangeId()).addReviewer(modifyInput);
 
-    ChangeInfo info =
-        gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    ChangeInfo info = gApi.changes().id(r.getChangeId()).get(DETAILED_LABELS);
     assertThat(info.reviewers)
         .isEqualTo(ImmutableMap.of(ReviewerState.REVIEWER, ImmutableList.of(acc)));
   }
@@ -329,10 +324,7 @@
       try {
         ChangeInfo info =
             Iterables.getOnlyElement(
-                gApi.changes()
-                    .query(r.getChangeId())
-                    .withOption(ListChangesOption.DETAILED_LABELS)
-                    .get());
+                gApi.changes().query(r.getChangeId()).withOption(DETAILED_LABELS).get());
         assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc)));
       } finally {
         notesMigration.setFailOnLoadForTest(false);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index c7c02b2..3cc21a6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -48,7 +48,6 @@
 import com.google.gson.stream.JsonReader;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -806,6 +805,6 @@
   }
 
   private Map<String, LabelInfo> getChangeLabels(String changeId) throws Exception {
-    return gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)).labels;
+    return gApi.changes().id(changeId).get(DETAILED_LABELS).labels;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
index dd0c7f0..b6ac5e9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -14,6 +14,8 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static org.junit.Assert.fail;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -119,6 +121,9 @@
 
     setApiUser(admin);
 
+    ChangeInfo c = gApi.changes().id(out._number).get(MESSAGES);
+    assertThat(c.messages.stream().map(m -> m.message)).containsExactly("Uploaded patch set 1");
+
     ReviewInput reviewIn = new ReviewInput();
     reviewIn.label("Code-Review", (short) 2);
     gApi.changes().id(out._number).current().review(reviewIn);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 2cd1800..d0e3ba5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
@@ -26,7 +27,6 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -39,7 +39,6 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -280,7 +279,7 @@
   private List<String> getMessages(Change.Id id) throws Exception {
     return gApi.changes()
         .id(id.get())
-        .get(EnumSet.of(ListChangesOption.MESSAGES))
+        .get(MESSAGES)
         .messages
         .stream()
         .map(m -> m.message)
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 53bf6a0..9bc09d2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -27,6 +28,7 @@
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
@@ -170,6 +172,14 @@
 
   ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException;
 
+  default ChangeInfo get(Iterable<ListChangesOption> options) throws RestApiException {
+    return get(Sets.newEnumSet(options, ListChangesOption.class));
+  }
+
+  default ChangeInfo get(ListChangesOption... options) throws RestApiException {
+    return get(Arrays.asList(options));
+  }
+
   /** {@code get} with {@link ListChangesOption} set to all except CHECK. */
   ChangeInfo get() throws RestApiException;
   /** {@code get} with {@link ListChangesOption} set to none. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index 9401c88..f9c4808 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -165,7 +165,7 @@
     }
 
     Iterable<UiAction.Description> descs =
-        uiActions.from(changeViews, changeResourceFactory.create(ctl));
+        uiActions.from(changeViews, changeResourceFactory.create(ctl.getNotes(), ctl.getUser()));
 
     // The followup action is a client-side only operation that does not
     // have a server side handler. It must be manually registered into the
@@ -198,7 +198,7 @@
       List<ActionVisitor> visitors,
       ChangeInfo changeInfo,
       RevisionInfo revisionInfo) {
-    if (!rsrc.getControl().getUser().isIdentifiedUser()) {
+    if (!rsrc.getUser().isIdentifiedUser()) {
       return ImmutableMap.of();
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
index 2ae9a86..fa26eec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -46,17 +47,20 @@
   private final FixReplacementInterpreter fixReplacementInterpreter;
   private final ChangeEditModifier changeEditModifier;
   private final ChangeEditJson changeEditJson;
+  private final ProjectCache projectCache;
 
   @Inject
   public ApplyFix(
       GitRepositoryManager gitRepositoryManager,
       FixReplacementInterpreter fixReplacementInterpreter,
       ChangeEditModifier changeEditModifier,
-      ChangeEditJson changeEditJson) {
+      ChangeEditJson changeEditJson,
+      ProjectCache projectCache) {
     this.gitRepositoryManager = gitRepositoryManager;
     this.fixReplacementInterpreter = fixReplacementInterpreter;
     this.changeEditModifier = changeEditModifier;
     this.changeEditJson = changeEditJson;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -65,7 +69,7 @@
           ResourceNotFoundException, PermissionBackendException {
     RevisionResource revisionResource = fixResource.getRevisionResource();
     Project.NameKey project = revisionResource.getProject();
-    ProjectState projectState = revisionResource.getControl().getProjectControl().getProjectState();
+    ProjectState projectState = projectCache.checkedGet(project);
     PatchSet patchSet = revisionResource.getPatchSet();
     ObjectId patchSetCommitId = ObjectId.fromString(patchSet.getRevision().get());
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 0064281..a4d12a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -378,6 +379,7 @@
 
   public static class Get implements RestReadView<ChangeEditResource> {
     private final FileContentUtil fileContentUtil;
+    private final ProjectCache projectCache;
 
     @Option(
       name = "--base",
@@ -387,8 +389,9 @@
     private boolean base;
 
     @Inject
-    Get(FileContentUtil fileContentUtil) {
+    Get(FileContentUtil fileContentUtil, ProjectCache projectCache) {
       this.fileContentUtil = fileContentUtil;
+      this.projectCache = projectCache;
     }
 
     @Override
@@ -397,7 +400,7 @@
         ChangeEdit edit = rsrc.getChangeEdit();
         return Response.ok(
             fileContentUtil.getContent(
-                rsrc.getChangeResource().getControl().getProjectControl().getProjectState(),
+                projectCache.checkedGet(rsrc.getChangeResource().getProject()),
                 base
                     ? ObjectId.fromString(edit.getBasePatchSet().getRevision().get())
                     : edit.getEditCommit(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index c2b93a6..56eb46b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -346,7 +346,7 @@
   }
 
   public ChangeInfo format(RevisionResource rsrc) throws OrmException {
-    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getChangeResource());
     return format(cd, Optional.of(rsrc.getPatchSet().getId()), true);
   }
 
@@ -1318,7 +1318,7 @@
       RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
       rw.parseBody(commit);
       if (setCommit) {
-        out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit);
+        out.commit = toCommit(project, rw, commit, has(WEB_LINKS), fillCommit);
       }
       if (addFooters) {
         Ref ref = repo.exactRef(ctl.getChange().getDest().get());
@@ -1345,7 +1345,9 @@
         && userProvider.get().isIdentifiedUser()) {
 
       actionJson.addRevisionActions(
-          changeInfo, out, new RevisionResource(changeResourceFactory.create(ctl), in));
+          changeInfo,
+          out,
+          new RevisionResource(changeResourceFactory.create(ctl.getNotes(), ctl.getUser()), in));
     }
 
     if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
@@ -1362,9 +1364,8 @@
   }
 
   CommitInfo toCommit(
-      ChangeControl ctl, RevWalk rw, RevCommit commit, boolean addLinks, boolean fillCommit)
+      Project.NameKey project, RevWalk rw, RevCommit commit, boolean addLinks, boolean fillCommit)
       throws IOException {
-    Project.NameKey project = ctl.getProject().getNameKey();
     CommitInfo info = new CommitInfo();
     if (fillCommit) {
       info.commit = commit.name();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index 69438d5..4166bf7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.extensions.restapi.RestResource;
@@ -35,18 +36,23 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class ChangeResource implements RestResource, HasETag {
+  private static final Logger log = LoggerFactory.getLogger(ChangeResource.class);
+
   /**
    * JSON format version number for ETag computations.
    *
@@ -59,7 +65,7 @@
       new TypeLiteral<RestView<ChangeResource>>() {};
 
   public interface Factory {
-    ChangeResource create(ChangeControl ctl);
+    ChangeResource create(ChangeNotes notes, CurrentUser user);
   }
 
   private static final String ZERO_ID_STRING = ObjectId.zeroId().name();
@@ -70,7 +76,9 @@
   private final PatchSetUtil patchSetUtil;
   private final PermissionBackend permissionBackend;
   private final StarredChangesUtil starredChangesUtil;
-  private final ChangeControl control;
+  private final ProjectCache projectCache;
+  private final ChangeNotes notes;
+  private final CurrentUser user;
 
   @Inject
   ChangeResource(
@@ -80,41 +88,40 @@
       PatchSetUtil patchSetUtil,
       PermissionBackend permissionBackend,
       StarredChangesUtil starredChangesUtil,
-      @Assisted ChangeControl control) {
+      ProjectCache projectCache,
+      @Assisted ChangeNotes notes,
+      @Assisted CurrentUser user) {
     this.db = db;
     this.accountCache = accountCache;
     this.approvalUtil = approvalUtil;
     this.patchSetUtil = patchSetUtil;
     this.permissionBackend = permissionBackend;
     this.starredChangesUtil = starredChangesUtil;
-    this.control = control;
+    this.projectCache = projectCache;
+    this.notes = notes;
+    this.user = user;
   }
 
   public PermissionBackend.ForChange permissions() {
-    return permissionBackend.user(getControl().getUser()).change(getNotes());
-  }
-
-  public ChangeControl getControl() {
-    return control;
+    return permissionBackend.user(user).change(notes);
   }
 
   public CurrentUser getUser() {
-    return getControl().getUser();
+    return user;
   }
 
   public Change.Id getId() {
-    return getControl().getId();
+    return notes.getChangeId();
   }
 
   /** @return true if {@link #getUser()} is the change's owner. */
   public boolean isUserOwner() {
-    CurrentUser user = getControl().getUser();
     Account.Id owner = getChange().getOwner();
     return user.isIdentifiedUser() && user.asIdentifiedUser().getAccountId().equals(owner);
   }
 
   public Change getChange() {
-    return getControl().getChange();
+    return notes.getChange();
   }
 
   public Project.NameKey getProject() {
@@ -122,7 +129,7 @@
   }
 
   public ChangeNotes getNotes() {
-    return getControl().getNotes();
+    return notes;
   }
 
   // This includes all information relevant for ETag computation
@@ -147,7 +154,7 @@
     }
     try {
       patchSetUtil
-          .byChange(db.get(), getNotes())
+          .byChange(db.get(), notes)
           .stream()
           .map(ps -> ps.getUploader())
           .forEach(accounts::add);
@@ -159,7 +166,7 @@
       // set of accounts that posted a message is too expensive. However everyone who posts a
       // message is automatically added as reviewer. Hence if we include removed reviewers we can
       // be sure that we have all accounts that posted messages on the change.
-      accounts.addAll(approvalUtil.getReviewers(db.get(), getNotes()).all());
+      accounts.addAll(approvalUtil.getReviewers(db.get(), notes).all());
     } catch (OrmException e) {
       // This ETag will be invalidated if it loads next time.
     }
@@ -167,7 +174,7 @@
 
     ObjectId noteId;
     try {
-      noteId = getNotes().loadRevision();
+      noteId = notes.loadRevision();
     } catch (OrmException e) {
       noteId = null; // This ETag will be invalidated if it loads next time.
     }
@@ -175,14 +182,21 @@
     // TODO(dborowitz): Include more NoteDb and other related refs, e.g. drafts
     // and edits.
 
-    for (ProjectState p : control.getProjectControl().getProjectState().tree()) {
+    Iterable<ProjectState> projectStateTree;
+    try {
+      projectStateTree = projectCache.checkedGet(getProject()).tree();
+    } catch (IOException e) {
+      log.error(String.format("could not load project %s while computing etag", getProject()));
+      projectStateTree = ImmutableList.of();
+    }
+
+    for (ProjectState p : projectStateTree) {
       hashObjectId(h, p.getConfig().getRevision(), buf);
     }
   }
 
   @Override
   public String getETag() {
-    CurrentUser user = control.getUser();
     Hasher h = Hashing.murmur3_128().newHasher();
     if (user.isIdentifiedUser()) {
       h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index 3556b42..c8f88fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -30,8 +30,6 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -50,7 +48,6 @@
   private final CreateChange createChange;
   private final ChangeResource.Factory changeResourceFactory;
   private final PermissionBackend permissionBackend;
-  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   ChangesCollection(
@@ -61,8 +58,7 @@
       ChangeFinder changeFinder,
       CreateChange createChange,
       ChangeResource.Factory changeResourceFactory,
-      PermissionBackend permissionBackend,
-      ChangeControl.GenericFactory changeControlFactory) {
+      PermissionBackend permissionBackend) {
     this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
@@ -71,7 +67,6 @@
     this.createChange = createChange;
     this.changeResourceFactory = changeResourceFactory;
     this.permissionBackend = permissionBackend;
-    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -98,7 +93,7 @@
     if (!canRead(change)) {
       throw new ResourceNotFoundException(id);
     }
-    return changeResourceFactory.create(controlFor(change));
+    return changeResourceFactory.create(change, user.get());
   }
 
   public ChangeResource parse(Change.Id id)
@@ -114,15 +109,15 @@
     if (!canRead(change)) {
       throw new ResourceNotFoundException(toIdString(id));
     }
-    return changeResourceFactory.create(controlFor(change));
+    return changeResourceFactory.create(change, user.get());
   }
 
   private static IdString toIdString(Change.Id id) {
     return IdString.fromDecoded(id.toString());
   }
 
-  public ChangeResource parse(ChangeControl control) {
-    return changeResourceFactory.create(control);
+  public ChangeResource parse(ChangeNotes notes, CurrentUser user) {
+    return changeResourceFactory.create(notes, user);
   }
 
   @SuppressWarnings("unchecked")
@@ -134,13 +129,4 @@
   private boolean canRead(ChangeNotes notes) throws PermissionBackendException {
     return permissionBackend.user(user).change(notes).database(db).test(ChangePermission.READ);
   }
-
-  private ChangeControl controlFor(ChangeNotes notes) throws ResourceNotFoundException {
-    try {
-      return changeControlFactory.controlFor(notes, user.get());
-    } catch (NoSuchChangeException e) {
-      throw new ResourceNotFoundException(
-          String.format("Change %s not found", notes.getChangeId()));
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index a36a1d3..157928b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -25,21 +25,30 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.io.IOException;
 
 public class Check
     implements RestReadView<ChangeResource>, RestModifyView<ChangeResource, FixInput> {
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
   private final ChangeJson.Factory jsonFactory;
+  private final ProjectControl.GenericFactory projectControlFactory;
 
   @Inject
-  Check(PermissionBackend permissionBackend, Provider<CurrentUser> user, ChangeJson.Factory json) {
+  Check(
+      PermissionBackend permissionBackend,
+      Provider<CurrentUser> user,
+      ChangeJson.Factory json,
+      ProjectControl.GenericFactory projectControlFactory) {
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.jsonFactory = json;
+    this.projectControlFactory = projectControlFactory;
   }
 
   @Override
@@ -49,8 +58,10 @@
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
-      throws RestApiException, OrmException, PermissionBackendException {
-    if (!rsrc.isUserOwner() && !rsrc.getControl().getProjectControl().isOwner()) {
+      throws RestApiException, OrmException, PermissionBackendException, NoSuchProjectException,
+          IOException {
+    if (!rsrc.isUserOwner()
+        && !projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()).isOwner()) {
       permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER);
     }
     return Response.withMustRevalidate(newChangeJson().fix(input).format(rsrc));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index f3c5f0a..f980ade 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -53,6 +54,7 @@
   private final Provider<CurrentUser> user;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
+  private final ProjectControl.GenericFactory projectControlFactory;
 
   @Inject
   CherryPick(
@@ -60,12 +62,14 @@
       Provider<CurrentUser> user,
       RetryHelper retryHelper,
       CherryPickChange cherryPickChange,
-      ChangeJson.Factory json) {
+      ChangeJson.Factory json,
+      ProjectControl.GenericFactory projectControlFactory) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
+    this.projectControlFactory = projectControlFactory;
   }
 
   @Override
@@ -81,7 +85,7 @@
     }
 
     String refName = RefNames.fullName(input.destination);
-    CreateChange.checkValidCLA(rsrc.getControl().getProjectControl());
+    CreateChange.checkValidCLA(projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()));
     permissionBackend
         .user(user)
         .project(rsrc.getChange().getProject())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
index 347070e..68db189 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
@@ -71,10 +71,7 @@
       throws RestApiException, UpdateException {
     try (BatchUpdate bu =
         updateFactory.create(
-            db.get(),
-            rsrc.getChange().getProject(),
-            rsrc.getControl().getUser(),
-            TimeUtil.nowTs())) {
+            db.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op(rsrc.getComment().key);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index fd425ef..b6b8088 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -67,6 +68,7 @@
   private final Provider<DeleteChangeOp> deleteChangeOpProvider;
   private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
   private final boolean allowDrafts;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   public DeleteDraftPatchSet(
@@ -76,7 +78,8 @@
       PatchSetUtil psUtil,
       Provider<DeleteChangeOp> deleteChangeOpProvider,
       DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
-      @GerritServerConfig Config cfg) {
+      @GerritServerConfig Config cfg,
+      ChangeControl.GenericFactory changeControlFactory) {
     super(retryHelper);
     this.db = db;
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -84,6 +87,7 @@
     this.deleteChangeOpProvider = deleteChangeOpProvider;
     this.accountPatchReviewStore = accountPatchReviewStore;
     this.allowDrafts = cfg.getBoolean("change", "allowDrafts", true);
+    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -220,7 +224,9 @@
               allowDrafts
                   && rsrc.getPatchSet().isDraft()
                   && psUtil.byChange(db.get(), rsrc.getNotes()).size() > 1
-                  && rsrc.getControl().canDeleteDraft(db.get()));
+                  && changeControlFactory
+                      .controlFor(rsrc.getNotes(), rsrc.getUser())
+                      .canDeleteDraft(db.get()));
     } catch (OrmException e) {
       throw new IllegalStateException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
index 827dfcd..311a25c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DownloadContent.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -27,22 +27,24 @@
 
 public class DownloadContent implements RestReadView<FileResource> {
   private final FileContentUtil fileContentUtil;
+  private final ProjectCache projectCache;
 
   @Option(name = "--parent")
   private Integer parent;
 
   @Inject
-  DownloadContent(FileContentUtil fileContentUtil) {
+  DownloadContent(FileContentUtil fileContentUtil, ProjectCache projectCache) {
     this.fileContentUtil = fileContentUtil;
+    this.projectCache = projectCache;
   }
 
   @Override
   public BinaryResult apply(FileResource rsrc)
       throws ResourceNotFoundException, IOException, NoSuchChangeException, OrmException {
     String path = rsrc.getPatchKey().get();
-    ProjectState projectState =
-        rsrc.getRevision().getControl().getProjectControl().getProjectState();
-    ObjectId revstr = ObjectId.fromString(rsrc.getRevision().getPatchSet().getRevision().get());
-    return fileContentUtil.downloadContent(projectState, revstr, path, parent);
+    RevisionResource rev = rsrc.getRevision();
+    ObjectId revstr = ObjectId.fromString(rev.getPatchSet().getRevision().get());
+    return fileContentUtil.downloadContent(
+        projectCache.checkedGet(rev.getProject()), revstr, path, parent);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
index 781216c..0b1b15d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -16,11 +16,10 @@
 
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.CurrentUser;
 import com.google.inject.TypeLiteral;
 
 public class DraftCommentResource implements RestResource {
@@ -35,12 +34,12 @@
     this.comment = c;
   }
 
-  public ChangeControl getControl() {
-    return rev.getControl();
+  public CurrentUser getUser() {
+    return rev.getUser();
   }
 
   public Change getChange() {
-    return getControl().getChange();
+    return rev.getChange();
   }
 
   public PatchSet getPatchSet() {
@@ -54,8 +53,4 @@
   String getId() {
     return comment.key.uuid;
   }
-
-  Account.Id getAuthorId() {
-    return getControl().getUser().getAccountId();
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
index 371127b..7269a60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
@@ -58,7 +58,7 @@
       throw new MethodNotAllowedException("zip format is disabled");
     }
     boolean close = true;
-    final Repository repo = repoManager.openRepository(rsrc.getControl().getProject().getNameKey());
+    final Repository repo = repoManager.openRepository(rsrc.getProject());
     try {
       final RevCommit commit;
       String name;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
index e33021ea..694379e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -55,7 +55,7 @@
       String rev = rsrc.getPatchSet().getRevision().get();
       RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
       rw.parseBody(commit);
-      CommitInfo info = json.noOptions().toCommit(rsrc.getControl(), rw, commit, addLinks, true);
+      CommitInfo info = json.noOptions().toCommit(rsrc.getProject(), rw, commit, addLinks, true);
       Response<CommitInfo> r = Response.ok(info);
       if (rsrc.isCacheable()) {
         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
index 5433653..f6b24b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.patch.ComparisonType;
 import com.google.gerrit.server.patch.Text;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,6 +45,7 @@
   private final GitRepositoryManager gitManager;
   private final PatchSetUtil psUtil;
   private final FileContentUtil fileContentUtil;
+  private final ProjectCache projectCache;
 
   @Option(name = "--parent")
   private Integer parent;
@@ -53,11 +55,13 @@
       Provider<ReviewDb> db,
       GitRepositoryManager gitManager,
       PatchSetUtil psUtil,
-      FileContentUtil fileContentUtil) {
+      FileContentUtil fileContentUtil,
+      ProjectCache projectCache) {
     this.db = db;
     this.gitManager = gitManager;
     this.psUtil = psUtil;
     this.fileContentUtil = fileContentUtil;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -76,7 +80,7 @@
           .base64();
     }
     return fileContentUtil.getContent(
-        rsrc.getRevision().getControl().getProjectControl().getProjectState(),
+        projectCache.checkedGet(rsrc.getRevision().getProject()),
         ObjectId.fromString(rsrc.getRevision().getPatchSet().getRevision().get()),
         path,
         parent);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index eec318b..b743a97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.git.LargeObjectException;
 import com.google.gerrit.server.patch.PatchScriptFactory;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -83,6 +84,7 @@
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
   private final Revisions revisions;
   private final WebLinks webLinks;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Option(name = "--base", metaVar = "REVISION")
   String base;
@@ -111,11 +113,13 @@
       ProjectCache projectCache,
       PatchScriptFactory.Factory patchScriptFactoryFactory,
       Revisions revisions,
-      WebLinks webLinks) {
+      WebLinks webLinks,
+      ChangeControl.GenericFactory changeControlFactory) {
     this.projectCache = projectCache;
     this.patchScriptFactoryFactory = patchScriptFactoryFactory;
     this.revisions = revisions;
     this.webLinks = webLinks;
+    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -135,33 +139,20 @@
 
     PatchScriptFactory psf;
     PatchSet basePatchSet = null;
+    ChangeControl ctl =
+        changeControlFactory.controlFor(
+            resource.getRevision().getNotes(), resource.getRevision().getUser());
+    PatchSet.Id pId = resource.getPatchKey().getParentKey();
+    String fileName = resource.getPatchKey().getFileName();
     if (base != null) {
       RevisionResource baseResource =
           revisions.parse(resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
       basePatchSet = baseResource.getPatchSet();
-      psf =
-          patchScriptFactoryFactory.create(
-              resource.getRevision().getControl(),
-              resource.getPatchKey().getFileName(),
-              basePatchSet.getId(),
-              resource.getPatchKey().getParentKey(),
-              prefs);
+      psf = patchScriptFactoryFactory.create(ctl, fileName, basePatchSet.getId(), pId, prefs);
     } else if (parentNum > 0) {
-      psf =
-          patchScriptFactoryFactory.create(
-              resource.getRevision().getControl(),
-              resource.getPatchKey().getFileName(),
-              parentNum - 1,
-              resource.getPatchKey().getParentKey(),
-              prefs);
+      psf = patchScriptFactoryFactory.create(ctl, fileName, parentNum - 1, pId, prefs);
     } else {
-      psf =
-          patchScriptFactoryFactory.create(
-              resource.getRevision().getControl(),
-              resource.getPatchKey().getFileName(),
-              null,
-              resource.getPatchKey().getParentKey(),
-              prefs);
+      psf = patchScriptFactoryFactory.create(ctl, fileName, null, pId, prefs);
     }
 
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java
index 9d40df4..88677d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java
@@ -80,7 +80,7 @@
       List<CommitInfo> result = new ArrayList<>(commits.size());
       ChangeJson changeJson = json.noOptions();
       for (RevCommit c : commits) {
-        result.add(changeJson.toCommit(rsrc.getControl(), rw, c, addLinks, true));
+        result.add(changeJson.toCommit(rsrc.getProject(), rw, c, addLinks, true));
       }
       return createResponse(rsrc, result);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
index 2275e06..b59c17c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -62,8 +61,7 @@
   @Override
   public BinaryResult apply(RevisionResource rsrc)
       throws ResourceConflictException, IOException, ResourceNotFoundException {
-    Project.NameKey project = rsrc.getControl().getProject().getNameKey();
-    final Repository repo = repoManager.openRepository(project);
+    final Repository repo = repoManager.openRepository(rsrc.getProject());
     boolean close = true;
     try {
       final RevWalk rw = new RevWalk(repo);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java
index c849134..2d078eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -52,6 +53,7 @@
   private final ChangeNotes.Factory notesFactory;
   private final Provider<ReviewDb> dbProvider;
   private final PatchSetUtil psUtil;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Option(
     name = "--claimed-original",
@@ -68,13 +70,15 @@
       ProjectCache projectCache,
       ChangeNotes.Factory notesFactory,
       Provider<ReviewDb> dbProvider,
-      PatchSetUtil psUtil) {
+      PatchSetUtil psUtil,
+      ChangeControl.GenericFactory changeControlFactory) {
     this.mergeUtilFactory = mergeUtilFactory;
     this.repoManager = repoManager;
     this.projectCache = projectCache;
     this.notesFactory = notesFactory;
     this.dbProvider = dbProvider;
     this.psUtil = psUtil;
+    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -84,23 +88,34 @@
     PatchSet currentPatchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
     if (currentPatchSet == null) {
       throw new ResourceConflictException("current revision is missing");
-    } else if (!rsrc.getControl().isPatchVisible(currentPatchSet, dbProvider.get())) {
+    } else if (!changeControlFactory
+        .controlFor(rsrc.getNotes(), rsrc.getUser())
+        .isPatchVisible(currentPatchSet, dbProvider.get())) {
       throw new AuthException("current revision not accessible");
     }
+    return getPureRevert(rsrc.getNotes());
+  }
+
+  public PureRevertInfo getPureRevert(ChangeNotes notes)
+      throws OrmException, IOException, BadRequestException, ResourceConflictException {
+    PatchSet currentPatchSet = psUtil.current(dbProvider.get(), notes);
+    if (currentPatchSet == null) {
+      throw new ResourceConflictException("current revision is missing");
+    }
 
     if (claimedOriginal == null) {
-      if (rsrc.getChange().getRevertOf() == null) {
+      if (notes.getChange().getRevertOf() == null) {
         throw new BadRequestException("no ID was provided and change isn't a revert");
       }
       PatchSet ps =
           psUtil.current(
               dbProvider.get(),
               notesFactory.createChecked(
-                  dbProvider.get(), rsrc.getProject(), rsrc.getChange().getRevertOf()));
+                  dbProvider.get(), notes.getProjectName(), notes.getChange().getRevertOf()));
       claimedOriginal = ps.getRevision().get();
     }
 
-    try (Repository repo = repoManager.openRepository(rsrc.getProject());
+    try (Repository repo = repoManager.openRepository(notes.getProjectName());
         ObjectInserter oi = repo.newObjectInserter();
         RevWalk rw = new RevWalk(repo)) {
       RevCommit claimedOriginalCommit;
@@ -120,7 +135,7 @@
       // Rebase claimed revert onto claimed original
       ThreeWayMerger merger =
           mergeUtilFactory
-              .create(projectCache.checkedGet(rsrc.getProject()))
+              .create(projectCache.checkedGet(notes.getProjectName()))
               .newThreeWayMerger(oi, repo.getConfig());
       merger.setBase(claimedRevertCommit.getParent(0));
       merger.merge(claimedRevertCommit, claimedOriginalCommit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index cb77fd1..2a7bd4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -65,14 +65,14 @@
   @Override
   public String getETag(RevisionResource rsrc) {
     Hasher h = Hashing.murmur3_128().newHasher();
-    CurrentUser user = rsrc.getControl().getUser();
+    CurrentUser user = rsrc.getUser();
     try {
       rsrc.getChangeResource().prepareETag(h, user);
       h.putBoolean(Submit.wholeTopicEnabled(config));
       ReviewDb db = dbProvider.get();
       ChangeSet cs = mergeSuperSet.get().completeChangeSet(db, rsrc.getChange(), user);
       for (ChangeData cd : cs.changes()) {
-        changeResourceFactory.create(cd.changeControl()).prepareETag(h, user);
+        changeResourceFactory.create(cd.notes(), user).prepareETag(h, user);
       }
       h.putBoolean(cs.furtherHiddenChanges());
     } catch (IOException | OrmException | PermissionBackendException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index cafd73b..929529d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -117,7 +117,7 @@
       return result;
     }
 
-    ChangeData cd = changeDataFactory.create(db.get(), resource.getControl());
+    ChangeData cd = changeDataFactory.create(db.get(), resource.getChangeResource());
     result.submitType = getSubmitType(cd, ps);
 
     try (Repository git = gitManager.openRepository(change.getProject())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index ca62719..81edada 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -97,7 +97,6 @@
 import com.google.gerrit.server.permissions.LabelPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -146,7 +145,7 @@
   private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
 
   private final Provider<ReviewDb> db;
-  private final ChangesCollection changes;
+  private final ChangeResource.Factory changeResourceFactory;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
@@ -167,7 +166,7 @@
   PostReview(
       Provider<ReviewDb> db,
       RetryHelper retryHelper,
-      ChangesCollection changes,
+      ChangeResource.Factory changeResourceFactory,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
@@ -185,7 +184,7 @@
       ProjectCache projectCache) {
     super(retryHelper);
     this.db = db;
-    this.changes = changes;
+    this.changeResourceFactory = changeResourceFactory;
     this.changeDataFactory = changeDataFactory;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
@@ -470,8 +469,8 @@
           String.format("on_behalf_of account %s cannot see change", reviewer.getAccountId()));
     }
 
-    ChangeControl ctl = rev.getControl().forUser(reviewer);
-    return new RevisionResource(changes.parse(ctl), rev.getPatchSet());
+    return new RevisionResource(
+        changeResourceFactory.create(rev.getNotes(), reviewer), rev.getPatchSet());
   }
 
   private void checkLabels(
@@ -571,7 +570,7 @@
   }
 
   private Set<String> getAffectedFilePaths(RevisionResource revision) throws OrmException {
-    ChangeData changeData = changeDataFactory.create(db.get(), revision.getControl());
+    ChangeData changeData = changeDataFactory.create(db.get(), revision.getChangeResource());
     return new HashSet<>(changeData.filePaths(revision.getPatchSet()));
   }
 
@@ -1108,7 +1107,7 @@
       if (ctx.getAccountId().equals(ctx.getChange().getOwner())) {
         return true;
       }
-      ChangeData cd = changeDataFactory.create(db.get(), ctx.getControl());
+      ChangeData cd = changeDataFactory.create(db.get(), ctx);
       ReviewerSet reviewers = cd.reviewers();
       if (reviewers.byState(REVIEWER).contains(ctx.getAccountId())) {
         return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index 5724941..3c83f81 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.git.MergeOpRepoManager;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gwtorm.server.OrmException;
@@ -104,8 +103,7 @@
     if (!change.getStatus().isOpen()) {
       throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
     }
-    ChangeControl control = rsrc.getControl();
-    if (!control.getUser().isIdentifiedUser()) {
+    if (!rsrc.getUser().isIdentifiedUser()) {
       throw new MethodNotAllowedException("Anonymous users cannot submit");
     }
 
@@ -116,8 +114,7 @@
       throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
           PermissionBackendException {
     ReviewDb db = dbProvider.get();
-    ChangeControl control = rsrc.getControl();
-    IdentifiedUser caller = control.getUser().asIdentifiedUser();
+    IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
     Change change = rsrc.getChange();
 
     @SuppressWarnings("resource") // Returned BinaryResult takes ownership and handles closing.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index d3f87cf..6b28ba3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -26,6 +26,8 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -75,20 +77,27 @@
 
     private final ChangeEditUtil editUtil;
     private final NotifyUtil notifyUtil;
+    private final ProjectControl.GenericFactory projectControlFactory;
 
     @Inject
-    Publish(RetryHelper retryHelper, ChangeEditUtil editUtil, NotifyUtil notifyUtil) {
+    Publish(
+        RetryHelper retryHelper,
+        ChangeEditUtil editUtil,
+        NotifyUtil notifyUtil,
+        ProjectControl.GenericFactory projectControlFactory) {
       super(retryHelper);
       this.editUtil = editUtil;
       this.notifyUtil = notifyUtil;
+      this.projectControlFactory = projectControlFactory;
     }
 
     @Override
     protected Response<?> applyImpl(
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
-        throws IOException, OrmException, RestApiException, UpdateException,
-            ConfigInvalidException {
-      CreateChange.checkValidCLA(rsrc.getControl().getProjectControl());
+        throws IOException, OrmException, RestApiException, UpdateException, ConfigInvalidException,
+            NoSuchProjectException {
+      CreateChange.checkValidCLA(
+          projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()));
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       if (!edit.isPresent()) {
         throw new ResourceConflictException(
@@ -99,7 +108,8 @@
       }
       editUtil.publish(
           updateFactory,
-          rsrc.getControl(),
+          rsrc.getNotes(),
+          rsrc.getUser(),
           edit.get(),
           in.notify,
           notifyUtil.resolveAccounts(in.notifyDetails));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index f448d92..39828ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -83,6 +84,7 @@
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final DraftPublished draftPublished;
   private final ProjectCache projectCache;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   public PublishDraftPatchSet(
@@ -95,7 +97,8 @@
       Provider<ReviewDb> dbProvider,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       DraftPublished draftPublished,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      ChangeControl.GenericFactory changeControlFactory) {
     super(retryHelper);
     this.accountResolver = accountResolver;
     this.approvalsUtil = approvalsUtil;
@@ -106,6 +109,7 @@
     this.replacePatchSetFactory = replacePatchSetFactory;
     this.draftPublished = draftPublished;
     this.projectCache = projectCache;
+    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -138,7 +142,10 @@
           .setLabel("Publish")
           .setTitle(String.format("Publish revision %d", rsrc.getPatchSet().getPatchSetId()))
           .setVisible(
-              rsrc.getPatchSet().isDraft() && rsrc.getControl().canPublish(dbProvider.get()));
+              rsrc.getPatchSet().isDraft()
+                  && changeControlFactory
+                      .controlFor(rsrc.getNotes(), rsrc.getUser())
+                      .canPublish(dbProvider.get()));
     } catch (OrmException e) {
       throw new IllegalStateException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
index 62742ec..4c9cf23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -71,11 +70,10 @@
       throws UpdateException, RestApiException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION);
 
-    ChangeControl ctl = rsrc.getControl();
     Op op = new Op(input != null ? input : new Input(), rsrc.getPatchSet().getId());
     try (BatchUpdate u =
         updateFactory.create(
-            dbProvider.get(), rsrc.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+            dbProvider.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       u.addOp(rsrc.getChange().getId(), op);
       u.execute();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 5412a69..c5693c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -91,10 +91,7 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            db.get(),
-            rsrc.getChange().getProject(),
-            rsrc.getControl().getUser(),
-            TimeUtil.nowTs())) {
+            db.get(), rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op(rsrc.getComment().key, in);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
index a1a5ab7..f994fc9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
@@ -35,6 +35,8 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -70,6 +72,8 @@
   private final PermissionBackend permissionBackend;
   private final PatchSetUtil psUtil;
   private final NotifyUtil notifyUtil;
+  private final ProjectCache projectCache;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   PutMessage(
@@ -81,7 +85,9 @@
       PermissionBackend permissionBackend,
       @GerritPersonIdent PersonIdent gerritIdent,
       PatchSetUtil psUtil,
-      NotifyUtil notifyUtil) {
+      NotifyUtil notifyUtil,
+      ProjectCache projectCache,
+      ChangeControl.GenericFactory changeControlFactory) {
     super(retryHelper);
     this.repositoryManager = repositoryManager;
     this.currentUserProvider = currentUserProvider;
@@ -91,6 +97,8 @@
     this.permissionBackend = permissionBackend;
     this.psUtil = psUtil;
     this.notifyUtil = notifyUtil;
+    this.projectCache = projectCache;
+    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -101,7 +109,9 @@
     PatchSet ps = psUtil.current(db.get(), resource.getNotes());
     if (ps == null) {
       throw new ResourceConflictException("current revision is missing");
-    } else if (!resource.getControl().isPatchVisible(ps, db.get())) {
+    } else if (!changeControlFactory
+        .controlFor(resource.getNotes(), resource.getUser())
+        .isPatchVisible(ps, db.get())) {
       throw new AuthException("current revision not accessible");
     }
 
@@ -112,7 +122,7 @@
 
     ensureCanEditCommitMessage(resource.getNotes());
     ensureChangeIdIsCorrect(
-        resource.getControl().getProjectControl().getProjectState().isRequireChangeID(),
+        projectCache.checkedGet(resource.getProject()).isRequireChangeID(),
         resource.getChange().getKey().get(),
         sanitizedCommitMessage);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index e25cd01..19f5055 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.RebaseUtil.Base;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -73,6 +74,8 @@
   private final RebaseUtil rebaseUtil;
   private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
+  private final Provider<CurrentUser> userProvider;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   public Rebase(
@@ -81,13 +84,17 @@
       RebaseChangeOp.Factory rebaseFactory,
       RebaseUtil rebaseUtil,
       ChangeJson.Factory json,
-      Provider<ReviewDb> dbProvider) {
+      Provider<ReviewDb> dbProvider,
+      Provider<CurrentUser> userProvider,
+      ChangeControl.GenericFactory changeControlFactory) {
     super(retryHelper);
     this.repoManager = repoManager;
     this.rebaseFactory = rebaseFactory;
     this.rebaseUtil = rebaseUtil;
     this.json = json;
     this.dbProvider = dbProvider;
+    this.userProvider = userProvider;
+    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -97,7 +104,7 @@
           NoSuchChangeException, PermissionBackendException {
     rsrc.permissions().database(dbProvider).check(ChangePermission.REBASE);
 
-    ChangeControl control = rsrc.getControl();
+    ChangeControl control = changeControlFactory.controlFor(rsrc.getNotes(), rsrc.getUser());
     Change change = rsrc.getChange();
     try (Repository repo = repoManager.openRepository(change.getProject());
         ObjectInserter oi = repo.newObjectInserter();
@@ -151,13 +158,14 @@
       throw new ResourceConflictException("base revision is missing: " + str);
     }
     PatchSet.Id baseId = base.patchSet().getId();
-    if (!base.control().isPatchVisible(base.patchSet(), db)) {
+    ChangeControl baseCtl = changeControlFactory.controlFor(base.notes(), userProvider.get());
+    if (!baseCtl.isPatchVisible(base.patchSet(), db)) {
       throw new AuthException("base revision not accessible: " + str);
     } else if (change.getId().equals(baseId.getParentKey())) {
       throw new ResourceConflictException("cannot rebase change onto itself");
     }
 
-    Change baseChange = base.control().getChange();
+    Change baseChange = base.notes().getChange();
     if (!baseChange.getProject().equals(change.getProject())) {
       throw new ResourceConflictException(
           "base change is in wrong project: " + baseChange.getProject());
@@ -221,12 +229,18 @@
       extends RetryingRestModifyView<ChangeResource, RebaseInput, ChangeInfo> {
     private final PatchSetUtil psUtil;
     private final Rebase rebase;
+    private final ChangeControl.GenericFactory changeControlFactory;
 
     @Inject
-    CurrentRevision(RetryHelper retryHelper, PatchSetUtil psUtil, Rebase rebase) {
+    CurrentRevision(
+        RetryHelper retryHelper,
+        PatchSetUtil psUtil,
+        Rebase rebase,
+        ChangeControl.GenericFactory changeControlFactory) {
       super(retryHelper);
       this.psUtil = psUtil;
       this.rebase = rebase;
+      this.changeControlFactory = changeControlFactory;
     }
 
     @Override
@@ -237,7 +251,9 @@
       PatchSet ps = psUtil.current(rebase.dbProvider.get(), rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
-      } else if (!rsrc.getControl().isPatchVisible(ps, rebase.dbProvider.get())) {
+      } else if (!changeControlFactory
+          .controlFor(rsrc.getNotes(), rsrc.getUser())
+          .isPatchVisible(ps, rebase.dbProvider.get())) {
         throw new AuthException("current revision not accessible");
       }
       return rebase.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 465a1b3..db705e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -163,7 +163,8 @@
     rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage);
     Base base =
         rebaseUtil.parseBase(
-            new RevisionResource(changeResourceFactory.create(ctl), originalPatchSet),
+            new RevisionResource(
+                changeResourceFactory.create(ctl.getNotes(), ctl.getUser()), originalPatchSet),
             baseCommitId.name());
 
     rebasedPatchSetId =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
index 173f522..fdb1cfc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -80,14 +79,14 @@
 
   @AutoValue
   abstract static class Base {
-    private static Base create(ChangeControl ctl, PatchSet ps) {
-      if (ctl == null) {
+    private static Base create(ChangeNotes notes, PatchSet ps) {
+      if (notes == null) {
         return null;
       }
-      return new AutoValue_RebaseUtil_Base(ctl, ps);
+      return new AutoValue_RebaseUtil_Base(notes, ps);
     }
 
-    abstract ChangeControl control();
+    abstract ChangeNotes notes();
 
     abstract PatchSet patchSet();
   }
@@ -99,20 +98,20 @@
     PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
     if (basePatchSetId != null) {
       Change.Id baseChangeId = basePatchSetId.getParentKey();
-      ChangeControl baseCtl = controlFor(rsrc, baseChangeId);
-      if (baseCtl != null) {
+      ChangeNotes baseNotes = notesFor(rsrc, baseChangeId);
+      if (baseNotes != null) {
         return Base.create(
-            controlFor(rsrc, basePatchSetId.getParentKey()),
-            psUtil.get(db, baseCtl.getNotes(), basePatchSetId));
+            notesFor(rsrc, basePatchSetId.getParentKey()),
+            psUtil.get(db, baseNotes, basePatchSetId));
       }
     }
 
     // Try parsing base as a change number (assume current patch set).
     Integer baseChangeId = Ints.tryParse(base);
     if (baseChangeId != null) {
-      ChangeControl baseCtl = controlFor(rsrc, new Change.Id(baseChangeId));
-      if (baseCtl != null) {
-        return Base.create(baseCtl, psUtil.current(db, baseCtl.getNotes()));
+      ChangeNotes baseNotes = notesFor(rsrc, new Change.Id(baseChangeId));
+      if (baseNotes != null) {
+        return Base.create(baseNotes, psUtil.current(db, baseNotes));
       }
     }
 
@@ -124,19 +123,18 @@
           continue;
         }
         if (ret == null || ret.patchSet().getId().get() < ps.getId().get()) {
-          ret = Base.create(rsrc.getControl().getProjectControl().controlFor(cd.notes()), ps);
+          ret = Base.create(cd.notes(), ps);
         }
       }
     }
     return ret;
   }
 
-  private ChangeControl controlFor(RevisionResource rsrc, Change.Id id) throws OrmException {
+  private ChangeNotes notesFor(RevisionResource rsrc, Change.Id id) throws OrmException {
     if (rsrc.getChange().getId().equals(id)) {
-      return rsrc.getControl();
+      return rsrc.getNotes();
     }
-    ChangeNotes notes = notesFactory.createChecked(dbProvider.get(), rsrc.getProject(), id);
-    return rsrc.getControl().getProjectControl().controlFor(notes);
+    return notesFactory.createChecked(dbProvider.get(), rsrc.getProject(), id);
   }
 
   /**
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 941f4dc..cfb588f 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
@@ -47,6 +47,8 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -93,6 +95,7 @@
   private final PersonIdent serverIdent;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeReverted changeReverted;
+  private final ProjectControl.GenericFactory projectControlFactory;
 
   @Inject
   Revert(
@@ -108,7 +111,8 @@
       ChangeJson.Factory json,
       @GerritPersonIdent PersonIdent serverIdent,
       ApprovalsUtil approvalsUtil,
-      ChangeReverted changeReverted) {
+      ChangeReverted changeReverted,
+      ProjectControl.GenericFactory projectControlFactory) {
     super(retryHelper);
     this.db = db;
     this.permissionBackend = permissionBackend;
@@ -122,19 +126,20 @@
     this.serverIdent = serverIdent;
     this.approvalsUtil = approvalsUtil;
     this.changeReverted = changeReverted;
+    this.projectControlFactory = projectControlFactory;
   }
 
   @Override
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input)
       throws IOException, OrmException, RestApiException, UpdateException, NoSuchChangeException,
-          PermissionBackendException {
+          PermissionBackendException, NoSuchProjectException {
     Change change = rsrc.getChange();
     if (change.getStatus() != Change.Status.MERGED) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
 
-    CreateChange.checkValidCLA(rsrc.getControl().getProjectControl());
+    CreateChange.checkValidCLA(projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()));
     permissionBackend.user(rsrc.getUser()).ref(change.getDest()).check(CREATE_CHANGE);
 
     Change.Id revertId =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index a582e2c..b9b2d1d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 import java.util.Optional;
 
@@ -62,12 +61,8 @@
     return change;
   }
 
-  public ChangeControl getControl() {
-    return getChangeResource().getControl();
-  }
-
   public Change getChange() {
-    return getControl().getChange();
+    return getChangeResource().getChange();
   }
 
   public Project.NameKey getProject() {
@@ -100,7 +95,7 @@
   }
 
   CurrentUser getUser() {
-    return getControl().getUser();
+    return getChangeResource().getUser();
   }
 
   RevisionResource doNotCache() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index 47edc48..fae911f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -45,17 +46,20 @@
   private final Provider<ReviewDb> dbProvider;
   private final ChangeEditUtil editUtil;
   private final PatchSetUtil psUtil;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   Revisions(
       DynamicMap<RestView<RevisionResource>> views,
       Provider<ReviewDb> dbProvider,
       ChangeEditUtil editUtil,
-      PatchSetUtil psUtil) {
+      PatchSetUtil psUtil,
+      ChangeControl.GenericFactory changeControlFactory) {
     this.views = views;
     this.dbProvider = dbProvider;
     this.editUtil = editUtil;
     this.psUtil = psUtil;
+    this.changeControlFactory = changeControlFactory;
   }
 
   @Override
@@ -97,7 +101,9 @@
   }
 
   private boolean visible(ChangeResource change, PatchSet ps) throws OrmException {
-    return change.getControl().isPatchVisible(ps, dbProvider.get());
+    return changeControlFactory
+        .controlFor(change.getNotes(), change.getUser())
+        .isPatchVisible(ps, dbProvider.get());
   }
 
   private List<RevisionResource> find(ChangeResource change, String id)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 74fd2b1..78aae96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -55,6 +55,7 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -321,7 +322,12 @@
     }
 
     ReviewDb db = dbProvider.get();
-    ChangeData cd = changeDataFactory.create(db, resource.getControl());
+    ChangeData cd;
+    try {
+      cd = changeDataFactory.create(db, resource.getChangeResource());
+    } catch (NoSuchChangeException e) {
+      return null; // submit not visible
+    }
     try {
       MergeOp.checkSubmitRule(cd, false);
     } catch (ResourceConflictException e) {
@@ -333,7 +339,7 @@
 
     ChangeSet cs;
     try {
-      cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getControl().getUser());
+      cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getUser());
     } catch (OrmException | IOException | PermissionBackendException e) {
       throw new OrmRuntimeException(
           "Could not determine complete set of changes to be submitted", e);
@@ -508,17 +514,20 @@
     private final Submit submit;
     private final ChangeJson.Factory json;
     private final PatchSetUtil psUtil;
+    private final ChangeControl.GenericFactory changeControlFactory;
 
     @Inject
     CurrentRevision(
         Provider<ReviewDb> dbProvider,
         Submit submit,
         ChangeJson.Factory json,
-        PatchSetUtil psUtil) {
+        PatchSetUtil psUtil,
+        ChangeControl.GenericFactory changeControlFactory) {
       this.dbProvider = dbProvider;
       this.submit = submit;
       this.json = json;
       this.psUtil = psUtil;
+      this.changeControlFactory = changeControlFactory;
     }
 
     @Override
@@ -528,7 +537,9 @@
       PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
-      } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
+      } else if (!changeControlFactory
+          .controlFor(rsrc.getNotes(), rsrc.getUser())
+          .isPatchVisible(ps, dbProvider.get())) {
         throw new AuthException("current revision not accessible");
       }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index 75b60c7..bb4a357 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -50,6 +51,7 @@
 
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
+  private final ProjectCache projectCache;
 
   @Inject
   SuggestChangeReviewers(
@@ -59,10 +61,12 @@
       PermissionBackend permissionBackend,
       Provider<CurrentUser> self,
       @GerritServerConfig Config cfg,
-      ReviewersUtil reviewersUtil) {
+      ReviewersUtil reviewersUtil,
+      ProjectCache projectCache) {
     super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
     this.permissionBackend = permissionBackend;
     this.self = self;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -74,7 +78,7 @@
     return reviewersUtil.suggestReviewers(
         rsrc.getNotes(),
         this,
-        rsrc.getControl().getProjectControl().getProjectState(),
+        projectCache.checkedGet(rsrc.getProject()),
         getVisibility(rsrc),
         excludeGroups);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index 9e93465..ad716e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -80,7 +80,10 @@
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator =
         new SubmitRuleEvaluator(
-            accountCache, accounts, emails, changeDataFactory.create(db.get(), rsrc.getControl()));
+            accountCache,
+            accounts,
+            emails,
+            changeDataFactory.create(db.get(), rsrc.getChangeResource()));
 
     List<SubmitRecord> records =
         evaluator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
index 5fb37e6..48b657e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -74,7 +74,10 @@
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator =
         new SubmitRuleEvaluator(
-            accountCache, accounts, emails, changeDataFactory.create(db.get(), rsrc.getControl()));
+            accountCache,
+            accounts,
+            emails,
+            changeDataFactory.create(db.get(), rsrc.getChangeResource()));
 
     SubmitTypeRecord rec =
         evaluator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 917c005..565ed9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.RepoContext;
@@ -149,7 +148,8 @@
    * Promote change edit to patch set, by squashing the edit into its parent.
    *
    * @param updateFactory factory for creating updates.
-   * @param ctl the {@code ChangeControl} of the change to which the change edit belongs
+   * @param notes the {@code ChangeNotes} of the change to which the change edit belongs
+   * @param user the current user
    * @param edit change edit to publish
    * @param notify Notify handling that defines to whom email notifications should be sent after the
    *     change edit is published.
@@ -161,7 +161,8 @@
    */
   public void publish(
       BatchUpdate.Factory updateFactory,
-      ChangeControl ctl,
+      ChangeNotes notes,
+      CurrentUser user,
       final ChangeEdit edit,
       NotifyHandling notify,
       ListMultimap<RecipientType, Account.Id> accountsToNotify)
@@ -180,7 +181,7 @@
       PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
       PatchSetInserter inserter =
           patchSetInserterFactory
-              .create(ctl.getNotes(), psId, squashed)
+              .create(notes, psId, squashed)
               .setNotify(notify)
               .setAccountsToNotify(accountsToNotify);
 
@@ -202,7 +203,7 @@
       }
 
       try (BatchUpdate bu =
-          updateFactory.create(db.get(), change.getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+          updateFactory.create(db.get(), change.getProject(), user, TimeUtil.nowTs())) {
         bu.setRepository(repo, rw, oi);
         bu.addOp(
             change.getId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 8c67c72..de95f61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -299,7 +299,7 @@
     recipients.add(
         getRecipientsFromFooters(ctx.getDb(), accountResolver, draft, commit.getFooterLines()));
     recipients.remove(ctx.getAccountId());
-    ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getControl());
+    ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx);
     MailRecipients oldRecipients = getRecipientsFromReviewers(cd.reviewers());
     Iterable<PatchSetApproval> newApprovals =
         approvalsUtil.addApprovalsForNewPatchSet(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java
index 84f7e27..326d395 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.errors.InvalidNameException;
@@ -29,6 +30,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeJson;
@@ -138,12 +140,23 @@
           BatchUpdate bu =
               updateFactory.create(db.get(), rsrc.getNameKey(), rsrc.getUser(), TimeUtil.nowTs())) {
         bu.setRepository(md.getRepository(), rw, objInserter);
-        ChangeInserter ins = changeInserterFactory.create(changeId, commit, RefNames.REFS_CONFIG);
-        ins.setMessage("First patchset").setValidate(false).setUpdateRef(false);
+        ChangeInserter ins = newInserter(changeId, commit);
         bu.insertChange(ins);
         bu.execute();
         return Response.created(jsonFactory.noOptions().format(ins.getChange()));
       }
     }
   }
+
+  // ProjectConfig doesn't currently support fusing into a BatchUpdate.
+  @SuppressWarnings("deprecation")
+  private ChangeInserter newInserter(Change.Id changeId, RevCommit commit) {
+    return changeInserterFactory
+        .create(changeId, commit, RefNames.REFS_CONFIG)
+        .setMessage(
+            // Same message as in ReceiveCommits.CreateRequest.
+            ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
+        .setValidate(false)
+        .setUpdateRef(false);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index bbcb811..bc418b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.InternalQuery;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountState;
@@ -123,7 +124,7 @@
    * @throws OrmException if query cannot be parsed
    */
   public List<AccountState> byPreferredEmail(String email) throws OrmException {
-    if (schema().hasField(AccountField.PREFERRED_EMAIL_EXACT)) {
+    if (hasPreferredEmailExact()) {
       return query(AccountPredicates.preferredEmailExact(email));
     }
 
@@ -144,7 +145,7 @@
   public Multimap<String, AccountState> byPreferredEmail(String... emails) throws OrmException {
     List<String> emailList = Arrays.asList(emails);
 
-    if (schema().hasField(AccountField.PREFERRED_EMAIL_EXACT)) {
+    if (hasPreferredEmailExact()) {
       List<List<AccountState>> r =
           query(
               emailList
@@ -176,4 +177,9 @@
   public List<AccountState> byWatchedProject(Project.NameKey project) throws OrmException {
     return query(AccountPredicates.watchedProject(project));
   }
+
+  private boolean hasPreferredEmailExact() {
+    Schema<AccountState> s = schema();
+    return (s != null && s.hasField(AccountField.PREFERRED_EMAIL_EXACT));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 3a1c60a..b750019 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -35,6 +35,8 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -60,6 +62,8 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.GetPureRevert;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -76,6 +80,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.update.ChangeContext;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
@@ -282,10 +287,12 @@
 
   public static class Factory {
     private final AssistedFactory assistedFactory;
+    private final ChangeControl.GenericFactory changeControlFactory;
 
     @Inject
-    Factory(AssistedFactory assistedFactory) {
+    Factory(AssistedFactory assistedFactory, ChangeControl.GenericFactory changeControlFactory) {
       this.assistedFactory = assistedFactory;
+      this.changeControlFactory = changeControlFactory;
     }
 
     public ChangeData create(ReviewDb db, Project.NameKey project, Change.Id id) {
@@ -310,6 +317,15 @@
           control.getNotes(),
           control);
     }
+
+    // TODO(hiesel): Remove these after ChangeControl is removed from ChangeData
+    public ChangeData create(ReviewDb db, ChangeResource rsrc) throws NoSuchChangeException {
+      return create(db, changeControlFactory.controlFor(rsrc.getNotes(), rsrc.getUser()));
+    }
+
+    public ChangeData create(ReviewDb db, ChangeContext ctx) throws NoSuchChangeException {
+      return create(db, changeControlFactory.controlFor(ctx.getNotes(), ctx.getUser()));
+    }
   }
 
   public interface AssistedFactory {
@@ -336,7 +352,7 @@
     ChangeData cd =
         new ChangeData(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-            null, null, null, null, null, null, project, id, null, null, null);
+            null, null, null, null, null, null, null, project, id, null, null, null);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
@@ -361,6 +377,7 @@
   private final PatchSetUtil psUtil;
   private final ProjectCache projectCache;
   private final TrackingFooters trackingFooters;
+  private final GetPureRevert pureRevert;
 
   // Required assisted injected fields.
   private final ReviewDb db;
@@ -432,6 +449,7 @@
       PatchSetUtil psUtil,
       ProjectCache projectCache,
       TrackingFooters trackingFooters,
+      GetPureRevert pureRevert,
       @Assisted ReviewDb db,
       @Assisted Project.NameKey project,
       @Assisted Change.Id id,
@@ -457,6 +475,7 @@
     this.projectCache = projectCache;
     this.starredChangesUtil = starredChangesUtil;
     this.trackingFooters = trackingFooters;
+    this.pureRevert = pureRevert;
 
     // May be null in tests when created via createForTest above, in which case lazy-loading will
     // intentionally fail with NPE. Still not marked @Nullable in the constructor, to force callers
@@ -1257,6 +1276,22 @@
     return starsOf.stars();
   }
 
+  /**
+   * @return {@code null} if {@code revertOf} is {@code null}; true if the change is a pure revert;
+   *     false otherwise.
+   */
+  @Nullable
+  public Boolean isPureRevert() throws OrmException {
+    if (change().getRevertOf() == null) {
+      return null;
+    }
+    try {
+      return pureRevert.getPureRevert(notes()).isPureRevert;
+    } catch (IOException | BadRequestException | ResourceConflictException e) {
+      throw new OrmException("could not compute pure revert", e);
+    }
+  }
+
   @Override
   public String toString() {
     MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
diff --git a/gerrit-server/src/main/java/gerrit/PRED_pure_revert_1.java b/gerrit-server/src/main/java/gerrit/PRED_pure_revert_1.java
new file mode 100644
index 0000000..f3721fb
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_pure_revert_1.java
@@ -0,0 +1,50 @@
+// 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 gerrit;
+
+import com.google.gerrit.rules.StoredValues;
+import com.google.gwtorm.server.OrmException;
+import com.googlecode.prolog_cafe.exceptions.JavaException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.Predicate;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.Term;
+
+/** Checks if change is a pure revert of the change it references in 'revertOf'. */
+public class PRED_pure_revert_1 extends Predicate.P1 {
+  public PRED_pure_revert_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    Boolean isPureRevert;
+    try {
+      isPureRevert = StoredValues.CHANGE_DATA.get(engine).isPureRevert();
+    } catch (OrmException e) {
+      throw new JavaException(this, 1, e);
+    }
+    if (!a1.unify(new IntegerTerm(Boolean.TRUE.equals(isPureRevert) ? 1 : 0), engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 451fc22..7da82b7 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -105,6 +105,9 @@
     },
 
     _positionTooltip(tooltip) {
+      // This flush is needed for tooltips to be positioned correctly in Firefox
+      // and Safari.
+      Polymer.dom.flush();
       const rect = this.getBoundingClientRect();
       const boxRect = tooltip.getBoundingClientRect();
       const parentRect = tooltip.parentElement.getBoundingClientRect();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index edd5389..238db9c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -94,9 +94,6 @@
       .separator {
         margin: 0 .25em;
       }
-      .expandInline {
-        padding-right: .25em;
-      }
       .patchSetSelect {
         max-width: 8em;
       }
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 95bfc70..3464fea 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
@@ -150,6 +150,9 @@
       #container.editLoaded .hideOnEdit {
         display: none;
       }
+      .expandInline {
+        padding-right: .25em;
+      }
       @media screen and (max-width: 50em) {
         .desktop {
           display: none;
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 61251af..5a8ddf6 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -198,7 +198,7 @@
      */
     timeEnd(name) {
       const baseTime = this._baselines[name] || 0;
-      const time = this.now() - baseTime;
+      const time = Math.round(this.now() - baseTime) + 'ms';
       this.reporter(TIMING.TYPE, TIMING.CATEGORY, name, time);
       delete this._baselines[name];
     },
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 67cd329..e88096b 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -85,10 +85,10 @@
       nowStub.returns(3.123);
       element.timeEnd('foo');
       assert.isTrue(element.reporter.calledWithExactly(
-          'timing-report', 'UI Latency', 'foo', 3.123
+          'timing-report', 'UI Latency', 'foo', '3ms'
       ));
       assert.isTrue(element.reporter.calledWithExactly(
-          'timing-report', 'UI Latency', 'bar', 1
+          'timing-report', 'UI Latency', 'bar', '1ms'
       ));
     });
 
@@ -104,7 +104,7 @@
         sandbox.stub(element, 'now').returns(42);
         element.pluginsLoaded();
         assert.isTrue(element.defaultReporter.calledWithExactly(
-            'timing-report', 'UI Latency', 'PluginsLoaded', 42
+            'timing-report', 'UI Latency', 'PluginsLoaded', '42ms'
         ));
       });