Merge "Give change metadata chips a disabled state"
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++) {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 2d8da15..ae28a05 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -60,6 +60,10 @@
         type: Boolean,
         value: false,
       },
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
       _showGroup: Boolean,
       _showGroupAuditLog: Boolean,
       _showGroupList: Boolean,
@@ -155,7 +159,7 @@
               },
             ],
           };
-          if (this._groupOwner) {
+          if (this._isAdmin || this._groupOwner) {
             linkCopy.subsection.children.push(
                 {
                   name: 'Audit Log',
@@ -242,16 +246,20 @@
 
     _computeGroupName(groupId) {
       if (!groupId) { return ''; }
+      const promises = [];
       this.$.restAPI.getGroupConfig(groupId).then(group => {
         this._groupName = group.name;
         this.reload();
-        this.$.restAPI.getIsGroupOwner(group.name).then(
+        promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+          this._isAdmin = isAdmin;
+        }));
+        promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
             isOwner => {
-              if (isOwner) {
-                this._groupOwner = true;
-                this.reload();
-              }
-            });
+              this._groupOwner = isOwner;
+            }));
+        return Promise.all(promises).then(() => {
+          this.reload();
+        });
       });
     },
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
index 9371b17..9e08381 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -67,9 +67,18 @@
         font-family: var(--font-family-bold);
         text-align: left;
       }
+      .canModify #groupMemberSearchInput,
+      .canModify #saveGroupMember,
+      .canModify .deleteHeader,
+      .canModify .deleteColumn,
+      .canModify #includedGroupSearchInput,
+      .canModify #saveIncludedGroups,
+      .canModify .deleteIncludedHeader,
+      .canModify #saveIncludedGroups {
+        display: none;
+      }
     </style>
-    <style include="gr-form-styles"></style>
-    <main class="gr-form-styles">
+    <main class$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
       <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
         Loading...
       </div>
@@ -84,15 +93,13 @@
                     id="groupMemberSearchInput"
                     text="{{_groupMemberSearch}}"
                     query="[[_queryMembers]]"
-                    placeholder="Name Or Email"
-                    hidden$="[[!_groupOwner]]">
+                    placeholder="Name Or Email">
                 </gr-autocomplete>
               </span>
               <gr-button
                   id="saveGroupMember"
                   on-tap="_handleSavingGroupMember"
-                  disabled="[[!_groupMemberSearch]]"
-                  hidden$="[[!_groupOwner]]">
+                  disabled="[[!_groupMemberSearch]]">
                 Add
               </gr-button>
               <div class="gr-form-styles">
@@ -100,9 +107,7 @@
                   <tr class="headerRow">
                     <th class="nameHeader">Name</th>
                     <th class="emailAddressHeader">Email Address</th>
-                    <th class="deleteHeader" hidden$="[[!_groupOwner]]">
-                      Delete Member
-                    </th>
+                    <th class="deleteHeader">Delete Member</th>
                   </tr>
                   <tbody>
                     <template is="dom-repeat" items="[[_groupMembers]]">
@@ -111,7 +116,7 @@
                           <gr-account-link account="[[item]]"></gr-account-link>
                         </td>
                         <td>[[item.email]]</td>
-                        <td hidden$="[[!_groupOwner]]">
+                        <td class="deleteColumn">
                           <gr-button
                               class="deleteButton"
                               on-tap="_handleDeleteMember">
@@ -133,15 +138,13 @@
                     id="includedGroupSearchInput"
                     text="{{_includedGroupSearch}}"
                     query="[[_queryIncludedGroup]]"
-                    placeholder="Group Name"
-                    hidden$="[[!_groupOwner]]">
+                    placeholder="Group Name">
                 </gr-autocomplete>
               </span>
               <gr-button
                   id="saveIncludedGroups"
                   on-tap="_handleSavingIncludedGroups"
-                  disabled="[[!_includedGroupSearch]]"
-                  hidden$="[[!_groupOwner]]">
+                  disabled="[[!_includedGroupSearch]]">
                 Add
               </gr-button>
               <div class="gr-form-styles">
@@ -149,7 +152,7 @@
                   <tr class="headerRow">
                     <th class="groupNameHeader">Group Name</th>
                     <th class="descriptionHeader">Description</th>
-                    <th class="deleteIncludedHeader" hidden$="[[!_groupOwner]]">
+                    <th class="deleteIncludedHeader">
                       Delete Included Group
                     </th>
                   </tr>
@@ -162,7 +165,7 @@
                           </a>
                         </td>
                         <td>[[item.description]]</td>
