Merge "Add acceptance tests for listing group includes"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 70e9cfd..e0b056b 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1109,8 +1109,9 @@
 ~~~~~~~~~~~~~~~~~~~
 
 The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK' rule cannot
-be overridden in the inheriting project. Any 'ALLOW' rule which conflicts with
-an inherited 'BLOCK' rule will not be honored.  Searching for 'BLOCK' rules, in
+be overridden in the inheriting project. Any 'ALLOW' rule, from a different
+access section or from an inheriting project, which conflicts with an
+inherited 'BLOCK' rule will not be honored.  Searching for 'BLOCK' rules, in
 the chain of parent projects, ignores the Exclusive flag that is normally
 applied to access sections.
 
@@ -1131,6 +1132,26 @@
 every vote from '-INFINITE..min' and 'max..INFINITE'. For the example above it
 means that the range '-1..+1' is not affected by this block.
 
+'BLOCK' and 'ALLOW' rules in the same access section
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When an access section of a project contains a 'BLOCK' and an 'ALLOW' rule for
+the same permission then this 'ALLOW' rule overrides the 'BLOCK' rule:
+
+====
+  [access "refs/heads/*"]
+    push = block group X
+    push = group Y
+====
+
+In this case a user which is a member of the group 'Y' will still be allowed to
+push to 'refs/heads/*' even if it is a member of the group 'X'.
+
+NOTE: An 'ALLOW' rule overrides a 'BLOCK' rule only when both of them are
+inside the same access section of the same project. An 'ALLOW' rule in a
+different access section of the same project or in any access section in an
+inheriting project cannot override a 'BLOCK' rule.
+
 Examples
 ~~~~~~~~
 
@@ -1160,6 +1181,23 @@
     pushTag = group Project Owners
 ====
 
+Let only a dedicated group vote in a special category
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Assume there is a more restrictive process for submitting changes in stable
+release branches which is manifested as a new voting category
+'Release-Process'. Assume we want to make sure that only a 'Release Engineers'
+group can vote in this category and that even project owners cannot approve
+this category. We have to block everyone except the 'Release Engineers' to vote
+in this category and, of course, allow 'Release Engineers' to vote in that
+category. In the "`All-Projects`" we define the following rules:
+
+====
+  [access "refs/heads/stable*"]
+    label-Release-Proces = block -1..+1 group Anonymous Users
+    label-Release-Proces = -1..+1 group Release Engineers
+====
+
 [[conversion_table]]
 Conversion table from 2.1.x series to 2.2.x series
 --------------------------------------------------
@@ -1328,6 +1366,13 @@
 command, but also to the web UI results pagination size.
 
 
+[[capability_accessDatabase]]
+Access Database
+~~~~~~~~~~~~~~~
+
+Allow users to access the database using the `gsql` command.
+
+
 [[capability_startReplication]]
 Start Replication
 ~~~~~~~~~~~~~~~~~
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 1da538d..b24616f 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -153,7 +153,6 @@
     "MyProject-Committers": {
       "kind": "gerritcodereview#group",
       "id": "9999c971bb4ab872aab759d8c49833ee6b9ff320",
-      "name": "MyProject-Committers",
       "url": "#/admin/groups/uuid-9999c971bb4ab872aab759d8c49833ee6b9ff320",
       "options": {
         "visible_to_all": true
@@ -954,8 +953,8 @@
 The request also succeeds if the group is already included in this
 group, but then the HTTP response code is `200 OK`.
 
-POST /groups/\{group-id\}/groups (Include Groups)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Include Groups
+~~~~~~~~~~~~~~
 [verse]
 'POST /groups/link:#group-id[\{group-id\}]/groups'
 
@@ -1112,7 +1111,9 @@
 |Field Name    ||Description
 |`kind`        ||`gerritcodereview#group`
 |`id`          ||The URL encoded UUID of the group.
-|`name`        ||The name of the group.
+|`name`        |
+not set if returned in a map where the group name is used as map key|
+The name of the group.
 |`url`         |optional|
 URL to information about the group. Typically a URL to a web page that
 permits users to apply to join the group, or manage their membership.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 3c77568..55eff49 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -129,6 +129,50 @@
   }
 ----
 
+[[create-project]]
+Create Project
+~~~~~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]'
+
+Creates a new project.
+
+In the request body additional data for the project can be provided as
+link:#project-input[ProjectInput].
+
+.Request
+----
+  PUT /projects/MyProject HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "description": "This is a demo project.",
+    "submit_type": "CHERRY_PICK",
+    "owners": [
+      "MyProject-Owners"
+    ]
+  }
+----
+
+As response the link:#project-info[ProjectInfo] entity is returned that
+describes the created project.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#project",
+    "id": "MyProject",
+    "name": "MyProject",
+    "parent": "All-Projects",
+    "description": "This is a demo project."
+  }
+----
+
 [[get-project-description]]
 Get Project Description
 ~~~~~~~~~~~~~~~~~~~~~~~
@@ -627,6 +671,56 @@
 |`branches`    |optional|Map of branch names to HEAD revisions.
 |===========================
 
+[[project-input]]
+ProjectInput
+~~~~~~~~~~~~
+The `ProjectInput` entity contains information for the creation of
+a new project.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=========================================
+|Field Name                  ||Description
+|`name`                      |optional|
+The name of the project (not encoded). +
+If set, must match the project name in the URL.
+|`parent`                    |optional|
+The name of the parent project. +
+If not set, the `All-Projects` project will be the parent project.
+|`description`               |optional|The description of the project.
+|`permissions_only`          |`false` if not set|
+Whether a permission-only project should be created.
+|`create_empty_commit`       |`false` if not set|
+Whether an empty initial commit should be created.
+|`submit_type`               |optional|
+The submit type that should be set for the project
+(`MERGE_IF_NECESSARY`, `REBASE_IF_NECESSARY`, `FAST_FORWARD_ONLY`,
+`MERGE_ALWAYS`, `CHERRY_PICK`). +
+If not set, `MERGE_IF_NECESSARY` is set as submit type.
+|`branches`                  |optional|
+A list of branches that should be initially created. +
+For the branch names the `refs/heads/` prefix can be omitted.
+|`owners`                    |optional|
+A list of groups that should be assigned as project owner. +
+Each group in the list must be specified as
+link:rest-api-groups.html#group-id[group-id]. +
+If not set, the link:config-gerrit.html#repository.name.ownerGroup[
+groups that are configured as default owners] are set as project
+owners.
+|`use_contributor_agreements`|`INHERIT` if not set|
+Whether contributor agreements should be used for the project  (`TRUE`,
+`FALSE`, `INHERIT`).
+|`use_signed_off_by`         |`INHERIT` if not set|
+Whether the usage of 'Signed-Off-By' footers is required for the
+project (`TRUE`, `FALSE`, `INHERIT`).
+|`use_content_merge`         |`INHERIT` if not set|
+Whether content merge should be enabled for the project (`TRUE`,
+`FALSE`, `INHERIT`). +
+`FALSE`, if the `submit_type` is `FAST_FORWARD_ONLY`.
+|`require_change_id`         |`INHERIT` if not set|
+Whether the usage of Change-Ids is required for the project (`TRUE`,
+`FALSE`, `INHERIT`).
+|=========================================
+
 [[project-parent-input]]
 ProjectParentInput
 ~~~~~~~~~~~~~~~~~~
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
index ce8831f..70526e1 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -52,6 +52,11 @@
 * Remove Reviewer is a new permission.
 * Pushing a signed tag is a new permission.
 * Editing the topic name is a new permission.
