Merge changes from topic 'api-tests'

* changes:
  BranchApi: Implement get and delete
  AbstractDaemonTest: Simplify helpers for getting changes
  Delete StarredChangesIT
  IdentifiedUser: Clear starred changes before rereading from API
  Implement list branches extension API
  Convert HashtagsIT to extension API
  Convert most of CreateProjectIT to extension API
  Convert GetAccountIT to extension API
  Convert CreateChangeIT to extension API
  Convert CommentsIT to extension API
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index e59ed4d..8fb679d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,15 +14,14 @@
 
 package com.google.gerrit.acceptance;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.block;
 
-import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.common.data.AccessSection;
@@ -60,7 +59,6 @@
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -77,7 +75,6 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 
@@ -328,19 +325,6 @@
     return push.to(ref);
   }
 
-  protected ChangeInfo getChange(String changeId, ListChangesOption... options)
-      throws IOException {
-    return getChange(adminSession, changeId, options);
-  }
-
-  protected ChangeInfo getChange(RestSession session, String changeId,
-      ListChangesOption... options) throws IOException {
-    String q = options.length > 0 ? "?o=" + Joiner.on("&o=").join(options) : "";
-    RestResponse r = session.get("/changes/" + changeId + q);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(), ChangeInfo.class);
-  }
-
   protected ChangeInfo info(String id)
       throws RestApiException {
     return gApi.changes().id(id).info();
@@ -358,9 +342,8 @@
 
   protected ChangeInfo get(String id, ListChangesOption... options)
       throws RestApiException {
-    EnumSet<ListChangesOption> s = EnumSet.noneOf(ListChangesOption.class);
-    s.addAll(Arrays.asList(options));
-    return gApi.changes().id(id).get(s);
+    return gApi.changes().id(id).get(
+        Sets.newEnumSet(Arrays.asList(options), ListChangesOption.class));
   }
 
   protected List<ChangeInfo> query(String q) throws RestApiException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 7dffd5d..829c76d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -55,11 +55,11 @@
     gApi.accounts()
         .self()
         .starChange(triplet);
-    assertThat(getChange(triplet).starred).isTrue();
+    assertThat(info(triplet).starred).isTrue();
     gApi.accounts()
         .self()
         .unstarChange(triplet);
-    assertThat(getChange(triplet).starred).isNull();
+    assertThat(info(triplet).starred).isNull();
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index 63c6493..af90d8f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -14,54 +14,51 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
+import static org.junit.Assert.fail;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
+@NoHttpd
 public class GetAccountIT extends AbstractDaemonTest {
   @Test
   public void getNonExistingAccount_NotFound() throws Exception {
-    assertThat(adminSession.get("/accounts/non-existing").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
+    try {
+      gApi.accounts().id("non-existing").get();
+      fail("Expected account to not exist");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
   }
 
   @Test
   public void getAccount() throws Exception {
     // by formatted string
-    testGetAccount("/accounts/"
-        + Url.encode(admin.fullName + " <" + admin.email + ">"), admin);
+    testGetAccount(admin.fullName + " <" + admin.email + ">", admin);
 
     // by email
-    testGetAccount("/accounts/" + admin.email, admin);
+    testGetAccount(admin.email, admin);
 
     // by full name
-    testGetAccount("/accounts/" + admin.fullName, admin);
+    testGetAccount(admin.fullName, admin);
 
     // by account ID
-    testGetAccount("/accounts/" + admin.id.get(), admin);
+    testGetAccount(Integer.toString(admin.id.get()), admin);
 
     // by user name
-    testGetAccount("/accounts/" + admin.username, admin);
+    testGetAccount(admin.username, admin);
 
     // by 'self'
-    testGetAccount("/accounts/self", admin);
+    testGetAccount("self", admin);
   }
 
-  private void testGetAccount(String url, TestAccount expectedAccount)
-      throws IOException {
-    RestResponse r = adminSession.get(url);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    assertAccountInfo(expectedAccount, newGson()
-        .fromJson(r.getReader(), AccountInfo.class));
+  private void testGetAccount(String id, TestAccount expectedAccount)
+      throws Exception {
+    assertAccountInfo(expectedAccount, gApi.accounts().id(id).get());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
deleted file mode 100644
index face299a..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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.account;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.Change;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-import java.io.IOException;
-
-public class StarredChangesIT extends AbstractDaemonTest {
-
-  @Test
-  public void starredChangeState() throws Exception {
-    Result c1 = createChange();
-    Result c2 = createChange();
-    assertThat(getChange(c1.getChangeId()).starred).isNull();
-    assertThat(getChange(c2.getChangeId()).starred).isNull();
-    starChange(true, c1.getPatchSetId().getParentKey());
-    starChange(true, c2.getPatchSetId().getParentKey());
-    assertThat(getChange(c1.getChangeId()).starred).isTrue();
-    assertThat(getChange(c2.getChangeId()).starred).isTrue();
-    starChange(false, c1.getPatchSetId().getParentKey());
-    starChange(false, c2.getPatchSetId().getParentKey());
-    assertThat(getChange(c1.getChangeId()).starred).isNull();
-    assertThat(getChange(c2.getChangeId()).starred).isNull();
-  }
-
-  private void starChange(boolean on, Change.Id id) throws IOException {
-    String url = "/accounts/self/starred.changes/" + id.get();
-    if (on) {
-      RestResponse r = adminSession.put(url);
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    } else {
-      RestResponse r = adminSession.delete(url);
-      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    }
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 6bcc54b..e6c5e97 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.common.EventSource;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -50,7 +51,6 @@
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
@@ -245,8 +245,8 @@
   }
 
   protected void assertCurrentRevision(String changeId, int expectedNum,
-      ObjectId expectedId) throws IOException {
-    ChangeInfo c = getChange(changeId, CURRENT_REVISION);
+      ObjectId expectedId) throws Exception {
+    ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertThat(c.currentRevision).isEqualTo(expectedId.name());
     assertThat(c.revisions.get(expectedId.name())._number).isEqualTo(expectedNum);
     Repository repo =
@@ -261,8 +261,8 @@
     }
   }
 
-  protected void assertApproved(String changeId) throws IOException {
-    ChangeInfo c = getChange(changeId, DETAILED_LABELS);
+  protected void assertApproved(String changeId) throws Exception {
+    ChangeInfo c = get(changeId, DETAILED_LABELS);
     LabelInfo cr = c.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).value).isEqualTo(2);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 311161a..d92270c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -16,18 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static org.junit.Assert.fail;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.testutil.ConfigSuite;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
+@NoHttpd
 public class CreateChangeIT extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config allowDraftsDisabled() {
@@ -38,9 +42,8 @@
   public void createEmptyChange_MissingBranch() throws Exception {
     ChangeInfo ci = new ChangeInfo();
     ci.project = project.get();
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
-    assertThat(r.getEntityContent()).contains("branch must be non-empty");
+    assertCreateFails(ci, BadRequestException.class,
+        "branch must be non-empty");
   }
 
   @Test
@@ -48,37 +51,34 @@
     ChangeInfo ci = new ChangeInfo();
     ci.project = project.get();
     ci.branch = "master";
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
-    assertThat(r.getEntityContent()).contains("commit message must be non-empty");
+    assertCreateFails(ci, BadRequestException.class,
+        "commit message must be non-empty");
   }
 
   @Test
   public void createEmptyChange_InvalidStatus() throws Exception {
     ChangeInfo ci = newChangeInfo(ChangeStatus.SUBMITTED);
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
-    assertThat(r.getEntityContent()).contains("unsupported change status");
+    assertCreateFails(ci, BadRequestException.class,
+        "unsupported change status");
   }
 
   @Test
   public void createNewChange() throws Exception {
-    assertChange(newChangeInfo(ChangeStatus.NEW));
+    assertCreateSucceeds(newChangeInfo(ChangeStatus.NEW));
   }
 
   @Test
   public void createDraftChange() throws Exception {
     assume().that(isAllowDrafts()).isTrue();
-    assertChange(newChangeInfo(ChangeStatus.DRAFT));
+    assertCreateSucceeds(newChangeInfo(ChangeStatus.DRAFT));
   }
 
   @Test
   public void createDraftChangeNotAllowed() throws Exception {
     assume().that(isAllowDrafts()).isFalse();
     ChangeInfo ci = newChangeInfo(ChangeStatus.DRAFT);
-    RestResponse r = adminSession.post("/changes/", ci);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
-    assertThat(r.getEntityContent()).contains("draft workflow is disabled");
+    assertCreateFails(ci, MethodNotAllowedException.class,
+        "draft workflow is disabled");
   }
 
   private ChangeInfo newChangeInfo(ChangeStatus status) {
@@ -91,13 +91,8 @@
     return in;
   }
 
-  private void assertChange(ChangeInfo in) throws Exception {
-    RestResponse r = adminSession.post("/changes/", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-
-    ChangeInfo info = newGson().fromJson(r.getReader(), ChangeInfo.class);
-    ChangeInfo out = get(info.changeId);
-
+  private void assertCreateSucceeds(ChangeInfo in) throws Exception {
+    ChangeInfo out = gApi.changes().create(in).get();
     assertThat(out.branch).isEqualTo(in.branch);
     assertThat(out.subject).isEqualTo(in.subject);
     assertThat(out.topic).isEqualTo(in.topic);
@@ -107,6 +102,18 @@
     assertThat(booleanToDraftStatus(draft)).isEqualTo(in.status);
   }
 
+  private void assertCreateFails(ChangeInfo in,
+      Class<? extends RestApiException> errType, String errSubstring)
+      throws Exception {
+    try {
+      gApi.changes().create(in);
+      fail("Expected " + errType.getSimpleName());
+    } catch (RestApiException expected) {
+      assertThat(expected).isInstanceOf(errType);
+      assertThat(expected.getMessage()).contains(errSubstring);
+    }
+  }
+
   private ChangeStatus booleanToDraftStatus(Boolean draft) {
     if (draft == null) {
       return ChangeStatus.NEW;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 26d6a1e..5f46312 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -16,218 +16,206 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.truth.IterableSubject;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gson.reflect.TypeToken;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-
+@NoHttpd
 public class HashtagsIT extends AbstractDaemonTest {
   @ConfigSuite.Default
   public static Config defaultConfig() {
     return NotesMigration.allEnabledConfig();
   }
 
-  private void assertResult(RestResponse r, List<String> expected)
-      throws IOException {
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    List<String> result = toHashtagList(r);
-    assertThat(result).containsExactlyElementsIn(expected);
-  }
-
   @Test
   public void testGetNoHashtags() throws Exception {
-    // GET hashtags on a change with no hashtags returns an empty list
-    String changeId = createChange().getChangeId();
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Get on a change with no hashtags returns an empty list.
+    PushOneCommit.Result r = createChange();
+    assertThatGet(r).isEmpty();
   }
 
   @Test
   public void testAddSingleHashtag() throws Exception {
-    String changeId = createChange().getChangeId();
+    PushOneCommit.Result r = createChange();
 
-    // POST adding a single hashtag returns a single hashtag
-    List<String> expected = Arrays.asList("tag2");
-    assertResult(POST(changeId, "tag2", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding a single hashtag returns a single hashtag.
+    addHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag2");
 
-    // POST adding another single hashtag to change that already has one
-    // hashtag returns a sorted list of hashtags with existing and new
-    expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding another single hashtag to change that already has one hashtag
+    // returns a sorted list of hashtags with existing and new.
+    addHashtags(r, "tag1");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
   }
 
   @Test
   public void testAddMultipleHashtags() throws Exception {
-    String changeId = createChange().getChangeId();
+    PushOneCommit.Result r = createChange();
 
-    // POST adding multiple hashtags returns a sorted list of hashtags
-    List<String> expected = Arrays.asList("tag1", "tag3");
-    assertResult(POST(changeId, "tag3, tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding multiple hashtags returns a sorted list of hashtags.
+    addHashtags(r, "tag3", "tag1");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
 
-    // POST adding multiple hashtags to change that already has hashtags
-    // returns a sorted list of hashtags with existing and new
-    expected = Arrays.asList("tag1", "tag2", "tag3", "tag4");
-    assertResult(POST(changeId, "tag2, tag4", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding multiple hashtags to change that already has hashtags returns a
+    // sorted list of hashtags with existing and new.
+    addHashtags(r, "tag2", "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3", "tag4").inOrder();
   }
 
   @Test
   public void testAddAlreadyExistingHashtag() throws Exception {
-    // POST adding a hashtag that already exists on the change returns a
-    // sorted list of hashtags without duplicates
-    String changeId = createChange().getChangeId();
-    List<String> expected = Arrays.asList("tag2");
-    assertResult(POST(changeId, "tag2", null), expected);
-    assertResult(GET(changeId), expected);
-    assertResult(POST(changeId, "tag2", null), expected);
-    assertResult(GET(changeId), expected);
-    expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "tag2, tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Adding a hashtag that already exists on the change returns a sorted list
+    // of hashtags without duplicates.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag2");
+    addHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag2");
+    addHashtags(r, "tag1", "tag2");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
   }
 
   @Test
   public void testHashtagsWithPrefix() throws Exception {
-    String changeId = createChange().getChangeId();
+    PushOneCommit.Result r = createChange();
 
-    // Leading # is stripped from added tag
-    List<String> expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "#tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from added tag.
+    addHashtags(r, "#tag1");
+    assertThatGet(r).containsExactly("tag1");
 
-    // Leading # is stripped from multiple added tags
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, "#tag2, #tag3", null), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from multiple added tags.
+    addHashtags(r, "#tag2", "#tag3");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
 
-    // Leading # is stripped from removed tag
-    expected = Arrays.asList("tag1", "tag3");
-    assertResult(POST(changeId, null, "#tag2"), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from removed tag.
+    removeHashtags(r, "#tag2");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
 
-    // Leading # is stripped from multiple removed tags
-    expected = Collections.emptyList();
-    assertResult(POST(changeId, null, "#tag1, #tag3"), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # is stripped from multiple removed tags.
+    removeHashtags(r, "#tag1", "#tag3");
+    assertThatGet(r).isEmpty();
 
-    // Leading # and space are stripped from added tag
-    expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "# tag1", null), expected);
-    assertResult(GET(changeId), expected);
+    // Leading # and space are stripped from added tag.
+    addHashtags(r, "# tag1");
+    assertThatGet(r).containsExactly("tag1");
 
-    // Multiple leading # are stripped from added tag
-    expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "##tag2", null), expected);
-    assertResult(GET(changeId), expected);
+    // Multiple leading # are stripped from added tag.
+    addHashtags(r, "##tag2");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
 
-    // Multiple leading spaces and # are stripped from added tag
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, " # # tag3", null), expected);
-    assertResult(GET(changeId), expected);
+    // Multiple leading spaces and # are stripped from added tag.
+    addHashtags(r, "# # tag3");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
   }
 
   @Test
   public void testRemoveSingleHashtag() throws Exception {
-    // POST removing a single tag from a change that only has that tag
-    // returns an empty list
-    String changeId = createChange().getChangeId();
-    List<String> expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "tag1", null), expected);
-    assertResult(POST(changeId, null, "tag1"), ImmutableList.<String>of());
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Removing a single tag from a change that only has that tag returns an
+    // empty list.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag1");
+    assertThatGet(r).containsExactly("tag1");
+    removeHashtags(r, "tag1");
+    assertThatGet(r).isEmpty();
 
-    // POST removing a single tag from a change that has multiple tags
-    // returns a sorted list of remaining tags
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, "tag1, tag2, tag3", null), expected);
-    expected = Arrays.asList("tag1", "tag3");
-    assertResult(POST(changeId, null, "tag2"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing a single tag from a change that has multiple tags returns a
+    // sorted list of remaining tags.
+    addHashtags(r, "tag1", "tag2", "tag3");
+    removeHashtags(r, "tag2");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
   }
 
   @Test
   public void testRemoveMultipleHashtags() throws Exception {
-    // POST removing multiple tags from a change that only has those tags
-    // returns an empty list
-    String changeId = createChange().getChangeId();
-    List<String> expected = Arrays.asList("tag1", "tag2");
-    assertResult(POST(changeId, "tag1, tag2", null), expected);
-    assertResult(POST(changeId, null, "tag1, tag2"), ImmutableList.<String>of());
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Removing multiple tags from a change that only has those tags returns an
+    // empty list.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag1", "tag2");
+    assertThatGet(r).containsExactly("tag1", "tag2").inOrder();
+    removeHashtags(r, "tag1", "tag2");
+    assertThatGet(r).isEmpty();
 
-    // POST removing multiple tags from a change that has multiple changes
-    // returns a sorted list of remaining changes
-    expected = Arrays.asList("tag1", "tag2", "tag3", "tag4");
-    assertResult(POST(changeId, "tag1, tag2, tag3, tag4", null), expected);
-    expected = Arrays.asList("tag2", "tag4");
-    assertResult(POST(changeId, null, "tag1, tag3"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing multiple tags from a change that has multiple tags returns a
+    // sorted list of remaining tags.
+    addHashtags(r, "tag1", "tag2", "tag3", "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3", "tag4").inOrder();
+    removeHashtags(r, "tag2", "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag3").inOrder();
   }
 
   @Test
   public void testRemoveNotExistingHashtag() throws Exception {
-    // POST removing a single hashtag from change that has no hashtags
-    // returns an empty list
-    String changeId = createChange().getChangeId();
-    assertResult(POST(changeId, null, "tag1"), ImmutableList.<String>of());
-    assertResult(GET(changeId), ImmutableList.<String>of());
+    // Removing a single hashtag from change that has no hashtags returns an
+    // empty list.
+    PushOneCommit.Result r = createChange();
+    removeHashtags(r, "tag1");
+    assertThatGet(r).isEmpty();
 
-    // POST removing a single non-existing tag from a change that only
-    // has one other tag returns a list of only one tag
-    List<String> expected = Arrays.asList("tag1");
-    assertResult(POST(changeId, "tag1", null), expected);
-    assertResult(POST(changeId, null, "tag4"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing a single non-existing tag from a change that only has one other
+    // tag returns a list of only one tag.
+    addHashtags(r, "tag1");
+    removeHashtags(r, "tag4");
+    assertThatGet(r).containsExactly("tag1");
 
-    // POST removing a single non-existing tag from a change that has multiple
-    // tags returns a sorted list of tags without any deleted
-    expected = Arrays.asList("tag1", "tag2", "tag3");
-    assertResult(POST(changeId, "tag1, tag2, tag3", null), expected);
-    assertResult(POST(changeId, null, "tag4"), expected);
-    assertResult(GET(changeId), expected);
+    // Removing a single non-existing tag from a change that has multiple tags
+    // returns a sorted list of tags without any deleted.
+    addHashtags(r, "tag1", "tag2", "tag3");
+    removeHashtags(r, "tag4");
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag3").inOrder();
   }
 
-  private RestResponse GET(String changeId) throws IOException {
-    return adminSession.get("/changes/" + changeId + "/hashtags/");
-  }
-
-  private RestResponse POST(String changeId, String toAdd, String toRemove)
-      throws IOException {
+  @Test
+  public void testAddAndRemove() throws Exception {
+    // Adding and remove hashtags in a single request performs correctly.
+    PushOneCommit.Result r = createChange();
+    addHashtags(r, "tag1", "tag2");
     HashtagsInput input = new HashtagsInput();
-    if (toAdd != null) {
-      input.add = new HashSet<>(
-          Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",")).split(toAdd)));
-    }
-    if (toRemove != null) {
-      input.remove = new HashSet<>(
-          Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",")).split(toRemove)));
-    }
-    return adminSession.post("/changes/" + changeId + "/hashtags/", input);
+    input.add = Sets.newHashSet("tag3", "tag4");
+    input.remove = Sets.newHashSet("tag1");
+    gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+    assertThatGet(r).containsExactly("tag2", "tag3", "tag4");
+
+    // Adding and removing the same hashtag actually removes it.
+    addHashtags(r, "tag1", "tag2");
+    input = new HashtagsInput();
+    input.add = Sets.newHashSet("tag3", "tag4");
+    input.remove = Sets.newHashSet("tag3");
+    gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+    assertThatGet(r).containsExactly("tag1", "tag2", "tag4");
   }
 
-  private static List<String> toHashtagList(RestResponse r)
-      throws IOException {
-    List<String> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<List<String>>() {}.getType());
-    return result;
+  private IterableSubject<
+        ? extends IterableSubject<?, String, Iterable<String>>,
+        String, Iterable<String>>
+      assertThatGet(PushOneCommit.Result r) throws Exception {
+    return assertThat((Iterable<String>) gApi.changes()
+        .id(r.getChange().getId().get())
+        .getHashtags());
+  }
+
+  private void addHashtags(PushOneCommit.Result r, String... toAdd)
+      throws Exception {
+    HashtagsInput input = new HashtagsInput();
+    input.add = Sets.newHashSet(toAdd);
+    gApi.changes()
+        .id(r.getChange().getId().get())
+        .setHashtags(input);
+  }
+
+  private void removeHashtags(PushOneCommit.Result r, String... toRemove)
+      throws Exception {
+    HashtagsInput input = new HashtagsInput();
+    input.remove = Sets.newHashSet(toRemove);
+    gApi.changes()
+        .id(r.getChange().getId().get())
+        .setHashtags(input);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
index 1efaa60..d4365c5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -18,6 +18,7 @@
     '//lib:guava',
     '//lib:junit',
     '//lib:truth',
+    '//gerrit-extension-api:api',
     '//gerrit-server:server',
   ],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
index c706e17..c860bf0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
@@ -16,39 +16,44 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.base.Predicate;
+import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 
 import java.util.List;
 
 public class BranchAssert {
-
   public static void assertBranches(List<BranchInfo> expectedBranches,
       List<BranchInfo> actualBranches) {
-    List<BranchInfo> missingBranches = Lists.newArrayList(actualBranches);
-    for (final BranchInfo b : expectedBranches) {
-      BranchInfo info =
-          Iterables.find(actualBranches, new Predicate<BranchInfo>() {
-            @Override
-            public boolean apply(BranchInfo info) {
-              return info.ref.equals(b.ref);
-            }
-          }, null);
-      assertThat(info).named("branch " + b.ref).isNotNull();
-      assertBranchInfo(b, info);
-      missingBranches.remove(info);
+    assertRefNames(refs(expectedBranches), actualBranches);
+    for (int i = 0; i < expectedBranches.size(); i++) {
+      assertBranchInfo(expectedBranches.get(i), actualBranches.get(i));
     }
-    assertThat(missingBranches).named("" + missingBranches).isEmpty();
+  }
+
+  public static void assertRefNames(Iterable<String> expectedRefs,
+      Iterable<BranchInfo> actualBranches) {
+    Iterable<String> actualNames = refs(actualBranches);
+    assertThat(actualNames).containsExactlyElementsIn(expectedRefs).inOrder();
   }
 
   public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) {
     assertThat(actual.ref).isEqualTo(expected.ref);
     if (expected.revision != null) {
-      assertThat(actual.revision).isEqualTo(expected.revision);
+      assertThat(actual.revision).named("revision of " + actual.ref)
+          .isEqualTo(expected.revision);
     }
-    assertThat(toBoolean(actual.canDelete)).isEqualTo(expected.canDelete);
+    assertThat(toBoolean(actual.canDelete)).named("can delete " + actual.ref)
+        .isEqualTo(toBoolean(expected.canDelete));
+  }
+
+  private static Iterable<String> refs(Iterable<BranchInfo> infos) {
+    return Iterables.transform(infos, new Function<BranchInfo, String>() {
+      @Override
+      public String apply(BranchInfo in) {
+        return in.ref;
+      }
+    });
   }
 
   private static boolean toBoolean(Boolean b) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 3cadf66..6d93060 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -18,17 +18,25 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.block;
+import static org.junit.Assert.fail;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.ProjectConfig;
 
-import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Constants;
 import org.junit.Before;
 import org.junit.Test;
 
+@NoHttpd
 public class CreateBranchIT extends AbstractDaemonTest {
   private Branch.NameKey branch;
 
@@ -39,65 +47,32 @@
 
   @Test
   public void createBranch_Forbidden() throws Exception {
-    RestResponse r =
-        userSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    setApiUser(user);
+    assertCreateFails(AuthException.class);
   }
 
   @Test
   public void createBranchByAdmin() throws Exception {
-    RestResponse r =
-        adminSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertCreateSucceeds();
   }
 
   @Test
   public void branchAlreadyExists_Conflict() throws Exception {
-    RestResponse r =
-        adminSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.put("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    assertCreateSucceeds();
+    assertCreateFails(ResourceConflictException.class);
   }
 
   @Test
   public void createBranchByProjectOwner() throws Exception {
     grantOwner();
-
-    RestResponse r =
-        userSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    setApiUser(user);
+    assertCreateSucceeds();
   }
 
   @Test
   public void createBranchByAdminCreateReferenceBlocked() throws Exception {
     blockCreateReference();
-    RestResponse r =
-        adminSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertCreateSucceeds();
   }
 
   @Test
@@ -105,10 +80,8 @@
       throws Exception {
     grantOwner();
     blockCreateReference();
-    RestResponse r =
-        userSession.put("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    setApiUser(user);
+    assertCreateFails(AuthException.class);
   }
 
   private void blockCreateReference() throws Exception {
@@ -120,4 +93,26 @@
   private void grantOwner() throws Exception {
     allow(Permission.OWNER, REGISTERED_USERS, "refs/*");
   }
+
+  private BranchApi branch() throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get());
+  }
+
+  private void assertCreateSucceeds() throws Exception {
+    BranchInfo created = branch().create(new BranchInput()).get();
+    assertThat(created.ref)
+        .isEqualTo(Constants.R_HEADS + branch.getShortName());
+  }
+
+  private void assertCreateFails(Class<? extends RestApiException> errType)
+      throws Exception {
+    try {
+      branch().create(new BranchInput());
+      fail("Expected " + errType.getSimpleName());
+    } catch (RestApiException expected) {
+      assertThat(expected).isInstanceOf(errType);
+    }
+  }
 }
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
index 78132a9..6f35733 100644
--- 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
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 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.fail;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
@@ -26,6 +27,10 @@
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -47,29 +52,7 @@
 
 public class CreateProjectIT extends AbstractDaemonTest {
   @Test
-  public void testCreateProjectApi() throws Exception {
-    final String newProjectName = "newProject";
-    ProjectInfo p = gApi.projects().create(newProjectName).get();
-    assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertThat(projectState).isNotNull();
-    assertProjectInfo(projectState.getProject(), p);
-    assertHead(newProjectName, "refs/heads/master");
-  }
-
-  @Test
-  public void testCreateProjectApiWithGitSuffix() throws Exception {
-    final String newProjectName = "newProject";
-    ProjectInfo p = gApi.projects().create(newProjectName + ".git").get();
-    assertThat(p.name).isEqualTo(newProjectName);
-    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertThat(projectState).isNotNull();
-    assertProjectInfo(projectState.getProject(), p);
-    assertHead(newProjectName, "refs/heads/master");
-  }
-
-  @Test
-  public void testCreateProject() throws Exception {
+  public void testCreateProjectHttp() throws Exception {
     final String newProjectName = "newProject";
     RestResponse r = adminSession.put("/projects/" + newProjectName);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
@@ -82,11 +65,17 @@
   }
 
   @Test
-  public void testCreateProjectWithGitSuffix() throws Exception {
+  public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.name = "otherName";
+    RestResponse r = adminSession.put("/projects/someName", in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  }
+
+  @Test
+  public void testCreateProject() throws Exception {
     final String newProjectName = "newProject";
-    RestResponse r = adminSession.put("/projects/" + newProjectName + ".git");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    ProjectInfo p = gApi.projects().create(newProjectName).get();
     assertThat(p.name).isEqualTo(newProjectName);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     assertThat(projectState).isNotNull();
@@ -95,25 +84,28 @@
   }
 
   @Test
-  public void testCreateProjectWithNameMismatch_BadRequest() throws Exception {
-    ProjectInput in = new ProjectInput();
-    in.name = "otherName";
-    RestResponse r = adminSession.put("/projects/someName", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+  public void testCreateProjectWithGitSuffix() throws Exception {
+    final String newProjectName = "newProject";
+    ProjectInfo p = gApi.projects().create(newProjectName + ".git").get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
   }
 
   @Test
   public void testCreateProjectWithProperties() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.description = "Test description";
     in.submitType = SubmitType.CHERRY_PICK;
     in.useContributorAgreements = InheritableBoolean.TRUE;
     in.useSignedOffBy = InheritableBoolean.TRUE;
     in.useContentMerge = InheritableBoolean.TRUE;
     in.requireChangeId = InheritableBoolean.TRUE;
-    RestResponse r = adminSession.put("/projects/" + newProjectName, in);
-    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    ProjectInfo p = gApi.projects().create(in).get();
     assertThat(p.name).isEqualTo(newProjectName);
     Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
     assertProjectInfo(project, p);
@@ -128,12 +120,15 @@
   @Test
   public void testCreateChildProject() throws Exception {
     final String parentName = "parent";
-    RestResponse r = adminSession.put("/projects/" + parentName);
-    r.consume();
-    final String childName = "child";
     ProjectInput in = new ProjectInput();
+    in.name = parentName;
+    gApi.projects().create(in);
+
+    final String childName = "child";
+    in = new ProjectInput();
+    in.name = childName;
     in.parent = parentName;
-    r = adminSession.put("/projects/" + childName, in);
+    gApi.projects().create(in);
     Project project = projectCache.get(new Project.NameKey(childName)).getProject();
     assertThat(project.getParentName()).isEqualTo(in.parent);
   }
@@ -142,21 +137,22 @@
   public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity()
       throws Exception {
     ProjectInput in = new ProjectInput();
+    in.name = "newProjectName";
     in.parent = "non-existing-project";
-    RestResponse r = adminSession.put("/projects/child", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    assertCreateFails(in, UnprocessableEntityException.class);
   }
 
   @Test
   public void testCreateProjectWithOwner() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.owners = Lists.newArrayListWithCapacity(3);
     in.owners.add("Anonymous Users"); // by name
     in.owners.add(SystemGroupBackend.REGISTERED_USERS.get()); // by UUID
     in.owners.add(Integer.toString(groupCache.get(
         new AccountGroup.NameKey("Administrators")).getId().get())); // by ID
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     Set<AccountGroup.UUID> expectedOwnerIds = Sets.newHashSetWithExpectedSize(3);
     expectedOwnerIds.add(SystemGroupBackend.ANONYMOUS_USERS);
@@ -169,17 +165,18 @@
   public void testCreateProjectWithNonExistingOwner_UnprocessableEntity()
       throws Exception {
     ProjectInput in = new ProjectInput();
+    in.name = "newProjectName";
     in.owners = Collections.singletonList("non-existing-group");
-    RestResponse r = adminSession.put("/projects/newProject", in);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+    assertCreateFails(in, UnprocessableEntityException.class);
   }
 
   @Test
   public void testCreatePermissionOnlyProject() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.permissionsOnly = true;
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     assertHead(newProjectName, RefNames.REFS_CONFIG);
   }
 
@@ -187,8 +184,9 @@
   public void testCreateProjectWithEmptyCommit() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.createEmptyCommit = true;
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     assertEmptyCommit(newProjectName, "refs/heads/master");
   }
 
@@ -196,12 +194,13 @@
   public void testCreateProjectWithBranches() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
+    in.name = newProjectName;
     in.createEmptyCommit = 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
-    adminSession.put("/projects/" + newProjectName, in);
+    gApi.projects().create(in);
     assertHead(newProjectName, "refs/heads/test");
     assertEmptyCommit(newProjectName, "refs/heads/test", "refs/heads/master",
         "refs/heads/release");
@@ -209,15 +208,18 @@
 
   @Test
   public void testCreateProjectWithoutCapability_Forbidden() throws Exception {
-    RestResponse r = userSession.put("/projects/newProject");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+    setApiUser(user);
+    ProjectInput in = new ProjectInput();
+    in.name = "newProject";
+    assertCreateFails(in, AuthException.class);
   }
 
   @Test
   public void testCreateProjectWhenProjectAlreadyExists_Conflict()
       throws Exception {
-    RestResponse r = adminSession.put("/projects/All-Projects");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+    ProjectInput in = new ProjectInput();
+    in.name = allProjects.get();
+    assertCreateFails(in, ResourceConflictException.class);
   }
 
   private AccountGroup.UUID groupUuid(String groupName) {
@@ -251,4 +253,14 @@
       }
     }
   }
+
+  private void assertCreateFails(ProjectInput in,
+      Class<? extends RestApiException> errType) throws Exception {
+    try {
+      gApi.projects().create(in);
+      fail("Expected " + errType.getSimpleName());
+    } catch (RestApiException expected) {
+      assertThat(expected).isInstanceOf(errType);
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 8be6c92..959c280 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -14,21 +14,25 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.block;
+import static org.junit.Assert.fail;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.ProjectConfig;
 
-import org.apache.http.HttpStatus;
 import org.junit.Before;
 import org.junit.Test;
 
+@NoHttpd
 public class DeleteBranchIT extends AbstractDaemonTest {
 
   private Branch.NameKey branch;
@@ -36,62 +40,31 @@
   @Before
   public void setUp() throws Exception {
     branch = new Branch.NameKey(project, "test");
-    adminSession.put("/projects/" + project.get()
-        + "/branches/" + branch.getShortName()).consume();
+    branch().create(new BranchInput());
   }
 
   @Test
   public void deleteBranch_Forbidden() throws Exception {
-    RestResponse r =
-        userSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
-    r.consume();
+    setApiUser(user);
+    assertDeleteForbidden();
   }
 
   @Test
   public void deleteBranchByAdmin() throws Exception {
-    RestResponse r =
-        adminSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
-    r.consume();
+    assertDeleteSucceeds();
   }
 
   @Test
   public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
-
-    RestResponse r =
-        userSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    r.consume();
-
-    r = userSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
-    r.consume();
+    setApiUser(user);
+    assertDeleteSucceeds();
   }
 
   @Test
   public void deleteBranchByAdminForcePushBlocked() throws Exception {
     blockForcePush();
-    RestResponse r =
-        adminSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get()
-        + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
-    r.consume();
+    assertDeleteSucceeds();
   }
 
   @Test
@@ -99,11 +72,8 @@
       throws Exception {
     grantOwner();
     blockForcePush();
-    RestResponse r =
-        userSession.delete("/projects/" + project.get()
-            + "/branches/" + branch.getShortName());
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
-    r.consume();
+    setApiUser(user);
+    assertDeleteForbidden();
   }
 
   private void blockForcePush() throws Exception {
@@ -115,4 +85,30 @@
   private void grantOwner() throws Exception {
     allow(Permission.OWNER, REGISTERED_USERS, "refs/*");
   }
+
+  private BranchApi branch() throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get());
+  }
+
+  private void assertDeleteSucceeds() throws Exception {
+    branch().delete();
+    try {
+      branch().get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
+  }
+
+  private void assertDeleteForbidden() throws Exception {
+    try {
+      branch().delete();
+      fail("Expected AuthException");
+    } catch (AuthException expected) {
+      // Expected.
+    }
+    branch().get();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 3f27272..3793c82 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -14,100 +14,87 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
+import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertRefNames;
+import static org.junit.Assert.fail;
 
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestProjectInput;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
-import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.ProjectApi.ListBranchesRequest;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 
-import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-
+@NoHttpd
 public class ListBranchesIT extends AbstractDaemonTest {
   @Test
   public void listBranchesOfNonExistingProject_NotFound() throws Exception {
-    assertThat(GET("/projects/non-existing/branches").getStatusCode())
-        .isEqualTo(HttpStatus.SC_NOT_FOUND);
+    try {
+      gApi.projects().name("non-existing").branches().get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
   }
 
   @Test
   public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
     blockRead(project, "refs/*");
-    assertThat(
-        userSession.get("/projects/" + project.get() + "/branches")
-            .getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+    setApiUser(user);
+    try {
+      gApi.projects().name(project.get()).branches().get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException expected) {
+      // Expected.
+    }
   }
 
   @Test
   @TestProjectInput(createEmptyCommit = false)
   public void listBranchesOfEmptyProject() throws Exception {
-    RestResponse r = adminSession.get("/projects/" + project + "/branches");
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("refs/meta/config",  null, false),
-        new BranchInfo[] {
-          new BranchInfo("HEAD", null, false)
-        });
-    assertBranches(expected, toBranchInfoList(r));
+    assertBranches(ImmutableList.of(
+          branch("HEAD", null, false),
+          branch("refs/meta/config",  null, false)),
+        list().get());
   }
 
   @Test
   public void listBranches() throws Exception {
-    pushTo("refs/heads/master");
-    String headCommit = repo().getRef("HEAD").getTarget().getObjectId().getName();
-    pushTo("refs/heads/dev");
-    String devCommit = repo().getRef("HEAD").getTarget().getObjectId().getName();
-    RestResponse r = adminSession.get("/projects/" + project.get() + "/branches");
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("refs/meta/config",  null, false),
-        new BranchInfo[] {
-          new BranchInfo("HEAD", "master", false),
-          new BranchInfo("refs/heads/master", headCommit, false),
-          new BranchInfo("refs/heads/dev", devCommit, true)
-        });
-    List<BranchInfo> result = toBranchInfoList(r);
-    assertBranches(expected, result);
-
-    // verify correct sorting
-    assertThat(result.get(0).ref).isEqualTo("HEAD");
-    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/dev");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/master");
+    String master = pushTo("refs/heads/master").getCommit().name();
+    String dev = pushTo("refs/heads/dev").getCommit().name();
+    assertBranches(ImmutableList.of(
+          branch("HEAD", "master", false),
+          branch("refs/meta/config",  null, false),
+          branch("refs/heads/dev", dev, true),
+          branch("refs/heads/master", master, false)),
+        list().get());
   }
 
   @Test
   public void listBranchesSomeHidden() throws Exception {
     blockRead(project, "refs/heads/dev");
-    pushTo("refs/heads/master");
-    String headCommit = repo().getRef("HEAD").getTarget().getObjectId().getName();
+    String master = pushTo("refs/heads/master").getCommit().name();
     pushTo("refs/heads/dev");
-    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
+    setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    List<BranchInfo> expected = Lists.asList(
-        new BranchInfo("HEAD", "master", false),
-        new BranchInfo[] {
-          new BranchInfo("refs/heads/master", headCommit, false),
-        });
-    assertBranches(expected, toBranchInfoList(r));
+    assertBranches(ImmutableList.of(
+          branch("HEAD", "master", false),
+          branch("refs/heads/master", master, false)),
+        list().get());
   }
 
   @Test
   public void listBranchesHeadHidden() throws Exception {
     blockRead(project, "refs/heads/master");
     pushTo("refs/heads/master");
-    pushTo("refs/heads/dev");
-    String devCommit = repo().getRef("HEAD").getTarget().getObjectId().getName();
-    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
+    String dev = pushTo("refs/heads/dev").getCommit().name();
+    setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    assertBranches(Collections.singletonList(new BranchInfo("refs/heads/dev",
-        devCommit, false)), toBranchInfoList(r));
+    assertBranches(ImmutableList.of(branch("refs/heads/dev", dev, false)),
+        list().get());
   }
 
   @Test
@@ -117,47 +104,40 @@
     pushTo("refs/heads/someBranch2");
     pushTo("refs/heads/someBranch3");
 
-    // using only limit
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches?n=4");
-    List<BranchInfo> result = toBranchInfoList(r);
-    assertThat(result).hasSize(4);
-    assertThat(result.get(0).ref).isEqualTo("HEAD");
-    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch1");
+    // Using only limit.
+    assertRefNames(ImmutableList.of(
+          "HEAD",
+          "refs/meta/config",
+          "refs/heads/master",
+          "refs/heads/someBranch1"),
+        list().withLimit(4).get());
 
-    // limit higher than total number of branches
-    r = adminSession.get("/projects/" + project.get() + "/branches?n=25");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(6);
-    assertThat(result.get(0).ref).isEqualTo("HEAD");
-    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(4).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(5).ref).isEqualTo("refs/heads/someBranch3");
+    // Limit higher than total number of branches.
+    assertRefNames(ImmutableList.of(
+          "HEAD",
+          "refs/meta/config",
+          "refs/heads/master",
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withLimit(25).get());
 
-    // using skip only
-    r = adminSession.get("/projects/" + project.get() + "/branches?s=2");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(4);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch3");
+    // Using start only.
+    assertRefNames(ImmutableList.of(
+          "refs/heads/master",
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withStart(2).get());
 
-    // skip more branches than the number of available branches
-    r = adminSession.get("/projects/" + project.get() + "/branches?s=7");
-    result = toBranchInfoList(r);
-    assertThat(result).isEmpty();
+    // Skip more branches than the number of available branches.
+    assertRefNames(ImmutableList.<String> of(), list().withStart(7).get());
 
-    // using skip and limit
-    r = adminSession.get("/projects/" + project.get() + "/branches?s=2&n=2");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(2);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch1");
+    // Ssing start and limit.
+    assertRefNames(ImmutableList.of(
+          "refs/heads/master",
+          "refs/heads/someBranch1"),
+        list().withStart(2).withLimit(2).get());
   }
 
   @Test
@@ -167,38 +147,34 @@
     pushTo("refs/heads/someBranch2");
     pushTo("refs/heads/someBranch3");
 
-    //using substring
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches?m=some");
-    List<BranchInfo> result = toBranchInfoList(r);
-    assertThat(result).hasSize(3);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch3");
+    // Using substring.
+    assertRefNames(ImmutableList.of(
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withSubstring("some").get());
 
-    r = adminSession.get("/projects/" + project.get() + "/branches?m=Branch");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(3);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/someBranch1");
-    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch2");
-    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch3");
+    assertRefNames(ImmutableList.of(
+          "refs/heads/someBranch1",
+          "refs/heads/someBranch2",
+          "refs/heads/someBranch3"),
+        list().withSubstring("Branch").get());
 
-    //using regex
-    r = adminSession.get("/projects/" + project.get() + "/branches?r=.*ast.*r");
-    result = toBranchInfoList(r);
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
+    // Using regex.
+    assertRefNames(ImmutableList.of("refs/heads/master"),
+        list().withRegex(".*ast.*r").get());
   }
 
-  private RestResponse GET(String endpoint) throws IOException {
-    return adminSession.get(endpoint);
+  private ListBranchesRequest list() throws Exception {
+    return gApi.projects().name(project.get()).branches();
   }
 
-  private static List<BranchInfo> toBranchInfoList(RestResponse r)
-      throws IOException {
-    List<BranchInfo> result =
-        newGson().fromJson(r.getReader(),
-            new TypeToken<List<BranchInfo>>() {}.getType());
-    return result;
+  private static BranchInfo branch(String ref, String revision,
+        boolean canDelete) {
+    BranchInfo info = new BranchInfo();
+    info.ref = ref;
+    info.revision = revision;
+    info.canDelete = canDelete ? true : null;
+    return info;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index d759ae3..c05b56f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -19,9 +19,11 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -34,21 +36,19 @@
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.ConfigSuite;
-import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.apache.http.HttpStatus;
 import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+@NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config noteDbEnabled() {
@@ -66,14 +66,18 @@
 
   private final Integer[] lines = {0, 1};
 
+  @Before
+  public void setUp() {
+    setApiUser(user);
+  }
+
   @Test
   public void createDraft() throws Exception {
     for (Integer line : lines) {
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          "file1", Side.REVISION, line, "comment 1");
+      DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
       addDraft(changeId, revId, comment);
       Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
       assertThat(result).hasSize(1);
@@ -93,8 +97,7 @@
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
       ReviewInput input = new ReviewInput();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          file, Side.REVISION, line, "comment 1");
+      CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
       input.comments = new HashMap<>();
       input.comments.put(comment.path, Lists.newArrayList(comment));
       revision(r).review(input);
@@ -111,8 +114,7 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          "file1", Side.REVISION, line, "comment 1");
+      DraftInput comment = newDraft("file1", Side.REVISION, line, "comment 1");
       addDraft(changeId, revId, comment);
       Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
       CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
@@ -132,7 +134,7 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
+      DraftInput comment = newDraft(
           "file1", Side.REVISION, line, "comment 1");
       CommentInfo returned = addDraft(changeId, revId, comment);
       CommentInfo actual = getDraftComment(changeId, revId, returned.id);
@@ -146,9 +148,8 @@
       PushOneCommit.Result r = createChange();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          "file1", Side.REVISION, line, "comment 1");
-      CommentInfo returned = addDraft(changeId, revId, comment);
+      DraftInput draft = newDraft("file1", Side.REVISION, line, "comment 1");
+      CommentInfo returned = addDraft(changeId, revId, draft);
       deleteDraft(changeId, revId, returned.id);
       Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
       assertThat(drafts).isEmpty();
@@ -167,8 +168,7 @@
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
       ReviewInput input = new ReviewInput();
-      ReviewInput.CommentInput comment = newCommentInfo(
-          file, Side.REVISION, line, "comment 1");
+      CommentInput comment = newComment(file, Side.REVISION, line, "comment 1");
       comment.updated = timestamp;
       input.comments = new HashMap<>();
       input.comments.put(comment.path, Lists.newArrayList(comment));
@@ -186,56 +186,37 @@
     }
   }
 
-  private CommentInfo addDraft(String changeId, String revId,
-      ReviewInput.CommentInput c) throws IOException {
-    RestResponse r = userSession.put(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts", c);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
-    return newGson().fromJson(r.getReader(), CommentInfo.class);
+  private CommentInfo addDraft(String changeId, String revId, DraftInput in)
+      throws Exception {
+    return gApi.changes().id(changeId).revision(revId).createDraft(in).get();
   }
 
-  private void updateDraft(String changeId, String revId,
-      ReviewInput.CommentInput c, String uuid) throws IOException {
-    RestResponse r = userSession.put(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid, c);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+  private void updateDraft(String changeId, String revId, DraftInput in,
+      String uuid) throws Exception {
+    gApi.changes().id(changeId).revision(revId).draft(uuid).update(in);
   }
 
   private void deleteDraft(String changeId, String revId, String uuid)
-      throws IOException {
-    RestResponse r = userSession.delete(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+      throws Exception {
+    gApi.changes().id(changeId).revision(revId).draft(uuid).delete();
   }
 
   private Map<String, List<CommentInfo>> getPublishedComments(String changeId,
-      String revId) throws IOException {
-    RestResponse r = userSession.get(
-        "/changes/" + changeId + "/revisions/" + revId + "/comments/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Type mapType = new TypeToken<Map<String, List<CommentInfo>>>() {}.getType();
-    return newGson().fromJson(r.getReader(), mapType);
+      String revId) throws Exception {
+    return gApi.changes().id(changeId).revision(revId).comments();
   }
 
   private Map<String, List<CommentInfo>> getDraftComments(String changeId,
-      String revId) throws IOException {
-    RestResponse r = userSession.get(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    Type mapType = new TypeToken<Map<String, List<CommentInfo>>>() {}.getType();
-    return newGson().fromJson(r.getReader(), mapType);
+      String revId) throws Exception {
+    return gApi.changes().id(changeId).revision(revId).drafts();
   }
 
   private CommentInfo getDraftComment(String changeId, String revId,
-      String uuid) throws IOException {
-    RestResponse r = userSession.get(
-        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid);
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    return newGson().fromJson(r.getReader(), CommentInfo.class);
+      String uuid) throws Exception {
+    return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
   }
 
-  private static void assertCommentInfo(ReviewInput.CommentInput expected,
-      CommentInfo actual) {
+  private static void assertCommentInfo(Comment expected, CommentInfo actual) {
     assertThat(actual.line).isEqualTo(expected.line);
     assertThat(actual.message).isEqualTo(expected.message);
     assertThat(actual.inReplyTo).isEqualTo(expected.inReplyTo);
@@ -258,21 +239,32 @@
     }
   }
 
-  private ReviewInput.CommentInput newCommentInfo(String path,
-      Side side, int line, String message) {
-    ReviewInput.CommentInput input = new ReviewInput.CommentInput();
-    input.path = path;
-    input.side = side;
-    input.line = line != 0 ? line : null;
-    input.message = message;
+  private static CommentInput newComment(String path, Side side, int line,
+      String message) {
+    CommentInput c = new CommentInput();
+    return populate(c, path, side, line, message);
+  }
+
+  private DraftInput newDraft(String path, Side side, int line,
+      String message) {
+    DraftInput d = new DraftInput();
+    return populate(d, path, side, line, message);
+  }
+
+  private static <C extends Comment> C populate(C c, String path, Side side,
+      int line, String message) {
+    c.path = path;
+    c.side = side;
+    c.line = line != 0 ? line : null;
+    c.message = message;
     if (line != 0) {
       Comment.Range range = new Comment.Range();
       range.startLine = 1;
       range.startCharacter = 1;
       range.endLine = 1;
       range.endCharacter = 5;
-      input.range = range;
+      c.range = range;
     }
-    return input;
+    return c;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index f88a2cb..91cb70e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -20,6 +20,10 @@
 public interface BranchApi {
   BranchApi create(BranchInput in) throws RestApiException;
 
+  BranchInfo get() throws RestApiException;
+
+  void delete() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -29,5 +33,15 @@
     public BranchApi create(BranchInput in) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public BranchInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void delete() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
new file mode 100644
index 0000000..adf215c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
@@ -0,0 +1,15 @@
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+import java.util.List;
+import java.util.Map;
+
+public class BranchInfo {
+  public String ref;
+  public String revision;
+  public Boolean canDelete;
+  public Map<String, ActionInfo> actions;
+  public List<WebLinkInfo> webLinks;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index a8e1efe..102b1ce 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -28,6 +28,54 @@
   String description() throws RestApiException;
   void description(PutDescriptionInput in) throws RestApiException;
 
+  ListBranchesRequest branches();
+
+  public abstract class ListBranchesRequest {
+    private int limit;
+    private int start;
+    private String substring;
+    private String regex;
+
+    public abstract List<BranchInfo> get() throws RestApiException;
+
+    public ListBranchesRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public ListBranchesRequest withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public ListBranchesRequest withSubstring(String substring) {
+      this.substring = substring;
+      return this;
+    }
+
+    public ListBranchesRequest withRegex(String regex) {
+      this.regex = regex;
+      return this;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+
+    public int getStart() {
+      return start;
+    }
+
+    public String getSubstring() {
+      return substring;
+    }
+
+    public String getRegex() {
+      return regex;
+    }
+
+  }
+
   List<ProjectInfo> children() throws RestApiException;
   List<ProjectInfo> children(boolean recursive) throws RestApiException;
   ChildProjectApi child(String name) throws RestApiException;
@@ -79,6 +127,11 @@
     }
 
     @Override
+    public ListBranchesRequest branches() {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public List<ProjectInfo> children() {
       throw new NotImplementedException();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index be5e000..fcfbeb2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
@@ -37,6 +39,7 @@
 import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
@@ -327,35 +330,29 @@
   @Override
   public Set<Change.Id> getStarredChanges() {
     if (starredChanges == null) {
-      if (dbProvider == null) {
-        throw new OutOfScopeException("Not in request scoped user");
-      }
-      Set<Change.Id> h = Sets.newHashSet();
+      checkRequestScope();
       try {
-        if (starredQuery != null) {
-          for (StarredChange sc : starredQuery) {
-            h.add(sc.getChangeId());
-          }
-          starredQuery = null;
-        } else {
-          for (StarredChange sc : dbProvider.get().starredChanges()
-              .byAccount(getAccountId())) {
-            h.add(sc.getChangeId());
-          }
-        }
-      } catch (OrmException e) {
-        log.warn("Cannot query starred by user changes", e);
+        starredChanges = starredChangeIds(
+            starredQuery != null ? starredQuery : starredQuery());
+      } catch (OrmException | OrmRuntimeException e) {
+        log.warn("Cannot query starred changes", e);
       }
-      starredChanges = Collections.unmodifiableSet(h);
     }
     return starredChanges;
   }
 
+  public Set<Change.Id> clearStarredChanges() {
+    // Async query may have started before an update that the caller expects
+    // to see the results of, so we can't trust it.
+    abortStarredChanges();
+    starredChanges = null;
+    return starredChanges;
+  }
+
   public void asyncStarredChanges() {
     if (starredChanges == null && dbProvider != null) {
       try {
-        starredQuery =
-            dbProvider.get().starredChanges().byAccount(getAccountId());
+        starredQuery = starredQuery();
       } catch (OrmException e) {
         log.warn("Cannot query starred by user changes", e);
         starredQuery = null;
@@ -374,12 +371,31 @@
     }
   }
 
+  private void checkRequestScope() {
+    if (dbProvider == null) {
+      throw new OutOfScopeException("Not in request scoped user");
+    }
+  }
+
+  private ResultSet<StarredChange> starredQuery() throws OrmException {
+    return dbProvider.get().starredChanges().byAccount(getAccountId());
+  }
+
+  private static ImmutableSet<Change.Id> starredChangeIds(
+      Iterable<StarredChange> scs) {
+    return FluentIterable.from(scs)
+        .transform(new Function<StarredChange, Change.Id>() {
+          @Override
+          public Change.Id apply(StarredChange in) {
+            return in.getChangeId();
+          }
+        }).toSet();
+  }
+
   @Override
   public Collection<AccountProjectWatch> getNotificationFilters() {
     if (notificationFilters == null) {
-      if (dbProvider == null) {
-        throw new OutOfScopeException("Not in request scoped user");
-      }
+      checkRequestScope();
       List<AccountProjectWatch> r;
       try {
         r = dbProvider.get().accountProjectWatches() //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 610e344..06c59ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -31,6 +31,8 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.Abandon;
 import com.google.gerrit.server.change.ChangeEdits;
 import com.google.gerrit.server.change.ChangeJson;
@@ -61,6 +63,7 @@
     ChangeApiImpl create(ChangeResource change);
   }
 
+  private final Provider<CurrentUser> user;
   private final Changes changeApi;
   private final Revisions revisions;
   private final RevisionApiImpl.Factory revisionApi;
@@ -79,7 +82,8 @@
   private final ChangeEdits.Detail editDetail;
 
   @Inject
-  ChangeApiImpl(Changes changeApi,
+  ChangeApiImpl(Provider<CurrentUser> user,
+      Changes changeApi,
       Revisions revisions,
       RevisionApiImpl.Factory revisionApi,
       Provider<SuggestReviewers> suggestReviewers,
@@ -95,6 +99,7 @@
       Check check,
       ChangeEdits.Detail editDetail,
       @Assisted ChangeResource change) {
+    this.user = user;
     this.changeApi = changeApi;
     this.revert = revert;
     this.revisions = revisions;
@@ -244,6 +249,10 @@
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        ((IdentifiedUser) u).clearStarredChanges();
+      }
       return changeJson.get().addOptions(s).format(change);
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve change", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 91809ec..c86c422 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -29,6 +29,8 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.CreateChange;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -43,16 +45,19 @@
 
 @Singleton
 class ChangesImpl implements Changes {
+  private final Provider<CurrentUser> user;
   private final ChangesCollection changes;
   private final ChangeApiImpl.Factory api;
   private final CreateChange createChange;
   private final Provider<QueryChanges> queryProvider;
 
   @Inject
-  ChangesImpl(ChangesCollection changes,
+  ChangesImpl(Provider<CurrentUser> user,
+      ChangesCollection changes,
       ChangeApiImpl.Factory api,
       CreateChange createChange,
       Provider<QueryChanges> queryProvider) {
+    this.user = user;
     this.changes = changes;
     this.api = api;
     this.createChange = createChange;
@@ -123,6 +128,10 @@
     }
 
     try {
+      CurrentUser u = user.get();
+      if (u.isIdentifiedUser()) {
+        ((IdentifiedUser) u).clearStarredChanges();
+      }
       List<?> result = qc.apply(TopLevelResource.INSTANCE);
       if (result.isEmpty()) {
         return ImmutableList.of();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 63d0902..57db303 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -319,7 +319,11 @@
   @Override
   public DraftApi createDraft(DraftInput in) throws RestApiException {
     try {
-      return draft(createDraft.apply(revision, in).value().id);
+      String id = createDraft.apply(revision, in).value().id;
+      // Reread change to pick up new notes refs.
+      return changes.id(revision.getChange().getId().get())
+          .revision(revision.getPatchSet().getId().get())
+          .draft(id);
     } catch (IOException | OrmException e) {
       throw new RestApiException("Cannot create draft", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
index 39166c3..0bb395f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -15,10 +15,16 @@
 package com.google.gerrit.server.api.projects;
 
 import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.BranchResource;
+import com.google.gerrit.server.project.BranchesCollection;
 import com.google.gerrit.server.project.CreateBranch;
+import com.google.gerrit.server.project.DeleteBranch;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -29,16 +35,21 @@
     BranchApiImpl create(ProjectResource project, String ref);
   }
 
+  private final BranchesCollection branches;
   private final CreateBranch.Factory createBranchFactory;
+  private final DeleteBranch deleteBranch;
   private final String ref;
   private final ProjectResource project;
 
   @Inject
-  BranchApiImpl(
+  BranchApiImpl(BranchesCollection branches,
       CreateBranch.Factory createBranchFactory,
+      DeleteBranch deleteBranch,
       @Assisted ProjectResource project,
       @Assisted String ref) {
+    this.branches = branches;
     this.createBranchFactory = createBranchFactory;
+    this.deleteBranch = deleteBranch;
     this.project = project;
     this.ref = ref;
   }
@@ -55,4 +66,26 @@
       throw new RestApiException("Cannot create branch", e);
     }
   }
+
+  @Override
+  public BranchInfo get() throws RestApiException {
+    try {
+      return resource().getBranchInfo();
+    } catch (IOException e) {
+      throw new RestApiException("Cannot read branch", e);
+    }
+  }
+
+  @Override
+  public void delete() throws RestApiException {
+    try {
+      deleteBranch.apply(resource(), new DeleteBranch.Input());
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot delete branch", e);
+    }
+  }
+
+  private BranchResource resource() throws RestApiException, IOException {
+    return branches.parse(project, IdString.fromDecoded(ref));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index f80762d..93e9546 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.api.projects;
 
+import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
+
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
 import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ChildProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -27,10 +30,11 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.ChildProjectsCollection;
 import com.google.gerrit.server.project.CreateProject;
 import com.google.gerrit.server.project.GetDescription;
+import com.google.gerrit.server.project.ListBranches;
 import com.google.gerrit.server.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
@@ -49,6 +53,7 @@
     ProjectApiImpl create(String name);
   }
 
+  private final Provider<CurrentUser> user;
   private final Provider<CreateProject.Factory> createProjectFactory;
   private final ProjectApiImpl.Factory projectApi;
   private final ProjectsCollection projects;
@@ -60,9 +65,11 @@
   private final ProjectJson projectJson;
   private final String name;
   private final BranchApiImpl.Factory branchApi;
+  private final Provider<ListBranches> listBranchesProvider;
 
   @AssistedInject
-  ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+  ProjectApiImpl(Provider<CurrentUser> user,
+      Provider<CreateProject.Factory> createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
       GetDescription getDescription,
@@ -71,14 +78,16 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      Provider<ListBranches> listBranchesProvider,
       @Assisted ProjectResource project) {
-    this(createProjectFactory, projectApi, projects, getDescription,
+    this(user, createProjectFactory, projectApi, projects, getDescription,
         putDescription, childApi, children, projectJson, branchApiFactory,
-        project, null);
+        listBranchesProvider, project, null);
   }
 
   @AssistedInject
-  ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+  ProjectApiImpl(Provider<CurrentUser> user,
+      Provider<CreateProject.Factory> createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
       GetDescription getDescription,
@@ -87,13 +96,15 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      Provider<ListBranches> listBranchesProvider,
       @Assisted String name) {
-    this(createProjectFactory, projectApi, projects, getDescription,
-        putDescription, childApi, children, projectJson, branchApiFactory, null,
-        name);
+    this(user, createProjectFactory, projectApi, projects, getDescription,
+        putDescription, childApi, children, projectJson, branchApiFactory,
+        listBranchesProvider, null, name);
   }
 
-  private ProjectApiImpl(Provider<CreateProject.Factory> createProjectFactory,
+  private ProjectApiImpl(Provider<CurrentUser> user,
+      Provider<CreateProject.Factory> createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
       GetDescription getDescription,
@@ -102,8 +113,10 @@
       ChildProjectsCollection children,
       ProjectJson projectJson,
       BranchApiImpl.Factory branchApiFactory,
+      Provider<ListBranches> listBranchesProvider,
       ProjectResource project,
       String name) {
+    this.user = user;
     this.createProjectFactory = createProjectFactory;
     this.projectApi = projectApi;
     this.projects = projects;
@@ -115,6 +128,7 @@
     this.project = project;
     this.name = name;
     this.branchApi = branchApiFactory;
+    this.listBranchesProvider = listBranchesProvider;
   }
 
   @Override
@@ -131,12 +145,11 @@
       if (in.name != null && !name.equals(in.name)) {
         throw new BadRequestException("name must match input.name");
       }
+      checkRequiresCapability(user, null, CreateProject.class);
       createProjectFactory.get().create(name)
           .apply(TopLevelResource.INSTANCE, in);
       return projectApi.create(projects.parse(name));
-    } catch (BadRequestException | UnprocessableEntityException
-        | ResourceNotFoundException | ProjectCreationFailedException
-        | IOException e) {
+    } catch (ProjectCreationFailedException | IOException e) {
       throw new RestApiException("Cannot create project: " + e.getMessage(), e);
     }
   }
@@ -165,6 +178,30 @@
   }
 
   @Override
+  public ListBranchesRequest branches() {
+    return new ListBranchesRequest() {
+      @Override
+      public List<BranchInfo> get() throws RestApiException {
+        return listBranches(this);
+      }
+    };
+  }
+
+  private List<BranchInfo> listBranches(ListBranchesRequest request)
+      throws RestApiException {
+    ListBranches list = listBranchesProvider.get();
+    list.setLimit(request.getLimit());
+    list.setStart(request.getStart());
+    list.setMatchSubstring(request.getSubstring());
+    list.setMatchRegex(request.getRegex());
+    try {
+      return list.apply(checkExists());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot list branches", e);
+    }
+  }
+
+  @Override
   public List<ProjectInfo> children() throws RestApiException {
     return children(false);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
index 4846c0b..d0c1e83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
@@ -30,9 +30,8 @@
 @Singleton
 public class GetHashtags implements RestReadView<ChangeResource> {
   @Override
-  public Response<? extends Set<String>> apply(ChangeResource req)
+  public Response<Set<String>> apply(ChangeResource req)
       throws AuthException, OrmException, IOException, BadRequestException {
-
     ChangeControl control = req.getControl();
     ChangeNotes notes = control.getNotes().load();
     Set<String> hashtags = notes.getHashtags();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index 6638f91..567241a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -38,12 +38,12 @@
   }
 
   @Override
-  public Response<? extends Set<String>> apply(ChangeResource req, HashtagsInput input)
+  public Response<Set<String>> apply(ChangeResource req, HashtagsInput input)
       throws AuthException, OrmException, IOException, BadRequestException,
       ResourceConflictException {
 
     try {
-      return Response.ok(hashtagsUtil.setHashtags(
+      return Response.<Set<String>> ok(hashtagsUtil.setHashtags(
           req.getControl(), input, true, true));
     } catch (IllegalArgumentException e) {
       throw new BadRequestException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
index 98531ce..7168b1b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.TypeLiteral;
 
 public class BranchResource extends ProjectResource {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
index e96d3a5..133993a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -21,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index 834dba5..d683742 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.errors.InvalidRevisionException;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -30,7 +31,6 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.CreateBranch.Input;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -172,7 +172,11 @@
           }
         }
 
-        return new BranchInfo(ref, revid.getName(), refControl.canDelete());
+        BranchInfo info = new BranchInfo();
+        info.ref = ref;
+        info.revision = revid.getName();
+        info.canDelete = refControl.canDelete() ? true : null;
+        return info;
       } catch (IOException err) {
         log.error("Cannot create branch \"" + name + "\"", err);
         throw err;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 4aba333..175aeca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -46,7 +46,7 @@
   private static final int MAX_LOCK_FAILURE_CALLS = 10;
   private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
 
-  static class Input {
+  public static class Input {
   }
 
   private final Provider<IdentifiedUser> identifiedUser;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
index 59b15d8..78878a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.Singleton;
 
 @Singleton
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index 2e09084..85a9264 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -50,7 +51,6 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -60,15 +60,28 @@
   private final WebLinks webLinks;
 
   @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of branches to list")
-  private int limit;
+  public void setLimit(int limit) {
+    this.limit = limit;
+  }
 
   @Option(name = "--start", aliases = {"-s"}, metaVar = "CNT", usage = "number of branches to skip")
-  private int start;
+  public void setStart(int start) {
+    this.start = start;
+  }
 
   @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match branches substring")
-  private String matchSubstring;
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
 
   @Option(name = "--regex", aliases = {"-r"}, metaVar = "REGEX", usage = "match branches regex")
+  public void setMatchRegex(String matchRegex) {
+    this.matchRegex = matchRegex;
+  }
+
+  private int limit;
+  private int start;
+  private String matchSubstring;
   private String matchRegex;
 
   @Inject
@@ -130,11 +143,13 @@
           target = target.substring(Constants.R_HEADS.length());
         }
 
-        BranchInfo b = new BranchInfo(ref.getName(), target, false);
+        BranchInfo b = new BranchInfo();
+        b.ref = ref.getName();
+        b.revision = target;
         branches.add(b);
 
         if (!Constants.HEAD.equals(ref.getName())) {
-          b.setCanDelete(targetRefControl.canDelete());
+          b.canDelete = targetRefControl.canDelete() ? true : null;
         }
         continue;
       }
@@ -232,9 +247,11 @@
 
   private BranchInfo createBranchInfo(Ref ref, RefControl refControl,
       Set<String> targets) {
-    BranchInfo info = new BranchInfo(ref.getName(),
-        ref.getObjectId() != null ? ref.getObjectId().name() : null,
-        !targets.contains(ref.getName()) && refControl.canDelete());
+    BranchInfo info = new BranchInfo();
+    info.ref = ref.getName();
+    info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
+    info.canDelete = !targets.contains(ref.getName()) && refControl.canDelete()
+        ? true : null;
     for (UiAction.Description d : UiActions.from(
         branchViews,
         new BranchResource(refControl.getProjectControl(), info),
@@ -250,22 +267,4 @@
     info.webLinks = links.isEmpty() ? null : links.toList();
     return info;
   }
-
-  public static class BranchInfo {
-    public String ref;
-    public String revision;
-    public Boolean canDelete;
-    public Map<String, ActionInfo> actions;
-    public List<WebLinkInfo> webLinks;
-
-    public BranchInfo(String ref, String revision, boolean canDelete) {
-      this.ref = ref;
-      this.revision = revision;
-      this.canDelete = canDelete;
-    }
-
-    void setCanDelete(boolean canDelete) {
-      this.canDelete = canDelete ? true : null;
-    }
-  }
 }
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 71c3104..e507de7 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
@@ -259,6 +259,7 @@
     switch (getCurrentUser().getAccessPath()) {
       case REST_API:
       case JSON_RPC:
+      case UNKNOWN:
         owner = isOwner();
         admin = getCurrentUser().getCapabilities().canAdministrateServer();
         break;
@@ -364,18 +365,13 @@
     }
 
     switch (getCurrentUser().getAccessPath()) {
-      case REST_API:
-      case JSON_RPC:
-      case SSH_COMMAND:
-        return getCurrentUser().getCapabilities().canAdministrateServer()
-            || (isOwner() && !isForceBlocked(Permission.PUSH))
-            || canPushWithForce();
-
       case GIT:
         return canPushWithForce();
 
       default:
-        return false;
+        return getCurrentUser().getCapabilities().canAdministrateServer()
+            || (isOwner() && !isForceBlocked(Permission.PUSH))
+            || canPushWithForce();
     }
   }