-                        <td hidden$="[[!_groupOwner]]">
+                        <td class="deleteColumn">
                           <gr-button
                               class="deleteIncludedGroupButton"
                               on-tap="_handleDeleteIncludedGroup">
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index cde6c66..81bb902 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -48,6 +48,10 @@
         type: Boolean,
         value: false,
       },
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     behaviors: [
@@ -68,17 +72,29 @@
 
       return this.$.restAPI.getGroupConfig(this.groupId).then(
           config => {
+            if (!config.name) { return; }
+
             this._groupName = config.name;
+
+            promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+              this._isAdmin = isAdmin ? true : false;
+            }));
+
             promises.push(this.$.restAPI.getIsGroupOwner(config.name)
-                .then(isOwner => { this._groupOwner = isOwner; }));
+                .then(isOwner => {
+                  this._groupOwner = isOwner ? true : false;
+                }));
+
             promises.push(this.$.restAPI.getGroupMembers(config.name).then(
                 members => {
                   this._groupMembers = members;
                 }));
+
             promises.push(this.$.restAPI.getIncludedGroup(config.name)
                 .then(includedGroup => {
                   this._includedGroups = includedGroup;
                 }));
+
             return Promise.all(promises).then(() => {
               this._loading = false;
             });
@@ -223,5 +239,9 @@
             return groups;
           });
     },
+
+    _computeHideItemClass(owner, admin) {
+      return admin || owner ? '' : 'canModify';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 76a182b..ef89e37 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -152,5 +152,23 @@
         done();
       });
     });
+
+    test('_computeHideItemClass returns string for admin', () => {
+      const admin = true;
+      const owner = false;
+      assert.equal(element._computeHideItemClass(owner, admin), '');
+    });
+
+    test('_computeHideItemClass returns hideItem for admin and owner', () => {
+      const admin = false;
+      const owner = false;
+      assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
+    });
+
+    test('_computeHideItemClass returns string for owner', () => {
+      const admin = false;
+      const owner = true;
+      assert.equal(element._computeHideItemClass(owner, admin), '');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
index 2f3d59a..0449ffa 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -71,13 +71,15 @@
                 <gr-autocomplete
                     id="groupNameInput"
                     text="{{_groupConfig.name}}"
-                    disabled="[[!_groupOwner]]"></gr-autocomplete>
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]"></gr-autocomplete>
               </span>
-              <gr-button
-                  id="inputUpdateNameBtn"
-                  on-tap="_handleSaveName"
-                  disabled="[[_computeButtonDisabled(_groupOwner, _rename)]]">
-                Rename Group</gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    id="inputUpdateNameBtn"
+                    on-tap="_handleSaveName"
+                    disabled="[[!_rename]]">
+                  Rename Group</gr-button>
+              </span>
             </fieldset>
             <h3 class$="[[_computeHeaderClass(_owner)]]">
               Owners
@@ -87,13 +89,15 @@
                 <gr-autocomplete
                     text="{{_groupConfig.owner}}"
                     query="[[_query]]"
-                    disabled$="[[!_groupOwner]]">
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
                 </gr-autocomplete>
               </span>
-              <gr-button
-                  on-tap="_handleSaveOwner"
-                  disabled="[[_computeButtonDisabled(_groupOwner, _owner)]]">
-                Change Owners</gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    on-tap="_handleSaveOwner"
+                    disabled="[[!_owner]]">
+                  Change Owners</gr-button>
+              </span>
             </fieldset>
             <h3 class$="[[_computeHeaderClass(_description)]]">
               Description
@@ -104,14 +108,15 @@
                     class="description"
                     autocomplete="on"
                     bind-value="{{_groupConfig.description}}"
-                    disabled="[[!_groupOwner]]"></iron-autogrow-textarea>
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]"></iron-autogrow-textarea>
               </div>
-              <gr-button
-                  on-tap="_handleSaveDescription"
-                  disabled=
-                      "[[_computeButtonDisabled(_groupOwner, _description)]]">
-                Save Description
-              </gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    on-tap="_handleSaveDescription"
+                    disabled="[[!_description]]">
+                  Save Description
+                </gr-button>
+              </span>
             </fieldset>
             <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
               Group Options
@@ -124,7 +129,7 @@
                 <span class="value">
                   <gr-select
                       bind-value="{{_groupConfig.options.visible_to_all}}">
-                    <select disabled$="[[!_groupOwner]]">
+                    <select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
                       <template is="dom-repeat" items="[[_submitTypes]]">
                         <option value="[[item.value]]">[[item.label]]</option>
                       </template>
@@ -132,12 +137,13 @@
                   </gr-select>
                 </span>
               </section>
