Merge "Add gr-icons to polygerrit"
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 3f6acf6..121fcad 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -124,12 +124,6 @@
 	or invalid value) and votes that are not permitted for the user are
 	silently ignored.
 
---strict-labels::
-	Require ability to vote on all specified labels before reviewing change.
-	If the vote is invalid (invalid label or invalid name), the vote is not
-	permitted for the user, or the vote is on an outdated or closed patch set,
-	return an error instead of silently discarding the vote.
-
 --tag::
 -t::
   Apply a 'TAG' to the change message, votes, and inline comments. The 'TAG'
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index fc43b97..d35772e 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -55,7 +55,7 @@
 [[createdb_mysql]]
 === MySQL
 
-Requirements: MySQL version 5.5 or later.
+Requirements: MySQL version 5.1 or later.
 
 This option is also more complicated than the H2 option. Just as with
 PostgreSQL it's also recommended for larger installations.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 983e5e2..f7028e0 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -483,6 +483,15 @@
 submitted by Rebase Always and Cherry Pick submit strategies as well as
 change being queried with COMMIT_FOOTERS option.
 
+[[merge-super-set-computation]]
+== Merge Super Set Computation
+
+The algorithm to compute the merge super set to detect changes that
+should be submitted together can be customized by implementing
+`com.google.gerrit.server.git.MergeSuperSetComputation`.
+MergeSuperSetComputation is a DynamicItem, so Gerrit may only have one
+implementation.
+
 [[receive-pack]]
 == Receive Pack Initializers
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index ddf7e7f..53ded48 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -79,19 +79,19 @@
 Before doing the release build, the `GERRIT_VERSION` in the `version.bzl`
 file must be updated, e.g. change it from `$version-SNAPSHOT` to `$version`.
 
-In addition the version must be updated in a number of pom.xml files.
+In addition the version must be updated in a number of `*_pom.xml` files.
 
 To do this run the `./tools/version.py` script and provide the new
 version as parameter, e.g.:
 
 ----
-  ./tools/version.py 2.5
+  version=2.15
+  ./tools/version.py $version
 ----
 
 Commit the changes and create a signed release tag on the new commit:
 
 ----
-  version=2.15
   git tag -s -m "v$version" "v$version"
 ----
 
@@ -137,7 +137,7 @@
 configuration] for deploying to Maven Central
 
 * Make sure that the version is updated in the `version.bzl` file and in
-the `pom.xml` files as described in the link:#update-versions[Update
+the `*_pom.xml` files as described in the link:#update-versions[Update
 Versions and Create Release Tag] section.
 
 * Push the WAR to Maven Central:
@@ -192,7 +192,7 @@
 +
 Use this URL for further testing of the artifacts in this repository,
 e.g. to try building a plugin against the plugin API in this repository
-update the version in the `pom.xml` and configure the repository:
+update the version in the `*_pom.xml` and configure the repository:
 +
 ----
   <repositories>
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 50afe40..d9e356d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6693,13 +6693,6 @@
 |`robot_comments`         |optional|
 The robot comments that should be added as a map that maps a file path
 to a list of link:#robot-comment-input[RobotCommentInput] entities.
-|`strict_labels`          |`true` if not set|
-Whether all labels are required to be within the user's permitted ranges
-based on access controls. +
-If `true`, attempting to use a label not granted to the user will fail
-the entire modify operation early. +
-If `false`, the operation will execute anyway, but the proposed labels
-will be modified to be the "best" value allowed by the access controls.
 |`drafts`                 |optional|
 Draft handling that defines how draft comments are handled that are
 already in the database but that were not also described in this
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 7eac992..45c5e34 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -124,6 +124,40 @@
 * `MEMBERS`: include list of direct group members.
 --
 
+==== Find groups that are owned by another group
+
+By setting `ownedBy` and specifying the link:#group-id[\{group-id\}] of another
+group, it is possible to find all the groups for which the owning group is the
+given group.
+
+.Request
+----
+  GET /groups/?ownedBy=7ca042f4d5847936fcb90ca91057673157fd06fc HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "MyProject-Committers": {
+      "id": "9999c971bb4ab872aab759d8c49833ee6b9ff320",
+      "url": "#/admin/groups/uuid-9999c971bb4ab872aab759d8c49833ee6b9ff320",
+      "options": {
+        "visible_to_all": true
+      },
+      "description":"contains all committers for MyProject",
+      "group_id": 551,
+      "owner": "MyProject-Owners",
+      "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc",
+      "created_on": "2013-02-01 09:59:32.126000000"
+    }
+  }
+----
+
 ==== Check if a group is owned by the calling user
 By setting the option `owned` and specifying a group to inspect with
 the option `group`/`g`, it is possible to find out if this group is
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
index e2e29c9..b6eaa22 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -39,9 +39,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
@@ -286,7 +284,7 @@
 
   private static class Receive implements ReceivePackFactory<Context> {
     private final Provider<CurrentUser> userProvider;
-    private final ProjectControl.GenericFactory projectControlFactory;
+    private final ProjectCache projectCache;
     private final AsyncReceiveCommits.Factory factory;
     private final TransferConfig config;
     private final DynamicSet<ReceivePackInitializer> receivePackInitializers;
@@ -297,7 +295,7 @@
     @Inject
     Receive(
         Provider<CurrentUser> userProvider,
-        ProjectControl.GenericFactory projectControlFactory,
+        ProjectCache projectCache,
         AsyncReceiveCommits.Factory factory,
         TransferConfig config,
         DynamicSet<ReceivePackInitializer> receivePackInitializers,
@@ -305,7 +303,7 @@
         ThreadLocalRequestContext threadContext,
         PermissionBackend permissionBackend) {
       this.userProvider = userProvider;
-      this.projectControlFactory = projectControlFactory;
+      this.projectCache = projectCache;
       this.factory = factory;
       this.config = config;
       this.receivePackInitializers = receivePackInitializers;
@@ -333,8 +331,14 @@
         throw new RuntimeException(e);
       }
       try {
-        ProjectControl ctl = projectControlFactory.controlFor(req.project, userProvider.get());
-        AsyncReceiveCommits arc = factory.create(ctl, db, null, ImmutableSetMultimap.of());
+        IdentifiedUser identifiedUser = userProvider.get().asIdentifiedUser();
+        ProjectState projectState = projectCache.checkedGet(req.project);
+        if (projectState == null) {
+          throw new RuntimeException(String.format("project %s not found", req.project));
+        }
+
+        AsyncReceiveCommits arc =
+            factory.create(projectState, identifiedUser, db, null, ImmutableSetMultimap.of());
         ReceivePack rp = arc.getReceivePack();
 
         Capable r = arc.canUpload();
@@ -342,17 +346,17 @@
           throw new ServiceNotAuthorizedException();
         }
 
-        rp.setRefLogIdent(ctl.getUser().asIdentifiedUser().newRefLogIdent());
+        rp.setRefLogIdent(identifiedUser.newRefLogIdent());
         rp.setTimeout(config.getTimeout());
         rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
 
         for (ReceivePackInitializer initializer : receivePackInitializers) {
-          initializer.init(ctl.getProject().getNameKey(), rp);
+          initializer.init(projectState.getNameKey(), rp);
         }
 
         rp.setPostReceiveHook(PostReceiveHookChain.newChain(Lists.newArrayList(postReceiveHooks)));
         return rp;
-      } catch (NoSuchProjectException | IOException e) {
+      } catch (IOException e) {
         throw new RuntimeException(e);
       }
     }
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 baa0a68..eef9d87 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
@@ -1582,7 +1582,6 @@
     // Exact request format made by GWT UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
     ReviewInput in = new ReviewInput();
     in.labels = ImmutableMap.of("Code-Review", (short) 0);
-    in.strictLabels = true;
     in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
     in.message = "comment";
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 305a2b0..2118f29 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -491,6 +491,33 @@
   }
 
   @Test
+  public void getGroupsByOwner() throws Exception {
+    String parent = createGroup("test-parent");
+    List<String> children =
+        Arrays.asList(createGroup("test-child1", parent), createGroup("test-child2", parent));
+
+    // By UUID
+    List<GroupInfo> owned =
+        gApi.groups().list().withOwnedBy(getFromCache(parent).getGroupUUID().get()).get();
+    assertThat(owned.stream().map(g -> g.name).collect(toList()))
+        .containsExactlyElementsIn(children);
+
+    // By name
+    owned = gApi.groups().list().withOwnedBy(parent).get();
+    assertThat(owned.stream().map(g -> g.name).collect(toList()))
+        .containsExactlyElementsIn(children);
+
+    // By group that does not own any others
+    owned = gApi.groups().list().withOwnedBy(owned.get(0).id).get();
+    assertThat(owned).isEmpty();
+
+    // By non-existing group
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("Group Not Found: does-not-exist");
+    gApi.groups().list().withOwnedBy("does-not-exist").get();
+  }
+
+  @Test
   public void onlyVisibleGroupsReturned() throws Exception {
     String newGroupName = name("newGroup");
     GroupInput in = new GroupInput();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 94dcf31..afa3147 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -204,7 +204,7 @@
     ConfigInput input = createTestConfigInput();
     setApiUser(user);
     exception.expect(AuthException.class);
-    exception.expectMessage("restricted to project owner");
+    exception.expectMessage("write config not permitted");
     gApi.projects().name(project.get()).config(input);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 36843a5..9b88e0d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -146,7 +146,6 @@
 
     ReviewInput in = new ReviewInput();
     in.onBehalfOf = user.id.toString();
-    in.strictLabels = true;
     in.label("Not-A-Label", 5);
 
     exception.expect(BadRequestException.class);
@@ -155,23 +154,6 @@
   }
 
   @Test
-  public void voteOnBehalfOfInvalidLabelIgnoredWithoutStrictLabels() throws Exception {
-    allowCodeReviewOnBehalfOf();
-    PushOneCommit.Result r = createChange();
-    RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
-
-    ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id.toString();
-    in.strictLabels = false;
-    in.label("Code-Review", 1);
-    in.label("Not-A-Label", 5);
-
-    revision.review(in);
-
-    assertThat(gApi.changes().id(r.getChangeId()).get().labels).doesNotContainKey("Not-A-Label");
-  }
-
-  @Test
   public void voteOnBehalfOfLabelNotPermitted() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     LabelType verified = Util.verified();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
index 43f046a..4f51e1f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.server.mail.send.EmailHeader;
+import java.net.URI;
 import java.util.Map;
 import org.junit.Test;
 
@@ -31,9 +32,7 @@
     // Check that the custom address was added as Reply-To
     assertThat(sender.getMessages()).hasSize(1);
     Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
-    assertThat(headers.get("Reply-To")).isInstanceOf(EmailHeader.String.class);
-    assertThat(((EmailHeader.String) headers.get("Reply-To")).getString())
-        .isEqualTo("custom@gerritcodereview.com");
+    assertThat(headerString(headers, "Reply-To")).isEqualTo("custom@gerritcodereview.com");
   }
 
   @Test
@@ -42,7 +41,30 @@
     // Check that the user's email was added as Reply-To
     assertThat(sender.getMessages()).hasSize(1);
     Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
-    assertThat(headers.get("Reply-To")).isInstanceOf(EmailHeader.String.class);
-    assertThat(((EmailHeader.String) headers.get("Reply-To")).getString()).contains(user.email);
+    assertThat(headerString(headers, "Reply-To")).contains(user.email);
+  }
+
+  @Test
+  public void outgoingMailHasListHeaders() throws Exception {
+    String changeId = createChangeWithReview(user);
+    // Check that the mail has the expected headers
+    assertThat(sender.getMessages()).hasSize(1);
+    Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
+    String hostname = URI.create(canonicalWebUrl.get()).getHost();
+    String listId = String.format("<gerrit-%s.%s>", project.get(), hostname);
+    String unsubscribeLink = String.format("<%ssettings>", canonicalWebUrl.get());
+    String threadId =
+        String.format(
+            "<gerrit.%s.%s@%s>",
+            gApi.changes().id(changeId).get().created.getTime(), changeId, hostname);
+    assertThat(headerString(headers, "List-Id")).isEqualTo(listId);
+    assertThat(headerString(headers, "List-Unsubscribe")).isEqualTo(unsubscribeLink);
+    assertThat(headerString(headers, "In-Reply-To")).isEqualTo(threadId);
+  }
+
+  private String headerString(Map<String, EmailHeader> headers, String name) {
+    EmailHeader header = headers.get(name);
+    assertThat(header).isInstanceOf(EmailHeader.String.class);
+    return ((EmailHeader.String) header).getString();
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
index 567d9ba..0243ba3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -80,6 +80,7 @@
     private String substring;
     private String suggest;
     private String regex;
+    private String ownedBy;
 
     public List<GroupInfo> get() throws RestApiException {
       Map<String, GroupInfo> map = getAsMap();
@@ -160,6 +161,11 @@
       return this;
     }
 
+    public ListRequest withOwnedBy(String ownedBy) {
+      this.ownedBy = ownedBy;
+      return this;
+    }
+
     public EnumSet<ListGroupsOption> getOptions() {
       return options;
     }
@@ -203,6 +209,10 @@
     public String getSuggest() {
       return suggest;
     }
+
+    public String getOwnedBy() {
+      return ownedBy;
+    }
   }
 
   /**
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
index 113651b..f851d5e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
@@ -60,7 +60,6 @@
 
   private native void init() /*-{
     this.labels = {};
-    this.strict_labels = true;
   }-*/;
 
   public final native void prePost() /*-{
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 329beab..efb8fc5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -34,8 +34,8 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -80,7 +80,7 @@
 public class GitOverHttpServlet extends GitServlet {
   private static final long serialVersionUID = 1L;
 
-  private static final String ATT_CONTROL = ProjectControl.class.getName();
+  private static final String ATT_STATE = ProjectState.class.getName();
   private static final String ATT_ARC = AsyncReceiveCommits.class.getName();
   private static final String ID_CACHE = "adv_bases";
 
@@ -145,18 +145,18 @@
     private final GitRepositoryManager manager;
     private final PermissionBackend permissionBackend;
     private final Provider<CurrentUser> userProvider;
-    private final ProjectControl.GenericFactory projectControlFactory;
+    private final ProjectCache projectCache;
 
     @Inject
     Resolver(
         GitRepositoryManager manager,
         PermissionBackend permissionBackend,
         Provider<CurrentUser> userProvider,
-        ProjectControl.GenericFactory projectControlFactory) {
+        ProjectCache projectCache) {
       this.manager = manager;
       this.permissionBackend = permissionBackend;
       this.userProvider = userProvider;
-      this.projectControlFactory = projectControlFactory;
+      this.projectCache = projectCache;
     }
 
     @Override
@@ -182,13 +182,11 @@
 
       try {
         Project.NameKey nameKey = new Project.NameKey(projectName);
-        ProjectControl pc;
-        try {
-          pc = projectControlFactory.controlFor(nameKey, user);
-        } catch (NoSuchProjectException err) {
-          throw new RepositoryNotFoundException(projectName);
+        ProjectState state = projectCache.checkedGet(nameKey);
+        if (state == null) {
+          throw new RepositoryNotFoundException(nameKey.get());
         }
-        req.setAttribute(ATT_CONTROL, pc);
+        req.setAttribute(ATT_STATE, state);
 
         try {
           permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
@@ -231,9 +229,9 @@
       up.setTimeout(config.getTimeout());
       up.setPreUploadHook(PreUploadHookChain.newChain(Lists.newArrayList(preUploadHooks)));
       up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
-      ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
+      ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
       for (UploadPackInitializer initializer : uploadPackInitializers) {
-        initializer.init(pc.getProject().getNameKey(), up);
+        initializer.init(state.getNameKey(), up);
       }
       return up;
     }
@@ -243,15 +241,18 @@
     private final VisibleRefFilter.Factory refFilterFactory;
     private final UploadValidators.Factory uploadValidatorsFactory;
     private final PermissionBackend permissionBackend;
+    private final Provider<CurrentUser> userProvider;
 
     @Inject
     UploadFilter(
         VisibleRefFilter.Factory refFilterFactory,
         UploadValidators.Factory uploadValidatorsFactory,
-        PermissionBackend permissionBackend) {
+        PermissionBackend permissionBackend,
+        Provider<CurrentUser> userProvider) {
       this.refFilterFactory = refFilterFactory;
       this.uploadValidatorsFactory = uploadValidatorsFactory;
       this.permissionBackend = permissionBackend;
+      this.userProvider = userProvider;
     }
 
     @Override
@@ -259,13 +260,13 @@
         throws IOException, ServletException {
       // The Resolver above already checked READ access for us.
       Repository repo = ServletUtils.getRepository(request);
-      ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL);
+      ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
       UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
 
       try {
         permissionBackend
-            .user(pc.getUser())
-            .project(pc.getProject().getNameKey())
+            .user(userProvider)
+            .project(state.getNameKey())
             .check(ProjectPermission.RUN_UPLOAD_PACK);
       } catch (AuthException e) {
         GitSmartHttpTools.sendError(
@@ -280,10 +281,10 @@
       // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
       // may have been overridden by a proxy server -- we'll try to avoid this.
       UploadValidators uploadValidators =
-          uploadValidatorsFactory.create(pc.getProject(), repo, request.getRemoteHost());
+          uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
       up.setPreUploadHook(
           PreUploadHookChain.newChain(Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
-      up.setAdvertiseRefsHook(refFilterFactory.create(pc.getProjectState(), repo));
+      up.setAdvertiseRefsHook(refFilterFactory.create(state, repo));
 
       next.doFilter(request, response);
     }
@@ -297,23 +298,27 @@
 
   static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
     private final AsyncReceiveCommits.Factory factory;
+    private final Provider<CurrentUser> userProvider;
 
     @Inject
-    ReceiveFactory(AsyncReceiveCommits.Factory factory) {
+    ReceiveFactory(AsyncReceiveCommits.Factory factory, Provider<CurrentUser> userProvider) {
       this.factory = factory;
+      this.userProvider = userProvider;
     }
 
     @Override
     public ReceivePack create(HttpServletRequest req, Repository db)
         throws ServiceNotAuthorizedException {
-      final ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
+      final ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
 
-      if (!(pc.getUser().isIdentifiedUser())) {
+      if (!(userProvider.get().isIdentifiedUser())) {
         // Anonymous users are not permitted to push.
         throw new ServiceNotAuthorizedException();
       }
 
-      AsyncReceiveCommits arc = factory.create(pc, db, null, ImmutableSetMultimap.of());
+      AsyncReceiveCommits arc =
+          factory.create(
+              state, userProvider.get().asIdentifiedUser(), db, null, ImmutableSetMultimap.of());
       ReceivePack rp = arc.getReceivePack();
       req.setAttribute(ATT_ARC, arc);
       return rp;
@@ -331,13 +336,16 @@
   static class ReceiveFilter implements Filter {
     private final Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache;
     private final PermissionBackend permissionBackend;
+    private final Provider<CurrentUser> userProvider;
 
     @Inject
     ReceiveFilter(
         @Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache,
-        PermissionBackend permissionBackend) {
+        PermissionBackend permissionBackend,
+        Provider<CurrentUser> userProvider) {
       this.cache = cache;
       this.permissionBackend = permissionBackend;
+      this.userProvider = userProvider;
     }
 
     @Override
@@ -348,13 +356,12 @@
       AsyncReceiveCommits arc = (AsyncReceiveCommits) request.getAttribute(ATT_ARC);
       ReceivePack rp = arc.getReceivePack();
       rp.getAdvertiseRefsHook().advertiseRefs(rp);
-      ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL);
-      Project.NameKey projectName = pc.getProject().getNameKey();
+      ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
 
       try {
         permissionBackend
-            .user(pc.getUser())
-            .project(pc.getProject().getNameKey())
+            .user(userProvider)
+            .project(state.getNameKey())
             .check(ProjectPermission.RUN_RECEIVE_PACK);
       } catch (AuthException e) {
         GitSmartHttpTools.sendError(
@@ -382,13 +389,13 @@
         return;
       }
 
-      if (!(pc.getUser().isIdentifiedUser())) {
+      if (!(userProvider.get().isIdentifiedUser())) {
         chain.doFilter(request, response);
         return;
       }
 
       AdvertisedObjectsCacheKey cacheKey =
-          AdvertisedObjectsCacheKey.create(pc.getUser().getAccountId(), projectName);
+          AdvertisedObjectsCacheKey.create(userProvider.get().getAccountId(), state.getNameKey());
 
       if (isGet) {
         cache.invalidate(cacheKey);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 1d116b7..6210385 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -134,7 +134,7 @@
 
           @Override
           protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-            toGerrit(req.getRequestURI(), req, rsp);
+            toGerrit(req.getRequestURI().substring(req.getContextPath().length()), req, rsp);
           }
         });
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 129ccf8..62232db 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SetParent;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -58,7 +57,6 @@
   @Inject
   ChangeProjectAccess(
       ProjectAccessFactory.Factory projectAccessFactory,
-      ProjectControl.Factory projectControlFactory,
       ProjectCache projectCache,
       GroupBackend groupBackend,
       MetaDataUpdate.User metaDataUpdateFactory,
@@ -74,19 +72,18 @@
       @Nullable @Assisted("parentProjectName") Project.NameKey parentProjectName,
       @Nullable @Assisted String message) {
     super(
-        projectControlFactory,
         groupBackend,
         metaDataUpdateFactory,
         allProjects,
         setParent,
         user.get(),
-        permissionBackend,
         projectName,
         base,
         sectionList,
         parentProjectName,
         message,
         contributorAgreements,
+        permissionBackend,
         true);
     this.projectAccessFactory = projectAccessFactory;
     this.projectCache = projectCache;
@@ -95,10 +92,7 @@
 
   @Override
   protected ProjectAccess updateProjectConfig(
-      ProjectControl projectControl,
-      ProjectConfig config,
-      MetaDataUpdate md,
-      boolean parentProjectUpdate)
+      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, NoSuchProjectException, ConfigInvalidException,
           PermissionBackendException {
     RevCommit commit = config.commit(md);
@@ -108,7 +102,7 @@
         RefNames.REFS_CONFIG,
         base,
         commit.getId(),
-        projectControl.getUser().asIdentifiedUser().getAccount());
+        user.asIdentifiedUser().getAccount());
 
     projectCache.evict(config.getProject());
     return projectAccessFactory.create(projectName).call();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 8c873a6..98f6b3f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -48,7 +48,7 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -70,7 +70,6 @@
   private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
-  private final ProjectControl.GenericFactory projectControlFactory;
   private final GroupControl.Factory groupControlFactory;
   private final MetaDataUpdate.Server metaDataUpdateFactory;
   private final AllProjectsName allProjectsName;
@@ -84,7 +83,6 @@
       ProjectCache projectCache,
       PermissionBackend permissionBackend,
       Provider<CurrentUser> user,
-      ProjectControl.GenericFactory projectControlFactory,
       GroupControl.Factory groupControlFactory,
       MetaDataUpdate.Server metaDataUpdateFactory,
       AllProjectsName allProjectsName,
@@ -94,7 +92,6 @@
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
     this.user = user;
-    this.projectControlFactory = projectControlFactory;
     this.groupControlFactory = groupControlFactory;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allProjectsName = allProjectsName;
@@ -107,7 +104,7 @@
   public ProjectAccess call()
       throws NoSuchProjectException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    ProjectControl pc = checkProjectControl();
+    ProjectState projectState = checkProjectState();
 
     // Load the current configuration from the repository, ensuring its the most
     // recent version available. If it differs from what was in the project
@@ -120,11 +117,11 @@
         md.setMessage("Update group names\n");
         config.commit(md);
         projectCache.evict(config.getProject());
-        pc = checkProjectControl();
+        projectState = checkProjectState();
       } else if (config.getRevision() != null
-          && !config.getRevision().equals(pc.getProjectState().getConfig().getRevision())) {
+          && !config.getRevision().equals(projectState.getConfig().getRevision())) {
         projectCache.evict(config.getProject());
-        pc = checkProjectControl();
+        projectState = checkProjectState();
       }
     }
 
@@ -133,11 +130,17 @@
     Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>();
     PermissionBackend.ForProject perm = permissionBackend.user(user).project(projectName);
     boolean checkReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
+    boolean canWriteProjectConfig = true;
+    try {
+      perm.check(ProjectPermission.WRITE_CONFIG);
+    } catch (AuthException e) {
+      canWriteProjectConfig = false;
+    }
 
     for (AccessSection section : config.getAccessSections()) {
       String name = section.getName();
       if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-        if (pc.isOwner()) {
+        if (canWriteProjectConfig) {
           local.add(section);
           ownerOf.add(name);
 
@@ -218,11 +221,11 @@
     detail.setLocal(local);
     detail.setOwnerOf(ownerOf);
     detail.setCanUpload(
-        pc.isOwner()
+        canWriteProjectConfig
             || (checkReadConfig && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE)));
-    detail.setConfigVisible(pc.isOwner() || checkReadConfig);
+    detail.setConfigVisible(canWriteProjectConfig || checkReadConfig);
     detail.setGroupInfo(buildGroupInfo(local));
-    detail.setLabelTypes(pc.getProjectState().getLabelTypes());
+    detail.setLabelTypes(projectState.getLabelTypes());
     detail.setFileHistoryLinks(getConfigFileLogLinks(projectName.get()));
     return detail;
   }
@@ -252,15 +255,15 @@
     return Maps.filterEntries(infos, in -> in.getValue() != null);
   }
 
-  private ProjectControl checkProjectControl()
+  private ProjectState checkProjectState()
       throws NoSuchProjectException, IOException, PermissionBackendException {
-    ProjectControl pc = projectControlFactory.controlFor(projectName, user.get());
+    ProjectState state = projectCache.checkedGet(projectName);
     try {
       permissionBackend.user(user).project(projectName).check(ProjectPermission.ACCESS);
     } catch (AuthException e) {
       throw new NoSuchProjectException(projectName);
     }
-    return pc;
+    return state;
   }
 
   private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index e92af7c..5bde72b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.common.ProjectAccessUtil.mergeSections;
 
 import com.google.common.base.MoreObjects;
@@ -38,10 +39,10 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.project.SetParent;
 import com.google.gwtorm.server.OrmException;
@@ -56,44 +57,43 @@
 
 public abstract class ProjectAccessHandler<T> extends Handler<T> {
 
-  private final ProjectControl.Factory projectControlFactory;
   protected final GroupBackend groupBackend;
+  protected final Project.NameKey projectName;
+  protected final ObjectId base;
+  protected final CurrentUser user;
+
   private final MetaDataUpdate.User metaDataUpdateFactory;
   private final AllProjectsName allProjects;
   private final Provider<SetParent> setParent;
   private final ContributorAgreementsChecker contributorAgreements;
-  private final CurrentUser user;
   private final PermissionBackend permissionBackend;
-
-  protected final Project.NameKey projectName;
-  protected final ObjectId base;
-  private List<AccessSection> sectionList;
   private final Project.NameKey parentProjectName;
+
   protected String message;
+
+  private List<AccessSection> sectionList;
   private boolean checkIfOwner;
+  private Boolean canWriteConfig;
 
   protected ProjectAccessHandler(
-      ProjectControl.Factory projectControlFactory,
       GroupBackend groupBackend,
       MetaDataUpdate.User metaDataUpdateFactory,
       AllProjectsName allProjects,
       Provider<SetParent> setParent,
       CurrentUser user,
-      PermissionBackend permissionBackend,
       Project.NameKey projectName,
       ObjectId base,
       List<AccessSection> sectionList,
       Project.NameKey parentProjectName,
       String message,
       ContributorAgreementsChecker contributorAgreements,
+      PermissionBackend permissionBackend,
       boolean checkIfOwner) {
-    this.projectControlFactory = projectControlFactory;
     this.groupBackend = groupBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allProjects = allProjects;
     this.setParent = setParent;
     this.user = user;
-    this.permissionBackend = permissionBackend;
 
     this.projectName = projectName;
     this.base = base;
@@ -101,6 +101,7 @@
     this.parentProjectName = parentProjectName;
     this.message = message;
     this.contributorAgreements = contributorAgreements;
+    this.permissionBackend = permissionBackend;
     this.checkIfOwner = checkIfOwner;
   }
 
@@ -109,10 +110,8 @@
       throws NoSuchProjectException, IOException, ConfigInvalidException, InvalidNameException,
           NoSuchGroupException, OrmException, UpdateParentFailedException,
           PermissionDeniedException, PermissionBackendException {
-    final ProjectControl projectControl = projectControlFactory.controlFor(projectName);
-
     try {
-      contributorAgreements.check(projectName, projectControl.getUser());
+      contributorAgreements.check(projectName, user);
     } catch (AuthException e) {
       throw new PermissionDeniedException(e.getMessage());
     }
@@ -126,7 +125,7 @@
         String name = section.getName();
 
         if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-          if (checkIfOwner && !projectControl.isOwner()) {
+          if (checkIfOwner && !canWriteConfig()) {
             continue;
           }
           replace(config, toDelete, section);
@@ -144,7 +143,7 @@
 
       for (String name : toDelete) {
         if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-          if (!checkIfOwner || projectControl.isOwner()) {
+          if (!checkIfOwner || canWriteConfig()) {
             config.remove(config.getAccessSection(name));
           }
 
@@ -161,8 +160,8 @@
           setParent
               .get()
               .validateParentUpdate(
-                  projectControl.getProject().getNameKey(),
-                  projectControl.getUser().asIdentifiedUser(),
+                  projectName,
+                  user.asIdentifiedUser(),
                   MoreObjects.firstNonNull(parentProjectName, allProjects).get(),
                   checkIfOwner);
         } catch (AuthException e) {
@@ -186,17 +185,14 @@
         md.setMessage("Modify access rules\n");
       }
 
-      return updateProjectConfig(projectControl, config, md, parentProjectUpdate);
+      return updateProjectConfig(config, md, parentProjectUpdate);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(projectName);
     }
   }
 
   protected abstract T updateProjectConfig(
-      ProjectControl projectControl,
-      ProjectConfig config,
-      MetaDataUpdate md,
-      boolean parentProjectUpdate)
+      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, NoSuchProjectException, ConfigInvalidException, OrmException,
           PermissionDeniedException, PermissionBackendException;
 
@@ -229,4 +225,20 @@
       ref.setUUID(group.getUUID());
     }
   }
+
+  /** Provide a local cache for {@code ProjectPermission.WRITE_CONFIG} capability. */
+  private boolean canWriteConfig() throws PermissionBackendException {
+    checkNotNull(user);
+
+    if (canWriteConfig != null) {
+      return canWriteConfig;
+    }
+    try {
+      permissionBackend.user(user).project(projectName).check(ProjectPermission.WRITE_CONFIG);
+      canWriteConfig = true;
+    } catch (AuthException e) {
+      canWriteConfig = false;
+    }
+    return canWriteConfig;
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index a8a120a..e7e0021 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -43,10 +43,10 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SetParent;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
@@ -83,7 +83,6 @@
 
   @Inject
   ReviewProjectAccess(
-      final ProjectControl.Factory projectControlFactory,
       PermissionBackend permissionBackend,
       GroupBackend groupBackend,
       MetaDataUpdate.User metaDataUpdateFactory,
@@ -104,19 +103,18 @@
       @Nullable @Assisted("parentProjectName") Project.NameKey parentProjectName,
       @Nullable @Assisted String message) {
     super(
-        projectControlFactory,
         groupBackend,
         metaDataUpdateFactory,
         allProjects,
         setParent,
         user.get(),
-        permissionBackend,
         projectName,
         base,
         sectionList,
         parentProjectName,
         message,
         contributorAgreements,
+        permissionBackend,
         false);
     this.db = db;
     this.permissionBackend = permissionBackend;
@@ -133,27 +131,16 @@
   @SuppressWarnings("deprecation")
   @Override
   protected Change.Id updateProjectConfig(
-      ProjectControl projectControl,
-      ProjectConfig config,
-      MetaDataUpdate md,
-      boolean parentProjectUpdate)
+      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, OrmException, PermissionDeniedException, PermissionBackendException {
-    PermissionBackend.ForRef metaRef =
-        permissionBackend
-            .user(projectControl.getUser())
-            .project(projectControl.getProject().getNameKey())
-            .ref(RefNames.REFS_CONFIG);
-    try {
-      metaRef.check(RefPermission.READ);
-    } catch (AuthException denied) {
+    PermissionBackend.ForProject perm = permissionBackend.user(user).project(config.getName());
+    if (!check(perm, ProjectPermission.READ_CONFIG)) {
       throw new PermissionDeniedException(RefNames.REFS_CONFIG + " not visible");
     }
-    if (!projectControl.isOwner()) {
-      try {
-        metaRef.check(RefPermission.CREATE_CHANGE);
-      } catch (AuthException denied) {
-        throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG);
-      }
+
+    if (!check(perm, ProjectPermission.WRITE_CONFIG)
+        && !check(perm.ref(RefNames.REFS_CONFIG), RefPermission.CREATE_CHANGE)) {
+      throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG);
     }
 
     md.setInsertChangeId(true);
@@ -169,8 +156,7 @@
         ObjectReader objReader = objInserter.newReader();
         RevWalk rw = new RevWalk(objReader);
         BatchUpdate bu =
-            updateFactory.create(
-                db, config.getProject().getNameKey(), projectControl.getUser(), TimeUtil.nowTs())) {
+            updateFactory.create(db, config.getProject().getNameKey(), user, TimeUtil.nowTs())) {
       bu.setRepository(md.getRepository(), rw, objInserter);
       bu.insertChange(
           changeInserterFactory
@@ -227,4 +213,24 @@
       }
     }
   }
+
+  private boolean check(PermissionBackend.ForRef perm, RefPermission p)
+      throws PermissionBackendException {
+    try {
+      perm.check(p);
+      return true;
+    } catch (AuthException denied) {
+      return false;
+    }
+  }
+
+  private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
+      throws PermissionBackendException {
+    try {
+      perm.check(p);
+      return true;
+    } catch (AuthException denied) {
+      return false;
+    }
+  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 6b5c157..1a5600f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -66,6 +66,7 @@
 import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.events.StreamEventsApiListener;
 import com.google.gerrit.server.git.GarbageCollectionModule;
+import com.google.gerrit.server.git.LocalMergeSuperSetComputation;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
@@ -472,6 +473,7 @@
     if (testSysModule != null) {
       modules.add(testSysModule);
     }
+    modules.add(new LocalMergeSuperSetComputation.Module());
     return cfgInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
index 7e6bf63..e3a1d66 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
@@ -40,7 +40,7 @@
       } else if (url.startsWith("jdbc:mariadb:")) {
         database.set("driver", "org.mariadb.jdbc.Driver");
       } else if (url.startsWith("jdbc:mysql:")) {
-        database.set("driver", "com.mysql.cj.jdbc.Driver");
+        database.set("driver", "com.mysql.jdbc.Driver");
       } else if (url.startsWith("jdbc:postgresql:")) {
         database.set("driver", "org.postgresql.Driver");
       }
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
index 7429cea..a82edb32 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
@@ -13,9 +13,9 @@
 # limitations under the License.
 
 [library "mysqlDriver"]
-  name = MySQL Connector/J 6.0.6
-  url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/6.0.6/mysql-connector-java-6.0.6.jar
-  sha1 = 1d19b184dbc596008cc71c83596f051c3ec4097f
+  name = MySQL Connector/J 5.1.43
+  url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.43/mysql-connector-java-5.1.43.jar
+  sha1 = dee9103eec0d877f3a21c82d4d9e9f4fbd2d6e0a
   remove = mysql-connector-java-.*[.]jar
 
 [library "mariadbDriver"]
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
index 63f7202..d7f6e30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -25,10 +25,10 @@
 import com.google.gerrit.server.args4j.ChangeIdHandler;
 import com.google.gerrit.server.args4j.ObjectIdHandler;
 import com.google.gerrit.server.args4j.PatchSetIdHandler;
-import com.google.gerrit.server.args4j.ProjectControlHandler;
+import com.google.gerrit.server.args4j.ProjectHandler;
 import com.google.gerrit.server.args4j.SocketAddressHandler;
 import com.google.gerrit.server.args4j.TimestampHandler;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
 import com.google.gerrit.util.cli.OptionHandlers;
@@ -51,7 +51,7 @@
     registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
     registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
-    registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
+    registerOptionHandler(ProjectState.class, ProjectHandler.class);
     registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
     registerOptionHandler(Timestamp.class, TimestampHandler.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index ee25d54..2971037 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -111,8 +111,7 @@
   private final AccountQueryBuilder accountQueryBuilder;
   private final Provider<AccountQueryProcessor> queryProvider;
   private final GroupBackend groupBackend;
-  private final GroupMembers.Factory groupMembersFactory;
-  private final Provider<CurrentUser> currentUser;
+  private final GroupMembers groupMembers;
   private final ReviewerRecommender reviewerRecommender;
   private final Metrics metrics;
 
@@ -122,8 +121,7 @@
       AccountQueryBuilder accountQueryBuilder,
       Provider<AccountQueryProcessor> queryProvider,
       GroupBackend groupBackend,
-      GroupMembers.Factory groupMembersFactory,
-      Provider<CurrentUser> currentUser,
+      GroupMembers groupMembers,
       ReviewerRecommender reviewerRecommender,
       Metrics metrics) {
     Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
@@ -131,9 +129,8 @@
     this.accountLoader = accountLoaderFactory.create(fillOptions);
     this.accountQueryBuilder = accountQueryBuilder;
     this.queryProvider = queryProvider;
-    this.currentUser = currentUser;
     this.groupBackend = groupBackend;
-    this.groupMembersFactory = groupMembersFactory;
+    this.groupMembers = groupMembers;
     this.reviewerRecommender = reviewerRecommender;
     this.metrics = metrics;
   }
@@ -303,10 +300,7 @@
     }
 
     try {
-      Set<Account> members =
-          groupMembersFactory
-              .create(currentUser.get())
-              .listAccounts(group.getUUID(), project.getNameKey());
+      Set<Account> members = groupMembers.listAccounts(group.getUUID(), project.getNameKey());
 
       if (members.isEmpty()) {
         return result;
@@ -330,9 +324,7 @@
           return result;
         }
       }
