Merge "Disable send button when user has made no changes"
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 871a4d2..6596eda 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -52,6 +52,9 @@
 
   )]}'
   {
+    "accounts": {
+      "visibility": "ALL"
+    },
     "auth": {
       "auth_type": "LDAP",
       "use_contributor_agreements": true,
@@ -190,45 +193,6 @@
   }
 ----
 
-[[check-access]]
-=== Check Access
---
-'POST /config/server/check.access'
---
-
-Runs access checks for other users.
-
-Input for the access checks that should be run must be provided in
-the request body inside a
-link:#access-check-input[AccessCheckInput] entity.
-
-.Request
-----
-  POST /config/server/check HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "project": "medium",
-    "account": "Kristen.Burns@gerritcodereview.com",
-    "ref": "refs/heads/secret/bla"
-  }
-----
-
-The result is a link:#access-check-info[AccessCheckInfo] entity
-detailing the read access of the given user for the given project (or
-project-ref combination).
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "message": "user Kristen Burns \u003cKristen.Burns@gerritcodereview.com\u003e (1000098) cannot see ref refs/heads/secret/master in project medium",
-    "status": 403
-  }
-----
 
 [[confirm-email]]
 === Confirm Email
@@ -1294,33 +1258,19 @@
 [[json-entities]]
 == JSON Entities
 