-              <gr-button
-                  on-tap="_handleSaveOptions"
-                  disabled=
-                         "[[_computeButtonDisabled(_groupOwner, _options)]]">
-                Save Group Options
-              </gr-button>
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                <gr-button
+                    on-tap="_handleSaveOptions"
+                    disabled="[[!_options]]">
+                  Save Group Options
+                </gr-button>
+              </span>
             </fieldset>
           </fieldset>
         </div>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 55bf3b0..9e0e8c6 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -56,11 +56,6 @@
         type: Boolean,
         value: true,
       },
-      _loggedIn: {
-        type: Boolean,
-        value: false,
-        observer: '_loggedInChanged',
-      },
       /** @type {?} */
       _groupConfig: Object,
       _groupName: Object,
@@ -80,6 +75,10 @@
           return this._getGroupSuggestions.bind(this);
         },
       },
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     observers: [
@@ -98,12 +97,22 @@
 
       return this.$.restAPI.getGroupConfig(this.groupId).then(
           config => {
-            this._groupConfig = config;
             this._groupName = config.name;
-            this.fire('title-change', {title: config.name});
-            this._loading = false;
+
+            this.$.restAPI.getIsAdmin().then(isAdmin => {
+              this._isAdmin = isAdmin ? true : false;
+            });
+
             this.$.restAPI.getIsGroupOwner(config.name)
-                .then(isOwner => { this._groupOwner = isOwner; });
+                .then(isOwner => {
+                  this._groupOwner = isOwner ? true : false;
+                });
+
+            this._groupConfig = config;
+
+            this.fire('title-change', {title: config.name});
+
+            this._loading = false;
           });
     },
 
@@ -111,18 +120,10 @@
       return loading ? 'loading' : '';
     },
 
-    _loggedInChanged(_loggedIn) {
-      if (!_loggedIn) { return; }
-    },
-
     _isLoading() {
       return this._loading || this._loading === undefined;
     },
 
-    _getLoggedIn() {
-      return this.$.restAPI.getLoggedIn();
-    },
-
     _handleSaveName() {
       return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
           .then(config => {
@@ -182,10 +183,6 @@
       this._options = true;
     },
 
-    _computeButtonDisabled(options, option) {
-      return !options || !option;
-    },
-
     _computeHeaderClass(configChanged) {
       return configChanged ? 'edited' : '';
     },
@@ -204,5 +201,9 @@
             return groups;
           });
     },
+
+    _computeGroupDisabled(owner, admin) {
+      return admin || owner ? false : true;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
index 75bd905..3c3b938 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -121,5 +121,29 @@
             done();
           });
     });