+* Raw database access with the `gsql` command is a new global capability.
++
+Previously site administrators had this capability by default.  Now it has
+to be explicitly assigned, even for site administrators.
+
 * link:https://code.google.com/p/gerrit/issues/detail?id=1585[Issue 1585]:
 Viewing other users' draft changes is a new permission.
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
new file mode 100644
index 0000000..3b249f3
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -0,0 +1,275 @@
+// Copyright (C) 2013 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.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectOwners;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.reflect.TypeToken;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gson.Gson;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class CreateProjectIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
+  private GitRepositoryManager git;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testCreateProject() throws IOException {
+    final String newProjectName = "newProject";
+    RestResponse r = session.put("/projects/" + newProjectName);
+    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    @SuppressWarnings("serial")
+    ProjectInfo p = (new Gson()).fromJson(r.getReader(), new TypeToken<ProjectInfo>() {}.getType());
+    assertEquals(newProjectName, p.name);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertNotNull(projectState);
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void testCreateProjectWithNameMismatch_BadRequest() throws IOException {
+    ProjectInput in = new ProjectInput();
+    in.name = "otherName";
+    RestResponse r = session.put("/projects/someName", in);
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreateProjectWithProperties() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.description = "Test description";
+    in.submit_type = SubmitType.CHERRY_PICK;
+    in.use_contributor_agreements = InheritableBoolean.TRUE;
+    in.use_signed_off_by = InheritableBoolean.TRUE;
+    in.use_content_merge = InheritableBoolean.TRUE;
+    in.require_change_id = InheritableBoolean.TRUE;
+    RestResponse r = session.put("/projects/" + newProjectName, in);
+    @SuppressWarnings("serial")
+    ProjectInfo p = (new Gson()).fromJson(r.getReader(), new TypeToken<ProjectInfo>() {}.getType());
+    assertEquals(newProjectName, p.name);
+    Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
+    assertProjectInfo(project, p);
+    assertEquals(in.description, project.getDescription());
+    assertEquals(in.submit_type, project.getSubmitType());
+    assertEquals(in.use_contributor_agreements, project.getUseContributorAgreements());
+    assertEquals(in.use_signed_off_by, project.getUseSignedOffBy());
+    assertEquals(in.use_content_merge, project.getUseContentMerge());
+    assertEquals(in.require_change_id, project.getRequireChangeID());
+  }
+
+  @Test
+  public void testCreateChildProject() throws IOException {
+    final String parentName = "parent";
+    RestResponse r = session.put("/projects/" + parentName);
+    r.consume();
+    final String childName = "child";
+    ProjectInput in = new ProjectInput();
+    in.parent = parentName;
+    r = session.put("/projects/" + childName, in);
+    Project project = projectCache.get(new Project.NameKey(childName)).getProject();
+    assertEquals(in.parent, project.getParentName());
+  }
+
+  public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity()
+      throws IOException {
+    ProjectInput in = new ProjectInput();
+    in.parent = "non-existing-project";
+    RestResponse r = session.put("/projects/child", in);
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreateProjectWithOwner() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.owners = Lists.newArrayListWithCapacity(3);
+    in.owners.add("Administrators"); // by name
+    in.owners.add(groupUuid("Registered Users").get()); // by group UUID
+    in.owners.add(Integer.toString(groupCache.get(new AccountGroup.NameKey("Anonymous Users"))
+        .getId().get())); // by legacy group ID
+    session.put("/projects/" + newProjectName, in);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    Set<AccountGroup.UUID> expectedOwnerIds = Sets.newHashSetWithExpectedSize(3);
+    expectedOwnerIds.add(groupUuid("Administrators"));
+    expectedOwnerIds.add(groupUuid("Registered Users"));
+    expectedOwnerIds.add(groupUuid("Anonymous Users"));
+    assertProjectOwners(expectedOwnerIds, projectState);
+  }
+
+  public void testCreateProjectWithNonExistingOwner_UnprocessableEntity()
+      throws IOException {
+    ProjectInput in = new ProjectInput();
+    in.owners = Collections.singletonList("non-existing-group");
+    RestResponse r = session.put("/projects/newProject", in);
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreatePermissionOnlyProject() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.permissions_only = true;
+    session.put("/projects/" + newProjectName, in);
+    assertHead(newProjectName, GitRepositoryManager.REF_CONFIG);
+  }
+
+  @Test
+  public void testCreateProjectWithEmptyCommit() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.create_empty_commit = true;
+    session.put("/projects/" + newProjectName, in);
+    assertEmptyCommit(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void testCreateProjectWithBranches() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.create_empty_commit = true;
+    in.branches = Lists.newArrayListWithCapacity(3);
+    in.branches.add("refs/heads/test");
+    in.branches.add("refs/heads/master");
+    in.branches.add("release"); // without 'refs/heads' prefix
+    session.put("/projects/" + newProjectName, in);
+    assertHead(newProjectName, "refs/heads/test");
+    assertEmptyCommit(newProjectName, "refs/heads/test", "refs/heads/master",
+        "refs/heads/release");
+  }
+
+  @Test
+  public void testCreateProjectWithoutCapability_Forbidden() throws OrmException,
+      JSchException, IOException {
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    RestResponse r = (new RestSession(user)).put("/projects/newProject");
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreateProjectWhenProjectAlreadyExists_Conflict()
+      throws OrmException, JSchException, IOException {
+    RestResponse r = session.put("/projects/All-Projects");
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+  }
+
+  private AccountGroup.UUID groupUuid(String groupName) {
+    return groupCache.get(new AccountGroup.NameKey(groupName)).getGroupUUID();
+  }
+
+  private void assertHead(String projectName, String expectedRef)
+      throws RepositoryNotFoundException, IOException {
+    Repository repo = git.openRepository(new Project.NameKey(projectName));
+    try {
+      assertEquals(expectedRef, repo.getRef(Constants.HEAD).getTarget()
+          .getName());
+    } finally {
+      repo.close();
+    }
+  }
+
+  private void assertEmptyCommit(String projectName, String... refs)
+      throws RepositoryNotFoundException, IOException {
+    Repository repo = git.openRepository(new Project.NameKey(projectName));
+    RevWalk rw = new RevWalk(repo);
+    TreeWalk tw = new TreeWalk(repo);
+    try {
+      for (String ref : refs) {
+        RevCommit commit = rw.lookupCommit(repo.getRef(ref).getObjectId());
+        rw.parseBody(commit);
+        tw.addTree(commit.getTree());
+        assertFalse("ref " + ref + " has non empty commit", tw.next());
+        tw.reset();
+      }
+    } finally {
+      tw.release();
+      rw.release();
+      repo.close();
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class ProjectInput {
+    String name;
+    String parent;
+    String description;
+    boolean permissions_only;
+    boolean create_empty_commit;
+    SubmitType submit_type;
+    List<String> branches;
+    List<String> owners;
+    InheritableBoolean use_contributor_agreements;
+    InheritableBoolean use_signed_off_by;
+    InheritableBoolean use_content_merge;
+    InheritableBoolean require_change_id;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
new file mode 100644
index 0000000..25ccbee
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2013 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.acceptance.rest.project;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectState;
+
+import java.util.Set;
+
+public class ProjectAssert {
+
+  public static void assertProjectInfo(Project project, ProjectInfo info) {
+    if (info.name != null) {
+      // 'name' is not set if returned in a map
+      assertEquals(project.getName(), info.name);
+    }
+    assertEquals(project.getName(), Url.decode(info.id));
+    Project.NameKey parentName = project.getParent(new Project.NameKey("All-Projects"));
+    assertEquals(parentName != null ? parentName.get() : null, info.parent);
+    assertEquals(project.getDescription(), Strings.nullToEmpty(info.description));
+  }
+
+  public static void assertProjectOwners(Set<AccountGroup.UUID> expectedOwners,
+      ProjectState state) {
+    for (AccountGroup.UUID g : state.getOwners()) {
+      assertTrue("unexpected owner group " + g, expectedOwners.remove(g));
+    }
+    assertTrue("missing owner groups: " + expectedOwners,
+        expectedOwners.isEmpty());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
new file mode 100644
index 0000000..72dd2d6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 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.acceptance.rest.project;
+
+public class ProjectInfo {
+  public String id;
+  public String name;
+  public String parent;
+  public String description;
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index 67e9de6..973cc6a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -21,6 +21,9 @@
 
 /** Server wide capabilities. Represented as {@link Permission} objects. */
 public class GlobalCapability {
+  /** Ability to access the database (with gsql). */
+  public static final String ACCESS_DATABASE = "accessDatabase";
+
   /**
    * Denotes the server's administrators.
    * <p>
@@ -81,6 +84,7 @@
 
   static {
     NAMES_ALL = new ArrayList<String>();
+    NAMES_ALL.add(ACCESS_DATABASE);
     NAMES_ALL.add(ADMINISTRATE_SERVER);
     NAMES_ALL.add(CREATE_ACCOUNT);
     NAMES_ALL.add(CREATE_GROUP);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
index a8aa045..0638906 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
@@ -22,7 +22,6 @@
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
@@ -35,12 +34,6 @@
   void projectDetail(Project.NameKey projectName,
       AsyncCallback<ProjectDetail> callback);
 
-  @Audit
-  @SignInRequired
-  void createNewProject(String projectName, String parentName,
-      boolean emptyCommit, boolean permissionsOnly,
-      AsyncCallback<VoidResult> callback);
-
   void projectAccess(Project.NameKey projectName,
       AsyncCallback<ProjectAccess> callback);
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/UnprocessableEntityException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/UnprocessableEntityException.java
new file mode 100644
index 0000000..b63697f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/UnprocessableEntityException.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Resource referenced in the request body is not found (HTTP 422 Unprocessable Entity). */
+public class UnprocessableEntityException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  public UnprocessableEntityException(String msg)  {
+    super(msg);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 9d65222..c8a7c6e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -146,6 +146,7 @@
 
 # Capability Names
 capabilityNames = \
+  accessDatabase, \
   administrateServer, \
   createAccount, \
   createGroup, \
@@ -159,7 +160,8 @@
   viewCaches, \
   viewConnections, \
   viewQueue
-administrateServer = Administrate Server 
+accessDatabase = Access Database
+administrateServer = Administrate Server
 createAccount = Create Account
 createGroup = Create Group
 createProject = Create Project
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index a6b0f17..f7d50f2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -38,6 +40,7 @@
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
@@ -45,7 +48,6 @@
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 public class CreateProjectScreen extends Screen {
   private Grid grid;
@@ -231,9 +233,8 @@
     }
 
     enableForm(false);
-    Util.PROJECT_SVC.createNewProject(projectName, parentName,
-        emptyCommit.getValue(), permissionsOnly.getValue(),
-        new GerritCallback<VoidResult>() {
+    ProjectApi.createProject(projectName, parentName, emptyCommit.getValue(),
+        permissionsOnly.getValue(), new AsyncCallback<VoidResult>() {
           @Override
           public void onSuccess(VoidResult result) {
             String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
new file mode 100644
index 0000000..a6676dc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 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.client.projects;
+
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class ProjectApi {
+  /** Create a new project */
+  public static void createProject(String projectName, String parent,
+      Boolean createEmptyCcommit, Boolean permissionsOnly,
+      AsyncCallback<VoidResult> asyncCallback) {
+    ProjectInput input = ProjectInput.create();
+    input.setName(projectName);
+    input.setParent(parent);
+    input.setPermissionsOnly(permissionsOnly);
+    input.setCreateEmptyCommit(createEmptyCcommit);
+    new RestApi("/projects/").id(projectName).ifNoneMatch()
+        .put(input, asyncCallback);
+  }
+
+  private static class ProjectInput extends JavaScriptObject {
+    static ProjectInput create() {
+      return (ProjectInput) createObject();
+    }
+
+    protected ProjectInput() {
+    }
+
+    final native void setName(String n) /*-{ if(n)this.name=n; }-*/;
+
+    final native void setParent(String p) /*-{ if(p)this.parent=p; }-*/;
+
+    final native void setPermissionsOnly(boolean po) /*-{ if(po)this.permissions_only=po; }-*/;
+
+    final native void setCreateEmptyCommit(boolean cc) /*-{ if(cc)this.create_empty_commit=cc; }-*/;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index edfdc23..0061cbf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -62,6 +62,7 @@
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
@@ -319,6 +320,9 @@
           Objects.firstNonNull(e.getMessage(), "Precondition failed"));
     } catch (ResourceNotFoundException e) {
       replyError(res, status = SC_NOT_FOUND, "Not found");
+    } catch (UnprocessableEntityException e) {
+      replyError(res, status = 422,
+          Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"));
     } catch (AmbiguousViewException e) {
       replyError(res, status = SC_NOT_FOUND, e.getMessage());
     } catch (MalformedJsonException e) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
deleted file mode 100644
index 149c866..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2011 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.httpd.rpc.project;
-
-import com.google.gerrit.common.errors.ProjectCreationFailedException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.project.PerformCreateProject;
-import com.google.gerrit.server.project.CreateProjectArgs;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collections;
-
-public class CreateProjectHandler extends Handler<VoidResult> {
-
-  interface Factory {
-    CreateProjectHandler create(@Assisted("projectName") String projectName,
-        @Assisted("parentName") String parentName,
-        @Assisted("emptyCommit") boolean emptyCommit,
-        @Assisted("permissionsOnly") boolean permissionsOnly);
-  }
-
-  private final PerformCreateProject.Factory createProjectFactory;
-  private final ProjectControl.Factory projectControlFactory;
-  private final String projectName;
-  private final String parentName;
-  private final boolean emptyCommit;
-  private final boolean permissionsOnly;
-
-  @Inject
-  public CreateProjectHandler(final PerformCreateProject.Factory createProjectFactory,
-      final ProjectControl.Factory projectControlFactory,
-      @Assisted("projectName") final String projectName,
-      @Assisted("parentName") final String parentName,
-      @Assisted("emptyCommit") final boolean emptyCommit,
-      @Assisted("permissionsOnly") final boolean permissionsOnly) {
-    this.createProjectFactory = createProjectFactory;
-    this.projectControlFactory = projectControlFactory;
-    this.projectName = projectName;
-    this.parentName = parentName;
-    this.emptyCommit = emptyCommit;
-    this.permissionsOnly = permissionsOnly;
-  }
-
-  @Override
-  public VoidResult call() throws ProjectCreationFailedException {
-    final CreateProjectArgs args = new CreateProjectArgs();
-    args.setProjectName(projectName);
-    if (!parentName.equals("")) {
-      final Project.NameKey nameKey = new Project.NameKey(parentName);
-      try {
-        args.newParent = projectControlFactory.validateFor(nameKey);
-      } catch (NoSuchProjectException e) {
-        throw new ProjectCreationFailedException("Parent project \""
-            + parentName + "\" does not exist.", e);
-      }
-    }
-    args.projectDescription = "";
-    args.submitType = SubmitType.MERGE_IF_NECESSARY;
-    args.branch = Collections.emptyList();
-    args.createEmptyCommit = emptyCommit;
-    args.permissionsOnly = permissionsOnly;
-
-    final PerformCreateProject createProject = createProjectFactory.create(args);
-    createProject.createProject();
-    return VoidResult.INSTANCE;
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
index 35014d2..05510ff 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
@@ -41,7 +41,6 @@
   private final ListBranches.Factory listBranchesFactory;
   private final VisibleProjectDetails.Factory visibleProjectDetailsFactory;
   private final ProjectAccessFactory.Factory projectAccessFactory;
-  private final CreateProjectHandler.Factory createProjectHandlerFactory;
   private final ProjectDetailFactory.Factory projectDetailFactory;
 
   @Inject
@@ -53,8 +52,7 @@
       final ListBranches.Factory listBranchesFactory,
       final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
       final ProjectAccessFactory.Factory projectAccessFactory,
-      final ProjectDetailFactory.Factory projectDetailFactory,
-      final CreateProjectHandler.Factory createNewProjectFactory) {
+      final ProjectDetailFactory.Factory projectDetailFactory) {
     this.addBranchFactory = addBranchFactory;
     this.changeProjectAccessFactory = changeProjectAccessFactory;
     this.reviewProjectAccessFactory = reviewProjectAccessFactory;
@@ -64,7 +62,6 @@
     this.visibleProjectDetailsFactory = visibleProjectDetailsFactory;
     this.projectAccessFactory = projectAccessFactory;
     this.projectDetailFactory = projectDetailFactory;
-    this.createProjectHandlerFactory = createNewProjectFactory;
   }
 
   @Override
@@ -131,12 +128,4 @@
     addBranchFactory.create(projectName, branchName, startingRevision).to(
         callback);
   }
-
-  @Override
-  public void createNewProject(String projectName, String parentName,
-      boolean emptyCommit, boolean permissionsOnly,
-      AsyncCallback<VoidResult> callback) {
-    createProjectHandlerFactory.create(projectName, parentName, emptyCommit,
-        permissionsOnly).to(callback);
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index e943e3fc..2d4f210 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
@@ -31,7 +31,6 @@
         factory(AddBranch.Factory.class);
         factory(ChangeProjectAccess.Factory.class);
         factory(ReviewProjectAccess.Factory.class);
-        factory(CreateProjectHandler.Factory.class);
         factory(ChangeProjectSettings.Factory.class);
         factory(DeleteBranches.Factory.class);
         factory(ListBranches.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index d9c42d5..706ae13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -130,6 +130,12 @@
       || canAdministrateServer();
   }
 
+
+  /** @return true if the user can access the database (with gsql). */
+  public boolean canAccessDatabase() {
+    return canPerform(GlobalCapability.ACCESS_DATABASE);
+  }
+
   /** @return true if the user can force replication to any configured destination. */
   public boolean canStartReplication() {
     return canPerform(GlobalCapability.START_REPLICATION)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index c0052b9..a432c6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE;
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
@@ -109,6 +110,7 @@
     have.put(VIEW_CONNECTIONS, cc.canViewConnections());
     have.put(VIEW_QUEUE, cc.canViewQueue());
     have.put(START_REPLICATION, cc.canStartReplication());
+    have.put(ACCESS_DATABASE, cc.canAccessDatabase());
 
     QueueProvider.QueueType queue = cc.getQueueType();
     if (queue != QueueProvider.QueueType.INTERACTIVE
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
index d270656..2a43841 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
@@ -40,7 +41,7 @@
 
   @Override
   public List<GroupInfo> apply(AccountResource resource)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     IdentifiedUser user = resource.getUser();
     Account.Id userId = user.getAccountId();
     List<GroupInfo> groups = Lists.newArrayList();
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 49fba6b..ab3db84 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,6 +91,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.PerformCreateProject;
 import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectControl;
@@ -185,6 +186,7 @@
     factory(ProjectState.Factory.class);
     factory(RebasedPatchSetSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
+    factory(PerformCreateProject.Factory.class);
     bind(PermissionCollection.Factory.class);
     bind(AccountVisibility.class)
         .toProvider(AccountVisibilityProvider.class)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 38489ed..4a36ba4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -23,11 +23,9 @@
 import com.google.gerrit.server.changedetail.PublishDraft;
 import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.patch.RemoveReviewer;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.PerformCreateProject;
 import com.google.gerrit.server.project.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SuggestParentCandidates;
@@ -40,7 +38,6 @@
     bind(RequestCleanup.class).in(RequestScoped.class);
     bind(RequestScopedReviewDbProvider.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
-    bind(MetaDataUpdate.User.class).in(RequestScoped.class);
     bind(ApprovalsUtil.class);
 
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
@@ -56,7 +53,6 @@
     factory(DeleteDraftPatchSet.Factory.class);
     factory(PublishDraft.Factory.class);
     factory(RemoveReviewer.Factory.class);
-    factory(PerformCreateProject.Factory.class);
     factory(SuggestParentCandidates.Factory.class);
     factory(BanCommit.Factory.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index 94791cb..79b80e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -179,7 +179,7 @@
     @Override
     public Object apply(IncludedGroupResource resource,
         PutIncludedGroup.Input input) throws ResourceNotFoundException,
-        OrmException {
+        MethodNotAllowedException, OrmException {
       // Do nothing, the group is already included.
       return get.get().apply(resource);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index f05694d..af336c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -79,7 +80,7 @@
   @Override
   public GroupInfo apply(TopLevelResource resource, Input input)
       throws ResourceNotFoundException, AuthException, BadRequestException,
-      OrmException, NameAlreadyUsedException {
+      NameAlreadyUsedException, MethodNotAllowedException, OrmException {
     if (input == null) {
       input = new Input();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
index adb04cd..9d17b62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
@@ -32,7 +33,7 @@
 
   @Override
   public GroupInfo apply(GroupResource rsrc) throws ResourceNotFoundException,
-      OrmException {
+      MethodNotAllowedException, OrmException {
     return json.format(rsrc);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
index 32cf9be..7bc5ee0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
@@ -30,7 +31,7 @@
 
   @Override
   public GroupInfo apply(GroupResource resource)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     return json.format(resource.getGroup());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
index 7e63816..d6ae1c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
@@ -30,7 +31,7 @@
 
   @Override
   public GroupInfo apply(IncludedGroupResource rsrc)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     return json.format(rsrc.getMemberDescription());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
index 797544d..169fbdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -36,7 +37,7 @@
 
   @Override
   public GroupInfo apply(GroupResource resource)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     AccountGroup group = resource.toAccountGroup();
     if (group == null) {
       throw new ResourceNotFoundException();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
index 6a729fc..dcf3667 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -64,15 +65,15 @@
     return this;
   }
 
-  public GroupInfo format(GroupResource rsrc) throws ResourceNotFoundException,
-      OrmException {
+  public GroupInfo format(GroupResource rsrc)
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     GroupInfo info = init(rsrc.getGroup());
     initMembersAndIncludes(rsrc, info);
     return info;
   }
 
   public GroupInfo format(GroupDescription.Basic group)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     GroupInfo info = init(group);
     if (options.contains(MEMBERS) || options.contains(INCLUDES)) {
       GroupResource rsrc =
@@ -106,7 +107,7 @@
   }
 
   private GroupInfo initMembersAndIncludes(GroupResource rsrc, GroupInfo info)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     if (options.contains(MEMBERS)) {
       info.members = listMembers.get().apply(rsrc);
     }
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 88a7ca8..97614ef 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
@@ -25,6 +25,7 @@
 import com.google.gerrit.common.groups.ListGroupsOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -146,7 +147,8 @@
         new TypeToken<Map<String, GroupInfo>>() {}.getType());
   }
 
-  public List<GroupInfo> get() throws ResourceNotFoundException, OrmException {
+  public List<GroupInfo> get() throws ResourceNotFoundException,
+      MethodNotAllowedException, OrmException {
     List<GroupInfo> groupInfos;
     if (user != null) {
       if (owned) {
@@ -187,7 +189,7 @@
   }
 
   private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, MethodNotAllowedException, OrmException {
     List<GroupInfo> groups = Lists.newArrayList();
     for (AccountGroup g : filterGroups(groupCache.all())) {
       GroupControl ctl = groupControlFactory.controlFor(g);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
index 3536186..31332d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
@@ -51,9 +52,9 @@
 
   @Override
   public List<GroupInfo> apply(GroupResource rsrc)
-      throws ResourceNotFoundException, OrmException {
+      throws MethodNotAllowedException, ResourceNotFoundException, OrmException {
     if (rsrc.toAccountGroup() == null) {
-      throw new ResourceNotFoundException(rsrc.getGroupUUID().get());
+      throw new MethodNotAllowedException();
     }
 
     boolean ownerOfParent = rsrc.getControl().isOwner();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index f586d55..a9aae17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
@@ -59,9 +60,9 @@
 
   @Override
   public List<AccountInfo> apply(final GroupResource resource)
-      throws ResourceNotFoundException, OrmException {
+      throws MethodNotAllowedException, ResourceNotFoundException, OrmException {
     if (resource.toAccountGroup() == null) {
-      throw new ResourceNotFoundException(resource.getGroupUUID().get());
+      throw new MethodNotAllowedException();
     }
     try {
       final Map<Account.Id, AccountInfo> members =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index 50efb68..b04f337 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -16,19 +16,14 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
 import java.util.jar.JarFile;
 
-class CleanupHandle extends WeakReference<ClassLoader> {
+class CleanupHandle {
   private final File tmpFile;
   private final JarFile jarFile;
 
   CleanupHandle(File tmpFile,
-      JarFile jarFile,
-      ClassLoader ref,
-      ReferenceQueue<ClassLoader> queue) {
-    super(ref, queue);
+      JarFile jarFile) {
     this.tmpFile = tmpFile;
     this.jarFile = jarFile;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 4e565d3..b3f3183 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -42,7 +43,6 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.ref.ReferenceQueue;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.text.SimpleDateFormat;
@@ -50,8 +50,11 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
@@ -72,8 +75,8 @@
   private final ConcurrentMap<String, Plugin> running;
   private final ConcurrentMap<String, Plugin> disabled;
   private final Map<String, FileSnapshot> broken;
-  private final ReferenceQueue<ClassLoader> cleanupQueue;
-  private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
+  private final Map<Plugin, CleanupHandle> cleanupHandles;
+  private final Queue<Plugin> toCleanup;
   private final Provider<PluginCleanerTask> cleaner;
   private final PluginScannerThread scanner;
 
@@ -91,8 +94,8 @@
     running = Maps.newConcurrentMap();
     disabled = Maps.newConcurrentMap();
     broken = Maps.newHashMap();
-    cleanupQueue = new ReferenceQueue<ClassLoader>();
-    cleanupHandles = Maps.newConcurrentMap();
+    toCleanup = Queues.newArrayDeque();
+    cleanupHandles = new Hashtable<Plugin,CleanupHandle>();
     cleaner = pct;
 
     long checkFrequency = ConfigUtil.getTimeUnit(cfg,
@@ -188,6 +191,15 @@
     }
   }
 
+  synchronized private void unloadPlugin(Plugin plugin) {
+    String name = plugin.getName();
+    log.info(String.format("Unloading plugin %s", name));
+    plugin.stop();
+    running.remove(name);
+    disabled.remove(name);
+    toCleanup.add(plugin);
+  }
+
   public void disablePlugins(Set<String> names) {
     synchronized (this) {
       for (String name : names) {
@@ -200,8 +212,7 @@
         File off = new File(pluginsDir, active.getName() + ".jar.disabled");
         active.getSrcJar().renameTo(off);
 
-        active.stop();
-        running.remove(name);
+        unloadPlugin(active);
         try {
           FileSnapshot snapshot = FileSnapshot.save(off);
           Plugin offPlugin = loadPlugin(name, off, snapshot);
@@ -254,12 +265,12 @@
     srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
     synchronized (this) {
       for (Plugin p : running.values()) {
-        p.stop();
+        unloadPlugin(p);
       }
       running.clear();
       disabled.clear();
       broken.clear();
-      if (cleanupHandles.size() > running.size()) {
+      if (!toCleanup.isEmpty()) {
         System.gc();
         processPendingCleanups();
       }
@@ -347,15 +358,14 @@
           && oldPlugin.canReload()
           && newPlugin.canReload();
       if (!reload && oldPlugin != null) {
-        oldPlugin.stop();
-        running.remove(name);
+        unloadPlugin(oldPlugin);
       }
       if (!newPlugin.isDisabled()) {
         newPlugin.start(env);
       }
       if (reload) {
         env.onReloadPlugin(oldPlugin, newPlugin);
-        oldPlugin.stop();
+        unloadPlugin(oldPlugin);
       } else if (!newPlugin.isDisabled()) {
         env.onStartPlugin(newPlugin);
       }
@@ -380,8 +390,7 @@
       }
     }
     for (String name : unload){
-      log.info(String.format("Unloading plugin %s", name));
-      running.remove(name).stop();
+      unloadPlugin(running.get(name));
     }
   }
 
@@ -398,16 +407,19 @@
   }
 
   synchronized int processPendingCleanups() {
-    CleanupHandle h;
-    while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
-      h.cleanup();
-      cleanupHandles.remove(h);
+    Iterator<Plugin> iterator = toCleanup.iterator();
+    while (iterator.hasNext()) {
+      Plugin plugin = iterator.next();
+      iterator.remove();
+
+      CleanupHandle cleanupHandle = cleanupHandles.remove(plugin);
+      cleanupHandle.cleanup();
     }
-    return Math.max(0, cleanupHandles.size() - running.size());
+    return toCleanup.size();
   }
 
   private void cleanInBackground() {
-    int cnt = Math.max(0, cleanupHandles.size() - running.size());
+    int cnt = toCleanup.size();
     if (0 < cnt) {
       cleaner.get().clean(cnt);
     }
@@ -451,20 +463,17 @@
       URL[] urls = {tmp.toURI().toURL()};
       ClassLoader parentLoader = parentFor(type);
       ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
-      final CleanupHandle cleanupHandle =
-          new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue);
-      cleanupHandle.enqueue();
-      cleanupHandles.put(cleanupHandle, Boolean.TRUE);
-
       Class<? extends Module> sysModule = load(sysName, pluginLoader);
       Class<? extends Module> sshModule = load(sshName, pluginLoader);
       Class<? extends Module> httpModule = load(httpName, pluginLoader);
-      keep = true;
-      return new Plugin(name,
+      Plugin plugin = new Plugin(name,
           srcJar, snapshot,
           jarFile, manifest,
           new File(dataDir, name), type, pluginLoader,
           sysModule, sshModule, httpModule);
+      cleanupHandles.put(plugin, new CleanupHandle(tmp, jarFile));
+      keep = true;
+      return plugin;
     } finally {
       if (!keep) {
         jarFile.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
new file mode 100644
index 0000000..51e6961
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2013 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.project;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.project.CreateProject.Input;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.CREATE_PROJECT)
+class CreateProject implements RestModifyView<TopLevelResource, Input> {
+  static class Input {
+    String name;
+    String parent;
+    String description;
+    boolean permissionsOnly;
+    boolean createEmptyCommit;
+    SubmitType submitType;
+    List<String> branches;
+    List<String> owners;
+    InheritableBoolean useContributorAgreements;
+    InheritableBoolean useSignedOffBy;
+    InheritableBoolean useContentMerge;
+    InheritableBoolean requireChangeId;
+  }
+
+  static interface Factory {
+    CreateProject create(String name);
+  }
+
+  private final PerformCreateProject.Factory createProjectFactory;
+  private final Provider<ProjectsCollection> projectsCollection;
+  private final Provider<GroupsCollection> groupsCollection;
+  private final ProjectJson json;
+  private final String name;
+
+  @Inject
+  CreateProject(PerformCreateProject.Factory performCreateProjectFactory,
+      Provider<ProjectsCollection> projectsCollection,
+      Provider<GroupsCollection> groupsCollection, ProjectJson json,
+      @Assisted String name) {
+    this.createProjectFactory = performCreateProjectFactory;
+    this.projectsCollection = projectsCollection;
+    this.groupsCollection = groupsCollection;
+    this.json = json;
+    this.name = name;
+  }
+
+  @Override
+  public Object apply(TopLevelResource resource, Input input)
+      throws BadRequestException, UnprocessableEntityException,
+      ProjectCreationFailedException {
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.name != null && !name.equals(input.name)) {
+      throw new BadRequestException("name must match URL");
+    }
+
+    final CreateProjectArgs args = new CreateProjectArgs();
+    args.setProjectName(name);
+    if (!Strings.isNullOrEmpty(input.parent)) {
+      try {
+        args.newParent =
+            projectsCollection.get().parse(input.parent).getControl();
+      } catch (ResourceNotFoundException e) {
+        throw new UnprocessableEntityException(String.format(
+            "parent project \"%s\" not found", input.parent));
+      }
+    }
+    args.createEmptyCommit = input.createEmptyCommit;
+    args.permissionsOnly = input.permissionsOnly;
+    args.projectDescription = Strings.emptyToNull(input.description);
+    args.submitType =
+        Objects.firstNonNull(input.submitType, SubmitType.MERGE_IF_NECESSARY);
+    args.branch = input.branches;
+    if (input.owners != null) {
+      List<AccountGroup.UUID> ownerIds =
+          Lists.newArrayListWithCapacity(input.owners.size());
+      for (String owner : input.owners) {
+        try {
+          ownerIds.add(groupsCollection.get().parse(owner).getGroupUUID());
+        } catch (ResourceNotFoundException e) {
+          throw new UnprocessableEntityException(String.format(
+              "group \"%s\" not found", owner));
+        }
+      }
+      args.ownerIds = ownerIds;
+    }
+    args.contributorAgreements =
+        Objects.firstNonNull(input.useContributorAgreements,
+            InheritableBoolean.INHERIT);
+    args.signedOffBy =
+        Objects.firstNonNull(input.useSignedOffBy, InheritableBoolean.INHERIT);
+    args.contentMerge =
+        input.submitType == SubmitType.FAST_FORWARD_ONLY
+            ? InheritableBoolean.FALSE : Objects.firstNonNull(
+                input.useContentMerge, InheritableBoolean.INHERIT);
+    args.changeIdRequired =
+        Objects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
+
+    Project p = createProjectFactory.create(args).createProject();
+    return Response.created(json.format(p));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 24c0f6f..7bbd2e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -41,6 +41,7 @@
     signedOffBy = InheritableBoolean.INHERIT;
     contentMerge = InheritableBoolean.INHERIT;
     changeIdRequired = InheritableBoolean.INHERIT;
+    submitType = SubmitType.MERGE_IF_NECESSARY;
   }
 
   public Project.NameKey getProject() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java
index 09f2163..a482278 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java
@@ -14,32 +14,20 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
 
 class GetProject implements RestReadView<ProjectResource> {
-  @Override
-  public Object apply(ProjectResource resource) {
-    Project project = resource.getControl().getProject();
-    ProjectInfo info = new ProjectInfo();
-    info.name = resource.getName();
-    info.parent = Strings.emptyToNull(project.getParentName());
-    info.description = Strings.emptyToNull(project.getDescription());
-    info.finish();
-    return info;
+
+  private final ProjectJson json;
+
+  @Inject
+  GetProject(ProjectJson json) {
+    this.json = json;
   }
 
-  static class ProjectInfo {
-    final String kind = "gerritcodereview#project";
-    String id;
-    String name;
-    String parent;
-    String description;
-
-    void finish() {
-      id = Url.encode(name);
-    }
+  @Override
+  public Object apply(ProjectResource rsrc) {
+    return json.format(rsrc);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 8ed0138..6b63db3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -19,6 +19,8 @@
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.project.CreateProject;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class Module extends RestApiModule {
   @Override
@@ -29,6 +31,7 @@
     DynamicMap.mapOf(binder(), PROJECT_KIND);
     DynamicMap.mapOf(binder(), DASHBOARD_KIND);
 
+    put(PROJECT_KIND).to(PutProject.class);
     get(PROJECT_KIND).to(GetProject.class);
     get(PROJECT_KIND, "description").to(GetDescription.class);
     put(PROJECT_KIND, "description").to(PutDescription.class);
@@ -41,5 +44,6 @@
     get(DASHBOARD_KIND).to(GetDashboard.class);
     put(DASHBOARD_KIND).to(SetDashboard.class);
     delete(DASHBOARD_KIND).to(DeleteDashboard.class);
+    install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
index 6f2a834..dd8722f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
@@ -99,7 +99,7 @@
     this.metaDataUpdateFactory = metaDataUpdateFactory;
   }
 
-  public void createProject() throws ProjectCreationFailedException {
+  public Project createProject() throws ProjectCreationFailedException {
     validateParameters();
     final Project.NameKey nameKey = createProjectArgs.getProject();
     try {
@@ -133,6 +133,8 @@
             && createProjectArgs.createEmptyCommit) {
           createEmptyCommits(repo, nameKey, createProjectArgs.branch);
         }
+
+        return projectCache.get(nameKey).getProject();
       } finally {
         repo.close();
       }
@@ -150,6 +152,7 @@
           if (repo.getObjectDatabase().exists()) {
             throw new ProjectCreationFailedException("project \"" + nameKey + "\" exists");
           }
+          throw err;
         } finally {
           repo.close();
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index e416ed7..483ecaf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -16,10 +16,13 @@
 
 import static com.google.gerrit.server.project.RefControl.isRE;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -73,7 +76,7 @@
       }
 
       boolean perUser = false;
-      List<AccessSection> sections = new ArrayList<AccessSection>();
+      Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap();
       for (SectionMatcher matcher : matcherList) {
         // If the matcher has to expand parameters and its prefix matches the
         // reference there is a very good chance the reference is actually user
@@ -93,9 +96,10 @@
         }
 
         if (matcher.match(ref, username)) {
-          sections.add(matcher.section);
+          sectionToProject.put(matcher.section, matcher.project);
         }
       }
+      List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
       sorter.sort(ref, sections);
 
       Set<SeenRule> seen = new HashSet<SeenRule>();
@@ -104,7 +108,9 @@
 
       HashMap<String, List<PermissionRule>> permissions =
           new HashMap<String, List<PermissionRule>>();
+      Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap();
       for (AccessSection section : sections) {
+        Project.NameKey project = sectionToProject.get(section);
         for (Permission permission : section.getPermissions()) {
           boolean exclusivePermissionExists =
               exclusiveGroupPermissions.contains(permission.getName());
@@ -124,6 +130,7 @@
                 permissions.put(permission.getName(), r);
               }
               r.add(rule);
+              ruleProps.put(rule, new ProjectRef(project, section.getName()));
             }
           }
 
@@ -133,16 +140,20 @@
         }
       }
 
-      return new PermissionCollection(permissions, perUser ? username : null);
+      return new PermissionCollection(permissions, ruleProps,
+          perUser ? username : null);
     }
   }
 
   private final Map<String, List<PermissionRule>> rules;
+  private final Map<PermissionRule, ProjectRef> ruleProps;
   private final String username;
 
   private PermissionCollection(Map<String, List<PermissionRule>> rules,
+      Map<PermissionRule, ProjectRef> ruleProps,
       String username) {
     this.rules = rules;
+    this.ruleProps = ruleProps;
     this.username = username;
   }
 
@@ -167,6 +178,10 @@
     return r != null ? r : Collections.<PermissionRule> emptyList();
   }
 
+  ProjectRef getRuleProps(PermissionRule rule) {
+    return ruleProps.get(rule);
+  }
+
   /**
    * Obtain all declared permission rules that match the reference.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
new file mode 100644
index 0000000..4b42528
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2013 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.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.inject.Inject;
+
+public class ProjectJson {
+
+  private final AllProjectsName allProjects;
+
+  @Inject
+  ProjectJson(AllProjectsName allProjects) {
+    this.allProjects = allProjects;
+  }
+
+  public ProjectInfo format(ProjectResource rsrc) {
+    return format(rsrc.getControl().getProject());
+  }
+
+  public ProjectInfo format(Project p) {
+    ProjectInfo info = new ProjectInfo();
+    info.name = p.getName();
+    Project.NameKey parentName = p.getParent(allProjects);
+    info.parent = parentName != null ? parentName.get() : null;
+    info.description = Strings.emptyToNull(p.getDescription());
+    info.finish();
+    return info;
+  }
+
+  static class ProjectInfo {
+    final String kind = "gerritcodereview#project";
+    String id;
+    String name;
+    String parent;
+    String description;
+
+    void finish() {
+      id = Url.encode(name);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java
new file mode 100644
index 0000000..0315fad
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2013 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.project;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+class ProjectRef {
+
+  final Project.NameKey project;
+  final String ref;
+
+  ProjectRef(Project.NameKey project, String ref) {
+    this.project = project;
+    this.ref = ref;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return other instanceof ProjectRef
+        && project.equals(((ProjectRef) other).project)
+        && ref.equals(((ProjectRef) other).ref);
+  }
+
+  @Override
+  public int hashCode() {
+    return project.hashCode() * 31 + ref.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return project + ", " + ref;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 413cbeb..e42be92f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -212,7 +212,8 @@
           section.setPermissions(copy);
         }
 
-        SectionMatcher matcher = SectionMatcher.wrap(section);
+        SectionMatcher matcher = SectionMatcher.wrap(getProject().getNameKey(),
+            section);
         if (matcher != null) {
           sm.add(matcher);
         }
@@ -350,4 +351,4 @@
     }
     return false;
   }
-}
\ No newline at end of file
+}
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 68e6941..c833726 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestCollection;
@@ -27,21 +28,24 @@
 import com.google.inject.Provider;
 
 public class ProjectsCollection implements
-    RestCollection<TopLevelResource, ProjectResource> {
+    RestCollection<TopLevelResource, ProjectResource>,
+    AcceptsCreate<TopLevelResource> {
   private final DynamicMap<RestView<ProjectResource>> views;
   private final Provider<ListProjects> list;
   private final ProjectControl.GenericFactory controlFactory;
   private final Provider<CurrentUser> user;
+  private final CreateProject.Factory createProjectFactory;
 
   @Inject
   ProjectsCollection(DynamicMap<RestView<ProjectResource>> views,
       Provider<ListProjects> list,
       ProjectControl.GenericFactory controlFactory,
-      Provider<CurrentUser> user) {
+      CreateProject.Factory factory, Provider<CurrentUser> user) {
     this.views = views;
     this.list = list;
     this.controlFactory = controlFactory;
     this.user = user;
+    this.createProjectFactory = factory;
   }
 
   @Override
@@ -52,10 +56,15 @@
   @Override
   public ProjectResource parse(TopLevelResource parent, IdString id)
       throws ResourceNotFoundException {
+    return parse(id.get());
+  }
+
+  public ProjectResource parse(String id)
+      throws ResourceNotFoundException {
     ProjectControl ctl;
     try {
       ctl = controlFactory.controlFor(
-          new Project.NameKey(id.get()),
+          new Project.NameKey(id),
           user.get());
     } catch (NoSuchProjectException e) {
       throw new ResourceNotFoundException(id);
@@ -70,4 +79,10 @@
   public DynamicMap<RestView<ProjectResource>> views() {
     return views;
   }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateProject create(TopLevelResource parent, IdString name) {
+    return createProjectFactory.create(name.get());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java
new file mode 100644
index 0000000..0a96cb5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 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.project;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.CreateProject.Input;
+
+public class PutProject implements RestModifyView<ProjectResource, Input> {
+  @Override
+  public Object apply(ProjectResource resource, Input input)
+      throws ResourceConflictException {
+    throw new ResourceConflictException("Project \"" + resource.getName()
+        + "\" already exists");
+  }
+}
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 229b062..59b7670 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
@@ -42,6 +44,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 
 /** Manages access control for Git references (aka branches, tags). */
@@ -117,18 +120,18 @@
    */
   public boolean isVisibleByRegisteredUsers() {
     List<PermissionRule> access = relevant.getPermission(Permission.READ);
+    Set<ProjectRef> allows = Sets.newHashSet();
+    Set<ProjectRef> blocks = Sets.newHashSet();
     for (PermissionRule rule : access) {
       if (rule.isBlock()) {
-        return false;
-      }
-    }
-    for (PermissionRule rule : access) {
-      if (rule.getGroup().getUUID().equals(AccountGroup.ANONYMOUS_USERS)
+        blocks.add(relevant.getRuleProps(rule));
+      } else if (rule.getGroup().getUUID().equals(AccountGroup.ANONYMOUS_USERS)
           || rule.getGroup().getUUID().equals(AccountGroup.REGISTERED_USERS)) {
-        return true;
+        allows.add(relevant.getRuleProps(rule));
       }
     }
-    return false;
+    blocks.removeAll(allows);
+    return blocks.isEmpty() && !allows.isEmpty();
   }
 
   /**
@@ -218,16 +221,7 @@
       // granting of powers beyond pushing to the configuration.
       return false;
     }
-    boolean result = false;
-    for (PermissionRule rule : access(Permission.PUSH)) {
-      if (rule.isBlock()) {
-        return false;
-      }
-      if (rule.getForce()) {
-        result = true;
-      }
-    }
-    return result;
+    return canForcePerform(Permission.PUSH);
   }
 
   /**
@@ -375,16 +369,7 @@
 
   /** @return true if this user can force edit topic names. */
   public boolean canForceEditTopicName() {
-    boolean result = false;
-    for (PermissionRule rule : access(Permission.EDIT_TOPIC_NAME)) {
-      if (rule.isBlock()) {
-        return false;
-      }
-      if (rule.getForce()) {
-        result = true;
-      }
-    }
-    return result;
+    return canForcePerform(Permission.EDIT_TOPIC_NAME);
   }
 
   /** All value ranges of any allowed label permission. */
@@ -416,39 +401,97 @@
     return null;
   }
 
-  private static PermissionRange toRange(String permissionName,
-      List<PermissionRule> ruleList) {
-    int min = 0;
-    int max = 0;
-    int blockMin = Integer.MIN_VALUE;
-    int blockMax = Integer.MAX_VALUE;
-    for (PermissionRule rule : ruleList) {
+  private static class AllowedRange {
+    private int allowMin = 0;
+    private int allowMax = 0;
+    private int blockMin = Integer.MIN_VALUE;
+    private int blockMax = Integer.MAX_VALUE;
+
+    void update(PermissionRule rule) {
       if (rule.isBlock()) {
         blockMin = Math.max(blockMin, rule.getMin());
         blockMax = Math.min(blockMax, rule.getMax());
       } else {
-        min = Math.min(min, rule.getMin());
-        max = Math.max(max, rule.getMax());
+        allowMin = Math.min(allowMin, rule.getMin());
+        allowMax = Math.max(allowMax, rule.getMax());
       }
     }
-    if (blockMin > Integer.MIN_VALUE) {
-      min = Math.max(min, blockMin + 1);
+
+    int getAllowMin() {
+      return allowMin;
     }
-    if (blockMax < Integer.MAX_VALUE) {
-      max = Math.min(max, blockMax - 1);
+    int getAllowMax() {
+      return allowMax;
     }
+    int getBlockMin() {
+      // ALLOW wins over BLOCK on the same project
+      return Math.min(blockMin, allowMin - 1);
+    }
+    int getBlockMax() {
+      // ALLOW wins over BLOCK on the same project
+      return Math.max(blockMax, allowMax + 1);
+    }
+  }
+
+  private PermissionRange toRange(String permissionName,
+      List<PermissionRule> ruleList) {
+    Map<ProjectRef, AllowedRange> ranges = Maps.newHashMap();
+    for (PermissionRule rule : ruleList) {
+      ProjectRef p = relevant.getRuleProps(rule);
+      AllowedRange r = ranges.get(p);
+      if (r == null) {
+        r = new AllowedRange();
+        ranges.put(p, r);
+      }
+      r.update(rule);
+    }
+    int allowMin = 0;
+    int allowMax = 0;
+    int blockMin = Integer.MIN_VALUE;
+    int blockMax = Integer.MAX_VALUE;
+    for (AllowedRange r : ranges.values()) {
+      allowMin = Math.min(allowMin, r.getAllowMin());
+      allowMax = Math.max(allowMax, r.getAllowMax());
+      blockMin = Math.max(blockMin, r.getBlockMin());
+      blockMax = Math.min(blockMax, r.getBlockMax());
+    }
+
+    // BLOCK wins over ALLOW across projects
+    int min = Math.max(allowMin, blockMin + 1);
+    int max = Math.min(allowMax, blockMax - 1);
     return new PermissionRange(permissionName, min, max);
   }
 
   /** True if the user has this permission. Works only for non labels. */
   boolean canPerform(String permissionName) {
     List<PermissionRule> access = access(permissionName);
+    Set<ProjectRef> allows = Sets.newHashSet();
+    Set<ProjectRef> blocks = Sets.newHashSet();
     for (PermissionRule rule : access) {
       if (rule.isBlock() && !rule.getForce()) {
-        return false;
+        blocks.add(relevant.getRuleProps(rule));
+      } else {
+        allows.add(relevant.getRuleProps(rule));
       }
     }
-    return !access.isEmpty();
+    blocks.removeAll(allows);
+    return blocks.isEmpty() && !allows.isEmpty();
+  }
+
+  /** True if the user has force this permission. Works only for non labels. */
+  private boolean canForcePerform(String permissionName) {
+    List<PermissionRule> access = access(permissionName);
+    Set<ProjectRef> allows = Sets.newHashSet();
+    Set<ProjectRef> blocks = Sets.newHashSet();
+    for (PermissionRule rule : access) {
+      if (rule.isBlock()) {
+        blocks.add(relevant.getRuleProps(rule));
+      } else if (rule.getForce()) {
+        allows.add(relevant.getRuleProps(rule));
+      }
+    }
+    blocks.removeAll(allows);
+    return blocks.isEmpty() && !allows.isEmpty();
   }
 
   /** Rules for the given permission, or the empty list. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
index 1c70b04..6f8af80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.Project;
 
 import dk.brics.automaton.Automaton;
 
@@ -31,33 +32,37 @@
  * faster selection of which sections are relevant to any given input reference.
  */
 abstract class SectionMatcher {
-  static SectionMatcher wrap(AccessSection section) {
+  static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
     String ref = section.getName();
     if (AccessSection.isValid(ref)) {
-      return wrap(ref, section);
+      return wrap(project, ref, section);
     } else {
       return null;
     }
   }
 
-  static SectionMatcher wrap(String pattern, AccessSection section) {
+  static SectionMatcher wrap(Project.NameKey project, String pattern,
+      AccessSection section) {
     if (pattern.contains("${")) {
-      return new ExpandParameters(pattern, section);
+      return new ExpandParameters(project, pattern, section);
 
     } else if (isRE(pattern)) {
-      return new Regexp(pattern, section);
+      return new Regexp(project, pattern, section);
 
     } else if (pattern.endsWith("/*")) {
-      return new Prefix(pattern.substring(0, pattern.length() - 1), section);
+      return new Prefix(project, pattern.substring(0, pattern.length() - 1),
+          section);
 
     } else {
-      return new Exact(pattern, section);
+      return new Exact(project, pattern, section);
     }
   }
 
+  final Project.NameKey project;
   final AccessSection section;
 
-  SectionMatcher(AccessSection section) {
+  SectionMatcher(Project.NameKey project, AccessSection section) {
+    this.project = project;
     this.section = section;
   }
 
@@ -66,8 +71,8 @@
   private static class Exact extends SectionMatcher {
     private final String expect;
 
-    Exact(String name, AccessSection section) {
-      super(section);
+    Exact(Project.NameKey project, String name, AccessSection section) {
+      super(project, section);
       expect = name;
     }
 
@@ -80,8 +85,8 @@
   private static class Prefix extends SectionMatcher {
     private final String prefix;
 
-    Prefix(String pfx, AccessSection section) {
-      super(section);
+    Prefix(Project.NameKey project, String pfx, AccessSection section) {
+      super(project, section);
       prefix = pfx;
     }
 
@@ -94,8 +99,8 @@
   private static class Regexp extends SectionMatcher {
     private final Pattern pattern;
 
-    Regexp(String re, AccessSection section) {
-      super(section);
+    Regexp(Project.NameKey project, String re, AccessSection section) {
+      super(project, section);
       pattern = Pattern.compile(re);
     }
 
@@ -109,8 +114,9 @@
     private final ParameterizedString template;
     private final String prefix;
 
-    ExpandParameters(String pattern, AccessSection section) {
-      super(section);
+    ExpandParameters(Project.NameKey project, String pattern,
+        AccessSection section) {
+      super(project, section);
       template = new ParameterizedString(pattern);
 
       if (isRE(pattern)) {
@@ -141,7 +147,7 @@
         u = username;
       }
 
-      SectionMatcher next = wrap(
+      SectionMatcher next = wrap(project,
           template.replace(Collections.singletonMap("username", u)),
           section);
       return next != null ? next.match(ref, username) : false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index e84cced..c1fb5de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -54,7 +54,7 @@
   }
 
   @Override
-  public Object apply(ProjectResource resource, Input input)
+  public String apply(ProjectResource resource, Input input)
       throws AuthException, BadRequestException, ResourceConflictException,
       Exception {
     ProjectControl ctl = resource.getControl();
@@ -83,11 +83,8 @@
         config.commit(md);
         cache.evict(ctl.getProject());
 
-        ListProjects.ProjectInfo info = new ListProjects.ProjectInfo();
-        info.setName(resource.getName());
-        info.parent = project.getParentName();
-        info.description = project.getDescription();
-        return info;
+        Project.NameKey parentName = project.getParent(allProjects);
+        return parentName != null ? parentName.get() : "";
       } finally {
         md.close();
       }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index ab5ad59..51ea5a2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
 import static com.google.gerrit.common.data.Permission.LABEL;
 import static com.google.gerrit.common.data.Permission.OWNER;
 import static com.google.gerrit.common.data.Permission.PUSH;
@@ -253,6 +254,139 @@
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote 2", range.contains(2));
   }
+
+  public void testUnblockNoForce() {
+    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(local, PUSH, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    assertTrue("u can push", u.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockForce() {
+    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    r.setBlock();
+    r.setForce(true);
+    grant(local, PUSH, devs, "refs/heads/*").setForce(true);
+
+    ProjectControl u = user(devs);
+    assertTrue("u can force push", u.controlForRef("refs/heads/master").canForceUpdate());
+  }
+
+  public void testUnblockForceWithAllowNoForce_NotPossible() {
+    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    r.setBlock();
+    r.setForce(true);
+    grant(local, PUSH, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    assertFalse("u can't force push", u.controlForRef("refs/heads/master").canForceUpdate());
+  }
+
+  public void testUnblockMoreSpecificRef_Fails() {
+    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(local, PUSH, devs, "refs/heads/master");
+
+    ProjectControl u = user(devs);
+    assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockLargerScope_Fails() {
+    grant(local, PUSH, anonymous, "refs/heads/master").setBlock();
+    grant(local, PUSH, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockInLocal_Fails() {
+    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(local, PUSH, fixers, "refs/heads/*");
+
+    ProjectControl f = user(fixers);
+    assertFalse("u can't push", f.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockInParentBlockInLocal() {
+    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(parent, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, devs, "refs/heads/*").setBlock();
+
+    ProjectControl d = user(devs);
+    assertFalse("u can't push", d.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockVisibilityByRegisteredUsers() {
+    grant(local, READ, anonymous, "refs/heads/*").setBlock();
+    grant(local, READ, registered, "refs/heads/*");
+
+    ProjectControl u = user(registered);
+    assertTrue("u can read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
+  }
+
+  public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() {
+    grant(parent, READ, anonymous, "refs/heads/*").setBlock();
+    grant(local, READ, registered, "refs/heads/*");
+
+    ProjectControl u = user(registered);
+    assertFalse("u can't read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
+  }
+
+  public void testUnblockForceEditTopicName() {
+    grant(local, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
+    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+
+    ProjectControl u = user(devs);
+    assertTrue("u can edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+  }
+
+  public void testUnblockInLocalForceEditTopicName_Fails() {
+    grant(parent, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
+    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+
+    ProjectControl u = user(registered);
+    assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+  }
+
+  public void testUnblockRange() {
+    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertTrue("u can vote -2", range.contains(-2));
+    assertTrue("u can vote +2", range.contains(2));
+  }
+
+  public void testUnblockRangeOnMoreSpecificRef_Fails() {
+    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/master");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertFalse("u can't vote -2", range.contains(-2));
+    assertFalse("u can't vote +2", range.contains(-2));
+  }
+
+  public void testUnblockRangeOnLargerScope_Fails() {
+    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/master").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertFalse("u can't vote -2", range.contains(-2));
+    assertFalse("u can't vote +2", range.contains(-2));
+  }
+
+  public void testUnblockInLocalRange_Fails() {
+    grant(parent, LABEL + "Code-Review", -1, 1, anonymous, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertFalse("u can't vote -2", range.contains(-2));
+    assertFalse("u can't vote 2", range.contains(2));
+  }
   // -----------------------------------------------------------------------
 
   private final Map<Project.NameKey, ProjectState> all;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 0ca8038..ecf370d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -25,7 +25,7 @@
 
 /** Opens a query processor. */
 @AdminHighPriorityCommand
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.ACCESS_DATABASE)
 @CommandMetaData(name = "gsql", descr = "Administrative interface to active database")
 final class AdminQueryShell extends SshCommand {
   @Inject
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index c04636c..98dd333 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -81,7 +82,8 @@
           identifiedUser, userFactory, accountGetGroups, json);
     }
 
-    void display(final PrintWriter out) throws ResourceNotFoundException, OrmException {
+    void display(final PrintWriter out) throws ResourceNotFoundException,
+        MethodNotAllowedException, OrmException {
       final ColumnFormatter formatter = new ColumnFormatter(out, '\t');
       for (final GroupInfo info : get()) {
         formatter.addColumn(Objects.firstNonNull(info.name, "n/a"));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index 18ce77f..5226962 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gson.JsonObject;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -55,6 +57,7 @@
   private final BufferedReader in;
   private final PrintWriter out;
   private final SchemaFactory<ReviewDb> dbFactory;
+  private final IdentifiedUser currentUser;
   private OutputFormat outputFormat = OutputFormat.PRETTY;
 
   private ReviewDb db;
@@ -63,12 +66,14 @@
 
   @Inject
   QueryShell(final SchemaFactory<ReviewDb> dbFactory,
+      final IdentifiedUser currentUser,
 
   @Assisted final InputStream in, @Assisted final OutputStream out)
       throws UnsupportedEncodingException {
     this.dbFactory = dbFactory;
     this.in = new BufferedReader(new InputStreamReader(in, "UTF-8"));
     this.out = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
+    this.currentUser = currentUser;
   }
 
   public void setOutputFormat(OutputFormat fmt) {
@@ -77,6 +82,7 @@
 
   public void run() {
     try {
+      checkPermission();
       db = dbFactory.open();
       try {
         connection = ((JdbcSchema) db).getConnection();
@@ -99,6 +105,8 @@
 
     } catch (SQLException err) {
       out.println("fatal: Cannot open connection: " + err.getMessage());
+    } catch (PermissionDeniedException err) {
+      out.println("fatal: " + err.getMessage());
     } finally {
       out.flush();
     }
@@ -106,6 +114,7 @@
 
   public void execute(String query) {
     try {
+      checkPermission();
       db = dbFactory.open();
       try {
         connection = ((JdbcSchema) db).getConnection();
@@ -127,11 +136,31 @@
 
     } catch (SQLException err) {
       out.println("fatal: Cannot open connection: " + err.getMessage());
+    } catch (PermissionDeniedException err) {
+      out.println("fatal: " + err.getMessage());
     } finally {
       out.flush();
     }
   }
 
+  /**
+   * Assert that the current user is permitted to perform raw queries.
+   * <p>
+   * As the @RequireCapability guards at various entry points of internal
+   * commands implicitly add administrators (which we want to avoid), we also
+   * check permissions within QueryShell and grant access only to those who
+   * canPerformRawQuery, regardless of whether they are administrators or not.
+   *
+   * @throws PermissionDeniedException
+   */
+  private void checkPermission() throws PermissionDeniedException {
+    if (!currentUser.getCapabilities().canAccessDatabase()) {
+      throw new PermissionDeniedException(String.format(
+          "%s does not have \"Perform Raw Query\" capability.",
+          currentUser.getUserName()));
+    }
+  }
+
   private void readEvalPrintLoop() {
     final StringBuilder buffer = new StringBuilder();
     boolean executed = false;