-    } catch (NoSuchGroupException e) {
-      return result;
-    } catch (NoSuchProjectException e) {
+    } catch (NoSuchGroupException | NoSuchProjectException e) {
       return result;
     }
 
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 12bd8ff..7971d30 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
@@ -420,7 +420,7 @@
   public static Set<Integer> getStarredPatchSets(Set<String> labels, String label) {
     return labels
         .stream()
-        .filter(l -> l.startsWith(label))
+        .filter(l -> l.startsWith(label + "/"))
         .filter(l -> Ints.tryParse(l.substring(label.length() + 1)) != null)
         .map(l -> Integer.valueOf(l.substring(label.length() + 1)))
         .collect(toSet());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
index 1666eb1..ffb405c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -34,6 +34,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.List;
+import java.util.Objects;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
@@ -72,6 +73,7 @@
         accountId,
         input
             .stream()
+            .filter(Objects::nonNull)
             .map(w -> ProjectWatchKey.create(new Project.NameKey(w.project), w.filter))
             .collect(toList()));
     accountCache.evict(accountId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index 4dc960d..f1f688c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -21,15 +21,14 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
@@ -37,28 +36,22 @@
 import java.util.Set;
 
 public class GroupMembers {
-  public interface Factory {
-    GroupMembers create(CurrentUser currentUser);
-  }
 
   private final GroupCache groupCache;
   private final GroupControl.Factory groupControlFactory;
   private final AccountCache accountCache;
-  private final ProjectControl.GenericFactory projectControl;
-  private final CurrentUser currentUser;
+  private final ProjectCache projectCache;
 
   @Inject
   GroupMembers(
       GroupCache groupCache,
       GroupControl.Factory groupControlFactory,
       AccountCache accountCache,
-      ProjectControl.GenericFactory projectControl,
-      @Assisted CurrentUser currentUser) {
+      ProjectCache projectCache) {
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
     this.accountCache = accountCache;
-    this.projectControl = projectControl;
-    this.currentUser = currentUser;
+    this.projectCache = projectCache;
   }
 
   public Set<Account> listAccounts(AccountGroup.UUID groupUUID, Project.NameKey project)
@@ -88,11 +81,13 @@
       return Collections.emptySet();
     }
 
-    final Iterable<AccountGroup.UUID> ownerGroups =
-        projectControl.controlFor(project, currentUser).getProjectState().getAllOwners();
+    ProjectState projectState = projectCache.checkedGet(project);
+    if (projectState == null) {
+      throw new NoSuchProjectException(project);
+    }
 
     final HashSet<Account> projectOwners = new HashSet<>();
-    for (AccountGroup.UUID ownerGroup : ownerGroups) {
+    for (AccountGroup.UUID ownerGroup : projectState.getAllOwners()) {
       if (!seen.contains(ownerGroup)) {
         projectOwners.addAll(listAccounts(ownerGroup, project, seen));
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index e1e72ba..f439f7d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.group.QueryGroups;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -119,7 +120,8 @@
 
     for (String project : req.getProjects()) {
       try {
-        list.addProject(projects.parse(tlr, IdString.fromDecoded(project)).getControl());
+        ProjectResource rsrc = projects.parse(tlr, IdString.fromDecoded(project));
+        list.addProject(rsrc.getProjectState());
       } catch (Exception e) {
         throw asRestApiException("Error looking up project " + project, e);
       }
@@ -131,6 +133,10 @@
 
     list.setVisibleToAll(req.getVisibleToAll());
 
+    if (req.getOwnedBy() != null) {
+      list.setOwnedBy(req.getOwnedBy());
+    }
+
     if (req.getUser() != null) {
       try {
         list.setUser(accounts.parse(req.getUser()).getAccountId());
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 0d4afd6..12c4244 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
@@ -79,7 +79,8 @@
     SetDashboardInput input = new SetDashboardInput();
     input.id = id;
     try {
-      set.apply(DashboardResource.projectDefault(project.getControl()), input);
+      set.apply(
+          DashboardResource.projectDefault(project.getProjectState(), project.getUser()), input);
     } catch (Exception e) {
       String msg = String.format("Cannot %s default dashboard", id != null ? "set" : "remove");
       throw asRestApiException(msg, e);
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 9fd4d48..92bf46e 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
@@ -378,7 +378,11 @@
 
   @Override
   public ConfigInfo config(ConfigInput in) throws RestApiException {
-    return putConfig.apply(checkExists(), in);
+    try {
+      return putConfig.apply(checkExists(), in);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list tags", e);
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectHandler.java
similarity index 83%
rename from gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectHandler.java
index 1823527..8959d97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectHandler.java
@@ -22,7 +22,8 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -36,23 +37,23 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class ProjectControlHandler extends OptionHandler<ProjectControl> {
-  private static final Logger log = LoggerFactory.getLogger(ProjectControlHandler.class);
+public class ProjectHandler extends OptionHandler<ProjectState> {
+  private static final Logger log = LoggerFactory.getLogger(ProjectHandler.class);
 
-  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
 
   @Inject
-  public ProjectControlHandler(
-      ProjectControl.GenericFactory projectControlFactory,
+  public ProjectHandler(
+      ProjectCache projectCache,
       PermissionBackend permissionBackend,
       Provider<CurrentUser> user,
       @Assisted final CmdLineParser parser,
       @Assisted final OptionDef option,
-      @Assisted final Setter<ProjectControl> setter) {
+      @Assisted final Setter<ProjectState> setter) {
     super(parser, option, setter);
-    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
     this.user = user;
   }
@@ -77,20 +78,21 @@
     String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
     Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix);
 
-    ProjectControl control;
+    ProjectState state;
     try {
-      control = projectControlFactory.controlFor(nameKey, user.get());
+      state = projectCache.checkedGet(nameKey);
+      if (state == null) {
+        throw new CmdLineException(owner, String.format("project %s not found", nameWithoutSuffix));
+      }
       permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
     } catch (AuthException e) {
       throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage());
-    } catch (NoSuchProjectException e) {
-      throw new CmdLineException(owner, e.getMessage());
     } catch (PermissionBackendException | IOException e) {
       log.warn("Cannot load project " + nameWithoutSuffix, e);
       throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage());
     }
 
-    setter.addValue(control);
+    setter.addValue(state);
     return 1;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index 157928b..bf0fc83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -22,11 +22,10 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -37,18 +36,12 @@
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
   private final ChangeJson.Factory jsonFactory;
-  private final ProjectControl.GenericFactory projectControlFactory;
 
   @Inject
-  Check(
-      PermissionBackend permissionBackend,
-      Provider<CurrentUser> user,
-      ChangeJson.Factory json,
-      ProjectControl.GenericFactory projectControlFactory) {
+  Check(PermissionBackend permissionBackend, Provider<CurrentUser> user, ChangeJson.Factory json) {
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.jsonFactory = json;
-    this.projectControlFactory = projectControlFactory;
   }
 
   @Override
@@ -60,9 +53,9 @@
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
       throws RestApiException, OrmException, PermissionBackendException, NoSuchProjectException,
           IOException {
-    if (!rsrc.isUserOwner()
-        && !projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()).isOwner()) {
-      permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER);
+    PermissionBackend.WithUser perm = permissionBackend.user(user);
+    if (!rsrc.isUserOwner()) {
+      perm.project(rsrc.getProject()).check(ProjectPermission.READ_CONFIG);
     }
     return Response.withMustRevalidate(newChangeJson().fix(input).format(rsrc));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 4f03f37..8b84f2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -49,7 +49,7 @@
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -92,7 +92,7 @@
   private final PatchSetInserter.Factory patchSetInserterFactory;
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeNotes.Factory changeNotesFactory;
-  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ProjectCache projectCache;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final NotifyUtil notifyUtil;
@@ -109,7 +109,7 @@
       PatchSetInserter.Factory patchSetInserterFactory,
       MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory changeNotesFactory,
-      ProjectControl.GenericFactory projectControlFactory,
+      ProjectCache projectCache,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil changeMessagesUtil,
       NotifyUtil notifyUtil) {
@@ -123,7 +123,7 @@
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeNotesFactory = changeNotesFactory;
-    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
     this.approvalsUtil = approvalsUtil;
     this.changeMessagesUtil = changeMessagesUtil;
     this.notifyUtil = notifyUtil;
@@ -197,10 +197,11 @@
       String commitMessage = ChangeIdUtil.insertId(input.message, computedChangeId).trim() + '\n';
 
       CodeReviewCommit cherryPickCommit;
-      ProjectControl projectControl =
-          projectControlFactory.controlFor(dest.getParentKey(), identifiedUser);
+      ProjectState projectState = projectCache.checkedGet(dest.getParentKey());
+      if (projectState == null) {
+        throw new NoSuchProjectException(dest.getParentKey());
+      }
       try {
-        ProjectState projectState = projectControl.getProjectState();
         cherryPickCommit =
             mergeUtilFactory
                 .create(projectState)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 58634a5..0022656 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -233,7 +233,7 @@
       input.drafts = DraftHandling.DELETE;
     }
     if (input.labels != null) {
-      checkLabels(revision, labelTypes, input.strictLabels, input.labels);
+      checkLabels(revision, labelTypes, input.labels);
     }
     if (input.comments != null) {
       cleanUpComments(input.comments);
@@ -443,12 +443,9 @@
     while (itr.hasNext()) {
       Map.Entry<String, Short> ent = itr.next();
       LabelType type = labelTypes.byLabel(ent.getKey());
-      if (type == null && in.strictLabels) {
+      if (type == null) {
         throw new BadRequestException(
             String.format("label \"%s\" is not a configured label", ent.getKey()));
-      } else if (type == null) {
-        itr.remove();
-        continue;
       }
 
       if (!caller.isInternalUser()) {
@@ -479,8 +476,7 @@
         changeResourceFactory.create(rev.getNotes(), reviewer), rev.getPatchSet());
   }
 
-  private void checkLabels(
-      RevisionResource rsrc, LabelTypes labelTypes, boolean strict, Map<String, Short> labels)
+  private void checkLabels(RevisionResource rsrc, LabelTypes labelTypes, Map<String, Short> labels)
       throws BadRequestException, AuthException, PermissionBackendException {
     PermissionBackend.ForChange perm = rsrc.permissions();
     Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator();
@@ -488,12 +484,8 @@
       Map.Entry<String, Short> ent = itr.next();
       LabelType lt = labelTypes.byLabel(ent.getKey());
       if (lt == null) {
-        if (strict) {
-          throw new BadRequestException(
-              String.format("label \"%s\" is not a configured label", ent.getKey()));
-        }
-        itr.remove();
-        continue;
+        throw new BadRequestException(
+            String.format("label \"%s\" is not a configured label", ent.getKey()));
       }
 
       if (ent.getValue() == null || ent.getValue() == 0) {
@@ -503,23 +495,16 @@
       }
 
       if (lt.getValue(ent.getValue()) == null) {
-        if (strict) {
-          throw new BadRequestException(
-              String.format("label \"%s\": %d is not a valid value", ent.getKey(), ent.getValue()));
-        }
-        itr.remove();
-        continue;
+        throw new BadRequestException(
+            String.format("label \"%s\": %d is not a valid value", ent.getKey(), ent.getValue()));
       }
 
       short val = ent.getValue();
       try {
         perm.check(new LabelPermission.WithValue(lt, val));
       } catch (AuthException e) {
-        if (strict) {
-          throw new AuthException(
-              String.format("Applying label \"%s\": %d is restricted", lt.getName(), val));
-        }
-        ent.setValue(perm.squashThenCheck(lt, val));
+        throw new AuthException(
+            String.format("Applying label \"%s\": %d is restricted", lt.getName(), val));
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index f642aa4..3664293 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -91,7 +91,7 @@
   private final PermissionBackend permissionBackend;
 
   private final GroupsCollection groupsCollection;
-  private final GroupMembers.Factory groupMembersFactory;
+  private final GroupMembers groupMembers;
   private final AccountLoader.Factory accountLoaderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeData.Factory changeDataFactory;
@@ -111,7 +111,7 @@
       ReviewerResource.Factory reviewerFactory,
       PermissionBackend permissionBackend,
       GroupsCollection groupsCollection,
-      GroupMembers.Factory groupMembersFactory,
+      GroupMembers groupMembers,
       AccountLoader.Factory accountLoaderFactory,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
@@ -130,7 +130,7 @@
     this.reviewerFactory = reviewerFactory;
     this.permissionBackend = permissionBackend;
     this.groupsCollection = groupsCollection;
-    this.groupMembersFactory = groupMembersFactory;
+    this.groupMembers = groupMembers;
     this.accountLoaderFactory = accountLoaderFactory;
     this.dbProvider = db;
     this.changeDataFactory = changeDataFactory;
@@ -287,10 +287,7 @@
     Set<Account.Id> reviewers = new HashSet<>();
     Set<Account> members;
     try {
-      members =
-          groupMembersFactory
-              .create(rsrc.getUser())
-              .listAccounts(group.getGroupUUID(), rsrc.getProject());
+      members = groupMembers.listAccounts(group.getGroupUUID(), rsrc.getProject());
     } catch (NoSuchGroupException e) {
       return fail(
           reviewer,
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 0e4e8b4..a387192 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
@@ -91,7 +91,6 @@
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
-import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalIdModule;
 import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
@@ -114,6 +113,7 @@
 import com.google.gerrit.server.git.EmailMerge;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.GitModules;
+import com.google.gerrit.server.git.MergeSuperSetComputation;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MergedByPushOp;
 import com.google.gerrit.server.git.NotesBranchUtil;
@@ -250,7 +250,6 @@
     factory(ChangeData.AssistedFactory.class);
     factory(ChangeJson.AssistedFactory.class);
     factory(CreateChangeSender.Factory.class);
-    factory(GroupMembers.Factory.class);
     factory(EmailMerge.Factory.class);
     factory(MergedSender.Factory.class);
     factory(MergeUtil.Factory.class);
@@ -377,6 +376,7 @@
     DynamicItem.itemOf(binder(), AccountPatchReviewStore.class);
     DynamicSet.setOf(binder(), AssigneeValidationListener.class);
     DynamicSet.setOf(binder(), ActionVisitor.class);
+    DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index 322d158..4991715 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -17,12 +17,15 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_REJECT_COMMITS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -70,6 +73,7 @@
   private final Provider<IdentifiedUser> currentUser;
   private final GitRepositoryManager repoManager;
   private final TimeZone tz;
+  private final PermissionBackend permissionBackend;
   private NotesBranchUtil.Factory notesBranchUtilFactory;
 
   @Inject
@@ -77,24 +81,23 @@
       Provider<IdentifiedUser> currentUser,
       GitRepositoryManager repoManager,
       @GerritPersonIdent PersonIdent gerritIdent,
-      NotesBranchUtil.Factory notesBranchUtilFactory) {
+      NotesBranchUtil.Factory notesBranchUtilFactory,
+      PermissionBackend permissionBackend) {
     this.currentUser = currentUser;
     this.repoManager = repoManager;
     this.notesBranchUtilFactory = notesBranchUtilFactory;
+    this.permissionBackend = permissionBackend;
     this.tz = gerritIdent.getTimeZone();
   }
 
   public BanCommitResult ban(
-      ProjectControl projectControl, List<ObjectId> commitsToBan, String reason)
-      throws PermissionDeniedException, LockFailureException, IOException {
-    if (!projectControl.isOwner()) {
-      throw new PermissionDeniedException("Not project owner: not permitted to ban commits");
-    }
+      Project.NameKey project, CurrentUser user, List<ObjectId> commitsToBan, String reason)
+      throws AuthException, LockFailureException, IOException, PermissionBackendException {
+    permissionBackend.user(user).project(project).check(ProjectPermission.BAN_COMMIT);
 
     final BanCommitResult result = new BanCommitResult();
     NoteMap banCommitNotes = NoteMap.newEmptyMap();
     // Add a note for each banned commit to notes.
-    final Project.NameKey project = projectControl.getProject().getNameKey();
     try (Repository repo = repoManager.openRepository(project);
         RevWalk revWalk = new RevWalk(repo);
         ObjectInserter inserter = repo.newObjectInserter()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalMergeSuperSetComputation.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalMergeSuperSetComputation.java
new file mode 100644
index 0000000..e681145
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalMergeSuperSetComputation.java
@@ -0,0 +1,260 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.registration.DynamicItem;
+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.CurrentUser;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of MergeSuperSet that does the computation of the merge super set
+ * sequentially on the local Gerrit instance.
+ */
+public class LocalMergeSuperSetComputation implements MergeSuperSetComputation {
+  private static final Logger log = LoggerFactory.getLogger(LocalMergeSuperSetComputation.class);
+
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      DynamicItem.bind(binder(), MergeSuperSetComputation.class)
+          .to(LocalMergeSuperSetComputation.class);
+    }
+  }
+
+  @AutoValue
+  abstract static class QueryKey {
+    private static QueryKey create(Branch.NameKey branch, Iterable<String> hashes) {
+      return new AutoValue_LocalMergeSuperSetComputation_QueryKey(
+          branch, ImmutableSet.copyOf(hashes));
+    }
+
+    abstract Branch.NameKey branch();
+
+    abstract ImmutableSet<String> hashes();
+  }
+
+  private final PermissionBackend permissionBackend;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final Map<QueryKey, List<ChangeData>> queryCache;
+  private final Map<Branch.NameKey, Optional<RevCommit>> heads;
+
+  @Inject
+  LocalMergeSuperSetComputation(
+      PermissionBackend permissionBackend, Provider<InternalChangeQuery> queryProvider) {
+    this.permissionBackend = permissionBackend;
+    this.queryProvider = queryProvider;
+    this.queryCache = new HashMap<>();
+    this.heads = new HashMap<>();
+  }
+
+  @Override
+  public ChangeSet completeWithoutTopic(
+      ReviewDb db, MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
+      throws OrmException, IOException, PermissionBackendException {
+    Collection<ChangeData> visibleChanges = new ArrayList<>();
+    Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
+
+    // For each target branch we run a separate rev walk to find open changes
+    // reachable from changes already in the merge super set.
+    ImmutableListMultimap<Branch.NameKey, ChangeData> bc =
+        byBranch(Iterables.concat(changeSet.changes(), changeSet.nonVisibleChanges()));
+    for (Branch.NameKey b : bc.keySet()) {
+      OpenRepo or = getRepo(orm, b.getParentKey());
+      List<RevCommit> visibleCommits = new ArrayList<>();
+      List<RevCommit> nonVisibleCommits = new ArrayList<>();
+      for (ChangeData cd : bc.get(b)) {
+        boolean visible = isVisible(db, changeSet, cd, user);
+
+        if (submitType(cd) == SubmitType.CHERRY_PICK) {
+          if (visible) {
+            visibleChanges.add(cd);
+          } else {
+            nonVisibleChanges.add(cd);
+          }
+
+          continue;
+        }
+
+        // Get the underlying git commit object
+        String objIdStr = cd.currentPatchSet().getRevision().get();
+        RevCommit commit = or.rw.parseCommit(ObjectId.fromString(objIdStr));
+
+        // Always include the input, even if merged. This allows
+        // SubmitStrategyOp to correct the situation later, assuming it gets
+        // returned by byCommitsOnBranchNotMerged below.
+        if (visible) {
+          visibleCommits.add(commit);
+        } else {
+          nonVisibleCommits.add(commit);
+        }
+      }
+
+      Set<String> visibleHashes =
+          walkChangesByHashes(visibleCommits, Collections.emptySet(), or, b);
+      Iterables.addAll(visibleChanges, byCommitsOnBranchNotMerged(or, db, b, visibleHashes));
+
+      Set<String> nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
+      Iterables.addAll(nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, b, nonVisibleHashes));
+    }
+
+    return new ChangeSet(visibleChanges, nonVisibleChanges);
+  }
+
+  private static ImmutableListMultimap<Branch.NameKey, ChangeData> byBranch(
+      Iterable<ChangeData> changes) throws OrmException {
+    ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
+        ImmutableListMultimap.builder();
+    for (ChangeData cd : changes) {
+      builder.put(cd.change().getDest(), cd);
+    }
+    return builder.build();
+  }
+
+  private OpenRepo getRepo(MergeOpRepoManager orm, Project.NameKey project) throws IOException {
+    try {
+      OpenRepo or = orm.getRepo(project);
+      checkState(or.rw.hasRevSort(RevSort.TOPO));
+      return or;
+    } catch (NoSuchProjectException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private boolean isVisible(ReviewDb db, ChangeSet changeSet, ChangeData cd, CurrentUser user)
+      throws PermissionBackendException {
+    boolean visible = changeSet.ids().contains(cd.getId());
+    if (visible
+        && !permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ)) {
+      // We thought the change was visible, but it isn't.
+      // This can happen if the ACL changes during the
+      // completeChangeSet computation, for example.
+      visible = false;
+    }
+    return visible;
+  }
+
+  private SubmitType submitType(ChangeData cd) throws OrmException {
+    SubmitTypeRecord str = cd.submitTypeRecord();
+    if (!str.isOk()) {
+      logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
+    }
+    return str.type;
+  }
+
+  private List<ChangeData> byCommitsOnBranchNotMerged(
+      OpenRepo or, ReviewDb db, Branch.NameKey branch, Set<String> hashes)
+      throws OrmException, IOException {
+    if (hashes.isEmpty()) {
+      return ImmutableList.of();
+    }
+    QueryKey k = QueryKey.create(branch, hashes);
+    List<ChangeData> cached = queryCache.get(k);
+    if (cached != null) {
+      return cached;
+    }
+
+    List<ChangeData> result = new ArrayList<>();
+    Iterable<ChangeData> destChanges =
+        MergeSuperSet.query(queryProvider.get())
+            .byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
+    for (ChangeData chd : destChanges) {
+      result.add(chd);
+    }
+    queryCache.put(k, result);
+    return result;
+  }
+
+  private Set<String> walkChangesByHashes(
+      Collection<RevCommit> sourceCommits, Set<String> ignoreHashes, OpenRepo or, Branch.NameKey b)
+      throws IOException {
+    Set<String> destHashes = new HashSet<>();
+    or.rw.reset();
+    markHeadUninteresting(or, b);
+    for (RevCommit c : sourceCommits) {
+      String name = c.name();
+      if (ignoreHashes.contains(name)) {
+        continue;
+      }
+      destHashes.add(name);
+      or.rw.markStart(c);
+    }
+    for (RevCommit c : or.rw) {
+      String name = c.name();
+      if (ignoreHashes.contains(name)) {
+        continue;
+      }
+      destHashes.add(name);
+    }
+
+    return destHashes;
+  }
+
+  private void markHeadUninteresting(OpenRepo or, Branch.NameKey b) throws IOException {
+    Optional<RevCommit> head = heads.get(b);
+    if (head == null) {
+      Ref ref = or.repo.getRefDatabase().exactRef(b.get());
+      head = ref != null ? Optional.of(or.rw.parseCommit(ref.getObjectId())) : Optional.empty();
+      heads.put(b, head);
+    }
+    if (head.isPresent()) {
+      or.rw.markUninteresting(head.get());
+    }
+  }
+
+  private void logErrorAndThrow(String msg) throws OrmException {
+    if (log.isErrorEnabled()) {
+      log.error(msg);
+    }
+    throw new OrmException(msg);
+  }
+}
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 58c183b..29627e3 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
@@ -17,29 +17,18 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -47,21 +36,10 @@
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Calculates the minimal superset of changes required to be merged.
@@ -74,36 +52,36 @@
  * included.
  */
 public class MergeSuperSet {
-  private static final Logger log = LoggerFactory.getLogger(MergeSuperSet.class);
 
-  public static void reloadChanges(ChangeSet cs) throws OrmException {
-    // Clear exactly the fields requested by query() below.
-    for (ChangeData cd : cs.changes()) {
+  public static void reloadChanges(ChangeSet changeSet) throws OrmException {
+    // Clear exactly the fields requested by query(InternalChangeQuery) below.
+    for (ChangeData cd : changeSet.changes()) {
       cd.reloadChange();
       cd.setPatchSets(null);
       cd.setMergeable(null);
     }
   }
 
-  @AutoValue
-  abstract static class QueryKey {
-    private static QueryKey create(Branch.NameKey branch, Iterable<String> hashes) {
-      return new AutoValue_MergeSuperSet_QueryKey(branch, ImmutableSet.copyOf(hashes));
-    }
-
-    abstract Branch.NameKey branch();
-
-    abstract ImmutableSet<String> hashes();
+  public static InternalChangeQuery query(InternalChangeQuery q) {
+    // Request fields required for completing the ChangeSet and converting to
+    // ChangeInfo without having to touch the database or opening the repository
+    // more than necessary. This provides reasonable performance when loading
+    // the change screen; callers that care about reading the latest value of
+    // these fields should clear them explicitly using reloadChanges().
+    Set<String> fields =
+        ImmutableSet.of(
+            ChangeField.CHANGE.getName(),
+            ChangeField.PATCH_SET.getName(),
+            ChangeField.MERGEABLE.getName());
+    return q.setRequestedFields(fields);
   }
 
   private final ChangeData.Factory changeDataFactory;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<MergeOpRepoManager> repoManagerProvider;
+  private final DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation;
   private final PermissionBackend permissionBackend;
   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;
@@ -114,16 +92,14 @@
       ChangeData.Factory changeDataFactory,
       Provider<InternalChangeQuery> queryProvider,
       Provider<MergeOpRepoManager> repoManagerProvider,
-      PermissionBackend permissionBackend,
-      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
+      DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation,
+      PermissionBackend permissionBackend) {
     this.cfg = cfg;
     this.changeDataFactory = changeDataFactory;
     this.queryProvider = queryProvider;
     this.repoManagerProvider = repoManagerProvider;
+    this.mergeSuperSetComputation = mergeSuperSetComputation;
     this.permissionBackend = permissionBackend;
-    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
-    queryCache = new HashMap<>();
-    heads = new HashMap<>();
   }
 
   public MergeSuperSet setMergeOpRepoManager(MergeOpRepoManager orm) {
@@ -136,14 +112,19 @@
   public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
       throws IOException, OrmException, PermissionBackendException {
     try {
+      if (orm == null) {
+        orm = repoManagerProvider.get();
+        closeOrm = true;
+      }
+
       ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
-      ChangeSet cs =
+      ChangeSet changeSet =
           new ChangeSet(
               cd, permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ));
       if (Submit.wholeTopicEnabled(cfg)) {
-        return completeChangeSetIncludingTopics(db, cs, user);
+        return completeChangeSetIncludingTopics(db, changeSet, user);
       }
-      return completeChangeSetWithoutTopic(db, cs, user);
+      return mergeSuperSetComputation.get().completeWithoutTopic(db, orm, changeSet, user);
     } finally {
       if (closeOrm && orm != null) {
         orm.close();
@@ -152,177 +133,13 @@
     }
   }
 
-  private SubmitType submitType(CurrentUser user, ChangeData cd, PatchSet ps) throws OrmException {
-    // Submit type prolog rules mean that the submit type can depend on the
-    // submitting user and the content of the change.
-    //
-    // If the current user can see the change, run that evaluation to get a
-    // preview of what would happen on submit.  If the current user can't see
-    // the change, instead of guessing who would do the submitting, rely on the
-    // project configuration and ignore the prolog rule.  If the prolog rule
-    // doesn't match that, we may pick the wrong submit type and produce a
-    // misleading (but still nonzero) count of the non visible changes that
-    // would be submitted together with the visible ones.
-    SubmitTypeRecord str =
-        ps == cd.currentPatchSet()
-            ? cd.submitTypeRecord()
-            : submitRuleEvaluatorFactory.create(user, cd).setPatchSet(ps).getSubmitType();
-    if (!str.isOk()) {
-      logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
-    }
-    return str.type;
-  }
-
-  private static ImmutableListMultimap<Branch.NameKey, ChangeData> byBranch(
-      Iterable<ChangeData> changes) throws OrmException {
-    ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
-        ImmutableListMultimap.builder();
-    for (ChangeData cd : changes) {
-      builder.put(cd.change().getDest(), cd);
-    }
-    return builder.build();
-  }
-
-  private Set<String> walkChangesByHashes(
-      Collection<RevCommit> sourceCommits, Set<String> ignoreHashes, OpenRepo or, Branch.NameKey b)
-      throws IOException {
-    Set<String> destHashes = new HashSet<>();
-    or.rw.reset();
-    markHeadUninteresting(or, b);
-    for (RevCommit c : sourceCommits) {
-      String name = c.name();
-      if (ignoreHashes.contains(name)) {
-        continue;
-      }
-      destHashes.add(name);
-      or.rw.markStart(c);
-    }
-    for (RevCommit c : or.rw) {
-      String name = c.name();
-      if (ignoreHashes.contains(name)) {
-        continue;
-      }
-      destHashes.add(name);
-    }
-
-    return destHashes;
-  }
-
-  private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes, CurrentUser user)
-      throws IOException, OrmException, PermissionBackendException {
-    Collection<ChangeData> visibleChanges = new ArrayList<>();
-    Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
-
-    // For each target branch we run a separate rev walk to find open changes
-    // reachable from changes already in the merge super set.
-    ImmutableListMultimap<Branch.NameKey, ChangeData> bc =
-        byBranch(Iterables.concat(changes.changes(), changes.nonVisibleChanges()));
-    for (Branch.NameKey b : bc.keySet()) {
-      OpenRepo or = getRepo(b.getParentKey());
-      List<RevCommit> visibleCommits = new ArrayList<>();
-      List<RevCommit> nonVisibleCommits = new ArrayList<>();
-      for (ChangeData cd : bc.get(b)) {
-        boolean visible = changes.ids().contains(cd.getId());
-        if (visible && !canRead(db, user, cd)) {
-          // We thought the change was visible, but it isn't.
-          // This can happen if the ACL changes during the
-          // completeChangeSet computation, for example.
-          visible = false;
-        }
-
-        // Pick a revision to use for traversal.  If any of the patch sets
-        // is visible, we use the most recent one.  Otherwise, use the current
-        // patch set.
-        PatchSet ps = cd.currentPatchSet();
-        if (submitType(user, cd, ps) == SubmitType.CHERRY_PICK) {
-          if (visible) {
-            visibleChanges.add(cd);
-          } else {
-            nonVisibleChanges.add(cd);
-          }
-
-          continue;
-        }
-
-        // Get the underlying git commit object
-        String objIdStr = ps.getRevision().get();
-        RevCommit commit = or.rw.parseCommit(ObjectId.fromString(objIdStr));
-
-        // Always include the input, even if merged. This allows
-        // SubmitStrategyOp to correct the situation later, assuming it gets
-        // returned by byCommitsOnBranchNotMerged below.
-        if (visible) {
-          visibleCommits.add(commit);
-        } else {
-          nonVisibleCommits.add(commit);
-        }
-      }
-
-      Set<String> visibleHashes =
-          walkChangesByHashes(visibleCommits, Collections.emptySet(), or, b);
-      Iterables.addAll(visibleChanges, byCommitsOnBranchNotMerged(or, db, b, visibleHashes));
-
-      Set<String> nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
-      Iterables.addAll(nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, b, nonVisibleHashes));
-    }
-
-    return new ChangeSet(visibleChanges, nonVisibleChanges);
-  }
-
-  private OpenRepo getRepo(Project.NameKey project) throws IOException {
-    if (orm == null) {
-      orm = repoManagerProvider.get();
-      closeOrm = true;
-    }
-    try {
-      OpenRepo or = orm.getRepo(project);
-      checkState(or.rw.hasRevSort(RevSort.TOPO));
-      return or;
-    } catch (NoSuchProjectException e) {
-      throw new IOException(e);
-    }
-  }
-
-  private void markHeadUninteresting(OpenRepo or, Branch.NameKey b) throws IOException {
-    Optional<RevCommit> head = heads.get(b);
-    if (head == null) {
-      Ref ref = or.repo.getRefDatabase().exactRef(b.get());
-      head = ref != null ? Optional.of(or.rw.parseCommit(ref.getObjectId())) : Optional.empty();
-      heads.put(b, head);
-    }
-    if (head.isPresent()) {
-      or.rw.markUninteresting(head.get());
-    }
-  }
-
-  private List<ChangeData> byCommitsOnBranchNotMerged(
-      OpenRepo or, ReviewDb db, Branch.NameKey branch, Set<String> hashes)
-      throws OrmException, IOException {
-    if (hashes.isEmpty()) {
-      return ImmutableList.of();
-    }
-    QueryKey k = QueryKey.create(branch, hashes);
-    List<ChangeData> cached = queryCache.get(k);
-    if (cached != null) {
-      return cached;
-    }
-
-    List<ChangeData> result = new ArrayList<>();
-    Iterable<ChangeData> destChanges =
-        query().byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
-    for (ChangeData chd : destChanges) {
-      result.add(chd);
-    }
-    queryCache.put(k, result);
-    return result;
-  }
-
   /**
-   * Completes {@code cs} with any additional changes from its topics
+   * Completes {@code changeSet} with any additional changes from its topics
    *
    * <p>{@link #completeChangeSetIncludingTopics} calls this repeatedly, alternating with {@link
-   * #completeChangeSetWithoutTopic}, to discover what additional changes should be submitted with a
-   * change until the set stops growing.
+   * MergeSuperSetComputation#completeWithoutTopic(ReviewDb, MergeOpRepoManager, ChangeSet,
+   * CurrentUser)}, to discover what additional changes should be submitted with a change until the
+   * set stops growing.
    *
    * <p>{@code topicsSeen} and {@code visibleTopicsSeen} keep track of topics already explored to
    * avoid wasted work.
@@ -331,7 +148,7 @@
    */
   private ChangeSet topicClosure(
       ReviewDb db,
-      ChangeSet cs,
+      ChangeSet changeSet,
       CurrentUser user,
       Set<String> topicsSeen,
       Set<String> visibleTopicsSeen)
@@ -339,13 +156,13 @@
     List<ChangeData> visibleChanges = new ArrayList<>();
     List<ChangeData> nonVisibleChanges = new ArrayList<>();
 
-    for (ChangeData cd : cs.changes()) {
+    for (ChangeData cd : changeSet.changes()) {
       visibleChanges.add(cd);
       String topic = cd.change().getTopic();
       if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) {
         continue;
       }
-      for (ChangeData topicCd : query().byTopicOpen(topic)) {
+      for (ChangeData topicCd : byTopicOpen(topic)) {
         if (canRead(db, user, topicCd)) {
           visibleChanges.add(topicCd);
         } else {
@@ -355,13 +172,13 @@
       topicsSeen.add(topic);
       visibleTopicsSeen.add(topic);
     }
-    for (ChangeData cd : cs.nonVisibleChanges()) {
+    for (ChangeData cd : changeSet.nonVisibleChanges()) {
       nonVisibleChanges.add(cd);
       String topic = cd.change().getTopic();
       if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) {
         continue;
       }
-      for (ChangeData topicCd : query().byTopicOpen(topic)) {
+      for (ChangeData topicCd : byTopicOpen(topic)) {
         nonVisibleChanges.add(topicCd);
       }
       topicsSeen.add(topic);
@@ -370,7 +187,7 @@
   }
 
   private ChangeSet completeChangeSetIncludingTopics(
-      ReviewDb db, ChangeSet changes, CurrentUser user)
+      ReviewDb db, ChangeSet changeSet, CurrentUser user)
       throws IOException, OrmException, PermissionBackendException {
     Set<String> topicsSeen = new HashSet<>();
     Set<String> visibleTopicsSeen = new HashSet<>();
@@ -380,37 +197,16 @@
     do {
       oldSeen = seen;
 
-      changes = completeChangeSetWithoutTopic(db, changes, user);
-      changes = topicClosure(db, changes, user, topicsSeen, visibleTopicsSeen);
+      changeSet = topicClosure(db, changeSet, user, topicsSeen, visibleTopicsSeen);
+      changeSet = mergeSuperSetComputation.get().completeWithoutTopic(db, orm, changeSet, user);
 
       seen = topicsSeen.size() + visibleTopicsSeen.size();
     } while (seen != oldSeen);
-    return changes;
+    return changeSet;
   }
 
-  private InternalChangeQuery query() {
-    // Request fields required for completing the ChangeSet and converting to
-    // ChangeInfo without having to touch the database or opening the repository
-    // more than necessary. This provides reasonable performance when loading
-    // the change screen; callers that care about reading the latest value of
-    // these fields should clear them explicitly using reloadChanges().
-    Set<String> fields =
-        ImmutableSet.of(
-            ChangeField.CHANGE.getName(),
-            ChangeField.PATCH_SET.getName(),
-            ChangeField.MERGEABLE.getName());
-    return queryProvider.get().setRequestedFields(fields);
-  }
-
-  private void logError(String msg) {
-    if (log.isErrorEnabled()) {
-      log.error(msg);
-    }
-  }
-
-  private void logErrorAndThrow(String msg) throws OrmException {
-    logError(msg);
-    throw new OrmException(msg);
+  private List<ChangeData> byTopicOpen(String topic) throws OrmException {
+    return query(queryProvider.get()).byTopicOpen(topic);
   }
 
   private boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSetComputation.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSetComputation.java
new file mode 100644
index 0000000..63405ba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSetComputation.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+
+/**
+ * Interface to compute the merge super set to detect changes that should be submitted together.
+ *
+ * <p>E.g. to speed up performance implementations could decide to do the computation in batches in
+ * parallel on different server nodes.
+ */
+@ExtensionPoint
+public interface MergeSuperSetComputation {
+
+  /**
+   * Compute the set of changes that should be submitted together. As input a set of changes is
+   * provided for which it is known that they should be submitted together. This method should
+   * complete the set by including open predecessor changes that need to be submitted as well. To
+   * decide whether open predecessor changes should be included the method must take the submit type
+   * into account (e.g. for changes with submit type "Cherry-Pick" open predecessor changes must not
+   * be included).
+   *
+   * <p>This method is invoked iteratively while new changes to be submitted together are discovered
+   * by expanding the topics of the changes. This method must not do any topic expansion on its own.
+   *
+   * @param db {@link ReviewDb} instance
+   * @param orm {@link MergeOpRepoManager} that should be used to access repositories
+   * @param changeSet A set of changes for which it is known that they should be submitted together
+   * @param user The user for which the visibility checks should be performed
+   * @return the completed set of changes that should be submitted together
+   */
+  ChangeSet completeWithoutTopic(
+      ReviewDb db, MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
+      throws OrmException, IOException, PermissionBackendException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 22834f3..cfd9595 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.project.ContributorAgreementsChecker;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -73,7 +74,8 @@
 
   public interface Factory {
     AsyncReceiveCommits create(
-        ProjectControl projectControl,
+        ProjectState projectState,
+        IdentifiedUser user,
         Repository repository,
         @Nullable MessageSender messageSender,
         SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers);
@@ -106,7 +108,7 @@
 
     private Worker(Collection<ReceiveCommand> commands) {
       this.commands = commands;
-      rc = factory.create(projectControl, rp, allRefsWatcher, extraReviewers);
+      rc = factory.create(projectState, user, rp, allRefsWatcher, extraReviewers);
       rc.init();
       rc.setMessageSender(messageSender);
       progress = new MultiProgressMonitor(new MessageSenderOutputStream(), "Processing changes");
@@ -170,8 +172,10 @@
   private final RequestScopePropagator scopePropagator;
   private final ReceiveConfig receiveConfig;
   private final ContributorAgreementsChecker contributorAgreements;
+  private final ProjectControl.GenericFactory projectControlFactory;
   private final long timeoutMillis;
-  private final ProjectControl projectControl;
+  private final ProjectState projectState;
+  private final IdentifiedUser user;
   private final Repository repo;
   private final MessageSender messageSender;
   private final SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers;
@@ -189,8 +193,10 @@
       TransferConfig transferConfig,
       Provider<LazyPostReceiveHookChain> lazyPostReceive,
       ContributorAgreementsChecker contributorAgreements,
+      ProjectControl.GenericFactory projectControlFactory,
       @Named(TIMEOUT_NAME) long timeoutMillis,
-      @Assisted ProjectControl projectControl,
+      @Assisted ProjectState projectState,
+      @Assisted IdentifiedUser user,
       @Assisted Repository repo,
       @Assisted @Nullable MessageSender messageSender,
       @Assisted SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers)
@@ -200,23 +206,23 @@
     this.scopePropagator = scopePropagator;
     this.receiveConfig = receiveConfig;
     this.contributorAgreements = contributorAgreements;
+    this.projectControlFactory = projectControlFactory;
     this.timeoutMillis = timeoutMillis;
-    this.projectControl = projectControl;
+    this.projectState = projectState;
+    this.user = user;
     this.repo = repo;
     this.messageSender = messageSender;
     this.extraReviewers = extraReviewers;
 
-    IdentifiedUser user = projectControl.getUser().asIdentifiedUser();
-    ProjectState state = projectControl.getProjectState();
-    Project.NameKey projectName = projectControl.getProject().getNameKey();
+    Project.NameKey projectName = projectState.getNameKey();
     rp = new ReceivePack(repo);
     rp.setAllowCreates(true);
     rp.setAllowDeletes(true);
     rp.setAllowNonFastForwards(true);
     rp.setRefLogIdent(user.newRefLogIdent());
     rp.setTimeout(transferConfig.getTimeout());
-    rp.setMaxObjectSizeLimit(transferConfig.getEffectiveMaxObjectSizeLimit(state));
-    rp.setCheckReceivedObjects(state.getConfig().getCheckReceivedObjects());
+    rp.setMaxObjectSizeLimit(transferConfig.getEffectiveMaxObjectSizeLimit(projectState));
+    rp.setCheckReceivedObjects(projectState.getConfig().getCheckReceivedObjects());
     rp.setRefFilter(new ReceiveRefFilter());
     rp.setAllowPushOptions(true);
     rp.setPreReceiveHook(this);
@@ -233,7 +239,7 @@
     List<AdvertiseRefsHook> advHooks = new ArrayList<>(4);
     allRefsWatcher = new AllRefsWatcher();
     advHooks.add(allRefsWatcher);
-    advHooks.add(refFilterFactory.create(state, repo).setShowMetadata(false));
+    advHooks.add(refFilterFactory.create(projectState, repo).setShowMetadata(false));
     advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
     advHooks.add(new HackPushNegotiateHook());
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
@@ -241,20 +247,27 @@
 
   /** Determine if the user can upload commits. */
   public Capable canUpload() throws IOException {
+    // TODO(hiesel): Remove dependency on ProjectControl
+    ProjectControl projectControl;
+    try {
+      projectControl = projectControlFactory.controlFor(projectState.getNameKey(), user);
+    } catch (NoSuchProjectException e) {
+      throw new IOException(e);
+    }
+
     Capable result = projectControl.canPushToAtLeastOneRef();
     if (result != Capable.OK) {
       return result;
     }
 
     try {
-      contributorAgreements.check(
-          projectControl.getProject().getNameKey(), projectControl.getUser());
+      contributorAgreements.check(projectState.getNameKey(), user);
     } catch (AuthException e) {
       return new Capable(e.getMessage());
     }
 
     if (receiveConfig.checkMagicRefs) {
-      return MagicBranch.checkMagicBranchRefs(repo, projectControl.getProject());
+      return MagicBranch.checkMagicBranchRefs(repo, projectState.getProject());
     }
     return Capable.OK;
   }
@@ -269,7 +282,7 @@
       log.warn(
           String.format(
               "Error in ReceiveCommits while processing changes for project %s",
-              projectControl.getProject().getName()),
+              projectState.getName()),
           e);
       rp.sendError("internal error while processing changes");
       // ReceiveCommits has tried its best to catch errors, so anything at this
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index aaed2e7..553087f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -128,12 +128,12 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.CreateRefControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -228,7 +228,8 @@
 
   interface Factory {
     ReceiveCommits create(
-        ProjectControl projectControl,
+        ProjectState projectState,
+        IdentifiedUser user,
         ReceivePack receivePack,
         AllRefsWatcher allRefsWatcher,
         SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers);
@@ -300,7 +301,6 @@
   private final CommitValidators.Factory commitValidatorsFactory;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final DynamicSet<ReceivePackInitializer> initializers;
-  private final IdentifiedUser user;
   private final MergedByPushOp.Factory mergedByPushOpFactory;
   private final NotesMigration notesMigration;
   private final PatchSetInfoFactory patchSetInfoFactory;
@@ -325,7 +325,8 @@
   // Assisted injected fields.
   private final AllRefsWatcher allRefsWatcher;
   private final ImmutableSetMultimap<ReviewerStateInternal, Account.Id> extraReviewers;
-  private final ProjectControl projectControl;
+  private final ProjectState projectState;
+  private final IdentifiedUser user;
   private final ReceivePack rp;
 
   // Immutable fields derived from constructor arguments.
@@ -406,7 +407,8 @@
       TagCache tagCache,
       CreateRefControl createRefControl,
       DynamicItem<ChangeReportFormatter> changeFormatterProvider,
-      @Assisted ProjectControl projectControl,
+      @Assisted ProjectState projectState,
+      @Assisted IdentifiedUser user,
       @Assisted ReceivePack rp,
       @Assisted AllRefsWatcher allRefsWatcher,
       @Assisted SetMultimap<ReviewerStateInternal, Account.Id> extraReviewers)
@@ -419,7 +421,6 @@
     this.changeInserterFactory = changeInserterFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeFormatter = changeFormatterProvider.get();
-    this.user = projectControl.getUser().asIdentifiedUser();
     this.db = db;
     this.editUtil = editUtil;
     this.hashtagsFactory = hashtagsFactory;
@@ -450,13 +451,14 @@
     // Assisted injected fields.
     this.allRefsWatcher = allRefsWatcher;
     this.extraReviewers = ImmutableSetMultimap.copyOf(extraReviewers);
-    this.projectControl = projectControl;
+    this.projectState = projectState;
+    this.user = user;
     this.rp = rp;
 
     // Immutable fields derived from constructor arguments.
     repo = rp.getRepository();
-    project = projectControl.getProject();
-    labelTypes = projectControl.getProjectState().getLabelTypes();
+    project = projectState.getProject();
+    labelTypes = projectState.getLabelTypes();
     permissions = permissionBackend.user(user).project(project.getNameKey());
     receiveId = RequestId.forProject(project.getNameKey());
     rejectCommits = BanCommit.loadRejectCommitsMap(rp.getRepository(), rp.getRevWalk());
@@ -474,8 +476,7 @@
     newChanges = Collections.emptyList();
 
     // Other settings populated during processing.
-    newChangeForAllNotInTarget =
-        projectControl.getProjectState().isCreateNewChangeForAllNotInTarget();
+    newChangeForAllNotInTarget = projectState.isCreateNewChangeForAllNotInTarget();
 
     // Handles for outputting back over the wire to the end user.
     messageSender = new ReceivePackMessageSender();
@@ -483,7 +484,7 @@
 
   void init() {
     for (ReceivePackInitializer i : initializers) {
-      i.init(projectControl.getProject().getNameKey(), rp);
+      i.init(projectState.getNameKey(), rp);
     }
   }
 
@@ -818,8 +819,7 @@
         continue;
       }
 
-      if (projectControl.getProjectState().isAllUsers()
-          && RefNames.REFS_USERS_SELF.equals(cmd.getRefName())) {
+      if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(cmd.getRefName())) {
         String newName = RefNames.refsUsers(user.getAccountId());
         logDebug("Swapping out command for {} to {}", RefNames.REFS_USERS_SELF, newName);
         final ReceiveCommand orgCmd = cmd;
@@ -870,8 +870,14 @@
 
       if (isConfig(cmd)) {
         logDebug("Processing {} command", cmd.getRefName());
-        if (!projectControl.isOwner()) {
-          reject(cmd, "not project owner");
+        try {
+          permissions.check(ProjectPermission.WRITE_CONFIG);
+        } catch (AuthException e) {
+          reject(
+              cmd,
+              String.format(
+                  "must be either project owner or have %s permission",
+                  ProjectPermission.WRITE_CONFIG.describeForException()));
           continue;
         }
 
@@ -926,16 +932,14 @@
                 ProjectConfigEntry configEntry = e.getProvider().get();
                 String value = pluginCfg.getString(e.getExportName());
                 String oldValue =
-                    projectControl
-                        .getProjectState()
+                    projectState
                         .getConfig()
                         .getPluginConfig(e.getPluginName())
                         .getString(e.getExportName());
                 if (configEntry.getType() == ProjectConfigEntryType.ARRAY) {
                   oldValue =
                       Arrays.stream(
-                              projectControl
-                                  .getProjectState()
+                              projectState
                                   .getConfig()
                                   .getPluginConfig(e.getPluginName())
                                   .getStringList(e.getExportName()))
@@ -943,7 +947,7 @@
                 }
 
                 if ((value == null ? oldValue != null : !value.equals(oldValue))
-                    && !configEntry.isEditable(projectControl.getProjectState())) {
+                    && !configEntry.isEditable(projectState)) {
                   reject(
                       cmd,
                       String.format(
@@ -1450,7 +1454,7 @@
       reject(cmd, "see help");
       return;
     }
-    if (projectControl.getProjectState().isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
+    if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
       logDebug("Handling {}", RefNames.REFS_USERS_SELF);
       ref = RefNames.refsUsers(user.getAccountId());
     }
@@ -1469,7 +1473,7 @@
 
     magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
     magicBranch.perm = permissions.ref(ref);
-    if (!projectControl.getProject().getState().permitsWrite()) {
+    if (!projectState.getProject().getState().permitsWrite()) {
       reject(cmd, "project state does not permit write");
       return;
     }
@@ -2491,7 +2495,7 @@
       replaceOp =
           replaceOpFactory
               .create(
-                  projectControl,
+                  projectState,
                   notes.getChange().getDest(),
                   checkMergedInto,
                   priorPatchSet,
@@ -2793,8 +2797,7 @@
     // TODO(dborowitz): Combine this BatchUpdate with the main one in
     // insertChangesAndPatchSets.
     try (BatchUpdate bu =
-            batchUpdateFactory.create(
-                db, projectControl.getProject().getNameKey(), user, TimeUtil.nowTs());
+            batchUpdateFactory.create(db, projectState.getNameKey(), user, TimeUtil.nowTs());
         ObjectInserter ins = repo.newObjectInserter();
         ObjectReader reader = ins.newReader();
         RevWalk rw = new RevWalk(reader)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 4455aed..9220bc9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -54,7 +54,7 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -84,7 +84,7 @@
 public class ReplaceOp implements BatchUpdateOp {
   public interface Factory {
     ReplaceOp create(
-        ProjectControl projectControl,
+        ProjectState projectState,
         Branch.NameKey dest,
         boolean checkMergedInto,
         @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
@@ -117,7 +117,7 @@
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ProjectCache projectCache;
 
-  private final ProjectControl projectControl;
+  private final ProjectState projectState;
   private final Branch.NameKey dest;
   private final boolean checkMergedInto;
   private final PatchSet.Id priorPatchSetId;
@@ -159,7 +159,7 @@
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       ProjectCache projectCache,
       @SendEmailExecutor ExecutorService sendEmailExecutor,
-      @Assisted ProjectControl projectControl,
+      @Assisted ProjectState projectState,
       @Assisted Branch.NameKey dest,
       @Assisted boolean checkMergedInto,
       @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
@@ -186,7 +186,7 @@
     this.projectCache = projectCache;
     this.sendEmailExecutor = sendEmailExecutor;
 
-    this.projectControl = projectControl;
+    this.projectState = projectState;
     this.dest = dest;
     this.checkMergedInto = checkMergedInto;
     this.priorPatchSetId = priorPatchSetId;
@@ -205,7 +205,7 @@
     ctx.getRevWalk().parseBody(commit);
     changeKind =
         changeKindCache.getChangeKind(
-            projectControl.getProject().getNameKey(),
+            projectState.getNameKey(),
             ctx.getRevWalk(),
             ctx.getRepoView().getConfig(),
             priorCommitId,
@@ -299,7 +299,7 @@
         approvalsUtil.addApprovalsForNewPatchSet(
             ctx.getDb(),
             update,
-            projectControl.getProjectState().getLabelTypes(),
+            projectState.getLabelTypes(),
             newPatchSet,
             ctx.getUser(),
             approvals);
@@ -314,7 +314,7 @@
     approvalsUtil.addReviewers(
         ctx.getDb(),
         update,
-        projectControl.getProjectState().getLabelTypes(),
+        projectState.getLabelTypes(),
         change,
         newPatchSet,
         info,
@@ -406,7 +406,7 @@
           continue;
         }
 
-        LabelType lt = projectControl.getProjectState().getLabelTypes().byLabel(a.getLabelId());
+        LabelType lt = projectState.getLabelTypes().byLabel(a.getLabelId());
         if (lt != null) {
           current.put(lt.getName(), a);
         }
@@ -496,8 +496,7 @@
     public void run() {
       try {
         ReplacePatchSetSender cm =
-            replacePatchSetFactory.create(
-                projectControl.getProject().getNameKey(), notes.getChangeId());
+            replacePatchSetFactory.create(projectState.getNameKey(), notes.getChangeId());
         cm.setFrom(ctx.getAccount().getId());
         cm.setPatchSet(newPatchSet, info);
         cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index 910468f..6fbfd67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
@@ -41,7 +42,6 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -56,6 +56,7 @@
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.function.Predicate;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
 import org.kohsuke.args4j.Option;
@@ -67,7 +68,7 @@
 
   protected final GroupCache groupCache;
 
-  private final List<ProjectControl> projects = new ArrayList<>();
+  private final List<ProjectState> projects = new ArrayList<>();
   private final Set<AccountGroup.UUID> groupsToInspect = new HashSet<>();
   private final GroupControl.Factory groupControlFactory;
   private final GroupControl.GenericFactory genericGroupControlFactory;
@@ -77,6 +78,7 @@
   private final GroupJson json;
   private final GroupBackend groupBackend;
   private final Groups groups;
+  private final GroupsCollection groupsCollection;
   private final Provider<ReviewDb> db;
 
   private EnumSet<ListGroupsOption> options = EnumSet.noneOf(ListGroupsOption.class);
@@ -88,13 +90,14 @@
   private String matchSubstring;
   private String matchRegex;
   private String suggest;
+  private String ownedBy;
 
   @Option(
     name = "--project",
     aliases = {"-p"},
     usage = "projects for which the groups should be listed"
   )
-  public void addProject(ProjectControl project) {
+  public void addProject(ProjectState project) {
     projects.add(project);
   }
 
@@ -209,6 +212,11 @@
     options.addAll(ListGroupsOption.fromBits(Integer.parseInt(hex, 16)));
   }
 
+  @Option(name = "--owned-by", usage = "list groups owned by the given group uuid")
+  public void setOwnedBy(String ownedBy) {
+    this.ownedBy = ownedBy;
+  }
+
   @Inject
   protected ListGroups(
       final GroupCache groupCache,
@@ -217,6 +225,7 @@
       final Provider<IdentifiedUser> identifiedUser,
       final IdentifiedUser.GenericFactory userFactory,
       final GetGroups accountGetGroups,
+      final GroupsCollection groupsCollection,
       GroupJson json,
       GroupBackend groupBackend,
       Groups groups,
@@ -230,6 +239,7 @@
     this.json = json;
     this.groupBackend = groupBackend;
     this.groups = groups;
+    this.groupsCollection = groupsCollection;
     this.db = db;
   }
 
@@ -241,13 +251,13 @@
     return user;
   }
 
-  public List<ProjectControl> getProjects() {
+  public List<ProjectState> getProjects() {
     return projects;
   }
 
   @Override
   public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
-      throws OrmException, BadRequestException {
+      throws OrmException, RestApiException {
     SortedMap<String, GroupInfo> output = new TreeMap<>();
     for (GroupInfo info : get()) {
       output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
@@ -256,7 +266,7 @@
     return output;
   }
 
-  public List<GroupInfo> get() throws OrmException, BadRequestException {
+  public List<GroupInfo> get() throws OrmException, RestApiException {
     if (!Strings.isNullOrEmpty(suggest)) {
       return suggestGroups();
     }
@@ -265,6 +275,10 @@
       throw new BadRequestException("Specify one of m/r");
     }
 
+    if (ownedBy != null) {
+      return getGroupsOwnedBy(ownedBy);
+    }
+
     if (owned) {
       return getGroupsOwnedBy(user != null ? userFactory.create(user) : identifiedUser.get());
     }
@@ -298,7 +312,6 @@
     if (!projects.isEmpty()) {
       return projects
           .stream()
-          .map(ProjectControl::getProjectState)
           .map(ProjectState::getAllGroups)
           .flatMap(Collection::stream)
           .map(GroupReference::getUUID)
@@ -318,9 +331,7 @@
     List<GroupReference> groupRefs =
         Lists.newArrayList(
             Iterables.limit(
-                groupBackend.suggest(
-                    suggest,
-                    projects.stream().findFirst().map(pc -> pc.getProjectState()).orElse(null)),
+                groupBackend.suggest(suggest, projects.stream().findFirst().orElse(null)),
                 limit <= 0 ? 10 : Math.min(limit, 10)));
 
     List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
@@ -349,6 +360,9 @@
     if (owned) {
       return true;
     }
+    if (ownedBy != null) {
+      return true;
+    }
     if (start != 0) {
       return true;
     }
@@ -364,14 +378,15 @@
     return false;
   }
 
-  private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user) throws OrmException {
+  private List<GroupInfo> filterGroupsOwnedBy(Predicate<GroupDescription.Internal> filter)
+      throws OrmException {
     Pattern pattern = getRegexPattern();
     Stream<GroupDescription.Internal> foundGroups =
         groups
             .getAll(db.get())
             .map(GroupDescriptions::forAccountGroup)
             .filter(group -> !isNotRelevant(pattern, group))
-            .filter(group -> isOwner(user, group))
+            .filter(filter)
             .sorted(GROUP_COMPARATOR)
             .skip(start);
     if (limit > 0) {
@@ -385,6 +400,15 @@
     return groupInfos;
   }
 
+  private List<GroupInfo> getGroupsOwnedBy(String id) throws OrmException, RestApiException {
+    String uuid = groupsCollection.parse(id).getGroupUUID().get();
+    return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
+  }
+
+  private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user) throws OrmException {
+    return filterGroupsOwnedBy(group -> isOwner(user, group));
+  }
+
   private boolean isOwner(CurrentUser user, GroupDescription.Internal group) {
     try {
       return genericGroupControlFactory.controlFor(user, group.getGroupUUID()).isOwner();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java
index d0abf9a..2a6bb8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -76,7 +76,19 @@
   RUN_RECEIVE_PACK,
 
   /** Can run upload pack. */
-  RUN_UPLOAD_PACK;
+  RUN_UPLOAD_PACK,
+
+  /** Allow read access to refs/meta/config. */
+  READ_CONFIG,
+
+  /** Allow write access to refs/meta/config. */
+  WRITE_CONFIG,
+
+  /** Allow banning commits from Gerrit preventing pushes of these commits. */
+  BAN_COMMIT,
+
+  /** Allow accessing the project's reflog. */
+  READ_REFLOG;
 
   private final String name;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
index 9f3dda1..607162e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
@@ -24,6 +24,7 @@
   DELETE(Permission.DELETE),
   UPDATE(Permission.PUSH),
   FORCE_UPDATE,
+  SET_HEAD,
 
   FORGE_AUTHOR(Permission.FORGE_AUTHOR),
   FORGE_COMMITTER(Permission.FORGE_COMMITTER),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
index 278b2af..040e7ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.errors.PermissionDeniedException;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.git.BanCommitResult;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.BanCommit.BanResultInfo;
 import com.google.gerrit.server.project.BanCommit.Input;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -61,7 +60,7 @@
   @Override
   protected BanResultInfo applyImpl(
       BatchUpdate.Factory updateFactory, ProjectResource rsrc, Input input)
-      throws RestApiException, UpdateException, IOException {
+      throws RestApiException, UpdateException, IOException, PermissionBackendException {
     BanResultInfo r = new BanResultInfo();
     if (input != null && input.commits != null && !input.commits.isEmpty()) {
       List<ObjectId> commitsToBan = new ArrayList<>(input.commits.size());
@@ -73,14 +72,11 @@
         }
       }
 
-      try {
-        BanCommitResult result = banCommit.ban(rsrc.getControl(), commitsToBan, input.reason);
-        r.newlyBanned = transformCommits(result.getNewlyBannedCommits());
-        r.alreadyBanned = transformCommits(result.getAlreadyBannedCommits());
-        r.ignored = transformCommits(result.getIgnoredObjectIds());
-      } catch (PermissionDeniedException e) {
-        throw new AuthException(e.getMessage());
-      }
+      BanCommitResult result =
+          banCommit.ban(rsrc.getNameKey(), rsrc.getUser(), commitsToBan, input.reason);
+      r.newlyBanned = transformCommits(result.getNewlyBannedCommits());
+      r.alreadyBanned = transformCommits(result.getAlreadyBannedCommits());
+      r.ignored = transformCommits(result.getIgnoredObjectIds());
     }
     return r;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
index 2e81af3..622b1dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.CurrentUser;
 import com.google.inject.TypeLiteral;
 import org.eclipse.jgit.lib.Ref;
 
@@ -26,8 +27,8 @@
   private final String refName;
   private final String revision;
 
-  public BranchResource(ProjectControl control, Ref ref) {
-    super(control);
+  public BranchResource(ProjectState projectState, CurrentUser user, Ref ref) {
+    super(projectState, user);
     this.refName = ref.getName();
     this.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
index a40eabb..52072d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -85,7 +85,7 @@
           .project(project)
           .ref(ref.isSymbolic() ? ref.getTarget().getName() : ref.getName())
           .check(RefPermission.READ);
-      return new BranchResource(parent.getControl(), ref);
+      return new BranchResource(parent.getProjectState(), parent.getUser(), ref);
     } catch (AuthException notAllowed) {
       throw new ResourceNotFoundException(id);
     } catch (RepositoryNotFoundException noRepo) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
index 0cd7d19..e008d66 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
@@ -53,7 +53,7 @@
   public ChildProjectResource parse(ProjectResource parent, IdString id)
       throws ResourceNotFoundException, IOException, PermissionBackendException {
     ProjectResource p = projectsCollection.parse(TopLevelResource.INSTANCE, id);
-    for (ProjectState pp : p.getControl().getProjectState().parents()) {
+    for (ProjectState pp : p.getProjectState().parents()) {
       if (parent.getNameKey().equals(pp.getProject().getNameKey())) {
         return new ChildProjectResource(parent, p.getProjectState());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
index eb0dde4..db0787c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -39,15 +40,15 @@
 public class ConfigInfoImpl extends ConfigInfo {
   public ConfigInfoImpl(
       boolean serverEnableSignedPush,
-      ProjectControl control,
+      ProjectState projectState,
+      CurrentUser user,
       TransferConfig config,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
       UiActions uiActions,
       DynamicMap<RestView<ProjectResource>> views) {
-    ProjectState projectState = control.getProjectState();
-    Project p = control.getProject();
+    Project p = projectState.getProject();
     this.description = Strings.emptyToNull(p.getDescription());
 
     InheritedBooleanInfo useContributorAgreements = new InheritedBooleanInfo();
@@ -130,11 +131,10 @@
       this.commentlinks.put(cl.name, cl);
     }
 
-    pluginConfig =
-        getPluginConfig(control.getProjectState(), pluginConfigEntries, cfgFactory, allProjects);
+    pluginConfig = getPluginConfig(projectState, pluginConfigEntries, cfgFactory, allProjects);
 
     actions = new TreeMap<>();
-    for (UiAction.Description d : uiActions.from(views, new ProjectResource(control))) {
+    for (UiAction.Description d : uiActions.from(views, new ProjectResource(projectState, user))) {
       actions.put(d.getId(), new ActionInfo(d));
     }
     this.theme = projectState.getTheme();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java
index 326d395..31b48cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateAccessChange.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
@@ -89,25 +90,23 @@
       throws PermissionBackendException, PermissionDeniedException, IOException,
           ConfigInvalidException, OrmException, InvalidNameException, UpdateException,
           RestApiException {
-    MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
-    List<AccessSection> removals = setAccess.getAccessSections(input.remove);
-    List<AccessSection> additions = setAccess.getAccessSections(input.add);
-
-    PermissionBackend.ForRef metaRef =
-        permissionBackend.user(rsrc.getUser()).project(rsrc.getNameKey()).ref(RefNames.REFS_CONFIG);
-    try {
-      metaRef.check(RefPermission.READ);
-    } catch (AuthException denied) {
+    PermissionBackend.ForProject forProject =
+        permissionBackend.user(rsrc.getUser()).project(rsrc.getNameKey());
+    if (!check(forProject, ProjectPermission.READ_CONFIG)) {
       throw new PermissionDeniedException(RefNames.REFS_CONFIG + " not visible");
     }
-    if (!rsrc.getControl().isOwner()) {
+    if (!check(forProject, ProjectPermission.WRITE_CONFIG)) {
       try {
-        metaRef.check(RefPermission.CREATE_CHANGE);
+        forProject.ref(RefNames.REFS_CONFIG).check(RefPermission.CREATE_CHANGE);
       } catch (AuthException denied) {
         throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG);
       }
     }
 
+    MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
+    List<AccessSection> removals = setAccess.getAccessSections(input.remove);
+    List<AccessSection> additions = setAccess.getAccessSections(input.add);
+
     Project.NameKey newParentProjectName =
         input.parent == null ? null : new Project.NameKey(input.parent);
 
@@ -159,4 +158,14 @@
         .setValidate(false)
         .setUpdateRef(false);
   }
+
+  private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
+      throws PermissionBackendException {
+    try {
+      perm.check(p);
+      return true;
+    } catch (AuthException denied) {
+      return false;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
index 61548c4..8e706a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
@@ -66,6 +66,7 @@
   private final TagCache tagCache;
   private final GitReferenceUpdated referenceUpdated;
   private final WebLinks links;
+  private final ProjectControl.GenericFactory projectControlFactory;
   private String ref;
 
   @Inject
@@ -76,6 +77,7 @@
       TagCache tagCache,
       GitReferenceUpdated referenceUpdated,
       WebLinks webLinks,
+      ProjectControl.GenericFactory projectControlFactory,
       @Assisted String ref) {
     this.permissionBackend = permissionBackend;
     this.identifiedUser = identifiedUser;
@@ -83,12 +85,13 @@
     this.tagCache = tagCache;
     this.referenceUpdated = referenceUpdated;
     this.links = webLinks;
+    this.projectControlFactory = projectControlFactory;
     this.ref = ref;
   }
 
   @Override
   public TagInfo apply(ProjectResource resource, TagInput input)
-      throws RestApiException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException, NoSuchProjectException {
     if (input == null) {
       input = new TagInput();
     }
@@ -101,7 +104,11 @@
 
     ref = RefUtil.normalizeTagRef(ref);
 
-    RefControl refControl = resource.getControl().controlForRef(ref);
+    // TODO(hiesel): Remove dependency on RefControl
+    RefControl refControl =
+        projectControlFactory
+            .controlFor(resource.getNameKey(), resource.getUser())
+            .controlForRef(ref);
     PermissionBackend.ForRef perm =
         permissionBackend.user(identifiedUser).project(resource.getNameKey()).ref(ref);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
index a3fd09e..87b6fdf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
 import com.google.inject.TypeLiteral;
 import org.eclipse.jgit.lib.Config;
 
@@ -23,31 +24,38 @@
   public static final TypeLiteral<RestView<DashboardResource>> DASHBOARD_KIND =
       new TypeLiteral<RestView<DashboardResource>>() {};
 
-  public static DashboardResource projectDefault(ProjectControl ctl) {
-    return new DashboardResource(ctl, null, null, null, true);
+  public static DashboardResource projectDefault(ProjectState projectState, CurrentUser user) {
+    return new DashboardResource(projectState, user, null, null, null, true);
   }
 
-  private final ProjectControl control;
+  private final ProjectState projectState;
+  private final CurrentUser user;
   private final String refName;
   private final String pathName;
   private final Config config;
   private final boolean projectDefault;
 
   public DashboardResource(
-      ProjectControl control,
+      ProjectState projectState,
+      CurrentUser user,
       String refName,
       String pathName,
       Config config,
       boolean projectDefault) {
-    this.control = control;
+    this.projectState = projectState;
+    this.user = user;
     this.refName = refName;
     this.pathName = pathName;
     this.config = config;
     this.projectDefault = projectDefault;
   }
 
-  public ProjectControl getControl() {
-    return control;
+  public ProjectState getProjectState() {
+    return projectState;
+  }
+
+  public CurrentUser getUser() {
+    return user;
   }
 
   public String getRefName() {
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 a9e8fd3..d5c591f 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
@@ -106,9 +106,8 @@
   public DashboardResource parse(ProjectResource parent, IdString id)
       throws ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    ProjectControl myCtl = parent.getControl();
     if (isDefaultDashboard(id)) {
-      return DashboardResource.projectDefault(myCtl);
+      return DashboardResource.projectDefault(parent.getProjectState(), parent.getUser());
     }
 
     DashboardInfo info;
@@ -118,10 +117,9 @@
       throw new ResourceNotFoundException(id);
     }
 
-    CurrentUser user = myCtl.getUser();
-    for (ProjectState ps : myCtl.getProjectState().tree()) {
+    for (ProjectState ps : parent.getProjectState().tree()) {
       try {
-        return parse(ps.controlFor(user), info, myCtl);
+        return parse(ps, parent.getProjectState(), parent.getUser(), info);
       } catch (AmbiguousObjectException | ConfigInvalidException | IncorrectObjectTypeException e) {
         throw new ResourceNotFoundException(id);
       } catch (ResourceNotFoundException e) {
@@ -138,16 +136,13 @@
     return ref;
   }
 
-  private DashboardResource parse(ProjectControl ctl, DashboardInfo info, ProjectControl myCtl)
+  private DashboardResource parse(
+      ProjectState parent, ProjectState current, CurrentUser user, DashboardInfo info)
       throws ResourceNotFoundException, IOException, AmbiguousObjectException,
           IncorrectObjectTypeException, ConfigInvalidException, PermissionBackendException {
     String ref = normalizeDashboardRef(info.ref);
     try {
-      permissionBackend
-          .user(ctl.getUser())
-          .project(ctl.getProject().getNameKey())
-          .ref(ref)
-          .check(RefPermission.READ);
+      permissionBackend.user(user).project(parent.getNameKey()).ref(ref).check(RefPermission.READ);
     } catch (AuthException e) {
       // Don't leak the project's existence
       throw new ResourceNotFoundException(info.id);
@@ -156,13 +151,13 @@
       throw new ResourceNotFoundException(info.id);
     }
 
-    try (Repository git = gitManager.openRepository(ctl.getProject().getNameKey())) {
+    try (Repository git = gitManager.openRepository(parent.getNameKey())) {
       ObjectId objId = git.resolve(ref + ":" + info.path);
       if (objId == null) {
         throw new ResourceNotFoundException(info.id);
       }
       BlobBasedConfig cfg = new BlobBasedConfig(null, git, objId);
-      return new DashboardResource(myCtl, ref, info.path, cfg, false);
+      return new DashboardResource(current, user, ref, info.path, cfg, false);
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException(info.id);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
index ef5e41d..f577436 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -75,7 +75,7 @@
         if (state != null) {
           return state.controlFor(user).asForProject().database(db);
         }
-        return FailedPermissionBackend.project("not found");
+        return FailedPermissionBackend.project("not found", new NoSuchProjectException(project));
       } catch (IOException e) {
         return FailedPermissionBackend.project("unavailable", e);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
index 07c52f9..6b05ca1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_REF;
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 import static com.google.gerrit.server.permissions.RefPermission.READ;
+import static com.google.gerrit.server.permissions.RefPermission.WRITE_CONFIG;
 import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.ImmutableBiMap;
@@ -49,6 +50,7 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -90,7 +92,6 @@
   private final ProjectJson projectJson;
   private final ProjectCache projectCache;
   private final MetaDataUpdate.Server metaDataUpdateFactory;
-  private final ProjectControl.GenericFactory projectControlFactory;
   private final GroupBackend groupBackend;
   private final GroupJson groupJson;
 
@@ -103,7 +104,6 @@
       ProjectCache projectCache,
       MetaDataUpdate.Server metaDataUpdateFactory,
       ProjectJson projectJson,
-      ProjectControl.GenericFactory projectControlFactory,
       GroupBackend groupBackend,
       GroupJson groupJson) {
     this.user = self;
@@ -112,7 +112,6 @@
     this.allProjectsName = allProjectsName;
     this.projectJson = projectJson;
     this.projectCache = projectCache;
-    this.projectControlFactory = projectControlFactory;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.groupBackend = groupBackend;
     this.groupJson = groupJson;
@@ -121,11 +120,11 @@
   public ProjectAccessInfo apply(Project.NameKey nameKey)
       throws ResourceNotFoundException, ResourceConflictException, IOException,
           PermissionBackendException, OrmException {
-    try {
-      return apply(new ProjectResource(projectControlFactory.controlFor(nameKey, user.get())));
-    } catch (NoSuchProjectException e) {
+    ProjectState state = projectCache.checkedGet(nameKey);
+    if (state == null) {
       throw new ResourceNotFoundException(nameKey.get());
     }
+    return apply(new ProjectResource(state, user.get()));
   }
 
   @Override
@@ -138,7 +137,7 @@
 
     Project.NameKey projectName = rsrc.getNameKey();
     ProjectAccessInfo info = new ProjectAccessInfo();
-    ProjectControl pc = createProjectControl(projectName);
+    ProjectState projectState = projectCache.checkedGet(projectName);
     PermissionBackend.ForProject perm = permissionBackend.user(user).project(projectName);
 
     ProjectConfig config;
@@ -149,12 +148,12 @@
         md.setMessage("Update group names\n");
         config.commit(md);
         projectCache.evict(config.getProject());
-        pc = createProjectControl(projectName);
+        projectState = projectCache.checkedGet(projectName);
         perm = permissionBackend.user(user).project(projectName);
       } else if (config.getRevision() != null
-          && !config.getRevision().equals(pc.getProjectState().getConfig().getRevision())) {
+          && !config.getRevision().equals(projectState.getConfig().getRevision())) {
         projectCache.evict(config.getProject());
-        pc = createProjectControl(projectName);
+        projectState = projectCache.checkedGet(projectName);
         perm = permissionBackend.user(user).project(projectName);
       }
     } catch (ConfigInvalidException e) {
@@ -166,25 +165,26 @@
     info.local = new HashMap<>();
     info.ownerOf = new HashSet<>();
     Map<AccountGroup.UUID, GroupInfo> visibleGroups = new HashMap<>();
-    boolean checkReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
+    boolean canReadConfig = check(perm, ProjectPermission.READ_CONFIG);
+    boolean canWriteConfig = check(perm, ProjectPermission.WRITE_CONFIG);
 
     for (AccessSection section : config.getAccessSections()) {
       String name = section.getName();
       if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-        if (pc.isOwner()) {
+        if (canWriteConfig) {
           info.local.put(name, createAccessSection(visibleGroups, section));
           info.ownerOf.add(name);
 
-        } else if (checkReadConfig) {
+        } else if (canReadConfig) {
           info.local.put(section.getName(), createAccessSection(visibleGroups, section));
         }
 
       } else if (RefConfigSection.isValid(name)) {
-        if (pc.controlForRef(name).isOwner()) {
+        if (check(perm, name, WRITE_CONFIG)) {
           info.local.put(name, createAccessSection(visibleGroups, section));
           info.ownerOf.add(name);
 
-        } else if (checkReadConfig) {
+        } else if (canReadConfig) {
           info.local.put(name, createAccessSection(visibleGroups, section));
 
         } else if (check(perm, name, READ)) {
@@ -232,7 +232,7 @@
       info.revision = config.getRevision().name();
     }
 
-    ProjectState parent = Iterables.getFirst(pc.getProjectState().parents(), null);
+    ProjectState parent = Iterables.getFirst(projectState.parents(), null);
     if (parent != null) {
       info.inheritsFrom = projectJson.format(parent.getProject());
     }
@@ -242,13 +242,13 @@
       info.ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
     }
 
-    info.isOwner = toBoolean(pc.isOwner());
+    info.isOwner = toBoolean(canWriteConfig);
     info.canUpload =
         toBoolean(
-            pc.isOwner()
-                || (checkReadConfig && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE)));
+            canWriteConfig
+                || (canReadConfig && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE)));
     info.canAdd = toBoolean(perm.testOrFalse(CREATE_REF));
-    info.configVisible = checkReadConfig || pc.isOwner();
+    info.configVisible = canReadConfig || canWriteConfig;
 
     info.groups =
         visibleGroups
@@ -291,6 +291,16 @@
     }
   }
 
+  private static boolean check(PermissionBackend.ForProject ctx, ProjectPermission perm)
+      throws PermissionBackendException {
+    try {
+      ctx.check(perm);
+      return true;
+    } catch (AuthException denied) {
+      return false;
+    }
+  }
+
   private AccessSectionInfo createAccessSection(
       Map<AccountGroup.UUID, GroupInfo> groups, AccessSection section) throws OrmException {
     AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
@@ -316,15 +326,6 @@
     return accessSectionInfo;
   }
 
-  private ProjectControl createProjectControl(Project.NameKey projectName)
-      throws IOException, ResourceNotFoundException {
-    try {
-      return projectControlFactory.controlFor(projectName, user.get());
-    } catch (NoSuchProjectException e) {
-      throw new ResourceNotFoundException(projectName.get());
-    }
-  }
-
   private static Boolean toBoolean(boolean value) {
     return value ? true : null;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index b1ba281..c2f816e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -59,7 +59,8 @@
   public ConfigInfo apply(ProjectResource resource) {
     return new ConfigInfoImpl(
         serverEnableSignedPush,
-        resource.getControl(),
+        resource.getProjectState(),
+        resource.getUser(),
         config,
         pluginConfigEntries,
         cfgFactory,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
index cdf23bb..d4d9a54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -52,64 +53,63 @@
   }
 
   @Override
-  public DashboardInfo apply(DashboardResource resource)
+  public DashboardInfo apply(DashboardResource rsrc)
       throws RestApiException, IOException, PermissionBackendException {
-    if (inherited && !resource.isProjectDefault()) {
+    if (inherited && !rsrc.isProjectDefault()) {
       throw new BadRequestException("inherited flag can only be used with default");
     }
 
-    String project = resource.getControl().getProject().getName();
-    if (resource.isProjectDefault()) {
+    if (rsrc.isProjectDefault()) {
       // The default is not resolved to a definition yet.
       try {
-        resource = defaultOf(resource.getControl());
+        rsrc = defaultOf(rsrc.getProjectState(), rsrc.getUser());
       } catch (ConfigInvalidException e) {
         throw new ResourceConflictException(e.getMessage());
       }
     }
 
     return DashboardsCollection.parse(
-        resource.getControl().getProject(),
-        resource.getRefName().substring(REFS_DASHBOARDS.length()),
-        resource.getPathName(),
-        resource.getConfig(),
-        project,
+        rsrc.getProjectState().getProject(),
+        rsrc.getRefName().substring(REFS_DASHBOARDS.length()),
+        rsrc.getPathName(),
+        rsrc.getConfig(),
+        rsrc.getProjectState().getName(),
         true);
   }
 
-  private DashboardResource defaultOf(ProjectControl ctl)
+  private DashboardResource defaultOf(ProjectState projectState, CurrentUser user)
       throws ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
-    String id = ctl.getProject().getLocalDefaultDashboard();
+    String id = projectState.getProject().getLocalDefaultDashboard();
     if (Strings.isNullOrEmpty(id)) {
-      id = ctl.getProject().getDefaultDashboard();
+      id = projectState.getProject().getDefaultDashboard();
     }
     if (isDefaultDashboard(id)) {
       throw new ResourceNotFoundException();
     } else if (!Strings.isNullOrEmpty(id)) {
-      return parse(ctl, id);
+      return parse(projectState, user, id);
     } else if (!inherited) {
       throw new ResourceNotFoundException();
     }
 
-    for (ProjectState ps : ctl.getProjectState().tree()) {
+    for (ProjectState ps : projectState.tree()) {
       id = ps.getProject().getDefaultDashboard();
       if (isDefaultDashboard(id)) {
         throw new ResourceNotFoundException();
       } else if (!Strings.isNullOrEmpty(id)) {
-        ctl = ps.controlFor(ctl.getUser());
-        return parse(ctl, id);
+        return parse(projectState, user, id);
       }
     }
     throw new ResourceNotFoundException();
   }
 
-  private DashboardResource parse(ProjectControl ctl, String id)
+  private DashboardResource parse(ProjectState projectState, CurrentUser user, String id)
       throws ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
     List<String> p = Lists.newArrayList(Splitter.on(':').limit(2).split(id));
     String ref = Url.encode(p.get(0));
     String path = Url.encode(p.get(1));
-    return dashboards.parse(new ProjectResource(ctl), IdString.fromUrl(ref + ':' + path));
+    return dashboards.parse(
+        new ProjectResource(projectState, user), IdString.fromUrl(ref + ':' + path));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
index 31dc7bf..daaf4ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -72,10 +73,14 @@
           }
           throw new AuthException("not allowed to see HEAD");
         } catch (MissingObjectException | IncorrectObjectTypeException e) {
-          if (rsrc.getControl().isOwner()) {
-            return head.getObjectId().name();
+          try {
+            permissionBackend
+                .user(rsrc.getUser())
+                .project(rsrc.getNameKey())
+                .check(ProjectPermission.WRITE_CONFIG);
+          } catch (AuthException ae) {
+            throw new AuthException("not allowed to see HEAD");
           }
-          throw new AuthException("not allowed to see HEAD");
         }
       }
       throw new ResourceNotFoundException(Constants.HEAD);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
index 44d6a4f..9643e09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.projects.ReflogEntryInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -24,6 +23,9 @@
 import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.args4j.TimestampHandler;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -40,6 +42,7 @@
   private static final Logger log = LoggerFactory.getLogger(GetReflog.class);
 
   private final GitRepositoryManager repoManager;
+  private final PermissionBackend permissionBackend;
 
   @Option(
     name = "--limit",
@@ -83,15 +86,18 @@
   private Timestamp to;
 
   @Inject
-  public GetReflog(GitRepositoryManager repoManager) {
+  public GetReflog(GitRepositoryManager repoManager, PermissionBackend permissionBackend) {
     this.repoManager = repoManager;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
-  public List<ReflogEntryInfo> apply(BranchResource rsrc) throws RestApiException, IOException {
-    if (!rsrc.getControl().isOwner()) {
-      throw new AuthException("not project owner");
-    }
+  public List<ReflogEntryInfo> apply(BranchResource rsrc)
+      throws RestApiException, IOException, PermissionBackendException {
+    permissionBackend
+        .user(rsrc.getUser())
+        .project(rsrc.getNameKey())
+        .check(ProjectPermission.READ_REFLOG);
 
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       ReflogReader r;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index b2edc6b..645058f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -179,7 +179,6 @@
       }
     }
 
-    ProjectControl pctl = rsrc.getControl();
     PermissionBackend.ForProject perm = permissionBackend.user(user).project(rsrc.getNameKey());
     List<BranchInfo> branches = new ArrayList<>(refs.size());
     for (Ref ref : refs) {
@@ -207,7 +206,9 @@
       }
 
       if (perm.ref(ref.getName()).test(RefPermission.READ)) {
-        branches.add(createBranchInfo(perm.ref(ref.getName()), ref, pctl, targets));
+        branches.add(
+            createBranchInfo(
+                perm.ref(ref.getName()), ref, rsrc.getProjectState(), rsrc.getUser(), targets));
       }
     }
     Collections.sort(branches, new BranchComparator());
@@ -234,14 +235,18 @@
   }
 
   private BranchInfo createBranchInfo(
-      PermissionBackend.ForRef perm, Ref ref, ProjectControl pctl, Set<String> targets) {
+      PermissionBackend.ForRef perm,
+      Ref ref,
+      ProjectState projectState,
+      CurrentUser user,
+      Set<String> targets) {
     BranchInfo info = new BranchInfo();
     info.ref = ref.getName();
     info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
     info.canDelete =
         !targets.contains(ref.getName()) && perm.testOrFalse(RefPermission.DELETE) ? true : null;
 
-    BranchResource rsrc = new BranchResource(pctl, ref);
+    BranchResource rsrc = new BranchResource(projectState, user, ref);
     for (UiAction.Description d : uiActions.from(branchViews, rsrc)) {
       if (info.actions == null) {
         info.actions = new TreeMap<>();
@@ -249,7 +254,7 @@
       info.actions.put(d.getId(), new ActionInfo(d));
     }
 
-    List<WebLinkInfo> links = webLinks.getBranchLinks(pctl.getProject().getName(), ref.getName());
+    List<WebLinkInfo> links = webLinks.getBranchLinks(projectState.getName(), ref.getName());
     info.webLinks = links.isEmpty() ? null : links;
     return info;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index dc3610c..afb796e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -355,17 +355,15 @@
           continue;
         }
 
-        final ProjectControl pctl = e.controlFor(currentUser);
         if (groupUuid != null
-            && !pctl.getProjectState()
-                .getLocalGroups()
+            && !e.getLocalGroups()
                 .contains(GroupReference.forGroup(groupsCollection.parseId(groupUuid.get())))) {
           continue;
         }
 
         ProjectInfo info = new ProjectInfo();
         if (showTree && !format.isJson()) {
-          treeMap.put(projectName, projectNodeFactory.create(pctl.getProject(), true));
+          treeMap.put(projectName, projectNodeFactory.create(e.getProject(), true));
           continue;
         }
 
@@ -396,8 +394,17 @@
               if (!type.matches(git)) {
                 continue;
               }
-
-              List<Ref> refs = getBranchRefs(projectName, pctl);
+              boolean canReadAllRefs;
+              try {
+                permissionBackend
+                    .user(currentUser)
+                    .project(e.getNameKey())
+                    .check(ProjectPermission.READ);
+                canReadAllRefs = true;
+              } catch (AuthException ae) {
+                canReadAllRefs = false;
+              }
+              List<Ref> refs = getBranchRefs(projectName, canReadAllRefs);
               if (!hasValidRef(refs)) {
                 continue;
               }
@@ -592,13 +599,13 @@
     stdout.flush();
   }
 
-  private List<Ref> getBranchRefs(Project.NameKey projectName, ProjectControl projectControl) {
+  private List<Ref> getBranchRefs(Project.NameKey projectName, boolean canReadAllRefs) {
     Ref[] result = new Ref[showBranch.size()];
     try (Repository git = repoManager.openRepository(projectName)) {
       PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
       for (int i = 0; i < showBranch.size(); i++) {
         Ref ref = git.findRef(showBranch.get(i));
-        if (all && projectControl.isOwner()) {
+        if (all && canReadAllRefs) {
           result[i] = ref;
         } else if (ref != null && ref.getObjectId() != null) {
           try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 1166970..f189fb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -201,7 +201,7 @@
   }
 
   /** Is this user a project owner? */
-  public boolean isOwner() {
+  boolean isOwner() {
     return (isDeclaredOwner() && !controlForRef("refs/*").isBlocked(Permission.OWNER)) || isAdmin();
   }
 
@@ -474,6 +474,12 @@
           return canRunReceivePack();
         case RUN_UPLOAD_PACK:
           return canRunUploadPack();
+
+        case BAN_COMMIT:
+        case READ_REFLOG:
+        case READ_CONFIG:
+        case WRITE_CONFIG:
+          return isOwner();
       }
       throw new PermissionBackendException(perm + " unsupported");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
index a91ba62..22b7bd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
@@ -24,33 +24,32 @@
   public static final TypeLiteral<RestView<ProjectResource>> PROJECT_KIND =
       new TypeLiteral<RestView<ProjectResource>>() {};
 
-  private final ProjectControl control;
+  private final ProjectState projectState;
+  private final CurrentUser user;
 
-  public ProjectResource(ProjectControl control) {
-    this.control = control;
+  public ProjectResource(ProjectState projectState, CurrentUser user) {
+    this.projectState = projectState;
+    this.user = user;
   }
 
   ProjectResource(ProjectResource rsrc) {
-    this.control = rsrc.getControl();
+    this.projectState = rsrc.getProjectState();
+    this.user = rsrc.getUser();
   }
 
   public String getName() {
-    return control.getProject().getName();
+    return projectState.getName();
   }
 
   public Project.NameKey getNameKey() {
-    return control.getProject().getNameKey();
+    return projectState.getNameKey();
   }
 
   public ProjectState getProjectState() {
-    return control.getProjectState();
+    return projectState;
   }
 
   public CurrentUser getUser() {
-    return getControl().getUser();
-  }
-
-  public ProjectControl getControl() {
-    return control;
+    return user;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
index 2a79470..8d7b156 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -47,7 +47,7 @@
   private final DynamicMap<RestView<ProjectResource>> views;
   private final Provider<ListProjects> list;
   private final Provider<QueryProjects> queryProjects;
-  private final ProjectControl.GenericFactory controlFactory;
+  private final ProjectCache projectCache;
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
   private final CreateProject.Factory createProjectFactory;
@@ -59,14 +59,14 @@
       DynamicMap<RestView<ProjectResource>> views,
       Provider<ListProjects> list,
       Provider<QueryProjects> queryProjects,
-      ProjectControl.GenericFactory controlFactory,
+      ProjectCache projectCache,
       PermissionBackend permissionBackend,
       CreateProject.Factory factory,
       Provider<CurrentUser> user) {
     this.views = views;
     this.list = list;
     this.queryProjects = queryProjects;
-    this.controlFactory = controlFactory;
+    this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.createProjectFactory = factory;
@@ -139,10 +139,8 @@
     }
 
     Project.NameKey nameKey = new Project.NameKey(id);
-    ProjectControl ctl;
-    try {
-      ctl = controlFactory.controlFor(nameKey, user.get());
-    } catch (NoSuchProjectException e) {
+    ProjectState state = projectCache.checkedGet(nameKey);
+    if (state == null) {
       return null;
     }
 
@@ -153,7 +151,7 @@
         return null; // Pretend like not found on access denied.
       }
     }
-    return new ProjectResource(ctl);
+    return new ProjectResource(state, user.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index c4a7eb4..9dd8b43 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.api.projects.ConfigValue;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -40,6 +39,9 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -68,6 +70,7 @@
   private final UiActions uiActions;
   private final DynamicMap<RestView<ProjectResource>> views;
   private final Provider<CurrentUser> user;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   PutConfig(
@@ -81,7 +84,8 @@
       AllProjectsName allProjects,
       UiActions uiActions,
       DynamicMap<RestView<ProjectResource>> views,
-      Provider<CurrentUser> user) {
+      Provider<CurrentUser> user,
+      PermissionBackend permissionBackend) {
     this.serverEnableSignedPush = serverEnableSignedPush;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
@@ -93,13 +97,13 @@
     this.uiActions = uiActions;
     this.views = views;
     this.user = user;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
-  public ConfigInfo apply(ProjectResource rsrc, ConfigInput input) throws RestApiException {
-    if (!rsrc.getControl().isOwner()) {
-      throw new AuthException("restricted to project owner");
-    }
+  public ConfigInfo apply(ProjectResource rsrc, ConfigInput input)
+      throws RestApiException, PermissionBackendException {
+    permissionBackend.user(user).project(rsrc.getNameKey()).check(ProjectPermission.WRITE_CONFIG);
     return apply(rsrc.getProjectState(), input);
   }
 
@@ -192,7 +196,8 @@
       ProjectState state = projectStateFactory.create(projectConfig);
       return new ConfigInfoImpl(
           serverEnableSignedPush,
-          state.controlFor(user.get()),
+          state,
+          user.get(),
           config,
           pluginConfigEntries,
           cfgFactory,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
index 78230bd..a2808fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -26,6 +26,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -36,25 +39,31 @@
 public class PutDescription implements RestModifyView<ProjectResource, DescriptionInput> {
   private final ProjectCache cache;
   private final MetaDataUpdate.Server updateFactory;
+  private final PermissionBackend permissionBackend;
 
   @Inject
-  PutDescription(ProjectCache cache, MetaDataUpdate.Server updateFactory) {
+  PutDescription(
+      ProjectCache cache,
+      MetaDataUpdate.Server updateFactory,
+      PermissionBackend permissionBackend) {
     this.cache = cache;
     this.updateFactory = updateFactory;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
   public Response<String> apply(ProjectResource resource, DescriptionInput input)
-      throws AuthException, ResourceConflictException, ResourceNotFoundException, IOException {
+      throws AuthException, ResourceConflictException, ResourceNotFoundException, IOException,
+          PermissionBackendException {
     if (input == null) {
       input = new DescriptionInput(); // Delete would set description to null.
     }
 
-    ProjectControl ctl = resource.getControl();
-    IdentifiedUser user = ctl.getUser().asIdentifiedUser();
-    if (!ctl.isOwner()) {
-      throw new AuthException("not project owner");
-    }
+    IdentifiedUser user = resource.getUser().asIdentifiedUser();
+    permissionBackend
+        .user(user)
+        .project(resource.getNameKey())
+        .check(ProjectPermission.WRITE_CONFIG);
 
     try (MetaDataUpdate md = updateFactory.create(resource.getNameKey())) {
       ProjectConfig config = ProjectConfig.read(md);
@@ -70,7 +79,7 @@
       md.setAuthor(user);
       md.setMessage(msg);
       config.commit(md);
-      cache.evict(ctl.getProject());
+      cache.evict(resource.getProjectState().getProject());
       md.getRepository().setGitwebDescription(project.getDescription());
 
       return Strings.isNullOrEmpty(project.getDescription())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index f693bf5..9a4fe96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -551,6 +551,8 @@
           return canUpdate();
         case FORCE_UPDATE:
           return canForceUpdate();
+        case SET_HEAD:
+          return projectControl.isOwner();
 
         case FORGE_AUTHOR:
           return canForgeAuthor();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefResource.java
index 124439f..ac2735d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefResource.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.server.CurrentUser;
+
 public abstract class RefResource extends ProjectResource {
 
-  public RefResource(ProjectControl control) {
-    super(control);
+  public RefResource(ProjectState projectState, CurrentUser user) {
+    super(projectState, user);
   }
 
   /** @return the ref's name */
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 757f327..8f980ee 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
@@ -95,6 +95,7 @@
 
     // Users with the remove reviewer permission, the branch owner, project
     // owner and site admin can remove anyone
+    // TODO(hiesel): Remove all Control usage
     ProjectControl ctl = projectControlFactory.controlFor(change.getProject(), currentUser);
     if (ctl.controlForRef(change.getDest()).isOwner() // branch owner
         || ctl.isOwner() // project owner
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
index e875388..c768315 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -93,9 +94,12 @@
             permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER);
             checkedAdmin = true;
           }
-        } else if (!rsrc.getControl().controlForRef(section.getName()).isOwner()) {
-          throw new AuthException(
-              "You are not allowed to edit permissions for ref: " + section.getName());
+        } else {
+          permissionBackend
+              .user(identifiedUser)
+              .project(rsrc.getNameKey())
+              .ref(section.getName())
+              .check(RefPermission.WRITE_CONFIG);
         }
       }
 
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 9aa9ae7..0dd5f85 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
@@ -18,7 +18,6 @@
 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;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -29,7 +28,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -42,6 +43,7 @@
   private final MetaDataUpdate.Server updateFactory;
   private final DashboardsCollection dashboards;
   private final Provider<GetDashboard> get;
+  private final PermissionBackend permissionBackend;
 
   @Option(name = "--inherited", usage = "set dashboard inherited by children")
   private boolean inherited;
@@ -51,30 +53,35 @@
       ProjectCache cache,
       MetaDataUpdate.Server updateFactory,
       DashboardsCollection dashboards,
-      Provider<GetDashboard> get) {
+      Provider<GetDashboard> get,
+      PermissionBackend permissionBackend) {
     this.cache = cache;
     this.updateFactory = updateFactory;
     this.dashboards = dashboards;
     this.get = get;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
-  public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
+  public Response<DashboardInfo> apply(DashboardResource rsrc, SetDashboardInput input)
       throws RestApiException, IOException, PermissionBackendException {
     if (input == null) {
       input = new SetDashboardInput(); // Delete would set input to null.
     }
     input.id = Strings.emptyToNull(input.id);
 
-    ProjectControl ctl = resource.getControl();
-    if (!ctl.isOwner()) {
-      throw new AuthException("not project owner");
-    }
+    permissionBackend
+        .user(rsrc.getUser())
+        .project(rsrc.getProjectState().getNameKey())
+        .check(ProjectPermission.WRITE_CONFIG);
 
     DashboardResource target = null;
     if (input.id != null) {
       try {
-        target = dashboards.parse(new ProjectResource(ctl), IdString.fromUrl(input.id));
+        target =
+            dashboards.parse(
+                new ProjectResource(rsrc.getProjectState(), rsrc.getUser()),
+                IdString.fromUrl(input.id));
       } catch (ResourceNotFoundException e) {
         throw new BadRequestException("dashboard " + input.id + " not found");
       } catch (ConfigInvalidException e) {
@@ -82,7 +89,7 @@
       }
     }
 
-    try (MetaDataUpdate md = updateFactory.create(ctl.getProject().getNameKey())) {
+    try (MetaDataUpdate md = updateFactory.create(rsrc.getProjectState().getNameKey())) {
       ProjectConfig config = ProjectConfig.read(md);
       Project project = config.getProject();
       if (inherited) {
@@ -100,10 +107,10 @@
       if (!msg.endsWith("\n")) {
         msg += "\n";
       }
-      md.setAuthor(ctl.getUser().asIdentifiedUser());
+      md.setAuthor(rsrc.getUser().asIdentifiedUser());
       md.setMessage(msg);
       config.commit(md);
-      cache.evict(ctl.getProject());
+      cache.evict(rsrc.getProjectState().getProject());
 
       if (target != null) {
         DashboardInfo info = get.get().apply(target);
@@ -112,7 +119,7 @@
       }
       return Response.none();
     } catch (RepositoryNotFoundException notFound) {
-      throw new ResourceNotFoundException(ctl.getProject().getName());
+      throw new ResourceNotFoundException(rsrc.getProjectState().getProject().getName());
     } catch (ConfigInvalidException e) {
       throw new ResourceConflictException(
           String.format("invalid project.config: %s", e.getMessage()));
@@ -135,7 +142,8 @@
         throws RestApiException, IOException, PermissionBackendException {
       SetDefaultDashboard set = setDefault.get();
       set.inherited = inherited;
-      return set.apply(DashboardResource.projectDefault(resource.getControl()), input);
+      return set.apply(
+          DashboardResource.projectDefault(resource.getProjectState(), resource.getUser()), input);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index eeb47df..b1a6999 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -28,6 +28,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.SetHead.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -53,29 +56,35 @@
   private final GitRepositoryManager repoManager;
   private final Provider<IdentifiedUser> identifiedUser;
   private final DynamicSet<HeadUpdatedListener> headUpdatedListeners;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   SetHead(
       GitRepositoryManager repoManager,
       Provider<IdentifiedUser> identifiedUser,
-      DynamicSet<HeadUpdatedListener> headUpdatedListeners) {
+      DynamicSet<HeadUpdatedListener> headUpdatedListeners,
+      PermissionBackend permissionBackend) {
     this.repoManager = repoManager;
     this.identifiedUser = identifiedUser;
     this.headUpdatedListeners = headUpdatedListeners;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
   public String apply(ProjectResource rsrc, Input input)
       throws AuthException, ResourceNotFoundException, BadRequestException,
-          UnprocessableEntityException, IOException {
-    if (!rsrc.getControl().isOwner()) {
-      throw new AuthException("restricted to project owner");
-    }
+          UnprocessableEntityException, IOException, PermissionBackendException {
     if (input == null || Strings.isNullOrEmpty(input.ref)) {
       throw new BadRequestException("ref required");
     }
     String ref = RefNames.fullName(input.ref);
 
+    permissionBackend
+        .user(rsrc.getUser())
+        .project(rsrc.getNameKey())
+        .ref(ref)
+        .check(RefPermission.SET_HEAD);
+
     try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       Map<String, Ref> cur = repo.getRefDatabase().exactRef(Constants.HEAD, ref);
       if (!cur.containsKey(ref)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
index fe4d68d..08ef669 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
 import com.google.inject.TypeLiteral;
 
 public class TagResource extends RefResource {
@@ -24,8 +25,8 @@
 
   private final TagInfo tagInfo;
 
-  public TagResource(ProjectControl control, TagInfo tagInfo) {
-    super(control);
+  public TagResource(ProjectState projectState, CurrentUser user, TagInfo tagInfo) {
+    super(projectState, user);
     this.tagInfo = tagInfo;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
index 78670ad..7ee0a8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
@@ -48,9 +48,9 @@
   }
 
   @Override
-  public TagResource parse(ProjectResource resource, IdString id)
+  public TagResource parse(ProjectResource rsrc, IdString id)
       throws ResourceNotFoundException, IOException {
-    return new TagResource(resource.getControl(), list.get().get(resource, id));
+    return new TagResource(rsrc.getProjectState(), rsrc.getUser(), list.get().get(rsrc, id));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 17296bc..19549d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -23,10 +23,15 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
+  private static final Logger logger = LoggerFactory.getLogger(ChangeIsVisibleToPredicate.class);
+
   protected final Provider<ReviewDb> db;
   protected final ChangeNotes.Factory notesFactory;
   protected final CurrentUser user;
@@ -64,6 +69,10 @@
               .database(db)
               .test(ChangePermission.READ);
     } catch (PermissionBackendException e) {
+      if (e.getCause() instanceof NoSuchProjectException) {
+        logger.info("No such project: {}", cd.project());
+        return false;
+      }
       throw new OrmException("unable to check permissions", e);
     }
     if (visible) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index c7a85f0..ccc11aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -476,7 +476,10 @@
           new ChangeIdPredicate(parseChangeId(triplet.get().id().get())));
     }
     if (PAT_LEGACY_ID.matcher(query).matches()) {
-      return new LegacyChangeIdPredicate(Change.Id.parse(query));
+      Integer id = Ints.tryParse(query);
+      if (id != null) {
+        return new LegacyChangeIdPredicate(new Change.Id(id));
+      }
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
       return new ChangeIdPredicate(parseChangeId(query));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 1917d6f..785ae38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -79,7 +79,7 @@
     for (PatchSetApproval p : object.currentApprovals()) {
       if (labelType.matches(p)) {
         hasVote = true;
-        if (match(object, p.getValue(), p.getAccountId(), labelType)) {
+        if (match(object, p.getValue(), p.getAccountId())) {
           return true;
         }
       }
@@ -105,7 +105,7 @@
     return null;
   }
 
-  protected boolean match(ChangeData cd, short value, Account.Id approver, LabelType type) {
+  protected boolean match(ChangeData cd, short value, Account.Id approver) {
     if (value != expVal) {
       return false;
     }
@@ -119,11 +119,11 @@
       return false;
     }
 
-    // Double check the value is still permitted for the user.
+    // Check the user has 'READ' permission.
     try {
       PermissionBackend.ForChange perm =
           permissionBackend.user(reviewer).database(dbProvider).change(cd);
-      return perm.test(ChangePermission.READ) && expVal == perm.squashByTest(type, value);
+      return perm.test(ChangePermission.READ);
     } catch (PermissionBackendException e) {
       return false;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 19c0515..e3ff21a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -58,7 +58,7 @@
     List<Predicate<ChangeData>> r = new ArrayList<>();
     r.add(new ProjectPredicate(projectState.getName()));
     try {
-      ProjectResource proj = new ProjectResource(projectState.controlFor(self.get()));
+      ProjectResource proj = new ProjectResource(projectState, self.get());
       ListChildProjects children = listChildProjects.get();
       children.setRecursive(true);
       for (ProjectInfo p : children.apply(proj)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index 5c658a8..c274e56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -151,7 +151,7 @@
       return "org.postgresql.Driver";
     }
     if (url.contains(MYSQL)) {
-      return "com.mysql.cj.jdbc.Driver";
+      return "com.mysql.jdbc.Driver";
     }
     if (url.contains(MARIADB)) {
       return "org.mariadb.jdbc.Driver";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
index ffa2293..f4f19f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
@@ -28,7 +28,7 @@
 
   @Inject
   MySql(@GerritServerConfig Config cfg) {
-    super("com.mysql.cj.jdbc.Driver");
+    super("com.mysql.jdbc.Driver");
     this.cfg = cfg;
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index e036495..3299e11 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.config.TrackingFootersProvider;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalMergeSuperSetComputation;
 import com.google.gerrit.server.git.PerThreadRequestScope;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.SendEmailExecutor;
@@ -202,7 +203,7 @@
             return CanonicalWebUrlProvider.class;
           }
         });
-    //Replacement of DiffExecutorModule to not use thread pool in the tests
+    // Replacement of DiffExecutorModule to not use thread pool in the tests
     install(
         new AbstractModule() {
           @Override
@@ -220,6 +221,7 @@
     install(new SignedTokenEmailTokenVerifier.Module());
     install(new GpgModule(cfg));
     install(new InMemoryAccountPatchReviewStore.Module());
+    install(new LocalMergeSuperSetComputation.Module());
 
     bind(AllAccountsIndexer.class).toProvider(Providers.of(null));
     bind(AllChangesIndexer.class).toProvider(Providers.of(null));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index b9a98b9..710b3dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
@@ -31,7 +30,7 @@
 
 public abstract class AbstractGitCommand extends BaseCommand {
   @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
-  protected ProjectControl projectControl;
+  protected ProjectState projectState;
 
   @Inject private SshScope sshScope;
 
@@ -41,12 +40,9 @@
 
   @Inject private SshScope.Context context;
 
-  @Inject private IdentifiedUser user;
-
   @Inject private IdentifiedUser.GenericFactory userFactory;
 
   protected Repository repo;
-  protected ProjectState state;
   protected Project.NameKey projectName;
   protected Project project;
 
@@ -69,7 +65,7 @@
 
             @Override
             public Project.NameKey getProjectName() {
-              return projectControl.getProjectState().getNameKey();
+              return projectState.getNameKey();
             }
           });
     } finally {
@@ -88,8 +84,7 @@
   }
 
   private void service() throws IOException, PermissionBackendException, Failure {
-    state = projectControl.getProjectState();
-    project = state.getProject();
+    project = projectState.getProject();
     projectName = project.getNameKey();
 
     try {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 6923ad1..fa3a0f5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -79,6 +79,8 @@
 
   private ExitCallback exit;
 
+  @Inject protected CurrentUser user;
+
   @Inject private SshScope sshScope;
 
   @Inject private CmdLineParser.Factory cmdLineParserFactory;
@@ -88,7 +90,6 @@
   @Inject @CommandExecutor private ScheduledThreadPoolExecutor executor;
 
   @Inject private PermissionBackend permissionBackend;
-  @Inject private CurrentUser user;
 
   @Inject private SshScope.Context context;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index 1c55f48..d5fc4547 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -67,15 +67,15 @@
   }
 
   public void addChange(
-      String id, Map<Change.Id, ChangeResource> changes, ProjectControl projectControl)
+      String id, Map<Change.Id, ChangeResource> changes, ProjectState projectState)
       throws UnloggedFailure, OrmException, PermissionBackendException {
-    addChange(id, changes, projectControl, true);
+    addChange(id, changes, projectState, true);
   }
 
   public void addChange(
       String id,
       Map<Change.Id, ChangeResource> changes,
-      ProjectControl projectControl,
+      ProjectState projectState,
       boolean useIndex)
       throws UnloggedFailure, OrmException, PermissionBackendException {
     List<ChangeNotes> matched = useIndex ? changeFinder.find(id) : changeFromNotesFactory(id);
@@ -89,7 +89,7 @@
     }
     for (ChangeNotes notes : matched) {
       if (!changes.containsKey(notes.getChangeId())
-          && inProject(projectControl, notes.getProjectName())
+          && inProject(projectState, notes.getProjectName())
           && (canMaintainServer
               || permissionBackend
                   .user(currentUser)
@@ -127,9 +127,9 @@
     }
   }
 
-  private boolean inProject(ProjectControl projectControl, Project.NameKey project) {
-    if (projectControl != null) {
-      return projectControl.getProject().getNameKey().equals(project);
+  private boolean inProject(ProjectState projectState, Project.NameKey project) {
+    if (projectState != null) {
+      return projectState.getNameKey().equals(project);
     }
 
     // No --project option, so they want every project.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 6649fcb..c6e00aa 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.common.ProjectInfo;
@@ -24,7 +26,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -57,21 +58,21 @@
     metaVar = "NAME",
     usage = "new parent project"
   )
-  private ProjectControl newParent;
+  private ProjectState newParent;
 
   @Option(
     name = "--children-of",
     metaVar = "NAME",
     usage = "parent project for which the child projects should be reparented"
   )
-  private ProjectControl oldParent;
+  private ProjectState oldParent;
 
   @Option(
     name = "--exclude",
     metaVar = "NAME",
     usage = "child project of old parent project which should not be reparented"
   )
-  private List<ProjectControl> excludedChildren = new ArrayList<>();
+  private List<ProjectState> excludedChildren = new ArrayList<>();
 
   @Argument(
     index = 0,
@@ -80,7 +81,7 @@
     metaVar = "NAME",
     usage = "projects to modify"
   )
-  private List<ProjectControl> children = new ArrayList<>();
+  private List<ProjectState> children = new ArrayList<>();
 
   @Inject private ProjectCache projectCache;
 
@@ -125,10 +126,8 @@
       }
     }
 
-    final List<Project.NameKey> childProjects = new ArrayList<>();
-    for (ProjectControl pc : children) {
-      childProjects.add(pc.getProject().getNameKey());
-    }
+    final List<Project.NameKey> childProjects =
+        children.stream().map(ProjectState::getNameKey).collect(toList());
     if (oldParent != null) {
       try {
         childProjects.addAll(getChildrenForReparenting(oldParent));
@@ -196,18 +195,18 @@
    * list of child projects does not contain projects that were specified to be excluded from
    * reparenting.
    */
-  private List<Project.NameKey> getChildrenForReparenting(ProjectControl parent)
+  private List<Project.NameKey> getChildrenForReparenting(ProjectState parent)
       throws PermissionBackendException {
     final List<Project.NameKey> childProjects = new ArrayList<>();
     final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size());
-    for (ProjectControl excludedChild : excludedChildren) {
+    for (ProjectState excludedChild : excludedChildren) {
       excluded.add(excludedChild.getProject().getNameKey());
     }
     final List<Project.NameKey> automaticallyExcluded = new ArrayList<>(excludedChildren.size());
     if (newParentKey != null) {
       automaticallyExcluded.addAll(getAllParents(newParentKey));
     }
-    for (ProjectInfo child : listChildProjects.apply(new ProjectResource(parent))) {
+    for (ProjectInfo child : listChildProjects.apply(new ProjectResource(parent, user))) {
       final Project.NameKey childName = new Project.NameKey(child.name);
       if (!excluded.contains(childName)) {
         if (!automaticallyExcluded.contains(childName)) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 22d7e9a..81347d8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -20,8 +20,8 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.server.project.BanCommit;
 import com.google.gerrit.server.project.BanCommit.BanResultInfo;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -51,7 +51,7 @@
     metaVar = "PROJECT",
     usage = "name of the project for which the commit should be banned"
   )
-  private ProjectControl projectControl;
+  private ProjectState projectState;
 
   @Argument(
     index = 1,
@@ -71,7 +71,7 @@
           BanCommit.Input.fromCommits(Lists.transform(commitsToBan, ObjectId::getName));
       input.reason = reason;
 
-      BanResultInfo r = banCommit.apply(new ProjectResource(projectControl), input);
+      BanResultInfo r = banCommit.apply(new ProjectResource(projectState, user), input);
       printCommits(r.newlyBanned, "The following commits were banned");
       printCommits(r.alreadyBanned, "The following commits were already banned");
       printCommits(r.ignored, "The following ids do not represent commits and were ignored");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
index 5962faa..fd1e189 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateBranchCommand.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -28,7 +28,7 @@
 public final class CreateBranchCommand extends SshCommand {
 
   @Argument(index = 0, required = true, metaVar = "PROJECT", usage = "name of the project")
-  private ProjectControl project;
+  private ProjectState project;
 
   @Argument(index = 1, required = true, metaVar = "NAME", usage = "name of branch to be created")
   private String name;
@@ -48,7 +48,7 @@
     try {
       BranchInput in = new BranchInput();
       in.revision = revision;
-      gApi.projects().name(project.getProject().getNameKey().get()).branch(name).create(in);
+      gApi.projects().name(project.getName()).branch(name).create(in);
     } catch (RestApiException e) {
       throw die(e);
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 0df2a80..d6ecb0a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SuggestParentCandidates;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -68,7 +68,7 @@
     metaVar = "NAME",
     usage = "parent project"
   )
-  private ProjectControl newParent;
+  private ProjectState newParent;
 
   @Option(name = "--permissions-only", usage = "create project for use only as parent")
   private boolean permissionsOnly;
@@ -188,7 +188,7 @@
           input.owners = Lists.transform(ownerIds, AccountGroup.UUID::get);
         }
         if (newParent != null) {
-          input.parent = newParent.getProject().getName();
+          input.parent = newParent.getName();
         }
         input.permissionsOnly = permissionsOnly;
         input.description = projectDescription;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index b0b26fa..25f0e77 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GarbageCollectionResult;
@@ -24,7 +25,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -54,7 +55,7 @@
     metaVar = "NAME",
     usage = "projects for which the Git garbage collection should be run"
   )
-  private List<ProjectControl> projects = new ArrayList<>();
+  private List<ProjectState> projects = new ArrayList<>();
 
   @Inject private ProjectCache projectCache;
 
@@ -80,10 +81,7 @@
     if (all) {
       projectNames = Lists.newArrayList(projectCache.all());
     } else {
-      projectNames = Lists.newArrayListWithCapacity(projects.size());
-      for (ProjectControl pc : projects) {
-        projectNames.add(pc.getProject().getNameKey());
-      }
+      projectNames = projects.stream().map(ProjectState::getNameKey).collect(toList());
     }
 
     GarbageCollectionResult result =
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java
index 476c25b..ba937a2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexProjectCommand.java
@@ -18,8 +18,8 @@
 
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.server.project.Index;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -40,7 +40,7 @@
     metaVar = "PROJECT",
     usage = "projects for which the changes should be indexed"
   )
-  private List<ProjectControl> projects = new ArrayList<>();
+  private List<ProjectState> projects = new ArrayList<>();
 
   @Override
   protected void run() throws UnloggedFailure, Failure, Exception {
@@ -50,14 +50,12 @@
     projects.stream().forEach(this::index);
   }
 
-  private void index(ProjectControl projectControl) {
+  private void index(ProjectState projectState) {
     try {
-      index.apply(new ProjectResource(projectControl), null);
+      index.apply(new ProjectResource(projectState, user), null);
     } catch (Exception e) {
       writeError(
-          "error",
-          String.format(
-              "Unable to index %s: %s", projectControl.getProject().getName(), e.getMessage()));
+          "error", String.format("Unable to index %s: %s", projectState.getName(), e.getMessage()));
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 275da7c..e467cc4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.VisibleRefFilter;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -59,7 +59,7 @@
     required = true,
     usage = "project for which the refs should be listed"
   )
-  private ProjectControl projectControl;
+  private ProjectState projectState;
 
   @Option(
     name = "--user",
@@ -87,13 +87,13 @@
       return;
     }
 
-    Project.NameKey projectName = projectControl.getProject().getNameKey();
+    Project.NameKey projectName = projectState.getNameKey();
     try (Repository repo = repoManager.openRepository(projectName);
         ManualRequestContext ctx = requestContext.openAs(userAccount.getId())) {
       try {
         Map<String, Ref> refsMap =
             refFilterFactory
-                .create(projectControl.getProjectState(), repo)
+                .create(projectState, repo)
                 .filter(repo.getRefDatabase().getRefs(ALL), false);
 
         for (String ref : refsMap.keySet()) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PatchSetParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PatchSetParser.java
index c3613b1..9fcd201 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PatchSetParser.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PatchSetParser.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
@@ -57,15 +57,15 @@
     this.changeFinder = changeFinder;
   }
 
-  public PatchSet parsePatchSet(String token, ProjectControl projectControl, String branch)
+  public PatchSet parsePatchSet(String token, ProjectState projectState, String branch)
       throws UnloggedFailure, OrmException {
     // By commit?
     //
     if (token.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
       InternalChangeQuery query = queryProvider.get();
       List<ChangeData> cds;
-      if (projectControl != null) {
-        Project.NameKey p = projectControl.getProject().getNameKey();
+      if (projectState != null) {
+        Project.NameKey p = projectState.getNameKey();
         if (branch != null) {
           cds = query.byBranchCommit(p.get(), branch, token);
         } else {
@@ -77,7 +77,7 @@
       List<PatchSet> matches = new ArrayList<>(cds.size());
       for (ChangeData cd : cds) {
         Change c = cd.change();
-        if (!(inProject(c, projectControl) && inBranch(c, branch))) {
+        if (!(inProject(c, projectState) && inBranch(c, branch))) {
           continue;
         }
         for (PatchSet ps : cd.patchSets()) {
@@ -106,19 +106,15 @@
       } catch (IllegalArgumentException e) {
         throw error("\"" + token + "\" is not a valid patch set");
       }
-      ChangeNotes notes = getNotes(projectControl, patchSetId.getParentKey());
+      ChangeNotes notes = getNotes(projectState, patchSetId.getParentKey());
       PatchSet patchSet = psUtil.get(db.get(), notes, patchSetId);
       if (patchSet == null) {
         throw error("\"" + token + "\" no such patch set");
       }
-      if (projectControl != null || branch != null) {
+      if (projectState != null || branch != null) {
         Change change = notes.getChange();
-        if (!inProject(change, projectControl)) {
-          throw error(
-              "change "
-                  + change.getId()
-                  + " not in project "
-                  + projectControl.getProject().getName());
+        if (!inProject(change, projectState)) {
+          throw error("change " + change.getId() + " not in project " + projectState.getName());
         }
         if (!inBranch(change, branch)) {
           throw error("change " + change.getId() + " not in branch " + branch);
@@ -130,10 +126,10 @@
     throw error("\"" + token + "\" is not a valid patch set");
   }
 
-  private ChangeNotes getNotes(@Nullable ProjectControl projectControl, Change.Id changeId)
+  private ChangeNotes getNotes(@Nullable ProjectState projectState, Change.Id changeId)
       throws OrmException, UnloggedFailure {
-    if (projectControl != null) {
-      return notesFactory.create(db.get(), projectControl.getProject().getNameKey(), changeId);
+    if (projectState != null) {
+      return notesFactory.create(db.get(), projectState.getNameKey(), changeId);
     }
     try {
       ChangeNotes notes = changeFinder.findOne(changeId);
@@ -143,12 +139,12 @@
     }
   }
 
-  private static boolean inProject(Change change, ProjectControl projectControl) {
-    if (projectControl == null) {
+  private static boolean inProject(Change change, ProjectState projectState) {
+    if (projectState == null) {
       // No --project option, so they want every project.
       return true;
     }
-    return projectControl.getProject().getNameKey().equals(change.getProject());
+    return projectState.getNameKey().equals(change.getProject());
   }
 
   private static boolean inBranch(Change change, String branch) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index 0f68d61..06fbcfc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -93,7 +93,7 @@
       throw new Failure(1, "fatal: unable to check permissions " + e);
     }
 
-    AsyncReceiveCommits arc = factory.create(projectControl, repo, null, reviewers);
+    AsyncReceiveCommits arc = factory.create(projectState, currentUser, repo, null, reviewers);
 
     Capable r = arc.canUpload();
     if (r != Capable.OK) {
@@ -110,9 +110,7 @@
       // we want to present this error to the user
       if (badStream.getCause() instanceof TooLargeObjectInPackException) {
         StringBuilder msg = new StringBuilder();
-        msg.append("Receive error on project \"")
-            .append(projectControl.getProject().getName())
-            .append("\"");
+        msg.append("Receive error on project \"").append(projectState.getName()).append("\"");
         msg.append(" (user ");
         msg.append(currentUser.getAccount().getUserName());
         msg.append(" account ");
@@ -127,9 +125,7 @@
       // Log what the heck is going on, as detailed as we can.
       //
       StringBuilder msg = new StringBuilder();
-      msg.append("Unpack error on project \"")
-          .append(projectControl.getProject().getName())
-          .append("\":\n");
+      msg.append("Unpack error on project \"").append(projectState.getName()).append("\":\n");
 
       msg.append("  AdvertiseRefsHook: ").append(rp.getAdvertiseRefsHook());
       if (rp.getAdvertiseRefsHook() == AdvertiseRefsHook.DEFAULT) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 2a82a26..1d764b9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -81,7 +80,7 @@
   )
   void addPatchSetId(String token) {
     try {
-      PatchSet ps = psParser.parsePatchSet(token, projectControl, branch);
+      PatchSet ps = psParser.parsePatchSet(token, projectState, branch);
       patchSets.add(ps);
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
@@ -95,7 +94,7 @@
     aliases = "-p",
     usage = "project containing the specified patch set(s)"
   )
-  private ProjectControl projectControl;
+  private ProjectState projectState;
 
   @Option(name = "--branch", aliases = "-b", usage = "branch containing the specified patch set(s)")
   private String branch;
@@ -135,12 +134,6 @@
   private boolean json;
 
   @Option(
-    name = "--strict-labels",
-    usage = "Strictly check if the labels specified can be applied to the given patch set(s)"
-  )
-  private boolean strictLabels;
-
-  @Option(
     name = "--tag",
     aliases = "-t",
     usage = "applies a tag to the given review",
@@ -274,7 +267,6 @@
     review.notify = notify;
     review.labels = new TreeMap<>();
     review.drafts = ReviewInput.DraftHandling.PUBLISH;
-    review.strictLabels = strictLabels;
     for (ApproveOption ao : optionList) {
       Short v = ao.value();
       if (v != null) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 656d377..466e8f0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -146,7 +146,6 @@
 
   @Inject private DeleteSshKey deleteSshKey;
 
-  private IdentifiedUser user;
   private AccountResource rsrc;
 
   @Override
@@ -182,7 +181,7 @@
       throws OrmException, IOException, UnloggedFailure, ConfigInvalidException,
           PermissionBackendException {
     user = genericUserFactory.create(id);
-    rsrc = new AccountResource(user);
+    rsrc = new AccountResource(user.asIdentifiedUser());
     try {
       for (String email : addEmails) {
         addEmail(email);
@@ -266,7 +265,7 @@
           ConfigInvalidException, PermissionBackendException {
     AccountSshKey sshKey =
         new AccountSshKey(new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
-    deleteSshKey.apply(new AccountResource.SshKey(user, sshKey), null);
+    deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
   }
 
   private void addEmail(String email)
@@ -288,10 +287,10 @@
     if (email.equals("ALL")) {
       List<EmailInfo> emails = getEmails.apply(rsrc);
       for (EmailInfo e : emails) {
-        deleteEmail.apply(new AccountResource.Email(user, e.email), new Input());
+        deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), e.email), new Input());
       }
     } else {
-      deleteEmail.apply(new AccountResource.Email(user, email), new Input());
+      deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), email), new Input());
     }
   }
 
@@ -300,7 +299,7 @@
           ConfigInvalidException {
     for (EmailInfo e : getEmails.apply(rsrc)) {
       if (e.email.equals(email)) {
-        putPreferred.apply(new AccountResource.Email(user, email), null);
+        putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
         return;
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
index ce4116d..eea57cd 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SetHead;
 import com.google.gerrit.server.project.SetHead.Input;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -29,7 +29,7 @@
 public class SetHeadCommand extends SshCommand {
 
   @Argument(index = 0, required = true, metaVar = "NAME", usage = "name of the project")
-  private ProjectControl project;
+  private ProjectState project;
 
   @Option(name = "--new-head", required = true, metaVar = "REF", usage = "new HEAD reference")
   private String newHead;
@@ -46,7 +46,7 @@
     Input input = new SetHead.Input();
     input.ref = newHead;
     try {
-      setHead.apply(new ProjectResource(project), input);
+      setHead.apply(new ProjectResource(project, user), input);
     } catch (UnprocessableEntityException e) {
       throw die(e);
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index c275af8..a963a35 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -17,11 +17,11 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
-import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.PutConfig;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -32,7 +32,7 @@
 @CommandMetaData(name = "set-project", description = "Change a project's settings")
 final class SetProjectCommand extends SshCommand {
   @Argument(index = 0, required = true, metaVar = "NAME", usage = "name of the project")
-  private ProjectControl projectControl;
+  private ProjectState projectState;
 
   @Option(
     name = "--description",
@@ -148,19 +148,19 @@
     configInput.useContentMerge = contentMerge;
     configInput.useContributorAgreements = contributorAgreements;
     configInput.useSignedOffBy = signedOffBy;
-    configInput.state = state;
+    configInput.state = state.getProject().getState();
     configInput.maxObjectSizeLimit = maxObjectSizeLimit;
     // Description is different to other parameters, null won't result in
     // keeping the existing description, it would delete it.
     if (Strings.emptyToNull(projectDescription) != null) {
       configInput.description = projectDescription;
     } else {
-      configInput.description = projectControl.getProject().getDescription();
+      configInput.description = projectState.getProject().getDescription();
     }
 
     try {
-      putConfig.apply(new ProjectResource(projectControl), configInput);
-    } catch (RestApiException e) {
+      putConfig.apply(new ProjectResource(projectState, user), configInput);
+    } catch (RestApiException | PermissionBackendException e) {
       throw die(e);
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 026f9b7..85cf467 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -46,7 +46,7 @@
   private static final Logger log = LoggerFactory.getLogger(SetReviewersCommand.class);
 
   @Option(name = "--project", aliases = "-p", usage = "project containing the change")
-  private ProjectControl projectControl;
+  private ProjectState projectState;
 
   @Option(
     name = "--add",
@@ -75,7 +75,7 @@
   )
   void addChange(String token) {
     try {
-      changeArgumentParser.addChange(token, changes, projectControl);
+      changeArgumentParser.addChange(token, changes, projectState);
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index 7049c7f..0d78279 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -51,8 +51,8 @@
   protected void runImpl() throws IOException, Failure {
     try {
       permissionBackend
-          .user(projectControl.getUser())
-          .project(projectControl.getProject().getNameKey())
+          .user(user)
+          .project(projectState.getNameKey())
           .check(ProjectPermission.RUN_UPLOAD_PACK);
     } catch (AuthException e) {
       throw new Failure(1, "fatal: upload-pack not permitted on this server");
@@ -61,7 +61,7 @@
     }
 
     final UploadPack up = new UploadPack(repo);
-    up.setAdvertiseRefsHook(refFilterFactory.create(projectControl.getProjectState(), repo));
+    up.setAdvertiseRefsHook(refFilterFactory.create(projectState, repo));
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
     up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
@@ -71,7 +71,7 @@
         uploadValidatorsFactory.create(project, repo, session.getRemoteAddressAsString()));
     up.setPreUploadHook(PreUploadHookChain.newChain(allPreUploadHooks));
     for (UploadPackInitializer initializer : uploadPackInitializers) {
-      initializer.init(projectControl.getProject().getNameKey(), up);
+      initializer.init(projectState.getNameKey(), up);
     }
     try {
       up.upload(in, out, err);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 9a3e6ab..41cc485b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -18,7 +18,6 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.AllowedFormats;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -122,7 +121,6 @@
 
   @Inject private PermissionBackend permissionBackend;
   @Inject private CommitsCollection commits;
-  @Inject private IdentifiedUser user;
   @Inject private AllowedFormats allowedFormats;
   private Options options = new Options();
 
@@ -250,7 +248,7 @@
       // Check reachability of the specific revision.
       try (RevWalk rw = new RevWalk(repo)) {
         RevCommit commit = rw.parseCommit(revId);
-        return commits.canRead(state, repo, commit);
+        return commits.canRead(projectState, repo, commit);
       }
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
index b44f0fc..1858f40 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.sshd.CommandModule;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
@@ -55,15 +54,13 @@
   }
 
   private final DynamicItem<LfsSshPluginAuth> auth;
-  private final Provider<CurrentUser> user;
 
   @Argument(index = 0, multiValued = true, metaVar = "PARAMS")
   private List<String> args = new ArrayList<>();
 
   @Inject
-  LfsPluginAuthCommand(DynamicItem<LfsSshPluginAuth> auth, Provider<CurrentUser> user) {
+  LfsPluginAuthCommand(DynamicItem<LfsSshPluginAuth> auth) {
     this.auth = auth;
-    this.user = user;
   }
 
   @Override
@@ -74,6 +71,6 @@
       throw new UnloggedFailure(1, CONFIGURATION_ERROR);
     }
 
-    stdout.print(pluginAuth.authenticate(user.get(), args));
+    stdout.print(pluginAuth.authenticate(user, args));
   }
 }
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 9a02fcd..e2f3dcf 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -50,6 +50,7 @@
 import com.google.gerrit.server.events.StreamEventsApiListener;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.GitRepositoryManagerModule;
+import com.google.gerrit.server.git.LocalMergeSuperSetComputation;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
@@ -325,6 +326,7 @@
     modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
+    modules.add(new LocalMergeSuperSetComputation.Module());
 
     // Plugin module needs to be inserted *before* the index module.
     // There is the concept of LifecycleModule, in Gerrit's own extension
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
index 9721e22..cb0a256 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
@@ -46,7 +46,7 @@
         <Set name="password">secretkey</Set>
 -->
 <!--  MySQL
-        <Set name="driverClassName">com.mysql.cj.jdbc.Driver</Set>
+        <Set name="driverClassName">com.mysql.jdbc.Driver</Set>
         <Set name="url">jdbc:mysql://localhost/reviewdb?user=gerrit&amp;password=secretkey</Set>
 -->
 <!--  MariaDB
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 412d67b..9b409e9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -103,12 +103,30 @@
       'j': '_handleJKey',
       'k': '_handleKKey',
       'n ]': '_handleNKey',
-      'o enter': '_handleEnterKey',
+      'o': '_handleOKey',
       'p [': '_handlePKey',
       'shift+r': '_handleRKey',
       's': '_handleSKey',
     },
 
+    listeners: {
+      keydown: '_scopedKeydownHandler',
+    },
+
+    /**
+     * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
+     * events must be scoped to a component level (e.g. `enter`) in order to not
+     * override native browser functionality.
+     *
+     * Context: Issue 7294
+     */
+    _scopedKeydownHandler(e) {
+      if (e.keyCode === 13) {
+        // Enter.
+        this._handleOKey(e);
+      }
+    },
+
     attached() {
       this._loadPreferences();
     },
@@ -231,7 +249,7 @@
       this.selectedIndex -= 1;
     },
 
-    _handleEnterKey(e) {
+    _handleOKey(e) {
       if (this.shouldSuppressKeyboardShortcut(e) ||
           this.modifierPressed(e)) { return; }
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 8f2468b..2cda515 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -666,7 +666,7 @@
 
     _handleActionTap(e) {
       e.preventDefault();
-      const el = e.currentTarget;
+      const el = Polymer.dom(e).localTarget;
       const key = el.getAttribute('data-action-key');
       if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX)) {
         this.fire(`${key}-tap`, {node: el});
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index fd114e9..f09e23a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -158,9 +158,6 @@
       .mobile {
         display: none;
       }
-      .expandInline {
-        padding-right: .25em;
-      }
       .reviewed {
         margin-left: 2em;
         width: 15em;
@@ -337,7 +334,7 @@
       </template>
     </div>
     <div
-        class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]"
+        class="row totalChanges"
         hidden$="[[_hideChangeTotals]]">
       <div class="total-stats">
         <span
@@ -358,7 +355,7 @@
       <div class="editFileControls showOnEdit"></div>
     </div>
     <div
-        class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]"
+        class="row totalChanges"
         hidden$="[[_hideBinaryChangeTotals]]">
       <div class="total-stats">
         <span class="added" aria-label="Total lines added">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 70f1d4d..25b5686 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -612,8 +612,7 @@
     },
 
     _handleRKey(e) {
-      if (this.shouldSuppressKeyboardShortcut(e) ||
-          this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
+      if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
         return;
       }
 
@@ -717,10 +716,6 @@
       return classes.join(' ');
     },
 
-    _computeExpandInlineClass(userPrefs) {
-      return userPrefs.expand_inline_diffs ? 'expandInline' : '';
-    },
-
     _computePathClass(path, expandedFilesRecord) {
       return this._isFileExpanded(path, expandedFilesRecord) ? 'path expanded' :
           'path';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index b01c105..f6e63b2b 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -580,10 +580,6 @@
       assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
       assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
           'clazz invisible');
-      assert.equal(element._computeExpandInlineClass(
-          {expand_inline_diffs: true}), 'expandInline');
-      assert.equal(element._computeExpandInlineClass(
-        {expand_inline_diffs: false}), '');
     });
 
     test('file review status', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 7023e0b..81f44b7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -93,7 +93,11 @@
         padding-top: 0;
       }
       .action {
-        margin-left: .5em;
+        margin-left: 1em;
+        --gr-button: {
+          color: #212121;
+        }
+        --gr-button-hover-color: rgba(33, 33, 33, .75);
       }
       .rightActions {
         display: flex;
@@ -282,18 +286,20 @@
           Unresolved
         </div>
         <div class="rightActions">
-          <gr-button class="action edit hideOnPublished" on-tap="_handleEdit">
-              Edit</gr-button>
-          <gr-button class="action save hideOnPublished" on-tap="_handleSave"
-              disabled$="[[_computeSaveDisabled(_messageText)]]">Save</gr-button>
-          <gr-button class="action cancel hideOnPublished"
+          <gr-button link class="action edit hideOnPublished"
+              on-tap="_handleEdit">Edit</gr-button>
+          <gr-button link class="action save hideOnPublished"
+              on-tap="_handleSave"
+              disabled$="[[_computeSaveDisabled(_messageText)]]">Save
+          </gr-button>
+          <gr-button link class="action cancel hideOnPublished"
               on-tap="_handleCancel" hidden>Cancel</gr-button>
-          <gr-button class="action discard hideOnPublished"
+          <gr-button link class="action discard hideOnPublished"
               on-tap="_handleDiscard">Discard</gr-button>
         </div>
       </div>
       <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
-        <gr-button class="action fix"
+        <gr-button link class="action fix"
             on-tap="_handleFix"
             disabled="[[robotButtonDisabled]]">
           Please Fix
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index f9ca92e..7e6d54d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -66,12 +66,15 @@
       .actions {
         border-top: 1px solid #ddd;
         display: flex;
-        justify-content: space-between;
+        justify-content: flex-end;
       }
       .beta {
         font-family: var(--font-family-bold);
         color: #888;
       }
+      gr-button {
+        margin-left: 1em;
+      }
     </style>
 
     <gr-overlay id="prefsOverlay" with-backdrop>
@@ -140,8 +143,10 @@
         </div>
       </div>
       <div class="actions">
-        <gr-button id="saveButton" primary on-tap="_handleSave">Save</gr-button>
-        <gr-button id="cancelButton" on-tap="_handleCancel">Cancel</gr-button>
+        <gr-button id="cancelButton" link on-tap="_handleCancel">
+            Cancel</gr-button>
+        <gr-button id="saveButton" link primary on-tap="_handleSave">
+            Save</gr-button>
       </div>
     </overlay>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
index 7ddf9a9..0f4119b 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
@@ -16,13 +16,15 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
-<link rel="import" href="../../../bower_components/paper-input/paper-input.html">
+<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
+<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -41,21 +43,30 @@
         margin-left: 1em;
         text-decoration: none;
       }
-      paper-input {
-        --paper-input-container: {
-          padding: 0;
-          min-width: 15em;
-        }
-        --paper-input-container-input: {
-          font-size: 1em;
-        }
-      }
       gr-confirm-dialog {
         width: 50em;
       }
       gr-confirm-dialog .main {
         width: 100%;
       }
+      gr-autocomplete {
+        --gr-autocomplete: {
+          border: 1px solid #d1d2d3;
+          border-radius: 2px;
+          font-size: 1em;
+          height: 2em;
+          padding: 0 .15em;
+        }
+      }
+      input {
+        border: 1px solid #d1d2d3;
+        border-radius: 2px;
+        font-size: 1em;
+        height: 2em;
+        margin: .5em 0;
+        padding: 0 .15em;
+        width: 100%;
+      }
     </style>
     <template is="dom-repeat" items="[[_actions]]" as="action">
       <gr-button
@@ -66,21 +77,56 @@
     <gr-overlay id="overlay" with-backdrop>
       <gr-confirm-dialog
           id="editDialog"
-          class="invisible"
+          class="invisible dialog"
           disabled$="[[!_isValidPath(_path)]]"
           confirm-label="Edit"
           on-confirm="_handleEditConfirm"
           on-cancel="_handleDialogCancel">
         <div class="header">Edit a file</div>
         <div class="main">
-          <!-- TODO(kaspern): Make this an autocomplete. -->
-          <paper-input
-              class="input"
-              label="Enter an existing or new full file path."
-              value="{{_path}}"></paper-input>
+          <gr-autocomplete
+              placeholder="Enter an existing or new full file path."
+              query="[[_query]]"
+              text="{{_path}}"></gr-autocomplete>
+        </div>
+      </gr-confirm-dialog>
+      <gr-confirm-dialog
+          id="deleteDialog"
+          class="invisible dialog"
+          disabled$="[[!_isValidPath(_path)]]"
+          confirm-label="Delete"
+          on-confirm="_handleDeleteConfirm"
+          on-cancel="_handleDialogCancel">
+        <div class="header">Delete a file</div>
+        <div class="main">
+          <gr-autocomplete
+              placeholder="Enter an existing full file path."
+              query="[[_query]]"
+              text="{{_path}}"></gr-autocomplete>
+        </div>
+      </gr-confirm-dialog>
+      <gr-confirm-dialog
+          id="renameDialog"
+          class="invisible dialog"
+          disabled$="[[!_computeRenameDisabled(_path, _newPath)]]"
+          confirm-label="Rename"
+          on-confirm="_handleRenameConfirm"
+          on-cancel="_handleDialogCancel">
+        <div class="header">Rename a file</div>
+        <div class="main">
+          <gr-autocomplete
+              placeholder="Enter an existing full file path."
+              query="[[_query]]"
+              text="{{_path}}"></gr-autocomplete>
+          <input
+              class="newPathInput"
+              is="iron-input"
+              bind-value="{{_newPath}}"
+              placeholder="Enter the new path."/>
         </div>
       </gr-confirm-dialog>
     </gr-overlay>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-edit-controls.js"></script>
 </dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index 62e8c7a..fd56bd3 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -16,9 +16,9 @@
 
   const Actions = {
     EDIT: {label: 'Edit', key: 'edit'},
-    /* TODO(kaspern): Implement these actions.
     DELETE: {label: 'Delete', key: 'delete'},
     RENAME: {label: 'Rename', key: 'rename'},
+    /* TODO(kaspern): Implement these actions.
     REVERT: {label: 'Revert', key: 'revert'},
     CHECKOUT: {label: 'Check out', key: 'checkout'},
     */
@@ -37,8 +37,22 @@
         type: String,
         value: '',
       },
+      _newPath: {
+        type: String,
+        value: '',
+      },
+      _query: {
+        type: Function,
+        value() {
+          return this._queryFiles.bind(this);
+        },
+      },
     },
 
+    behaviors: [
+      Gerrit.PatchSetBehavior,
+    ],
+
     _handleTap(e) {
       e.preventDefault();
       const action = Polymer.dom(e).localTarget.id;
@@ -47,6 +61,12 @@
         case Actions.EDIT.key:
           this.openEditDialog();
           return;
+        case Actions.DELETE.key:
+          this.openDeleteDialog();
+          return;
+        case Actions.RENAME.key:
+          this.openRenameDialog();
+          return;
       }
     },
 
@@ -55,37 +75,96 @@
       return this._showDialog(this.$.editDialog);
     },
 
+    openDeleteDialog(opt_path) {
+      if (opt_path) { this._path = opt_path; }
+      return this._showDialog(this.$.deleteDialog);
+    },
+
+    openRenameDialog(opt_path) {
+      if (opt_path) { this._path = opt_path; }
+      return this._showDialog(this.$.renameDialog);
+    },
+
     /**
      * Given a path string, checks that it is a valid file path.
      * @param {string} path
      * @return {boolean}
      */
     _isValidPath(path) {
-      return path.length && !path.endsWith('/');
+      // Double negation needed for strict boolean return type.
+      return !!path.length && !path.endsWith('/');
+    },
+
+    _computeRenameDisabled(path, newPath) {
+      return this._isValidPath(path) && this._isValidPath(newPath);
+    },
+
+    /**
+     * Given a dom event, gets the dialog that lies along this event path.
+     * @param {!Event} e
+     * @return {!Element}
+     */
+    _getDialogFromEvent(e) {
+      return Polymer.dom(e).path.find(element => {
+        if (!element.classList) { return false; }
+        return element.classList.contains('dialog');
+      });
     },
 
     _showDialog(dialog) {
       return this.$.overlay.open().then(() => {
         dialog.classList.toggle('invisible', false);
-        dialog.querySelector('.input').focus();
+        dialog.querySelector('gr-autocomplete').focus();
         this.async(() => { this.$.overlay.center(); }, 1);
       });
     },
 
     _closeDialog(dialog) {
-      dialog.querySelectorAll('.input').forEach(input => { input.value = ''; });
+      // Dialog may have autocompletes and plain inputs -- as these have
+      // different properties representing their bound text, it is easier to
+      // just make two separate queries.
+      dialog.querySelectorAll('gr-autocomplete')
+          .forEach(input => { input.text = ''; });
+      dialog.querySelectorAll('input')
+          .forEach(input => { input.bindValue = ''; });
+
       dialog.classList.toggle('invisible', true);
       return this.$.overlay.close();
     },
 
     _handleDialogCancel(e) {
-      this._closeDialog(Polymer.dom(e).localTarget);
+      this._closeDialog(this._getDialogFromEvent(e));
     },
 
     _handleEditConfirm(e) {
       const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path);
       Gerrit.Nav.navigateToRelativeUrl(url);
-      this._closeDialog(Polymer.dom(e).localTarget);
+      this._closeDialog(this._getDialogFromEvent(e));
+    },
+
+    _handleDeleteConfirm(e) {
+      this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
+          .then(res => {
+            if (!res.ok) { return; }
+            this._closeDialog(this._getDialogFromEvent(e));
+            Gerrit.Nav.navigateToChange(this.change);
+          });
+    },
+
+    _handleRenameConfirm(e) {
+      return this.$.restAPI.renameFileInChangeEdit(this.change._number,
+          this._path, this._newPath).then(res => {
+            if (!res.ok) { return; }
+            this._closeDialog(this._getDialogFromEvent(e));
+            Gerrit.Nav.navigateToChange(this.change);
+          });
+    },
+
+    _queryFiles(input) {
+      return this.$.restAPI.queryChangeFiles(this.change._number,
+          this.EDIT_NAME, input).then(res => res.map(file => {
+            return {name: file};
+          }));
     },
   });
 })();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index 6da4e32..1d5ab25 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -37,12 +37,16 @@
   let sandbox;
   let showDialogSpy;
   let closeDialogSpy;
+  let queryStub;
 
   setup(() => {
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
+    element.change = {_number: '42'};
     showDialogSpy = sandbox.spy(element, '_showDialog');
     closeDialogSpy = sandbox.spy(element, '_closeDialog');
+    queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
+        .returns(Promise.resolve([]));
     flushAsynchronousOperations();
   });
 
@@ -50,7 +54,7 @@
 
   test('all actions exist', () => {
     assert.equal(Polymer.dom(element.root).querySelectorAll('gr-button').length,
-        1);
+        element._actions.length);
   });
 
   suite('edit button CUJ', () => {
@@ -63,11 +67,22 @@
       ];
     });
 
+    test('_isValidPath', () => {
+      assert.isFalse(element._isValidPath(''));
+      assert.isFalse(element._isValidPath('test/'));
+      assert.isFalse(element._isValidPath('/'));
+      assert.isTrue(element._isValidPath('test/path.cpp'));
+      assert.isTrue(element._isValidPath('test.js'));
+    });
+
     test('edit', () => {
       MockInteractions.tap(element.$$('#edit'));
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.editDialog.disabled);
-        element._path = 'src/test.cpp';
+        assert.isFalse(queryStub.called);
+        element.$.editDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
+        assert.isTrue(queryStub.called);
         assert.isFalse(element.$.editDialog.disabled);
         MockInteractions.tap(element.$.editDialog.$$('gr-button[primary]'));
         for (const stub of navStubs) { assert.isTrue(stub.called); }
@@ -79,7 +94,8 @@
       MockInteractions.tap(element.$$('#edit'));
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.editDialog.disabled);
-        element._path = 'src/test.cpp';
+        element.$.editDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
         assert.isFalse(element.$.editDialog.disabled);
         MockInteractions.tap(element.$.editDialog.$$('gr-button'));
         for (const stub of navStubs) { assert.isFalse(stub.called); }
@@ -89,12 +105,183 @@
     });
   });
 
+  suite('delete button CUJ', () => {
+    let navStub;
+    let deleteStub;
+
+    setup(() => {
+      navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+      deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
+    });
+
+    test('delete', () => {
+      deleteStub.returns(Promise.resolve({ok: true}));
+      MockInteractions.tap(element.$$('#delete'));
+      return showDialogSpy.lastCall.returnValue.then(() => {
+        assert.isTrue(element.$.deleteDialog.disabled);
+        assert.isFalse(queryStub.called);
+        element.$.deleteDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
+        assert.isTrue(queryStub.called);
+        assert.isFalse(element.$.deleteDialog.disabled);
+        MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
+        flushAsynchronousOperations();
+
+        assert.isTrue(deleteStub.called);
+
+        return deleteStub.lastCall.returnValue.then(() => {
+          assert.isTrue(navStub.called);
+          assert.isTrue(closeDialogSpy.called);
+        });
+      });
+    });
+
+    test('delete fails', () => {
+      deleteStub.returns(Promise.resolve({ok: false}));
+      MockInteractions.tap(element.$$('#delete'));
+      return showDialogSpy.lastCall.returnValue.then(() => {
+        assert.isTrue(element.$.deleteDialog.disabled);
+        assert.isFalse(queryStub.called);
+        element.$.deleteDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
+        assert.isTrue(queryStub.called);
+        assert.isFalse(element.$.deleteDialog.disabled);
+        MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
+        flushAsynchronousOperations();
+
+        assert.isTrue(deleteStub.called);
+
+        return deleteStub.lastCall.returnValue.then(() => {
+          assert.isFalse(navStub.called);
+          assert.isFalse(closeDialogSpy.called);
+        });
+      });
+    });
+
+    test('cancel', () => {
+      MockInteractions.tap(element.$$('#delete'));
+      return showDialogSpy.lastCall.returnValue.then(() => {
+        assert.isTrue(element.$.deleteDialog.disabled);
+        element.$.deleteDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
+        assert.isFalse(element.$.deleteDialog.disabled);
+        MockInteractions.tap(element.$.deleteDialog.$$('gr-button'));
+        assert.isFalse(navStub.called);
+        assert.isTrue(closeDialogSpy.called);
+        assert.equal(element._path, '');
+      });
+    });
+  });
+
+  suite('rename button CUJ', () => {
+    let navStub;
+    let renameStub;
+
+    setup(() => {
+      navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+      renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+    });
+
+    test('rename', () => {
+      renameStub.returns(Promise.resolve({ok: true}));
+      MockInteractions.tap(element.$$('#rename'));
+      return showDialogSpy.lastCall.returnValue.then(() => {
+        assert.isTrue(element.$.renameDialog.disabled);
+        assert.isFalse(queryStub.called);
+        element.$.renameDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
+        assert.isTrue(queryStub.called);
+        assert.isTrue(element.$.renameDialog.disabled);
+
+        element.$.renameDialog.querySelector('.newPathInput').bindValue =
+            'src/test.newPath';
+
+        assert.isFalse(element.$.renameDialog.disabled);
+        MockInteractions.tap(element.$.renameDialog.$$('gr-button[primary]'));
+        flushAsynchronousOperations();
+
+        assert.isTrue(renameStub.called);
+
+        return renameStub.lastCall.returnValue.then(() => {
+          assert.isTrue(navStub.called);
+          assert.isTrue(closeDialogSpy.called);
+        });
+      });
+    });
+
+    test('rename fails', () => {
+      renameStub.returns(Promise.resolve({ok: false}));
+      MockInteractions.tap(element.$$('#rename'));
+      return showDialogSpy.lastCall.returnValue.then(() => {
+        assert.isTrue(element.$.renameDialog.disabled);
+        assert.isFalse(queryStub.called);
+        element.$.renameDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
+        assert.isTrue(queryStub.called);
+        assert.isTrue(element.$.renameDialog.disabled);
+
+        element.$.renameDialog.querySelector('.newPathInput').bindValue =
+            'src/test.newPath';
+
+        assert.isFalse(element.$.renameDialog.disabled);
+        MockInteractions.tap(element.$.renameDialog.$$('gr-button[primary]'));
+        flushAsynchronousOperations();
+
+        assert.isTrue(renameStub.called);
+
+        return renameStub.lastCall.returnValue.then(() => {
+          assert.isFalse(navStub.called);
+          assert.isFalse(closeDialogSpy.called);
+        });
+      });
+    });
+
+    test('cancel', () => {
+      MockInteractions.tap(element.$$('#rename'));
+      return showDialogSpy.lastCall.returnValue.then(() => {
+        assert.isTrue(element.$.renameDialog.disabled);
+        element.$.renameDialog.querySelector('gr-autocomplete').text =
+            'src/test.cpp';
+        element.$.renameDialog.querySelector('.newPathInput').bindValue =
+            'src/test.newPath';
+        assert.isFalse(element.$.renameDialog.disabled);
+        MockInteractions.tap(element.$.renameDialog.$$('gr-button'));
+        assert.isFalse(navStub.called);
+        assert.isTrue(closeDialogSpy.called);
+        assert.equal(element._path, '');
+        assert.equal(element._newPath, '');
+      });
+    });
+  });
+
   test('openEditDialog', () => {
     return element.openEditDialog('test/path.cpp').then(() => {
       assert.isFalse(element.$.editDialog.hasAttribute('hidden'));
-      assert.equal(element.$.editDialog.querySelector('.input').value,
+      assert.equal(element.$.editDialog.querySelector('gr-autocomplete').text,
           'test/path.cpp');
     });
   });
+
+  test('_getDialogFromEvent', () => {
+    const spy = sandbox.spy(element, '_getDialogFromEvent');
+    element.addEventListener('tap', element._getDialogFromEvent);
+
+    MockInteractions.tap(element.$.editDialog);
+    flushAsynchronousOperations();
+    assert.equal(spy.lastCall.returnValue.id, 'editDialog');
+
+    MockInteractions.tap(element.$.deleteDialog);
+    flushAsynchronousOperations();
+    assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
+
+    MockInteractions.tap(
+        element.$.deleteDialog.querySelector('gr-autocomplete'));
+    flushAsynchronousOperations();
+    assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
+
+    MockInteractions.tap(element);
+    flushAsynchronousOperations();
+    assert.notOk(spy.lastCall.returnValue);
+  });
 });
 </script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index c8d3319..2d3f1c3 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -62,7 +62,8 @@
     },
 
     _handleDeleteButton(e) {
-      const index = parseInt(e.target.getAttribute('data-index'), 10);
+      const index = parseInt(Polymer.dom(e).localTarget
+          .getAttribute('data-index'), 10);
       const email = this._emails[index];
       this.push('_emailsToRemove', email);
       this.splice('_emails', index, 1);
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
index d5067aa..fb868e8 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -58,7 +58,8 @@
     },
 
     _showKey(e) {
-      const index = parseInt(e.target.getAttribute('data-index'), 10);
+      const el = Polymer.dom(e).localTarget;
+      const index = parseInt(el.getAttribute('data-index'), 10);
       this._keyToView = this._keys[index];
       this.$.viewKeyOverlay.open();
     },
@@ -68,7 +69,8 @@
     },
 
     _handleDeleteKey(e) {
-      const index = parseInt(e.target.getAttribute('data-index'), 10);
+      const el = Polymer.dom(e).localTarget;
+      const index = parseInt(el.getAttribute('data-index'), 10);
       this.push('_keysToRemove', this._keys[index]);
       this.splice('_keys', index, 1);
       this.hasUnsavedChanges = true;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index c0449be..431ac0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -46,6 +46,7 @@
       },
       suggestions: {
         type: Array,
+        value: () => [],
         observer: '_resetCursorStops',
       },
       _suggestionEls: {
@@ -151,8 +152,12 @@
     },
 
     _resetCursorStops() {
-      Polymer.dom.flush();
-      this._suggestionEls = this.$.suggestions.querySelectorAll('li');
+      if (this.suggestions.length > 0) {
+        Polymer.dom.flush();
+        this._suggestionEls = this.$.suggestions.querySelectorAll('li');
+      } else {
+        this._suggestionEls = [];
+      }
     },
 
     _resetCursorIndex() {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 7aa7abf..ab847c4 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -38,31 +38,29 @@
         color: red;
       }
     </style>
-    <div>
-      <input
-          id="input"
-          class$="[[_computeClass(borderless)]]"
-          is="iron-input"
-          disabled$="[[disabled]]"
-          bind-value="{{text}}"
-          placeholder="[[placeholder]]"
-          on-keydown="_handleKeydown"
-          on-focus="_onInputFocus"
-          on-blur="_onInputBlur"
-          autocomplete="off"/>
-      <gr-autocomplete-dropdown
-          vertical-align="top"
-          vertical-offset="20"
-          horizontal-align="auto"
-          id="suggestions"
-          on-item-selected="_handleItemSelect"
-          on-keydown="_handleKeydown"
-          suggestions="[[_suggestions]]"
-          role="listbox"
-          index="[[_index]]"
-          position-target="[[_inputElement]]">
-      </gr-autocomplete-dropdown>
-    </div>
+    <input
+        id="input"
+        class$="[[_computeClass(borderless)]]"
+        is="iron-input"
+        disabled$="[[disabled]]"
+        bind-value="{{text}}"
+        placeholder="[[placeholder]]"
+        on-keydown="_handleKeydown"
+        on-focus="_onInputFocus"
+        on-blur="_onInputBlur"
+        autocomplete="off"/>
+    <gr-autocomplete-dropdown
+        vertical-align="top"
+        vertical-offset="20"
+        horizontal-align="auto"
+        id="suggestions"
+        on-item-selected="_handleItemSelect"
+        on-keydown="_handleKeydown"
+        suggestions="[[_suggestions]]"
+        role="listbox"
+        index="[[_index]]"
+        position-target="[[_inputElement]]">
+    </gr-autocomplete-dropdown>
   </template>
   <script src="gr-autocomplete.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index d5eaa0f..f2f218b 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -64,7 +64,7 @@
 
     _handleSchemeTap(e) {
       e.preventDefault();
-      const el = Polymer.dom(e).rootTarget;
+      const el = Polymer.dom(e).localTarget;
       this.selectedScheme = el.getAttribute('data-scheme');
       if (this._loggedIn) {
         this.$.restAPI.savePreferences({download_scheme: this.selectedScheme});
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 c206b20..033d01b 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
@@ -904,6 +904,17 @@
           patchRange.patchNum);
     },
 
+    /**
+     * @param {number|string} changeNum
+     * @param {number|string} patchNum
+     * @param {string} query
+     * @return {!Promise<!Object>}
+     */
+    queryChangeFiles(changeNum, patchNum, query) {
+      return this._getChangeURLAndFetch(changeNum,
+          `/files?q=${encodeURIComponent(query)}`, patchNum);
+    },
+
     getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) {
       return this.getChangeFiles(changeNum, patchRange).then(
           this._normalizeChangeFilesResponse.bind(this));
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 eb4f418..aa7f9a0 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
@@ -701,6 +701,15 @@
       assert.equal(sendStub.lastCall.args[1], '/projects/x%2Fy');
     });
 
+    test('queryChangeFiles', () => {
+      const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+          .returns(Promise.resolve());
+      return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
+        assert.deepEqual(fetchStub.lastCall.args,
+            ['42', '/files?q=test%2Fpath.js', 'edit']);
+      });
+    });
+
     test('getProjects', () => {
       sandbox.stub(element, '_fetchSharedCacheURL');
       element.getProjects('test', 25);
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 7cc37d8..a690192 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -26,6 +26,7 @@
   const behaviorsPath = '../behaviors/';
 
   // Elements tests.
+  /* eslint-disable max-len */
   const elements = [
     // This seemed to be flakey when it was farther down the list. Keep at the
     // beginning.
@@ -158,6 +159,7 @@
     'shared/gr-tooltip-content/gr-tooltip-content_test.html',
     'shared/gr-tooltip/gr-tooltip_test.html',
   ];
+  /* eslint-enable max-len */
   for (let file of elements) {
     file = elementsPath + file;
     testFiles.push(file);
@@ -165,6 +167,7 @@
   }
 
   // Behaviors tests.
+  /* eslint-disable max-len */
   const behaviors = [
     'async-foreach-behavior/async-foreach-behavior_test.html',
     'base-url-behavior/base-url-behavior_test.html',
@@ -178,6 +181,7 @@
     'gr-path-list-behavior/gr-path-list-behavior_test.html',
     'gr-tooltip-behavior/gr-tooltip-behavior_test.html',
   ];
+  /* eslint-enable max-len */
   for (let file of behaviors) {
     // Behaviors do not utilize the DOM, so no shadow DOM test is necessary.
     file = behaviorsPath + file;
diff --git a/gerrit-acceptance-framework/pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
similarity index 100%
rename from gerrit-acceptance-framework/pom.xml
rename to tools/maven/gerrit-acceptance-framework_pom.xml
diff --git a/gerrit-extension-api/pom.xml b/tools/maven/gerrit-extension-api_pom.xml
similarity index 100%
rename from gerrit-extension-api/pom.xml
rename to tools/maven/gerrit-extension-api_pom.xml
diff --git a/gerrit-plugin-api/pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
similarity index 100%
rename from gerrit-plugin-api/pom.xml
rename to tools/maven/gerrit-plugin-api_pom.xml
diff --git a/gerrit-plugin-gwtui/pom.xml b/tools/maven/gerrit-plugin-gwtui_pom.xml
similarity index 100%
rename from gerrit-plugin-gwtui/pom.xml
rename to tools/maven/gerrit-plugin-gwtui_pom.xml
diff --git a/gerrit-war/pom.xml b/tools/maven/gerrit-war_pom.xml
similarity index 100%
rename from gerrit-war/pom.xml
rename to tools/maven/gerrit-war_pom.xml
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index f7b5aa8..2e1c1a9 100755
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -56,7 +56,7 @@
 for spec in args.s:
   artifact, packaging_type, src = spec.split(':')
   exe = cmd + [
-    '-DpomFile=%s' % path.join(root, '%s/pom.xml' % artifact),
+    '-DpomFile=%s' % path.join(root, 'tools', 'maven', '%s_pom.xml' % artifact),
     '-Dpackaging=%s' % packaging_type,
     '-Dfile=%s' % src,
   ]
diff --git a/tools/release-announcement-template.txt b/tools/release-announcement-template.txt
index 87f5d49..2702f57 100644
--- a/tools/release-announcement-template.txt
+++ b/tools/release-announcement-template.txt
@@ -7,7 +7,7 @@
 http://gerrit-documentation.storage.googleapis.com/Documentation/{{ data.version }}/index.html
 {% if data.previous %}
 Log of changes since {{ data.previous }}:
-https://gerrit.googlesource.com/gerrit/+log/v{{ data.previous }}..v{{ data.version }}
+https://gerrit.googlesource.com/gerrit/+log/v{{ data.previous }}..v{{ data.version }}?no-merges
 {% endif %}
 Download:
 https://gerrit-releases.storage.googleapis.com/gerrit-{{ data.version }}.war
diff --git a/tools/version.py b/tools/version.py
index fed6d5d..72b0134 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -48,7 +48,7 @@
 for project in ['gerrit-acceptance-framework', 'gerrit-extension-api',
                 'gerrit-plugin-api', 'gerrit-plugin-gwtui',
                 'gerrit-war']:
-  pom = os.path.join(project, 'pom.xml')
+  pom = os.path.join('tools', 'maven', '%s_pom.xml' % project)
   replace_in_file(pom, src_pattern)
 
 src_pattern = re.compile(r'^(GERRIT_VERSION = ")([-.\w]+)(")$', re.MULTILINE)