+
+    test('_computeGroupDisabled return false for admin', () => {
+      const admin = true;
+      const owner = false;
+      assert.equal(element._computeGroupDisabled(owner, admin), false);
+    });
+
+    test('_computeGroupDisabled return true for admin', () => {
+      const admin = false;
+      const owner = false;
+      assert.equal(element._computeGroupDisabled(owner, admin), true);
+    });
+
+    test('_computeGroupDisabled return false for owner', () => {
+      const admin = false;
+      const owner = true;
+      assert.equal(element._computeGroupDisabled(owner, admin), false);
+    });
+
+    test('_computeGroupDisabled return true for owner', () => {
+      const admin = false;
+      const owner = false;
+      assert.equal(element._computeGroupDisabled(owner, admin), true);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index d5506ad..20fd147 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -16,6 +16,13 @@
 
   Polymer({
     is: 'gr-label-score-row',
+
+    /**
+     * Fired when any label is changed.
+     *
+     * @event labels-changed
+     */
+
     properties: {
       /**
        * @type {{ name: string }}
@@ -96,6 +103,7 @@
       // nothing and then to the new item.
       if (!e.target.selectedItem) { return; }
       this._selectedValueText = e.target.selectedItem.getAttribute('title');
+      this.fire('labels-changed');
     },
 
     _computeAnyPermittedLabelValues(permittedLabels, label) {
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 537bd25..da450e1 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -103,6 +103,8 @@
     });
 
     test('label picker', () => {
+      const labelsChangedHandler = sandbox.stub();
+      element.addEventListener('labels-changed', labelsChangedHandler);
       assert.ok(element.$$('iron-selector'));
       MockInteractions.tap(element.$$(
           'gr-button[value="-1"]'));
@@ -112,6 +114,7 @@
           .textContent.trim(), '-1');
       assert.strictEqual(
           element.$.selectedValueLabel.textContent.trim(), 'bad');
+      assert.isTrue(labelsChangedHandler.called);
     });
 
     test('correct item is selected', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 2f9f78c..24673ff 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -241,6 +241,7 @@
             id="labelScores"
             account="[[_account]]"
             change="[[change]]"
+            on-labels-changed="_handleLabelsChanged"
             permitted-labels=[[permittedLabels]]></gr-label-scores>
       </section>
       <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
@@ -265,7 +266,7 @@
       <section>
         <gr-button
             primary
-            disabled="[[_isState(knownLatestState, 'not-latest')]]"
+            disabled="[[_computeSendButtonDisabled(knownLatestState, _sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged)]]"
             class="action send"
             on-tap="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
         <template is="dom-if" if="[[canBeStarted]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 752f15a..9f4a79a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -34,6 +34,11 @@
     NOT_LATEST: 'not-latest',
   };
 
+  const ButtonLabels = {
+    START_REVIEW: 'Start review',
+    SEND: 'Send',
+  };
+
   // TODO(logan): Remove once the fix for issue 6841 is stable on
   // googlesource.com.
   const START_REVIEW_MESSAGE = 'This change is ready for review.';
@@ -175,6 +180,14 @@
         computed: '_computeCCsEnabled(serverConfig)',
       },
       _savingComments: Boolean,
+      _reviewersMutated: {
+        type: Boolean,
+        value: false,
+      },
+      _labelsChanged: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     FocusTarget,
@@ -268,12 +281,14 @@
 
     _ccsChanged(splices) {
       if (splices && splices.indexSplices) {
+        this._reviewersMutated = true;
         this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC);
       }
     },
 
     _reviewersChanged(splices) {
       if (splices && splices.indexSplices) {
+        this._reviewersMutated = true;
         this._processReviewerChange(splices.indexSplices,
             ReviewerTypes.REVIEWER);
         let key;
@@ -768,6 +783,11 @@
       });
     },
 
+    _handleLabelsChanged() {
+      this._labelsChanged = Object.keys(
+          this.$.labelScores.getLabelValues()).length !== 0;
+    },
+
     _isState(knownLatestState, value) {
       return knownLatestState === value;
     },
@@ -778,7 +798,7 @@
     },
 
     _computeSendButtonLabel(canBeStarted) {
-      return canBeStarted ? 'Start review' : 'Send';
+      return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
     },
 
     _computeCCsEnabled(serverConfig) {
@@ -788,5 +808,17 @@
     _computeSavingLabelClass(savingComments) {
       return savingComments ? 'saving' : '';
     },
+
+    _computeSendButtonDisabled(knownLatestState, buttonLabel, drafts, text,
+        reviewersMutated, labelsChanged) {
+      if (this._isState(knownLatestState, LatestPatchState.NOT_LATEST)) {
+        return true;
+      }
+      if (buttonLabel === ButtonLabels.START_REVIEW) {
+        return false;
+      }
+      return !(drafts.length || text.length || reviewersMutated ||
+          labelsChanged);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 0f90019..9a83259 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -55,6 +55,8 @@
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({}); },
         getAccount() { return Promise.resolve({}); },
+        getChange() { return Promise.resolve({}); },
+        getChangeSuggestedReviewers() { return Promise.resolve([]); },
       });
 
       element = fixture('basic');
@@ -1007,5 +1009,20 @@
         });
       });
     });
+
+    test('_computeSendButtonDisabled', () => {
+      const fn = element._computeSendButtonDisabled.bind(element);
+      assert.isTrue(fn('not-latest'));
+      assert.isFalse(fn('latest', 'Start review'));
+      assert.isTrue(fn('latest', 'Send', [], '', false, false));
+      // Mock nonempty comment draft array.
+      assert.isFalse(fn('latest', 'Send', ['test'], '', false, false));
+      // Mock nonempty change message.
+      assert.isFalse(fn('latest', 'Send', [], 'test', false, false));
+      // Mock reviewers mutated.
+      assert.isFalse(fn('latest', 'Send', [], '', true, false));
+      // Mock labels changed.
+      assert.isFalse(fn('latest', 'Send', [], '', false, true));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index f07daa5..2ef65f4 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -23,6 +23,9 @@
 <link rel="import" href="../gr-reporting/gr-reporting.html">
 
 <dom-module id="gr-router">
+  <template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
   <script src="../../../bower_components/page/page.js"></script>
   <script src="gr-router.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index acc9d4f..3a724bb 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -154,10 +154,6 @@
     is: 'gr-router',
 
     properties: {
-      _restAPI: {
-        type: Object,
-        value: () => document.createElement('gr-rest-api-interface'),
-      },
       _app: {
         type: Object,
         value: app,
@@ -277,8 +273,12 @@
     _normalizeLegacyRouteParams(params) {
       if (!params.changeNum) { return Promise.resolve(); }
 
-      return this._restAPI.getFromProjectLookup(params.changeNum)
+      return this.$.restAPI.getFromProjectLookup(params.changeNum)
           .then(project => {
+            // Do nothing if the lookup request failed. This avoids an infinite
+            // loop of project lookups.
+            if (!project) { return; }
+
             params.project = project;
             this._normalizePatchRangeParams(params);
             this._redirect(this._generateUrl(params));
@@ -370,7 +370,7 @@
      *     (if it resolves).
      */
     _redirectIfNotLoggedIn(data) {
-      return this._restAPI.getLoggedIn().then(loggedIn => {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
           return Promise.resolve();
         } else {
@@ -382,7 +382,7 @@
 
     /**  Page.js middleware that warms the REST API's logged-in cache line. */
     _loadUserMiddleware(ctx, next) {
-      this._restAPI.getLoggedIn().then(() => { next(); });
+      this.$.restAPI.getLoggedIn().then(() => { next(); });
     },
 
     /**
@@ -447,7 +447,8 @@
       this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
           true);
 
-      this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute');
+      this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
+          true);
 
       this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
           '_handleGroupListOffsetRoute', true);
@@ -584,7 +585,7 @@
         this._redirect(newUrl);
         return null;
       }
-      return this._restAPI.getLoggedIn().then(loggedIn => {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
           this._redirect('/dashboard/self');
         } else {
@@ -599,7 +600,7 @@
         return;
       }
 
-      return this._restAPI.getLoggedIn().then(loggedIn => {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
         if (!loggedIn) {
           if (data.params[0].toLowerCase() === 'self') {
             this._redirectToLogin(data.canonicalPath);
@@ -937,7 +938,7 @@
         this._redirect(this._generateUrl(params));
       } else {
         this._setParams(params);
-        this._restAPI.setInProjectLookup(params.changeNum,
+        this.$.restAPI.setInProjectLookup(params.changeNum,
             params.project);
       }
     },
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index a9231b6..c294b22 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -133,6 +133,7 @@
         '_handleGroupListFilterOffsetRoute',
         '_handleGroupListFilterRoute',
         '_handleGroupListOffsetRoute',
+        '_handleGroupMembersRoute',
         '_handleGroupRoute',
         '_handlePluginListFilterOffsetRoute',
         '_handlePluginListFilterRoute',
@@ -153,7 +154,6 @@
         '_handleDefaultRoute',
         '_handleChangeLegacyRoute',
         '_handleDiffLegacyRoute',
-        '_handleGroupMembersRoute',
         '_handleImproperlyEncodedPlusRoute',
         '_handlePassThroughRoute',
         '_handleProjectAccessRoute',
@@ -183,7 +183,7 @@
     });
 
     test('_redirectIfNotLoggedIn while logged in', () => {
-      sandbox.stub(element._restAPI, 'getLoggedIn')
+      sandbox.stub(element.$.restAPI, 'getLoggedIn')
           .returns(Promise.resolve(true));
       const data = {canonicalPath: ''};
       const redirectStub = sandbox.stub(element, '_redirectToLogin');
@@ -193,7 +193,7 @@
     });
 
     test('_redirectIfNotLoggedIn while logged out', () => {
-      sandbox.stub(element._restAPI, 'getLoggedIn')
+      sandbox.stub(element.$.restAPI, 'getLoggedIn')
           .returns(Promise.resolve(false));
       const redirectStub = sandbox.stub(element, '_redirectToLogin');
       const data = {canonicalPath: ''};
@@ -304,34 +304,51 @@
 
       setup(() => {
         projectLookupStub = sandbox
-            .stub(element._restAPI, 'getFromProjectLookup')
-            .returns(Promise.resolve('foo/bar'));
+            .stub(element.$.restAPI, 'getFromProjectLookup');
         sandbox.stub(element, '_generateUrl');
       });
 
       suite('_normalizeLegacyRouteParams', () => {
         let rangeStub;
+        let redirectStub;
 
         setup(() => {
           rangeStub = sandbox.stub(element, '_normalizePatchRangeParams')
               .returns(Promise.resolve());
+          redirectStub = sandbox.stub(element, '_redirect');
         });
 
         test('w/o changeNum', () => {
+          projectLookupStub.returns(Promise.resolve('foo/bar'));
           const params = {};
           return element._normalizeLegacyRouteParams(params).then(() => {
             assert.isFalse(projectLookupStub.called);
             assert.isFalse(rangeStub.called);
             assert.isNotOk(params.project);
+            assert.isFalse(redirectStub.called);
           });
         });
 
         test('w/ changeNum', () => {
+          projectLookupStub.returns(Promise.resolve('foo/bar'));
           const params = {changeNum: 1234};
           return element._normalizeLegacyRouteParams(params).then(() => {
             assert.isTrue(projectLookupStub.called);
             assert.isTrue(rangeStub.called);
             assert.equal(params.project, 'foo/bar');
+            assert.isTrue(redirectStub.calledOnce);
+          });
+        });
+
+        test('halts on project lookup failure', () => {
+          projectLookupStub.returns(Promise.resolve(undefined));
+
+          const params = {changeNum: 1234};
+          return element._normalizeLegacyRouteParams(params).then(() => {
+            assert.isTrue(projectLookupStub.called);
+            assert.isFalse(rangeStub.called);
+            assert.isUndefined(params.project);
+            assert.isFalse(redirectStub.called);
           });
         });
       });
@@ -472,7 +489,7 @@
         });
 
         test('redirects to dahsboard if logged in', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(true));
           const data = {
             canonicalPath: '/', path: '/', querystring: '', hash: '',
@@ -485,7 +502,7 @@
         });
 
         test('redirects to open changes if not logged in', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(false));
           const data = {
             canonicalPath: '/', path: '/', querystring: '', hash: '',
@@ -592,7 +609,7 @@
         });
 
         test('own dahsboard but signed out redirects to login', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(false));
           const data = {canonicalPath: '/dashboard', params: {0: 'seLF'}};
           const result = element._handleDashboardRoute(data);
@@ -605,7 +622,7 @@
         });
 
         test('non-self dahsboard but signed out does not redirect', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(false));
           const data = {canonicalPath: '/dashboard', params: {0: 'foo'}};
           const result = element._handleDashboardRoute(data);
@@ -619,7 +636,7 @@
         });
 
         test('dahsboard while signed in sets params', () => {
-          sandbox.stub(element._restAPI, 'getLoggedIn')
+          sandbox.stub(element.$.restAPI, 'getLoggedIn')
               .returns(Promise.resolve(true));
           const data = {canonicalPath: '/dashboard', params: {0: 'foo'}};
           const result = element._handleDashboardRoute(data);
@@ -1051,7 +1068,7 @@
           setup(() => {
             normalizeRangeStub = sandbox.stub(element,
                 '_normalizePatchRangeParams');
-            sandbox.stub(element._restAPI, 'setInProjectLookup');
+            sandbox.stub(element.$.restAPI, 'setInProjectLookup');
           });
 
           test('needs redirect', () => {
@@ -1103,7 +1120,7 @@
         test('_handleDiffEditRoute', () => {
           const normalizeRangeStub =
               sandbox.stub(element, '_normalizePatchRangeParams');
-          sandbox.stub(element._restAPI, 'setInProjectLookup');
+          sandbox.stub(element.$.restAPI, 'setInProjectLookup');
           const ctx = {
             params: [
               'foo/bar', // 0 Project
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index c93ed0c..43343de 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -49,6 +49,17 @@
           </tr>
         </thead>
         <tbody>
+          <tr>
+            <td>Number</td>
+            <td
+                class="checkboxContainer"
+                on-tap="_handleTargetTap">
+              <input
+                  type="checkbox"
+                  name="number"
+                  checked$="[[showNumber]]">
+            </td>
+          </tr>
           <template is="dom-repeat" items="[[columnNames]]">
             <tr>
               <td>[[item]]</td>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 50a1146..4d87f9e 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -22,6 +22,10 @@
         type: Array,
         notify: true,
       },
+      showNumber: {
+        type: Boolean,
+        notify: true,
+      },
     },
 
     behaviors: [
@@ -53,6 +57,12 @@
         // The target is the checkbox itself.
         checkbox = Polymer.dom(e).rootTarget;
       }
+
+      if (checkbox.name === 'number') {
+        this.showNumber = checkbox.checked;
+        return;
+      }
+
       this.set('displayedColumns',
           this._updateDisplayedColumns(
               this.displayedColumns, checkbox.name, checkbox.checked));
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 61093b9..00ef16f 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -62,15 +62,17 @@
       const rows = element.$$('tbody').querySelectorAll('tr');
       let tds;
 
-      assert.equal(rows.length, element.columnNames.length);
+      // The `+ 1` is for the number column, which isn't included in the change
+      // table behavior's list.
+      assert.equal(rows.length, element.columnNames.length + 1);
       for (let i = 0; i < columns.length; i++) {
-        tds = rows[i].querySelectorAll('td');
+        tds = rows[i + 1].querySelectorAll('td');
         assert.equal(tds[0].textContent, columns[i]);
       }
     });
 
     test('hide item', () => {
-      const checkbox = element.$$('table input');
+      const checkbox = element.$$('table tr:nth-child(2) input');
       const isChecked = checkbox.checked;
       const displayedLength = element.displayedColumns.length;
       assert.isTrue(isChecked);
@@ -78,8 +80,7 @@
       MockInteractions.tap(checkbox);
       flushAsynchronousOperations();
 
-      assert.equal(element.displayedColumns.length,
-          displayedLength - 1);
+      assert.equal(element.displayedColumns.length, displayedLength - 1);
     });
 
     test('show item', () => {
@@ -91,7 +92,7 @@
         'Updated',
       ]);
       flushAsynchronousOperations();
-      const checkbox = element.$$('table input');
+      const checkbox = element.$$('table tr:nth-child(2) input');
       const isChecked = checkbox.checked;
       const displayedLength = element.displayedColumns.length;
       assert.isFalse(isChecked);
@@ -105,9 +106,9 @@
     });
 
     test('_handleTargetTap', () => {
-      const checkbox = element.$$('table input');
+      const checkbox = element.$$('table tr:nth-child(2) input');
       let originalDisplayedColumns = element.displayedColumns;
-      const td = element.$$('table .checkboxContainer');
+      const td = element.$$('table tr:nth-child(2) .checkboxContainer');
       const displayedColumnStub =
           sandbox.stub(element, '_updateDisplayedColumns');
 
@@ -125,6 +126,20 @@
           checkbox.checked));
     });
 
+    test('_handleTargetTap on number', () => {
+      element.showNumber = false;
+      const checkbox = element.$$('table tr:nth-child(1) input');
+      const displayedColumnStub =
+          sandbox.stub(element, '_updateDisplayedColumns');
+
+      MockInteractions.tap(checkbox);
+      assert.isFalse(displayedColumnStub.called);
+      assert.isTrue(element.showNumber);
+
+      MockInteractions.tap(checkbox);
+      assert.isFalse(element.showNumber);
+    });
+
     test('_updateDisplayedColumns', () => {
       let name = 'Subject';
       let checked = false;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index d039a3b..cacccda 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -321,6 +321,7 @@
         </h2>
         <fieldset id="changeTableColumns">
           <gr-change-table-editor
+              show-number="{{_showNumber}}"
               displayed-columns="{{_localChangeTableColumns}}">
           </gr-change-table-editor>
           <gr-button
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index c3332b3..25dd259 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -121,6 +121,8 @@
        * For testing purposes.
        */
       _loadingPromise: Object,
+
+      _showNumber: Boolean,
     },
 
     behaviors: [
@@ -132,7 +134,7 @@
       '_handlePrefsChanged(_localPrefs.*)',
       '_handleDiffPrefsChanged(_diffPrefs.*)',
       '_handleMenuChanged(_localMenu.splices)',
-      '_handleChangeTableChanged(_localChangeTableColumns)',
+      '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
     ],
 
     attached() {
@@ -147,6 +149,7 @@
 
       promises.push(this.$.restAPI.getPreferences().then(prefs => {
         this.prefs = prefs;
+        this._showNumber = !!prefs.legacycid_in_change_table;
         this._copyPrefs('_localPrefs', 'prefs');
         this._cloneMenu();
         this._cloneChangeTableColumns();
@@ -303,6 +306,7 @@
 
     _handleSaveChangeTable() {
       this.set('prefs.change_table', this._localChangeTableColumns);
+      this.set('prefs.legacycid_in_change_table', this._showNumber);
       this._cloneChangeTableColumns();
       return this.$.restAPI.savePreferences(this.prefs).then(() => {
         this._changeTableChanged = false;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index e1182c1..868a9e3 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -386,6 +386,25 @@
       assert.isTrue(element.$.emailEditor.loadData.calledOnce);
     });
 
+    test('_handleSaveChangeTable', () => {
+      let newColumns = ['Owner', 'Project', 'Branch'];
+      element._localChangeTableColumns = newColumns.slice(0);
+      element._showNumber = false;
+      const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns');
+      element._handleSaveChangeTable();
+      assert.isTrue(cloneStub.calledOnce);
+      assert.deepEqual(element.prefs.change_table, newColumns);
+      assert.isNotOk(element.prefs.legacycid_in_change_table);
+
+      newColumns = ['Size'];
+      element._localChangeTableColumns = newColumns;
+      element._showNumber = true;
+      element._handleSaveChangeTable();
+      assert.isTrue(cloneStub.calledTwice);
+      assert.deepEqual(element.prefs.change_table, newColumns);
+      assert.isTrue(element.prefs.legacycid_in_change_table);
+    });
+
     suite('_getFilterDocsLink', () => {
       test('with http: docs base URL', () => {
         const base = 'http://example.com/';
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 210c42f..06a3fcf 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -274,7 +274,7 @@
 
     getGroupConfig(group) {
       const encodeName = encodeURIComponent(group);
-      return this._fetchSharedCacheURL('/groups/' + encodeName + '/detail');
+      return this.fetchJSON(`/groups/${encodeName}/detail`);
     },
 
     /**
@@ -348,8 +348,8 @@
      */
     getIsGroupOwner(groupName) {
       const encodeName = encodeURIComponent(groupName);
-      return this._fetchSharedCacheURL('/groups/?owned&q=' + encodeName)
-          .then(configs => configs.hasOwnProperty(encodeName));
+      return this._fetchSharedCacheURL(`/groups/?owned&q=${encodeName}`)
+          .then(configs => configs.hasOwnProperty(groupName));
     },
 
     getGroupMembers(groupName) {
@@ -1645,7 +1645,7 @@
     setChangeTopic(changeNum, topic) {
       const p = {topic};
       return this.getChangeURLAndSend(changeNum, 'PUT', null, '/topic', p)
-          .then(this.getResponseObject);
+          .then(this.getResponseObject.bind(this));
     },
 
     /**
@@ -1655,7 +1655,7 @@
      */
     setChangeHashtag(changeNum, hashtag) {
       return this.getChangeURLAndSend(changeNum, 'POST', null, '/hashtags',
-          hashtag).then(this.getResponseObject);
+          hashtag).then(this.getResponseObject.bind(this));
     },
 
     deleteAccountHttpPassword() {
@@ -1669,7 +1669,7 @@
      */
     generateAccountHttpPassword() {
       return this.send('PUT', '/accounts/self/password.http', {generate: true})
-          .then(this.getResponseObject);
+          .then(this.getResponseObject.bind(this));
     },
 
     getAccountSSHKeys() {
@@ -1772,7 +1772,7 @@
       const endpoint = `/comments/${commentID}/delete`;
       const payload = {reason};
       return this.getChangeURLAndSend(changeNum, 'POST', patchNum, endpoint,
-          payload).then(this.getResponseObject);
+          payload).then(this.getResponseObject.bind(this));
     },
 
     /**
@@ -1811,7 +1811,13 @@
     getFromProjectLookup(changeNum) {
       const project = this._projectLookup[changeNum];
       if (project) { return Promise.resolve(project); }
-      return this.getChange(changeNum).then(change => {
+
+      const onError = response => {
+        // Fire a page error so that the visual 404 is displayed.
+        this.fire('page-error', {response});
+      };
+
+      return this.getChange(changeNum, onError).then(change => {
         if (!change || !change.project) { return; }
         this.setInProjectLookup(changeNum, change.project);
         return change.project;
@@ -1842,7 +1848,7 @@
 
    /**
     * Alias for _changeBaseURL.then(fetchJSON).
-     * @todo(beckysiegel) clean up comments
+    * @todo(beckysiegel) clean up comments
     * @param {string|number} changeNum
     * @param {string} endpoint
     * @param {?string|number=} opt_patchNum gets passed as null.
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 2058f59..eb4f418 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -865,8 +865,7 @@
       });
 
       test('getChange succeeds, no project', () => {
-        sandbox.stub(element, 'getChange')
-            .returns(Promise.resolve({}));
+        sandbox.stub(element, 'getChange').returns(Promise.resolve());
         return element.getFromProjectLookup().then(val => {
           assert.strictEqual(val, undefined);
           assert.deepEqual(element._projectLookup, {});
@@ -959,5 +958,29 @@
         assert.deepEqual(result, obj);
       });
     });
+
+    test('setChangeTopic', () => {
+      const sendSpy = sandbox.spy(element, 'getChangeURLAndSend');
+      return element.setChangeTopic(123, 'foo-bar').then(() => {
+        assert.isTrue(sendSpy.calledOnce);
+        assert.deepEqual(sendSpy.lastCall.args[4], {topic: 'foo-bar'});
+      });
+    });
+
+    test('setChangeHashtag', () => {
+      const sendSpy = sandbox.spy(element, 'getChangeURLAndSend');
+      return element.setChangeHashtag(123, 'foo-bar').then(() => {
+        assert.isTrue(sendSpy.calledOnce);
+        assert.equal(sendSpy.lastCall.args[4], 'foo-bar');
+      });
+    });
+
+    test('generateAccountHttpPassword', () => {
+      const sendSpy = sandbox.spy(element, 'send');
+      return element.generateAccountHttpPassword().then(() => {
+        assert.isTrue(sendSpy.calledOnce);
+        assert.deepEqual(sendSpy.lastCall.args[2], {generate: true});
+      });
+    });
   });
 </script>