-[[access-check-info]]
-=== AccessCheckInfo
-The `AccessCheckInfo` entity is the result of a
-an access check.
+[[accounts-config-info]]
+=== AccountsConfigInfo
+The `AccountsConfigInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#accounts[accounts]
+section.
 
-[options="header",cols="1,^1,5"]
-|=========================================
-|Field Name                  ||Description
-|`status`||The HTTP status code for the access.
-200 means success, 403 means denied and 404 means the project does not
-exist.
-|`message`|optional|A clarifying message if `status` is not 200.
-|=========================================
-
-[[access-check-input]]
-=== AccessCheckInput
-The `AccessCheckInput` entity is a tuple of (account, project)  or
-(account, project, ref) for which we want to check access.
-
-[options="header",cols="1,^1,5"]
-|=========================================
-|Field Name                  ||Description
-|`account`||The account for which to check access
-|`project`||The project for which to check access
-|`ref`|optional|The refname for which to check access
-|=========================================
-
+[options="header",cols="1,6"]
+|=============================
+|Field Name           |Description
+|`visibility`         |
+link:config-gerrit.html#accounts.visibility[Visibility setting for
+accounts].
+|=============================
 
 [[auth-info]]
 === AuthInfo
@@ -1743,6 +1693,10 @@
 [options="header",cols="1,^1,5"]
 |=======================================
 |Field Name                ||Description
+|`accounts`                ||
+Information about the configuration from the
+link:config-gerrit.html#accounts[accounts] section as
+link:#accounts-config-info[AccountsConfigInfo] entity.
 |`auth`                    ||
 Information about the authentication configuration as
 link:#auth-info[AuthInfo] entity.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index f083d073..9d76d34 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1112,7 +1112,6 @@
   }
 ----
 
-
 [[create-access-change]]
 === Create Access Rights Change for review.
 --
@@ -1178,6 +1177,46 @@
   }
 ----
 
+[[check-access]]
+=== Check Access
+--
+'POST /projects/MyProject/check.access'
+--
+
+Runs access checks for other users. This requires the
+link:access-control.html#capability_administrateServer[Administrate Server]
+global capability.
+
+Input for the access checks that should be run must be provided in
+the request body inside a
+link:#access-check-input[AccessCheckInput] entity.
+
+.Request
+----
+  POST /projects/MyProject/check.access HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "account": "Kristen.Burns@gerritcodereview.com",
+    "ref": "refs/heads/secret/bla"
+  }
+----
+
+The result is a link:#access-check-info[AccessCheckInfo] entity
+detailing the read access of the given user for the given project (or
+project-ref combination).
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "message": "user Kristen Burns \u003cKristen.Burns@gerritcodereview.com\u003e (1000098) cannot see ref refs/heads/secret/bla in project MyProject",
+    "status": 403
+  }
+----
 
 [[index]]
 === Index all changes in a project
@@ -2564,6 +2603,31 @@
 [[json-entities]]
 == JSON Entities
 
+[[access-check-info]]
+=== AccessCheckInfo
+
+The `AccessCheckInfo` entity is the result of an access check.
+
+[options="header",cols="1,^1,5"]
+|=========================================
+|Field Name                  ||Description
+|`status`                    ||The HTTP status code for the access.
+200 means success, 403 means denied and 404 means the project does not exist.
+|`message`                   |optional|A clarifying message if `status` is not 200.
+|=========================================
+
+[[access-check-input]]
+=== AccessCheckInput
+The `AccessCheckInput` entity is either an account or
+(account, ref) tuple for which we want to check access.
+
+[options="header",cols="1,^1,5"]
+|=========================================
+|Field Name                  ||Description
+|`account`                   ||The account for which to check access
+|`ref`                       |optional|The refname for which to check access
+|=========================================
+
 [[ban-input]]
 === BanInput
 The `BanInput` entity contains information for banning commits in a
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 7f7fe00..d399e2b 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
@@ -3507,6 +3507,42 @@
   }
 
   @Test
+  public void mute() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+
+    PushOneCommit.Result r = createChange();
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes().id(r.getChangeId()).addReviewer(in);
+
+    setApiUser(user);
+    assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse();
+    gApi.changes().id(r.getChangeId()).mute(true);
+    assertThat(gApi.changes().id(r.getChangeId()).muted()).isTrue();
+
+    setApiUser(user2);
+    sender.clear();
+    amendChange(r.getChangeId());
+
+    setApiUser(user);
+    assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    assertThat(messages.get(0).rcpt()).containsExactly(user.emailAddress);
+  }
+
+  @Test
+  public void cannotMuteOwnChange() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("cannot mute own change");
+    gApi.changes().id(changeId).mute(true);
+  }
+
+  @Test
   public void cannotSetInvalidLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
similarity index 63%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 53c89c5..b471efc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -12,24 +12,24 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest.account;
+package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import java.util.List;
-import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -79,46 +79,59 @@
   }
 
   @Test
-  public void invalidInputs() {
-    List<AccessCheckInput> inputs =
-        ImmutableList.of(
-            new AccessCheckInput(),
-            new AccessCheckInput(user.email, null, null),
-            new AccessCheckInput(null, normalProject.toString(), null),
-            new AccessCheckInput("doesnotexist@invalid.com", normalProject.toString(), null));
-    for (AccessCheckInput input : inputs) {
-      try {
-        gApi.config().server().checkAccess(input);
-        fail(String.format("want RestApiException for %s", newGson().toJson(input)));
-      } catch (RestApiException e) {
+  public void emptyInput() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("input requires 'account'");
+    gApi.projects().name(normalProject.get()).checkAccess(new AccessCheckInput());
+  }
 
-      }
+  @Test
+  public void nonexistentEmail() throws Exception {
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("cannot find account doesnotexist@invalid.com");
+    gApi.projects()
+        .name(normalProject.get())
+        .checkAccess(new AccessCheckInput("doesnotexist@invalid.com", null));
+  }
+
+  private static class TestCase {
+    AccessCheckInput input;
+    String project;
+    int want;
+
+    TestCase(String mail, String project, String ref, int want) {
+      this.input = new AccessCheckInput(mail, ref);
+      this.project = project;
+      this.want = want;
     }
   }
 
   @Test
-  public void accessible() {
-    Map<AccessCheckInput, Integer> inputs =
-        ImmutableMap.of(
-            new AccessCheckInput(user.email, normalProject.get(), null), 200,
-            new AccessCheckInput(user.email, secretProject.get(), null), 403,
-            new AccessCheckInput(user.email, "nonexistent", null), 404,
-            new AccessCheckInput(privilegedUser.email, normalProject.get(), null), 200,
-            new AccessCheckInput(privilegedUser.email, secretProject.get(), null), 200);
+  public void accessible() throws Exception {
+    List<TestCase> inputs =
+        ImmutableList.of(
+            new TestCase(user.email, normalProject.get(), null, 200),
+            new TestCase(user.email, secretProject.get(), null, 403),
+            new TestCase(user.email, secretRefProject.get(), "refs/heads/secret/master", 403),
+            new TestCase(
+                privilegedUser.email, secretRefProject.get(), "refs/heads/secret/master", 200),
+            new TestCase(privilegedUser.email, normalProject.get(), null, 200),
+            new TestCase(privilegedUser.email, secretProject.get(), null, 200));
 
-    for (Map.Entry<AccessCheckInput, Integer> entry : inputs.entrySet()) {
-      String in = newGson().toJson(entry.getKey());
+    for (TestCase tc : inputs) {
+      String in = newGson().toJson(tc.input);
       AccessCheckInfo info = null;
 
       try {
-        info = gApi.config().server().checkAccess(entry.getKey());
+        info = gApi.projects().name(tc.project).checkAccess(tc.input);
       } catch (RestApiException e) {
-        fail(String.format("check.check(%s): exception %s", in, e));
+        fail(String.format("check.access(%s, %s): exception %s", tc.project, in, e));
       }
 
-      int want = entry.getValue();
+      int want = tc.want;
       if (want != info.status) {
-        fail(String.format("check.access(%s) = %d, want %d", in, info.status, want));
+        fail(
+            String.format("check.access(%s, %s) = %d, want %d", tc.project, in, info.status, want));
       }
 
       switch (want) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
index 64f4106..bfe6b8b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -30,6 +30,6 @@
   @Test
   public void dashboardDoesNotExist() throws Exception {
     exception.expect(ResourceNotFoundException.class);
-    gApi.projects().name(project.get()).dashboard("dashboard").get();
+    gApi.projects().name(project.get()).dashboard("my:dashboard").get();
   }
 }
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 66dcaf0..e188afd 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
@@ -408,7 +408,7 @@
     revisionInfo =
         changeJsonFactory
             .create(opts)
-            .getRevisionInfo(cd.changeControl(), cd.patchSet(new PatchSet.Id(changeId, 1)));
+            .getRevisionInfo(cd, cd.patchSet(new PatchSet.Id(changeId, 1)));
   }
 
   private void visitedCurrentRevisionActionsAssertions(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index b8c2cf8..08f0699 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.testutil.TestTimeUtil;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -77,6 +78,15 @@
   }
 
   @Test
+  public void addInvalidHashtag() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("hashtags may not contain commas");
+    addHashtags(r, "invalid,hashtag");
+  }
+
+  @Test
   public void addMultipleHashtags() throws Exception {
     PushOneCommit.Result r = createChange();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 9a23a25..492245b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.common.InstallPluginInput;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
@@ -36,6 +37,9 @@
       "Gerrit.install(function(self){});\n".getBytes(UTF_8);
 
   @Test
+  // accounts
+  @GerritConfig(name = "accounts.visibility", value = "VISIBLE_GROUP")
+
   // auth
   @GerritConfig(name = "auth.type", value = "HTTP")
   @GerritConfig(name = "auth.contributorAgreements", value = "true")
@@ -78,6 +82,9 @@
   public void serverConfig() throws Exception {
     ServerInfo i = gApi.config().server().getInfo();
 
+    // accounts
+    assertThat(i.accounts.visibility).isEqualTo(AccountVisibility.VISIBLE_GROUP);
+
     // auth
     assertThat(i.auth.authType).isEqualTo(AuthType.HTTP);
     assertThat(i.auth.editableAccountFields)
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 7081245..96dfac1 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
@@ -126,6 +126,13 @@
   void mute(boolean mute) throws RestApiException;
 
   /**
+   * Check if this change is muted.
+   *
+   * @return true if the change is muted.
+   */
+  boolean muted() throws RestApiException;
+
+  /**
    * Create a new change that reverts this change.
    *
    * @see Changes#id(int)
@@ -579,6 +586,11 @@
     }
 
     @Override
+    public boolean muted() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public PureRevertInfo pureRevert() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java
index 80a537c..7b7c19d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java
@@ -18,13 +18,11 @@
 
 public class AccessCheckInput {
   public String account;
-  public String project;
 
   @Nullable public String ref;
 
-  public AccessCheckInput(String account, String project, @Nullable String ref) {
+  public AccessCheckInput(String account, @Nullable String ref) {
     this.account = account;
-    this.project = project;
     this.ref = ref;
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
index de59cee..ba81698 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -36,8 +36,6 @@
 
   ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
 
-  AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException;
-
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -79,10 +77,5 @@
     public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
       throw new NotImplementedException();
     }
-
-    @Override
-    public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
-      throw new NotImplementedException();
-    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardInfo.java
index 775224c..f629294 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardInfo.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.extensions.api.projects;
 
-import com.google.common.base.Joiner;
-import com.google.gerrit.extensions.restapi.Url;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -34,9 +32,5 @@
   public String title;
   public List<DashboardSectionInfo> sections = new ArrayList<>();
 
-  public DashboardInfo(String ref, String name) {
-    this.ref = ref;
-    this.path = name;
-    this.id = Joiner.on(':').join(Url.encode(ref), Url.encode(path));
-  }
+  public DashboardInfo() {}
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 8687ba3e..a61b68a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
+import com.google.gerrit.extensions.api.config.AccessCheckInfo;
+import com.google.gerrit.extensions.api.config.AccessCheckInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -39,6 +41,8 @@
 
   ChangeInfo accessChange(ProjectAccessInput p) throws RestApiException;
 
+  AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException;
+
   ConfigInfo config() throws RestApiException;
 
   ConfigInfo config(ConfigInput in) throws RestApiException;
@@ -181,11 +185,21 @@
     }
 
     @Override
+    public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeInfo accessChange(ProjectAccessInput input) throws RestApiException {
       throw new NotImplementedException();
     }
 
     @Override
+    public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ConfigInfo config() throws RestApiException {
       throw new NotImplementedException();
     }
@@ -196,11 +210,6 @@
     }
 
     @Override
-    public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public void description(DescriptionInput in) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibility.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountVisibility.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibility.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountVisibility.java
index 9957134..32ec318 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibility.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountVisibility.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.account;
+package com.google.gerrit.extensions.common;
 
 /** Visibility level of other accounts to a given user. */
 public enum AccountVisibility {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountsInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountsInfo.java
new file mode 100644
index 0000000..e1c2825
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountsInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class AccountsInfo {
+  public AccountVisibility visibility;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
index 2eb63f4..8904f0a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
@@ -17,6 +17,7 @@
 import java.util.Map;
 
 public class ServerInfo {
+  public AccountsInfo accounts;
   public AuthInfo auth;
   public ChangeConfigInfo change;
   public DownloadInfo download;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SetDashboardInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SetDashboardInput.java
new file mode 100644
index 0000000..13d2b9d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SetDashboardInput.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class SetDashboardInput {
+  @DefaultInput public String id;
+  public String commitMessage;
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index a28aa64..32d8dd8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCacheImpl;
-import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.FakeRealm;
@@ -68,6 +68,7 @@
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -167,6 +168,7 @@
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ProjectState.Factory.class);
+    factory(SubmitRuleEvaluator.Factory.class);
 
     bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
     bind(AccountVisibility.class).toProvider(AccountVisibilityProvider.class).in(SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 61ab3a1..13c24e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -334,30 +334,32 @@
     return MUTE_LABEL + "/" + change.currentPatchSetId().get();
   }
 
-  public void mute(Account.Id accountId, Project.NameKey project, Change change)
-      throws OrmException, IllegalLabelException {
+  public void mute(ChangeResource rsrc) throws OrmException, IllegalLabelException {
     star(
-        accountId,
-        project,
-        change.getId(),
-        ImmutableSet.of(getMuteLabel(change)),
+        rsrc.getUser().asIdentifiedUser().getAccountId(),
+        rsrc.getProject(),
+        rsrc.getChange().getId(),
+        ImmutableSet.of(getMuteLabel(rsrc.getChange())),
         ImmutableSet.of());
   }
 
-  public void unmute(Account.Id accountId, Project.NameKey project, Change change)
-      throws OrmException, IllegalLabelException {
+  public void unmute(ChangeResource rsrc) throws OrmException, IllegalLabelException {
     star(
-        accountId,
-        project,
-        change.getId(),
+        rsrc.getUser().asIdentifiedUser().getAccountId(),
+        rsrc.getProject(),
+        rsrc.getChange().getId(),
         ImmutableSet.of(),
-        ImmutableSet.of(getMuteLabel(change)));
+        ImmutableSet.of(getMuteLabel(rsrc.getChange())));
   }
 
   public boolean isMutedBy(Change change, Account.Id accountId) throws OrmException {
     return getLabels(accountId, change.getId()).contains(getMuteLabel(change));
   }
 
+  public boolean isMuted(ChangeResource rsrc) throws OrmException {
+    return isMutedBy(rsrc.getChange(), rsrc.getUser().asIdentifiedUser().getAccountId());
+  }
+
   private static StarRef readLabels(Repository repo, String refName) throws IOException {
     Ref ref = repo.exactRef(refName);
     if (ref == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index bb118a3..6f25703 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibilityProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibilityProvider.java
index 4521cd5..ef0a917 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibilityProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibilityProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
index cbfb7f5..e6ac0f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index d006c7e..ab47f3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -677,7 +677,7 @@
   @Override
   public boolean ignored() throws RestApiException {
     try {
-      return stars.isIgnoredBy(change.getId(), change.getUser().getAccountId());
+      return stars.isIgnored(change);
     } catch (OrmException e) {
       throw asRestApiException("Cannot check if ignored", e);
     }
@@ -699,6 +699,15 @@
   }
 
   @Override
+  public boolean muted() throws RestApiException {
+    try {
+      return stars.isMuted(change);
+    } catch (OrmException e) {
+      throw asRestApiException("Cannot check if muted", e);
+    }
+  }
+
+  @Override
   public PureRevertInfo pureRevert() throws RestApiException {
     return pureRevert(null);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
index 87118c7..2148d97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -17,8 +17,6 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.gerrit.common.Version;
-import com.google.gerrit.extensions.api.config.AccessCheckInfo;
-import com.google.gerrit.extensions.api.config.AccessCheckInput;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
 import com.google.gerrit.extensions.api.config.Server;
@@ -26,7 +24,6 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.config.CheckAccess;
 import com.google.gerrit.server.config.CheckConsistency;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.GetDiffPreferences;
@@ -46,7 +43,6 @@
   private final SetDiffPreferences setDiffPreferences;
   private final GetServerInfo getServerInfo;
   private final Provider<CheckConsistency> checkConsistency;
-  private final Provider<CheckAccess> checkAccess;
 
   @Inject
   ServerImpl(
@@ -55,15 +51,13 @@
       GetDiffPreferences getDiffPreferences,
       SetDiffPreferences setDiffPreferences,
       GetServerInfo getServerInfo,
-      Provider<CheckConsistency> checkConsistency,
-      Provider<CheckAccess> checkAccess) {
+      Provider<CheckConsistency> checkConsistency) {
     this.getPreferences = getPreferences;
     this.setPreferences = setPreferences;
     this.getDiffPreferences = getDiffPreferences;
     this.setDiffPreferences = setDiffPreferences;
     this.getServerInfo = getServerInfo;
     this.checkConsistency = checkConsistency;
-    this.checkAccess = checkAccess;
   }
 
   @Override
@@ -126,13 +120,4 @@
       throw asRestApiException("Cannot check consistency", e);
     }
   }
-
-  @Override
-  public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
-    try {
-      return checkAccess.get().apply(new ConfigResource(), in);
-    } catch (Exception e) {
-      throw asRestApiException("Cannot check access", e);
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
index 28ed494..58cb59e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
@@ -34,24 +34,24 @@
 
 public class DashboardApiImpl implements DashboardApi {
   interface Factory {
-    DashboardApiImpl create(ProjectResource project, String name);
+    DashboardApiImpl create(ProjectResource project, String id);
   }
 
   private final DashboardsCollection dashboards;
-  private final Provider<GetDashboard> getDashboard;
+  private final Provider<GetDashboard> get;
   private final ProjectResource project;
-  private final String name;
+  private final String id;
 
   @Inject
   DashboardApiImpl(
       DashboardsCollection dashboards,
-      Provider<GetDashboard> getDashboard,
+      Provider<GetDashboard> get,
       @Assisted ProjectResource project,
-      @Assisted String name) {
+      @Assisted String id) {
     this.dashboards = dashboards;
-    this.getDashboard = getDashboard;
+    this.get = get;
     this.project = project;
-    this.name = name;
+    this.id = id;
   }
 
   @Override
@@ -62,7 +62,7 @@
   @Override
   public DashboardInfo get(boolean inherited) throws RestApiException {
     try {
-      return getDashboard.get().setInherited(inherited).apply(resource());
+      return get.get().setInherited(inherited).apply(resource());
     } catch (IOException | PermissionBackendException | ConfigInvalidException e) {
       throw asRestApiException("Cannot read dashboard", e);
     }
@@ -71,6 +71,6 @@
   private DashboardResource resource()
       throws ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    return dashboards.parse(project, IdString.fromDecoded(name));
+    return dashboards.parse(project, IdString.fromDecoded(id));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 60dc474..5012280 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -19,6 +19,8 @@
 
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
+import com.google.gerrit.extensions.api.config.AccessCheckInfo;
+import com.google.gerrit.extensions.api.config.AccessCheckInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ChildProjectApi;
@@ -44,6 +46,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.CheckAccess;
 import com.google.gerrit.server.project.ChildProjectsCollection;
 import com.google.gerrit.server.project.CommitsCollection;
 import com.google.gerrit.server.project.CreateAccessChange;
@@ -100,6 +103,7 @@
   private final CommitsCollection commitsCollection;
   private final CommitApiImpl.Factory commitApi;
   private final DashboardApiImpl.Factory dashboardApi;
+  private final CheckAccess checkAccess;
 
   @AssistedInject
   ProjectApiImpl(
@@ -127,6 +131,7 @@
       CommitsCollection commitsCollection,
       CommitApiImpl.Factory commitApi,
       DashboardApiImpl.Factory dashboardApi,
+      CheckAccess checkAccess,
       @Assisted ProjectResource project) {
     this(
         user,
@@ -154,6 +159,7 @@
         commitsCollection,
         commitApi,
         dashboardApi,
+        checkAccess,
         null);
   }
 
@@ -183,6 +189,7 @@
       CommitsCollection commitsCollection,
       CommitApiImpl.Factory commitApi,
       DashboardApiImpl.Factory dashboardApi,
+      CheckAccess checkAccess,
       @Assisted String name) {
     this(
         user,
@@ -210,6 +217,7 @@
         commitsCollection,
         commitApi,
         dashboardApi,
+        checkAccess,
         name);
   }
 
@@ -239,6 +247,7 @@
       CommitsCollection commitsCollection,
       CommitApiImpl.Factory commitApi,
       DashboardApiImpl.Factory dashboardApi,
+      CheckAccess checkAccess,
       String name) {
     this.user = user;
     this.permissionBackend = permissionBackend;
@@ -251,7 +260,6 @@
     this.children = children;
     this.projectJson = projectJson;
     this.project = project;
-    this.name = name;
     this.branchApi = branchApiFactory;
     this.tagApi = tagApiFactory;
     this.getAccess = getAccess;
@@ -266,6 +274,8 @@
     this.commitApi = commitApi;
     this.createAccessChange = createAccessChange;
     this.dashboardApi = dashboardApi;
+    this.checkAccess = checkAccess;
+    this.name = name;
   }
 
   @Override
@@ -314,6 +324,15 @@
   }
 
   @Override
+  public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
+    try {
+      return checkAccess.apply(checkExists(), in);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check access rights", e);
+    }
+  }
+
+  @Override
   public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
     try {
       return setAccess.apply(checkExists(), p);
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 ffea604..18e373f 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
@@ -119,7 +119,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.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.RemoveReviewerControl;
@@ -166,7 +165,7 @@
       ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().fastEvalLabels(true).build();
 
   public static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
-      ImmutableSet.of(ALL_REVISIONS, MESSAGES);
+      ImmutableSet.of(ALL_REVISIONS, CHANGE_ACTIONS, CHECK, CURRENT_ACTIONS, MESSAGES);
 
   @Singleton
   public static class Factory {
@@ -430,9 +429,9 @@
   }
 
   private ChangeInfo checkOnly(ChangeData cd) {
-    ChangeControl ctl;
+    ChangeNotes notes;
     try {
-      ctl = cd.changeControl().forUser(userProvider.get());
+      notes = cd.notes();
     } catch (OrmException e) {
       String msg = "Error loading change";
       log.warn(msg + " " + cd.getId(), e);
@@ -444,7 +443,7 @@
       return info;
     }
 
-    ConsistencyChecker.Result result = checkerProvider.get().check(ctl.getNotes(), fix);
+    ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix);
     ChangeInfo info;
     Change c = result.change();
     if (c != null) {
@@ -477,10 +476,9 @@
           PermissionBackendException {
     ChangeInfo out = new ChangeInfo();
     CurrentUser user = userProvider.get();
-    ChangeControl ctl = cd.changeControl().forUser(user);
 
     if (has(CHECK)) {
-      out.problems = checkerProvider.get().check(ctl.getNotes(), fix).problems();
+      out.problems = checkerProvider.get().check(cd.notes(), fix).problems();
       // If any problems were fixed, the ChangeData needs to be reloaded.
       for (ProblemInfo p : out.problems) {
         if (p.status == ProblemInfo.Status.FIXED) {
@@ -549,7 +547,7 @@
       }
     }
 
-    out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS));
+    out.labels = labelsFor(perm, cd, has(LABELS), has(DETAILED_LABELS));
 
     if (out.labels != null && has(DETAILED_LABELS)) {
       // If limited to specific patch sets but not the current patch set, don't
@@ -564,7 +562,7 @@
 
       out.reviewers = reviewerMap(cd.reviewers(), cd.reviewersByEmail(), false);
       out.pendingReviewers = reviewerMap(cd.pendingReviewers(), cd.pendingReviewersByEmail(), true);
-      out.removableReviewers = removableReviewers(ctl, out);
+      out.removableReviewers = removableReviewers(cd, out);
     }
 
     setSubmitter(cd, out);
@@ -585,14 +583,14 @@
       src = null;
     }
     if (needMessages) {
-      out.messages = messages(ctl, cd, src);
+      out.messages = messages(cd, src);
     }
     finish(out);
 
     // This block must come after the ChangeInfo is mostly populated, since
     // it will be passed to ActionVisitors as-is.
     if (needRevisions) {
-      out.revisions = revisions(ctl, cd, src, limitToPsId, out);
+      out.revisions = revisions(cd, src, limitToPsId, out);
       if (out.revisions != null) {
         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
           if (entry.getValue().isCurrent) {
@@ -604,7 +602,7 @@
     }
 
     if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) {
-      actionJson.addChangeActions(out, ctl.getNotes());
+      actionJson.addChangeActions(out, cd.notes());
     }
 
     if (has(TRACKING_IDS)) {
@@ -658,20 +656,12 @@
   }
 
   private Map<String, LabelInfo> labelsFor(
-      PermissionBackend.ForChange perm,
-      ChangeControl ctl,
-      ChangeData cd,
-      boolean standard,
-      boolean detailed)
+      PermissionBackend.ForChange perm, ChangeData cd, boolean standard, boolean detailed)
       throws OrmException, PermissionBackendException {
     if (!standard && !detailed) {
       return null;
     }
 
-    if (ctl == null) {
-      return null;
-    }
-
     LabelTypes labelTypes = cd.getLabelTypes();
     Map<String, LabelWithStatus> withStatus =
         cd.change().getStatus() == Change.Status.MERGED
@@ -1095,8 +1085,8 @@
     return result;
   }
 
-  private Collection<ChangeMessageInfo> messages(
-      ChangeControl ctl, ChangeData cd, Map<PatchSet.Id, PatchSet> map) throws OrmException {
+  private Collection<ChangeMessageInfo> messages(ChangeData cd, Map<PatchSet.Id, PatchSet> map)
+      throws OrmException {
     List<ChangeMessage> messages = cmUtil.byChange(db.get(), cd.notes());
     if (messages.isEmpty()) {
       return Collections.emptyList();
@@ -1106,7 +1096,7 @@
     for (ChangeMessage message : messages) {
       PatchSet.Id patchNum = message.getPatchSetId();
       PatchSet ps = patchNum != null ? map.get(patchNum) : null;
-      if (patchNum == null || ctl.isPatchVisible(ps, db.get())) {
+      if (patchNum == null || cd.changeControl().isPatchVisible(ps, db.get())) {
         ChangeMessageInfo cmi = new ChangeMessageInfo();
         cmi.id = message.getKey().get();
         cmi.author = accountLoader.get(message.getAuthor());
@@ -1124,8 +1114,8 @@
     return result;
   }
 
-  private Collection<AccountInfo> removableReviewers(ChangeControl ctl, ChangeInfo out)
-      throws PermissionBackendException, NoSuchChangeException {
+  private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
+      throws PermissionBackendException, NoSuchChangeException, OrmException {
     // Although this is called removableReviewers, this method also determines
     // which CCs are removable.
     //
@@ -1147,7 +1137,7 @@
         Account.Id id = new Account.Id(ai._accountId);
 
         if (removeReviewerControl.testRemoveReviewer(
-            ctl.getNotes(), ctl.getUser(), id, MoreObjects.firstNonNull(ai.value, 0))) {
+            cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) {
           removable.add(id);
         } else {
           fixed.add(id);
@@ -1164,7 +1154,7 @@
       for (AccountInfo ai : ccs) {
         if (ai._accountId != null) {
           Account.Id id = new Account.Id(ai._accountId);
-          if (removeReviewerControl.testRemoveReviewer(ctl.getNotes(), ctl.getUser(), id, 0)) {
+          if (removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) {
             removable.add(id);
           }
         }
@@ -1208,9 +1198,9 @@
   }
 
   @Nullable
-  private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
+  private Repository openRepoIfNecessary(Project.NameKey project) throws IOException {
     if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
-      return repoManager.openRepository(ctl.getProject().getNameKey());
+      return repoManager.openRepository(project);
     }
     return null;
   }
@@ -1221,14 +1211,13 @@
   }
 
   private Map<String, RevisionInfo> revisions(
-      ChangeControl ctl,
       ChangeData cd,
       Map<PatchSet.Id, PatchSet> map,
       Optional<PatchSet.Id> limitToPsId,
       ChangeInfo changeInfo)
       throws PatchListNotAvailableException, GpgException, OrmException, IOException {
     Map<String, RevisionInfo> res = new LinkedHashMap<>();
-    try (Repository repo = openRepoIfNecessary(ctl);
+    try (Repository repo = openRepoIfNecessary(cd.project());
         RevWalk rw = newRevWalk(repo)) {
       for (PatchSet in : map.values()) {
         PatchSet.Id id = in.getId();
@@ -1238,10 +1227,10 @@
         } else if (limitToPsId.isPresent()) {
           want = id.equals(limitToPsId.get());
         } else {
-          want = id.equals(ctl.getChange().currentPatchSetId());
+          want = id.equals(cd.change().currentPatchSetId());
         }
-        if (want && ctl.isPatchVisible(in, db.get())) {
-          res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo, rw, false, changeInfo));
+        if (want && cd.changeControl().isPatchVisible(in, db.get())) {
+          res.put(in.getRevision().get(), toRevisionInfo(cd, in, repo, rw, false, changeInfo));
         }
       }
       return res;
@@ -1275,20 +1264,18 @@
     return map;
   }
 
-  public RevisionInfo getRevisionInfo(ChangeControl ctl, PatchSet in)
+  public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
       throws PatchListNotAvailableException, GpgException, OrmException, IOException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    try (Repository repo = openRepoIfNecessary(ctl);
+    try (Repository repo = openRepoIfNecessary(cd.project());
         RevWalk rw = newRevWalk(repo)) {
-      RevisionInfo rev =
-          toRevisionInfo(ctl, changeDataFactory.create(db.get(), ctl), in, repo, rw, true, null);
+      RevisionInfo rev = toRevisionInfo(cd, in, repo, rw, true, null);
       accountLoader.fill();
       return rev;
     }
   }
 
   private RevisionInfo toRevisionInfo(
-      ChangeControl ctl,
       ChangeData cd,
       PatchSet in,
       @Nullable Repository repo,
@@ -1296,7 +1283,7 @@
       boolean fillCommit,
       @Nullable ChangeInfo changeInfo)
       throws PatchListNotAvailableException, GpgException, OrmException, IOException {
-    Change c = ctl.getChange();
+    Change c = cd.change();
     RevisionInfo out = new RevisionInfo();
     out.isCurrent = in.getId().equals(c.currentPatchSetId());
     out._number = in.getId().get();
@@ -1304,7 +1291,7 @@
     out.created = in.getCreatedOn();
     out.uploader = accountLoader.get(in.getUploader());
     out.draft = in.isDraft() ? true : null;
-    out.fetch = makeFetchMap(ctl, in);
+    out.fetch = makeFetchMap(cd, in);
     out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
     out.description = in.getDescription();
 
@@ -1321,7 +1308,7 @@
         out.commit = toCommit(project, rw, commit, has(WEB_LINKS), fillCommit);
       }
       if (addFooters) {
-        Ref ref = repo.exactRef(ctl.getChange().getDest().get());
+        Ref ref = repo.exactRef(cd.change().getDest().get());
         RevCommit mergeTip = null;
         if (ref != null) {
           mergeTip = rw.parseCommit(ref.getObjectId());
@@ -1331,7 +1318,7 @@
             mergeUtilFactory
                 .create(projectCache.get(project))
                 .createCommitMessageOnSubmit(
-                    commit, mergeTip, ctl.getNotes(), ctl.getUser(), in.getId());
+                    commit, mergeTip, cd.notes(), userProvider.get(), in.getId());
       }
     }
 
@@ -1348,7 +1335,7 @@
       actionJson.addRevisionActions(
           changeInfo,
           out,
-          new RevisionResource(changeResourceFactory.create(ctl.getNotes(), ctl.getUser()), in));
+          new RevisionResource(changeResourceFactory.create(cd.notes(), userProvider.get()), in));
     }
 
     if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
@@ -1396,7 +1383,7 @@
     return info;
   }
 
-  private Map<String, FetchInfo> makeFetchMap(ChangeControl ctl, PatchSet in) throws OrmException {
+  private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in) throws OrmException {
     Map<String, FetchInfo> r = new LinkedHashMap<>();
 
     for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
@@ -1407,11 +1394,12 @@
         continue;
       }
 
-      if (!scheme.isAuthSupported() && !ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
+      if (!scheme.isAuthSupported()
+          && !cd.changeControl().forUser(anonymous).isPatchVisible(in, db.get())) {
         continue;
       }
 
-      String projectName = ctl.getProject().getNameKey().get();
+      String projectName = cd.project().get();
       String url = scheme.getUrl(projectName);
       String refName = in.getRefName();
       FetchInfo fetchInfo = new FetchInfo(url, refName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
index e9b0af2..42b56b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
@@ -23,6 +23,18 @@
 import java.util.regex.Pattern;
 
 public class HashtagsUtil {
+  public static class InvalidHashtagException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    static InvalidHashtagException hashtagsMayNotContainCommas() {
+      return new InvalidHashtagException("hashtags may not contain commas");
+    }
+
+    InvalidHashtagException(String message) {
+      super(message);
+    }
+  }
+
   private static final CharMatcher LEADER = CharMatcher.whitespace().or(CharMatcher.is('#'));
   private static final String PATTERN = "(?:\\s|\\A)#[\\p{L}[0-9]-_]+";
 
@@ -43,14 +55,14 @@
     return result;
   }
 
-  static Set<String> extractTags(Set<String> input) throws IllegalArgumentException {
+  static Set<String> extractTags(Set<String> input) throws InvalidHashtagException {
     if (input == null) {
       return Collections.emptySet();
     }
     HashSet<String> result = new HashSet<>();
     for (String hashtag : input) {
       if (hashtag.contains(",")) {
-        throw new IllegalArgumentException("Hashtags may not contain commas");
+        throw InvalidHashtagException.hashtagsMayNotContainCommas();
       }
       hashtag = cleanupHashtag(hashtag);
       if (!hashtag.isEmpty()) {
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 929529d..8e0d9d9 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
@@ -26,9 +26,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ChangeUtil;
-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.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -64,38 +61,32 @@
   private boolean otherBranches;
 
   private final GitRepositoryManager gitManager;
-  private final AccountCache accountCache;
-  private final Accounts accounts;
-  private final Emails emails;
   private final ProjectCache projectCache;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeData.Factory changeDataFactory;
   private final Provider<ReviewDb> db;
   private final ChangeIndexer indexer;
   private final MergeabilityCache cache;
+  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   @Inject
   Mergeable(
       GitRepositoryManager gitManager,
-      AccountCache accountCache,
-      Accounts accounts,
-      Emails emails,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
       ChangeData.Factory changeDataFactory,
       Provider<ReviewDb> db,
       ChangeIndexer indexer,
-      MergeabilityCache cache) {
+      MergeabilityCache cache,
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
     this.gitManager = gitManager;
-    this.accountCache = accountCache;
-    this.accounts = accounts;
-    this.emails = emails;
     this.projectCache = projectCache;
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeDataFactory = changeDataFactory;
     this.db = db;
     this.indexer = indexer;
     this.cache = cache;
+    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
   }
 
   public void setOtherBranches(boolean otherBranches) {
@@ -152,9 +143,7 @@
 
   private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet) throws OrmException {
     SubmitTypeRecord rec =
-        new SubmitRuleEvaluator(accountCache, accounts, emails, cd)
-            .setPatchSet(patchSet)
-            .getSubmitType();
+        submitRuleEvaluatorFactory.create(cd).setPatchSet(patchSet).getSubmitType();
     if (rec.status != SubmitTypeRecord.Status.OK) {
       throw new OrmException("Submit type rule failed: " + rec);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
index 5fb3ef6..9da993b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
@@ -14,17 +14,15 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -35,12 +33,10 @@
 
   public static class Input {}
 
-  private final Provider<IdentifiedUser> self;
   private final StarredChangesUtil stars;
 
   @Inject
-  Mute(Provider<IdentifiedUser> self, StarredChangesUtil stars) {
-    this.self = self;
+  Mute(StarredChangesUtil stars) {
     this.stars = stars;
   }
 
@@ -49,32 +45,33 @@
     return new UiAction.Description()
         .setLabel("Mute")
         .setTitle("Mute the change to unhighlight it in the dashboard")
-        .setVisible(!rsrc.isUserOwner() && isMuteable(rsrc.getChange()));
+        .setVisible(isMuteable(rsrc));
   }
 
   @Override
   public Response<String> apply(ChangeResource rsrc, Input input)
       throws RestApiException, OrmException, IllegalLabelException {
-    if (rsrc.isUserOwner() || isMuted(rsrc.getChange())) {
-      // early exit for own changes and already muted changes
-      return Response.ok("");
+    if (rsrc.isUserOwner()) {
+      throw new BadRequestException("cannot mute own change");
     }
-    stars.mute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange());
+    if (!isMuted(rsrc)) {
+      stars.mute(rsrc);
+    }
     return Response.ok("");
   }
 
-  private boolean isMuted(Change change) {
+  private boolean isMuted(ChangeResource rsrc) {
     try {
-      return stars.isMutedBy(change, self.get().getAccountId());
+      return stars.isMuted(rsrc);
     } catch (OrmException e) {
       log.error("failed to check muted star", e);
     }
     return false;
   }
 
-  private boolean isMuteable(Change change) {
+  private boolean isMuteable(ChangeResource rsrc) {
     try {
-      return !isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId());
+      return !rsrc.isUserOwner() && !isMuted(rsrc) && !stars.isIgnored(rsrc);
     } catch (OrmException e) {
       log.error("failed to check ignored star", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 9b1a5dc..4b2f5b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -27,10 +27,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.permissions.LabelPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -49,30 +46,24 @@
   private final Provider<ReviewDb> db;
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
-  private final AccountCache accountCache;
-  private final Accounts accounts;
-  private final Emails emails;
   private final ApprovalsUtil approvalsUtil;
   private final AccountLoader.Factory accountLoaderFactory;
+  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   @Inject
   ReviewerJson(
       Provider<ReviewDb> db,
       PermissionBackend permissionBackend,
       ChangeData.Factory changeDataFactory,
-      AccountCache accountCache,
-      Accounts accounts,
-      Emails emails,
       ApprovalsUtil approvalsUtil,
-      AccountLoader.Factory accountLoaderFactory) {
+      AccountLoader.Factory accountLoaderFactory,
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
     this.db = db;
     this.permissionBackend = permissionBackend;
     this.changeDataFactory = changeDataFactory;
-    this.accountCache = accountCache;
-    this.accounts = accounts;
-    this.emails = emails;
     this.approvalsUtil = approvalsUtil;
     this.accountLoaderFactory = accountLoaderFactory;
+    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
   }
 
   public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
@@ -133,7 +124,8 @@
     PatchSet ps = cd.currentPatchSet();
     if (ps != null) {
       for (SubmitRecord rec :
-          new SubmitRuleEvaluator(accountCache, accounts, emails, cd)
+          submitRuleEvaluatorFactory
+              .create(cd)
               .setFastEvalLabels(true)
               .setAllowDraft(true)
               .evaluate()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
index 724598a..1f17dd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.change.HashtagsUtil.InvalidHashtagException;
 import com.google.gerrit.server.extensions.events.HashtagsEdited;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -99,31 +100,31 @@
     ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
     ChangeNotes notes = update.getNotes().load();
 
-    Set<String> existingHashtags = notes.getHashtags();
-    Set<String> updated = new HashSet<>();
-    toAdd = new HashSet<>(extractTags(input.add));
-    toRemove = new HashSet<>(extractTags(input.remove));
-
     try {
+      Set<String> existingHashtags = notes.getHashtags();
+      Set<String> updated = new HashSet<>();
+      toAdd = new HashSet<>(extractTags(input.add));
+      toRemove = new HashSet<>(extractTags(input.remove));
+
       for (HashtagValidationListener validator : validationListeners) {
         validator.validateHashtags(update.getChange(), toAdd, toRemove);
       }
-    } catch (ValidationException e) {
+
+      updated.addAll(existingHashtags);
+      toAdd.removeAll(existingHashtags);
+      toRemove.retainAll(existingHashtags);
+      if (updated()) {
+        updated.addAll(toAdd);
+        updated.removeAll(toRemove);
+        update.setHashtags(updated);
+        addMessage(ctx, update);
+      }
+
+      updatedHashtags = ImmutableSortedSet.copyOf(updated);
+      return true;
+    } catch (ValidationException | InvalidHashtagException e) {
       throw new BadRequestException(e.getMessage());
     }
-
-    updated.addAll(existingHashtags);
-    toAdd.removeAll(existingHashtags);
-    toRemove.retainAll(existingHashtags);
-    if (updated()) {
-      updated.addAll(toAdd);
-      updated.removeAll(toRemove);
-      update.setHashtags(updated);
-      addMessage(ctx, update);
-    }
-
-    updatedHashtags = ImmutableSortedSet.copyOf(updated);
-    return true;
   }
 
   private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
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 bb4a357..fe21858 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.ReviewersUtil;
 import com.google.gerrit.server.ReviewersUtil.VisibilityControl;
-import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.RefPermission;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index 47fb513..2ed80718 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewersUtil;
-import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
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 ad716e6..371c796 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
@@ -24,10 +24,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.RulesCache;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -42,10 +39,8 @@
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
-  private final AccountCache accountCache;
   private final AccountLoader.Factory accountInfoFactory;
-  private final Accounts accounts;
-  private final Emails emails;
+  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   @Option(name = "--filters", usage = "impact of filters in parent projects")
   private Filters filters = Filters.RUN;
@@ -55,17 +50,13 @@
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RulesCache rules,
-      AccountCache accountCache,
       AccountLoader.Factory infoFactory,
-      Accounts accounts,
-      Emails emails) {
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
-    this.accountCache = accountCache;
     this.accountInfoFactory = infoFactory;
-    this.accounts = accounts;
-    this.emails = emails;
+    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
   }
 
   @Override
@@ -79,10 +70,7 @@
     }
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator =
-        new SubmitRuleEvaluator(
-            accountCache,
-            accounts,
-            emails,
+        submitRuleEvaluatorFactory.create(
             changeDataFactory.create(db.get(), rsrc.getChangeResource()));
 
     List<SubmitRecord> records =
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 48b657e..0dd1019 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
@@ -25,9 +25,6 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.RulesCache;
-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.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -37,11 +34,9 @@
 
 public class TestSubmitType implements RestModifyView<RevisionResource, TestSubmitRuleInput> {
   private final Provider<ReviewDb> db;
-  private final AccountCache accountCache;
-  private final Accounts accounts;
-  private final Emails emails;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
+  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   @Option(name = "--filters", usage = "impact of filters in parent projects")
   private Filters filters = Filters.RUN;
@@ -49,17 +44,13 @@
   @Inject
   TestSubmitType(
       Provider<ReviewDb> db,
-      AccountCache accountCache,
-      Accounts accounts,
-      Emails emails,
       ChangeData.Factory changeDataFactory,
-      RulesCache rules) {
+      RulesCache rules,
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
     this.db = db;
-    this.accountCache = accountCache;
-    this.accounts = accounts;
-    this.emails = emails;
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
+    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
   }
 
   @Override
@@ -73,10 +64,7 @@
     }
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator =
-        new SubmitRuleEvaluator(
-            accountCache,
-            accounts,
-            emails,
+        submitRuleEvaluatorFactory.create(
             changeDataFactory.create(db.get(), rsrc.getChangeResource()));
 
     SubmitTypeRecord rec =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java
index a10e754..2bad16c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.StarredChangesUtil;
@@ -50,7 +49,7 @@
 
   @Override
   public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, OrmException, IllegalLabelException {
+      throws OrmException, IllegalLabelException {
     if (isIgnored(rsrc)) {
       stars.unignore(rsrc);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
index 586f3e6..16d6d88 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
@@ -15,16 +15,12 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -36,12 +32,10 @@
 
   public static class Input {}
 
-  private final Provider<IdentifiedUser> self;
   private final StarredChangesUtil stars;
 
   @Inject
-  Unmute(Provider<IdentifiedUser> self, StarredChangesUtil stars) {
-    this.self = self;
+  Unmute(StarredChangesUtil stars) {
     this.stars = stars;
   }
 
@@ -50,35 +44,24 @@
     return new UiAction.Description()
         .setLabel("Unmute")
         .setTitle("Unmute the change")
-        .setVisible(!rsrc.isUserOwner() && isUnMuteable(rsrc.getChange()));
+        .setVisible(isMuted(rsrc));
   }
 
   @Override
   public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, OrmException, IllegalLabelException {
-    if (rsrc.isUserOwner() || !isMuted(rsrc.getChange())) {
-      // early exit for own changes and not muted changes
-      return Response.ok("");
+      throws OrmException, IllegalLabelException {
+    if (isMuted(rsrc)) {
+      stars.unmute(rsrc);
     }
-    stars.unmute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange());
     return Response.ok("");
   }
 
-  private boolean isMuted(Change change) {
+  private boolean isMuted(ChangeResource rsrc) {
     try {
-      return stars.isMutedBy(change, self.get().getAccountId());
+      return stars.isMuted(rsrc);
     } catch (OrmException e) {
       log.error("failed to check muted star", e);
     }
     return false;
   }
-
-  private boolean isUnMuteable(Change change) {
-    try {
-      return isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId());
-    } catch (OrmException e) {
-      log.error("failed to check ignored star", e);
-    }
-    return false;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 08abfbb..8751787 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
+import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.CloneCommand;
 import com.google.gerrit.extensions.config.DownloadCommand;
@@ -83,7 +84,6 @@
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.ChangeUserName;
@@ -166,6 +166,7 @@
 import com.google.gerrit.server.project.ProjectNode;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
@@ -264,6 +265,7 @@
     bind(PermissionCollection.Factory.class);
     bind(AccountVisibility.class).toProvider(AccountVisibilityProvider.class).in(SINGLETON);
     factory(ProjectOwnerGroupsProvider.Factory.class);
+    factory(SubmitRuleEvaluator.Factory.class);
 
     bind(AuthBackend.class).to(UniversalAuthBackend.class).in(SINGLETON);
     DynamicSet.setOf(binder(), AuthBackend.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index 1afcc33..90e2838 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.extensions.client.UiType;
+import com.google.gerrit.extensions.common.AccountsInfo;
 import com.google.gerrit.extensions.common.AuthInfo;
 import com.google.gerrit.extensions.common.ChangeConfigInfo;
 import com.google.gerrit.extensions.common.DownloadInfo;
@@ -41,6 +42,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.server.EnableSignedPush;
+import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.change.AllowedFormats;
@@ -69,6 +71,7 @@
   private static final String KEY_TOKEN = "token";
 
   private final Config config;
+  private final AccountVisibilityProvider accountVisibilityProvider;
   private final AuthConfig authConfig;
   private final Realm realm;
   private final DynamicMap<DownloadScheme> downloadSchemes;
@@ -92,6 +95,7 @@
   @Inject
   public GetServerInfo(
       @GerritServerConfig Config config,
+      AccountVisibilityProvider accountVisibilityProvider,
       AuthConfig authConfig,
       Realm realm,
       DynamicMap<DownloadScheme> downloadSchemes,
@@ -112,6 +116,7 @@
       ChangeIndexCollection indexes,
       SitePaths sitePaths) {
     this.config = config;
+    this.accountVisibilityProvider = accountVisibilityProvider;
     this.authConfig = authConfig;
     this.realm = realm;
     this.downloadSchemes = downloadSchemes;
@@ -136,6 +141,7 @@
   @Override
   public ServerInfo apply(ConfigResource rsrc) throws MalformedURLException {
     ServerInfo info = new ServerInfo();
+    info.accounts = getAccountsInfo(accountVisibilityProvider);
     info.auth = getAuthInfo(authConfig, realm);
     info.change = getChangeInfo(config);
     info.download =
@@ -157,6 +163,12 @@
     return info;
   }
 
+  private AccountsInfo getAccountsInfo(AccountVisibilityProvider accountVisibilityProvider) {
+    AccountsInfo info = new AccountsInfo();
+    info.visibility = accountVisibilityProvider.get();
+    return info;
+  }
+
   private AuthInfo getAuthInfo(AuthConfig cfg, Realm realm) {
     AuthInfo info = new AuthInfo();
     info.authType = cfg.getAuthType();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 4f93a1a..7bf5ad5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -37,7 +37,6 @@
     get(CONFIG_KIND, "version").to(GetVersion.class);
     get(CONFIG_KIND, "info").to(GetServerInfo.class);
     post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class);
-    post(CONFIG_KIND, "check.access").to(CheckAccess.class);
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
     get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
index f367e23..be308a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.GpgException;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -88,8 +87,7 @@
   public RevisionInfo revisionInfo(Project.NameKey project, PatchSet ps)
       throws OrmException, PatchListNotAvailableException, GpgException, IOException {
     ChangeData cd = changeDataFactory.create(db.get(), project, ps.getId().getParentKey());
-    ChangeControl ctl = cd.changeControl();
-    return changeJson.getRevisionInfo(ctl, ps);
+    return changeJson.getRevisionInfo(cd, ps);
   }
 
   public AccountInfo accountInfo(Account a) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index 978ef3e..85e5d06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -31,9 +31,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
@@ -100,9 +97,6 @@
     abstract ImmutableSet<String> hashes();
   }
 
-  private final AccountCache accountCache;
-  private final Accounts accounts;
-  private final Emails emails;
   private final ChangeData.Factory changeDataFactory;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<MergeOpRepoManager> repoManagerProvider;
@@ -110,6 +104,7 @@
   private final Config cfg;
   private final Map<QueryKey, List<ChangeData>> queryCache;
   private final Map<Branch.NameKey, Optional<RevCommit>> heads;
+  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   private MergeOpRepoManager orm;
   private boolean closeOrm;
@@ -117,21 +112,17 @@
   @Inject
   MergeSuperSet(
       @GerritServerConfig Config cfg,
-      AccountCache accountCache,
-      Accounts accounts,
-      Emails emails,
       ChangeData.Factory changeDataFactory,
       Provider<InternalChangeQuery> queryProvider,
       Provider<MergeOpRepoManager> repoManagerProvider,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
     this.cfg = cfg;
-    this.accountCache = accountCache;
-    this.accounts = accounts;
-    this.emails = emails;
     this.changeDataFactory = changeDataFactory;
     this.queryProvider = queryProvider;
     this.repoManagerProvider = repoManagerProvider;
     this.permissionBackend = permissionBackend;
+    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
     queryCache = new HashMap<>();
     heads = new HashMap<>();
   }
@@ -181,9 +172,7 @@
     SubmitTypeRecord str =
         ps == cd.currentPatchSet()
             ? cd.submitTypeRecord()
-            : new SubmitRuleEvaluator(accountCache, accounts, emails, cd)
-                .setPatchSet(ps)
-                .getSubmitType();
+            : submitRuleEvaluatorFactory.create(cd).setPatchSet(ps).getSubmitType();
     if (!str.isOk()) {
       logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
similarity index 73%
rename from gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
index c89684c..281e37e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.project;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
@@ -21,9 +21,9 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 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;
@@ -42,35 +41,29 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
-public class CheckAccess implements RestModifyView<ConfigResource, AccessCheckInput> {
-  private final Provider<IdentifiedUser> currentUser;
+public class CheckAccess implements RestModifyView<ProjectResource, AccessCheckInput> {
   private final AccountResolver accountResolver;
   private final Provider<ReviewDb> db;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
 
   @Inject
   CheckAccess(
-      Provider<IdentifiedUser> currentUser,
       AccountResolver resolver,
       Provider<ReviewDb> db,
       IdentifiedUser.GenericFactory userFactory,
-      ProjectCache projectCache,
       PermissionBackend permissionBackend) {
-    this.currentUser = currentUser;
     this.accountResolver = resolver;
     this.db = db;
     this.userFactory = userFactory;
-    this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
   }
 
   @Override
-  public AccessCheckInfo apply(ConfigResource unused, AccessCheckInput input)
+  public AccessCheckInfo apply(ProjectResource rsrc, AccessCheckInput input)
       throws OrmException, PermissionBackendException, RestApiException, IOException,
           ConfigInvalidException {
-    permissionBackend.user(currentUser.get()).check(GlobalPermission.ADMINISTRATE_SERVER);
+    permissionBackend.user(rsrc.getUser()).check(GlobalPermission.ADMINISTRATE_SERVER);
 
     if (input == null) {
       throw new BadRequestException("input is required");
@@ -78,32 +71,23 @@
     if (Strings.isNullOrEmpty(input.account)) {
       throw new BadRequestException("input requires 'account'");
     }
-    if (Strings.isNullOrEmpty(input.project)) {
-      throw new BadRequestException("input requires 'project'");
-    }
 
     Account match = accountResolver.find(db.get(), input.account);
     if (match == null) {
-      throw new BadRequestException(String.format("cannot find account %s", input.account));
+      throw new UnprocessableEntityException(
+          String.format("cannot find account %s", input.account));
     }
 
     AccessCheckInfo info = new AccessCheckInfo();
 
-    Project.NameKey key = new Project.NameKey(input.project);
-    if (projectCache.get(key) == null) {
-      info.message = String.format("project %s does not exist", key);
-      info.status = HttpServletResponse.SC_NOT_FOUND;
-      return info;
-    }
-
     IdentifiedUser user = userFactory.create(match.getId());
     try {
-      permissionBackend.user(user).project(key).check(ProjectPermission.ACCESS);
+      permissionBackend.user(user).project(rsrc.getNameKey()).check(ProjectPermission.ACCESS);
     } catch (AuthException | PermissionBackendException e) {
       info.message =
           String.format(
               "user %s (%s) cannot see project %s",
-              user.getNameEmail(), user.getAccount().getId(), key);
+              user.getNameEmail(), user.getAccount().getId(), rsrc.getName());
       info.status = HttpServletResponse.SC_FORBIDDEN;
       return info;
     }
@@ -112,14 +96,14 @@
       try {
         permissionBackend
             .user(user)
-            .ref(new Branch.NameKey(key, input.ref))
+            .ref(new Branch.NameKey(rsrc.getNameKey(), input.ref))
             .check(RefPermission.READ);
       } catch (AuthException | PermissionBackendException e) {
         info.status = HttpServletResponse.SC_FORBIDDEN;
         info.message =
             String.format(
                 "user %s (%s) cannot see ref %s in project %s",
-                user.getNameEmail(), user.getAccount().getId(), input.ref, key);
+                user.getNameEmail(), user.getAccount().getId(), input.ref, rsrc.getName());
         return info;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index 3ca62e1..70271b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
@@ -32,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.UrlEncoded;
@@ -167,6 +169,14 @@
     return views;
   }
 
+  static DashboardInfo newDashboardInfo(String ref, String path) {
+    DashboardInfo info = new DashboardInfo();
+    info.ref = ref;
+    info.path = path;
+    info.id = Joiner.on(':').join(Url.encode(ref), Url.encode(path));
+    return info;
+  }
+
   static DashboardInfo parse(
       Project definingProject,
       String refName,
@@ -174,7 +184,7 @@
       Config config,
       String project,
       boolean setDefault) {
-    DashboardInfo info = new DashboardInfo(refName, path);
+    DashboardInfo info = newDashboardInfo(refName, path);
     info.project = project;
     info.definingProject = definingProject.getName();
     String query = config.getString("dashboard", null, "title");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
index 926abd5..7296311 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.common.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -29,7 +30,7 @@
 import java.io.IOException;
 
 @Singleton
-class DeleteDashboard implements RestModifyView<DashboardResource, SetDashboard.Input> {
+class DeleteDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
   private final Provider<SetDefaultDashboard> defaultSetter;
 
   @Inject
@@ -38,12 +39,12 @@
   }
 
   @Override
-  public Response<DashboardInfo> apply(DashboardResource resource, SetDashboard.Input input)
+  public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
       throws AuthException, BadRequestException, ResourceConflictException,
           ResourceNotFoundException, MethodNotAllowedException, IOException,
           PermissionBackendException {
     if (resource.isProjectDefault()) {
-      SetDashboard.Input in = new SetDashboard.Input();
+      SetDashboardInput in = new SetDashboardInput();
       in.commitMessage = input != null ? input.commitMessage : null;
       return defaultSetter.get().apply(resource, in);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 1f33bc3..d0753eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -49,6 +49,7 @@
     get(PROJECT_KIND, "access").to(GetAccess.class);
     post(PROJECT_KIND, "access").to(SetAccess.class);
     put(PROJECT_KIND, "access:review").to(CreateAccessChange.class);
+    post(PROJECT_KIND, "check.access").to(CheckAccess.class);
 
     get(PROJECT_KIND, "parent").to(GetParent.class);
     put(PROJECT_KIND, "parent").to(SetParent.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index f205686..ded2bf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -23,6 +24,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.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -46,9 +49,9 @@
   /** @throws AuthException if this user is not allowed to remove this approval. */
   public void checkRemoveReviewer(
       ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
-      throws PermissionBackendException, AuthException, NoSuchChangeException {
+      throws PermissionBackendException, AuthException, NoSuchChangeException, OrmException {
     if (canRemoveReviewerWithoutPermissionCheck(
-        notes, currentUser, approval.getAccountId(), approval.getValue())) {
+        notes.getChange(), currentUser, approval.getAccountId(), approval.getValue())) {
       return;
     }
 
@@ -61,22 +64,22 @@
 
   /** @return true if the user is allowed to remove this reviewer. */
   public boolean testRemoveReviewer(
-      ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
-      throws PermissionBackendException, NoSuchChangeException {
-    if (canRemoveReviewerWithoutPermissionCheck(notes, currentUser, reviewer, value)) {
+      ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
+      throws PermissionBackendException, NoSuchChangeException, OrmException {
+    if (canRemoveReviewerWithoutPermissionCheck(cd.change(), currentUser, reviewer, value)) {
       return true;
     }
     return permissionBackend
         .user(currentUser)
-        .change(notes)
+        .change(cd)
         .database(dbProvider)
         .test(ChangePermission.REMOVE_REVIEWER);
   }
 
   private boolean canRemoveReviewerWithoutPermissionCheck(
-      ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
-      throws NoSuchChangeException {
-    if (!notes.getChange().getStatus().isOpen()) {
+      Change change, CurrentUser currentUser, Account.Id reviewer, int value)
+      throws NoSuchChangeException, OrmException {
+    if (!change.getStatus().isOpen()) {
       return false;
     }
 
@@ -84,14 +87,15 @@
       Account.Id aId = currentUser.getAccountId();
       if (aId.equals(reviewer)) {
         return true; // A user can always remove themselves.
-      } else if (aId.equals(notes.getChange().getOwner()) && 0 <= value) {
+      } else if (aId.equals(change.getOwner()) && 0 <= value) {
         return true; // The change owner may remove any zero or positive score.
       }
     }
 
     // Users with the remove reviewer permission, the branch owner, project
     // owner and site admin can remove anyone
-    ChangeControl changeControl = changeControlFactory.controlFor(notes, currentUser);
+    ChangeControl changeControl =
+        changeControlFactory.controlFor(dbProvider.get(), change, currentUser);
     if (changeControl.getRefControl().isOwner() // branch owner
         || changeControl.getProjectControl().isOwner() // project owner
         || changeControl.getProjectControl().isAdmin()) { // project admin
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
index f216f40..9222322 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
@@ -15,28 +15,22 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.common.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.SetDashboard.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 
 @Singleton
-class SetDashboard implements RestModifyView<DashboardResource, Input> {
-  static class Input {
-    @DefaultInput String id;
-    String commitMessage;
-  }
-
+class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
   private final Provider<SetDefaultDashboard> defaultSetter;
 
   @Inject
@@ -45,7 +39,7 @@
   }
 
   @Override
-  public Response<DashboardInfo> apply(DashboardResource resource, Input input)
+  public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
       throws AuthException, BadRequestException, ResourceConflictException,
           MethodNotAllowedException, ResourceNotFoundException, IOException,
           PermissionBackendException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
index fa03e45..256b6f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -17,6 +17,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.common.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.SetDashboard.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -36,7 +36,7 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Option;
 
-class SetDefaultDashboard implements RestModifyView<DashboardResource, Input> {
+class SetDefaultDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
   private final ProjectCache cache;
   private final MetaDataUpdate.Server updateFactory;
   private final DashboardsCollection dashboards;
@@ -58,11 +58,11 @@
   }
 
   @Override
-  public Response<DashboardInfo> apply(DashboardResource resource, Input input)
+  public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
       throws AuthException, BadRequestException, ResourceConflictException,
           ResourceNotFoundException, IOException, PermissionBackendException {
     if (input == null) {
-      input = new Input(); // Delete would set input to null.
+      input = new SetDashboardInput(); // Delete would set input to null.
     }
     input.id = Strings.emptyToNull(input.id);
 
@@ -119,7 +119,7 @@
     }
   }
 
-  static class CreateDefault implements RestModifyView<ProjectResource, SetDashboard.Input> {
+  static class CreateDefault implements RestModifyView<ProjectResource, SetDashboardInput> {
     private final Provider<SetDefaultDashboard> setDefault;
 
     @Option(name = "--inherited", usage = "set dashboard inherited by children")
@@ -131,7 +131,7 @@
     }
 
     @Override
-    public Response<DashboardInfo> apply(ProjectResource resource, Input input)
+    public Response<DashboardInfo> apply(ProjectResource resource, SetDashboardInput input)
         throws AuthException, BadRequestException, ResourceConflictException,
             ResourceNotFoundException, IOException, PermissionBackendException {
       SetDefaultDashboard set = setDefault.get();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 5ab629a..87d10c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -32,6 +32,8 @@
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
 import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -84,28 +86,31 @@
     }
   }
 
+  public interface Factory {
+    SubmitRuleEvaluator create(ChangeData cd);
+  }
+
   private final AccountCache accountCache;
   private final Accounts accounts;
   private final Emails emails;
   private final ChangeData cd;
-  private final ChangeControl control;
 
   private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.defaults();
   private SubmitRuleOptions opts;
   private PatchSet patchSet;
   private boolean logErrors = true;
   private long reductionsConsumed;
+  private ChangeControl control;
 
   private Term submitRule;
 
-  public SubmitRuleEvaluator(
-      AccountCache accountCache, Accounts accounts, Emails emails, ChangeData cd)
-      throws OrmException {
+  @Inject
+  SubmitRuleEvaluator(
+      AccountCache accountCache, Accounts accounts, Emails emails, @Assisted ChangeData cd) {
     this.accountCache = accountCache;
     this.accounts = accounts;
     this.emails = emails;
     this.cd = cd;
-    this.control = cd.changeControl();
   }
 
   /**
@@ -219,6 +224,12 @@
    */
   public List<SubmitRecord> evaluate() {
     initOptions();
+    try {
+      initChange();
+    } catch (OrmException e) {
+      return ruleError("Error looking up change " + cd.getId(), e);
+    }
+
     Change c = control.getChange();
     if (!opts.allowClosed() && c.getStatus().isClosed()) {
       SubmitRecord rec = new SubmitRecord();
@@ -226,12 +237,6 @@
       return Collections.singletonList(rec);
     }
     if (!opts.allowDraft()) {
-      try {
-        initPatchSet();
-      } catch (OrmException e) {
-        return ruleError(
-            "Error looking up patch set " + control.getChange().currentPatchSetId(), e);
-      }
       if (c.getStatus() == Change.Status.DRAFT || patchSet.isDraft()) {
         return cannotSubmitDraft();
       }
@@ -409,9 +414,9 @@
   public SubmitTypeRecord getSubmitType() {
     initOptions();
     try {
-      initPatchSet();
+      initChange();
     } catch (OrmException e) {
-      return typeError("Error looking up patch set " + control.getChange().currentPatchSetId(), e);
+      return typeError("Error looking up change " + cd.getId(), e);
     }
 
     try {
@@ -679,7 +684,11 @@
     }
   }
 
-  private void initPatchSet() throws OrmException {
+  private void initChange() throws OrmException {
+    if (control == null) {
+      control = cd.changeControl();
+    }
+
     if (patchSet == null) {
       patchSet = cd.currentPatchSet();
       if (patchSet == null) {
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 b750019..2a3ff45 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
@@ -59,9 +59,6 @@
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.StarRef;
-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;
@@ -352,22 +349,19 @@
     ChangeData cd =
         new ChangeData(
             null, 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, project, id, null, null, null);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
 
   // Injected fields.
   private @Nullable final StarredChangesUtil starredChangesUtil;
-  private final AccountCache accountCache;
-  private final Accounts accounts;
   private final AllUsersName allUsersName;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeMessagesUtil cmUtil;
   private final ChangeNotes.Factory notesFactory;
   private final CommentsUtil commentsUtil;
-  private final Emails emails;
   private final GitRepositoryManager repoManager;
   private final IdentifiedUser.GenericFactory userFactory;
   private final MergeUtil.Factory mergeUtilFactory;
@@ -378,6 +372,7 @@
   private final ProjectCache projectCache;
   private final TrackingFooters trackingFooters;
   private final GetPureRevert pureRevert;
+  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   // Required assisted injected fields.
   private final ReviewDb db;
@@ -431,15 +426,12 @@
   @Inject
   private ChangeData(
       @Nullable StarredChangesUtil starredChangesUtil,
-      AccountCache accountCache,
-      Accounts accounts,
       ApprovalsUtil approvalsUtil,
       AllUsersName allUsersName,
       ChangeControl.GenericFactory changeControlFactory,
       ChangeMessagesUtil cmUtil,
       ChangeNotes.Factory notesFactory,
       CommentsUtil commentsUtil,
-      Emails emails,
       GitRepositoryManager repoManager,
       IdentifiedUser.GenericFactory userFactory,
       MergeUtil.Factory mergeUtilFactory,
@@ -450,21 +442,19 @@
       ProjectCache projectCache,
       TrackingFooters trackingFooters,
       GetPureRevert pureRevert,
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
       @Assisted ReviewDb db,
       @Assisted Project.NameKey project,
       @Assisted Change.Id id,
       @Assisted @Nullable Change change,
       @Assisted @Nullable ChangeNotes notes,
       @Assisted @Nullable ChangeControl control) {
-    this.accountCache = accountCache;
-    this.accounts = accounts;
     this.approvalsUtil = approvalsUtil;
     this.allUsersName = allUsersName;
     this.changeControlFactory = changeControlFactory;
     this.cmUtil = cmUtil;
     this.notesFactory = notesFactory;
     this.commentsUtil = commentsUtil;
-    this.emails = emails;
     this.repoManager = repoManager;
     this.userFactory = userFactory;
     this.mergeUtilFactory = mergeUtilFactory;
@@ -476,6 +466,7 @@
     this.starredChangesUtil = starredChangesUtil;
     this.trackingFooters = trackingFooters;
     this.pureRevert = pureRevert;
+    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
 
     // 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
@@ -1032,10 +1023,7 @@
       if (!lazyLoad) {
         return Collections.emptyList();
       }
-      records =
-          new SubmitRuleEvaluator(accountCache, accounts, emails, this)
-              .setOptions(options)
-              .evaluate();
+      records = submitRuleEvaluatorFactory.create(this).setOptions(options).evaluate();
       submitRecords.put(options, records);
     }
     return records;
@@ -1052,8 +1040,7 @@
 
   public SubmitTypeRecord submitTypeRecord() throws OrmException {
     if (submitTypeRecord == null) {
-      submitTypeRecord =
-          new SubmitRuleEvaluator(accountCache, accounts, emails, this).getSubmitType();
+      submitTypeRecord = submitRuleEvaluatorFactory.create(this).getSubmitType();
     }
     return submitTypeRecord;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 7b53a30..1fe982f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -25,9 +25,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
@@ -75,15 +72,13 @@
   }
 
   private final ReviewDb db;
-  private final AccountCache accountCache;
-  private final Accounts accounts;
-  private final Emails emails;
   private final GitRepositoryManager repoManager;
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryProcessor queryProcessor;
   private final EventFactory eventFactory;
   private final TrackingFooters trackingFooters;
   private final CurrentUser user;
+  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
   private boolean includePatchSets;
@@ -102,25 +97,21 @@
   @Inject
   OutputStreamQuery(
       ReviewDb db,
-      AccountCache accountCache,
-      Accounts accounts,
-      Emails emails,
       GitRepositoryManager repoManager,
       ChangeQueryBuilder queryBuilder,
       ChangeQueryProcessor queryProcessor,
       EventFactory eventFactory,
       TrackingFooters trackingFooters,
-      CurrentUser user) {
+      CurrentUser user,
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
     this.db = db;
-    this.accountCache = accountCache;
-    this.accounts = accounts;
-    this.emails = emails;
     this.repoManager = repoManager;
     this.queryBuilder = queryBuilder;
     this.queryProcessor = queryProcessor;
     this.eventFactory = eventFactory;
     this.trackingFooters = trackingFooters;
     this.user = user;
+    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
   }
 
   void setLimit(int n) {
@@ -259,10 +250,7 @@
     if (includeSubmitRecords) {
       eventFactory.addSubmitRecords(
           c,
-          new SubmitRuleEvaluator(accountCache, accounts, emails, d)
-              .setAllowClosed(true)
-              .setAllowDraft(true)
-              .evaluate());
+          submitRuleEvaluatorFactory.create(d).setAllowClosed(true).setAllowDraft(true).evaluate());
     }
 
     if (includeCommitMessage) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index c1fc0ec..b288bd5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
-
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -28,7 +25,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.server.change.ChangeJson;
-import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
@@ -138,15 +134,11 @@
     int cnt = queries.size();
     List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries));
 
-    boolean requireLazyLoad =
-        containsAnyOf(options, ImmutableSet.of(DETAILED_LABELS, LABELS))
-            && !qb.getArgs().getSchema().hasField(ChangeField.STORED_SUBMIT_RECORD_LENIENT);
-
     ChangeJson cjson = json.create(options);
     cjson.setPluginDefinedAttributesFactory(this.imp);
     List<List<ChangeInfo>> res =
         cjson
-            .lazyLoad(requireLazyLoad || containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD))
+            .lazyLoad(containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD))
             .formatQueryResults(results);
 
     for (int n = 0; n < cnt; n++) {