Support listing of branches via REST

GET on /projects/<project-name>/branches returns a list with the
branches of the project.

Change-Id: Ic30e4fc563799e9a4352c4ed01c4db5316abb835
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index bb83048..879a35e 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -483,6 +483,54 @@
   done.
 ----
 
+[[branch-endpoints]]
+Branch Endpoints
+----------------
+
+[[list-branches]]
+List Branches
+~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/branches/'
+
+List the branches of a project.
+
+As result a list of link:#branch-info[BranchInfo] entries is
+returned.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/branches/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "HEAD",
+      "revision": "master"
+    },
+    {
+      "ref": "refs/meta/config",
+      "revision": "76016386a0d8ecc7b6be212424978bb45959d668"
+    },
+    {
+      "ref": "refs/heads/master",
+      "revision": "67ebf73496383c6777035e374d2d664009e2aa5c"
+    },
+    {
+      "ref": "refs/heads/stable",
+      "revision": "64ca533bd0eb5252d2fee83f63da67caae9b4674",
+      "can_delete": true
+    }
+  ]
+----
+
 [[child-project-endpoints]]
 Child Project Endpoints
 -----------------------
@@ -871,6 +919,20 @@
 JSON Entities
 -------------
 
+[[branch-info]]
+BranchInfo
+~~~~~~~~~~
+The `BranchInfo` entity contains information about a branch.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=========================
+|Field Name  ||Description
+|`ref`       ||The ref of the branch.
+|`revision`  ||The revision to which the branch points.
+|`can_delete`|`false` if not set|
+Whether the calling user can delete this branch.
+|=========================
+
 [[dashboard-info]]
 DashboardInfo
 ~~~~~~~~~~~~~
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index 2366423..7330337 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
@@ -14,31 +14,24 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ListBranchesResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 class ListBranches extends Handler<ListBranchesResult> {
   interface Factory {
@@ -46,127 +39,37 @@
   }
 
   private final ProjectControl.Factory projectControlFactory;
-  private final GitRepositoryManager repoManager;
+  private final Provider<com.google.gerrit.server.project.ListBranches> listBranchesProvider;
 
   private final Project.NameKey projectName;
 
   @Inject
   ListBranches(final ProjectControl.Factory projectControlFactory,
-      final GitRepositoryManager repoManager,
-
+      final Provider<com.google.gerrit.server.project.ListBranches> listBranchesProvider,
       @Assisted final Project.NameKey name) {
     this.projectControlFactory = projectControlFactory;
-    this.repoManager = repoManager;
+    this.listBranchesProvider = listBranchesProvider;
 
     this.projectName = name;
   }
 
   @Override
   public ListBranchesResult call() throws NoSuchProjectException, IOException {
-    final ProjectControl pctl = projectControlFactory.validateFor( //
-        projectName, //
-        ProjectControl.OWNER | ProjectControl.VISIBLE);
-
-    final List<Branch> branches = new ArrayList<Branch>();
-    Branch headBranch = null;
-    Branch configBranch = null;
-    final Set<String> targets = new HashSet<String>();
-
-    final Repository db;
+    ProjectControl pctl =
+        projectControlFactory.validateFor(projectName, ProjectControl.OWNER
+            | ProjectControl.VISIBLE);
     try {
-      db = repoManager.openRepository(projectName);
-    } catch (RepositoryNotFoundException noGitRepository) {
-      return new ListBranchesResult(branches, false, true);
-    }
-    try {
-      final Map<String, Ref> all = db.getAllRefs();
-
-      if (!all.containsKey(Constants.HEAD)) {
-        // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
-        // filtered it out. If we ask for it individually we can find the
-        // underlying target and put it into the map anyway.
-        //
-        try {
-          Ref head = db.getRef(Constants.HEAD);
-          if (head != null) {
-            all.put(Constants.HEAD, head);
-          }
-        } catch (IOException e) {
-          // Ignore the failure reading HEAD.
-        }
+      List<Branch> branches = Lists.newArrayList();
+      List<BranchInfo> branchInfos = listBranchesProvider.get().apply(new ProjectResource(pctl));
+      for (BranchInfo info : branchInfos) {
+        Branch b = new Branch(new Branch.NameKey(projectName, info.ref));
+        b.setRevision(new RevId(info.revision));
+        b.setCanDelete(Objects.firstNonNull(info.canDelete, false));
+        branches.add(b);
       }
-
-      for (final Ref ref : all.values()) {
-        if (ref.isSymbolic()) {
-          targets.add(ref.getTarget().getName());
-        }
-      }
-
-      for (final Ref ref : all.values()) {
-        if (ref.isSymbolic()) {
-          // A symbolic reference to another branch, instead of
-          // showing the resolved value, show the name it references.
-          //
-          String target = ref.getTarget().getName();
-          RefControl targetRefControl = pctl.controlForRef(target);
-          if (!targetRefControl.isVisible()) {
-            continue;
-          }
-          if (target.startsWith(Constants.R_HEADS)) {
-            target = target.substring(Constants.R_HEADS.length());
-          }
-
-          Branch b = createBranch(ref.getName());
-          b.setRevision(new RevId(target));
-
-          if (Constants.HEAD.equals(ref.getName())) {
-            b.setCanDelete(false);
-            headBranch = b;
-          } else {
-            b.setCanDelete(targetRefControl.canDelete());
-            branches.add(b);
-          }
-          continue;
-        }
-
-        final RefControl refControl = pctl.controlForRef(ref.getName());
-        if (refControl.isVisible()) {
-          if (ref.getName().startsWith(Constants.R_HEADS)) {
-            branches.add(createBranch(ref, refControl, targets));
-          } else if (GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
-            configBranch = createBranch(ref, refControl, targets);
-          }
-        }
-      }
-    } finally {
-      db.close();
+      return new ListBranchesResult(branches, pctl.canAddRefs(), false);
+    } catch (ResourceNotFoundException e) {
+      throw new NoSuchProjectException(projectName);
     }
-    Collections.sort(branches, new Comparator<Branch>() {
-      @Override
-      public int compare(final Branch a, final Branch b) {
-        return a.getName().compareTo(b.getName());
-      }
-    });
-    if (configBranch != null) {
-      branches.add(0, configBranch);
-    }
-    if (headBranch != null) {
-      branches.add(0, headBranch);
-    }
-    return new ListBranchesResult(branches, pctl.canAddRefs(), false);
-  }
-
-  private Branch createBranch(final Ref ref, final RefControl refControl,
-      final Set<String> targets) {
-    final Branch b = createBranch(ref.getName());
-    if (ref.getObjectId() != null) {
-      b.setRevision(new RevId(ref.getObjectId().name()));
-    }
-    b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
-    return b;
-  }
-
-  private Branch createBranch(final String name) {
-    return new Branch(new Branch.NameKey(projectName, name));
   }
 }
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
index 5cfde6a..7265470 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.project.RefControl;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.StandardKeyEncoder;
+import com.google.inject.Provider;
 
 import org.easymock.IExpectationSetters;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
@@ -84,6 +85,7 @@
 
     mockDb = createStrictMock(Repository.class);
     pc = createStrictMock(ProjectControl.class);
+    expect(pc.getProject()).andReturn(new Project(name)).anyTimes();
     pcf = createStrictMock(ProjectControl.Factory.class);
     grm = createStrictMock(GitRepositoryManager.class);
     refMocks = new ArrayList<RefControl>();
@@ -125,7 +127,7 @@
     validate().andThrow(err);
     doReplay();
     try {
-      new ListBranches(pcf, grm, name).call();
+      new ListBranches(pcf, createListBranchesProvider(grm), name).call();
       fail("did not throw when expected not authorized");
     } catch (NoSuchProjectException e2) {
       assertSame(err, e2);
@@ -161,7 +163,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
     assertNotNull(r.getBranches());
@@ -302,7 +305,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
 
@@ -330,7 +334,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
     assertTrue(r.getBranches().isEmpty());
@@ -359,7 +364,8 @@
     expectLastCall();
 
     doReplay();
-    final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
+    final ListBranchesResult r =
+        new ListBranches(pcf, createListBranchesProvider(grm), name).call();
     doVerify();
     assertNotNull(r);
 
@@ -371,4 +377,14 @@
     assertEquals("bar", r.getBranches().get(1).getShortName());
     assertFalse(r.getBranches().get(1).getCanDelete());
   }
+
+  private static Provider<com.google.gerrit.server.project.ListBranches> createListBranchesProvider(
+      final GitRepositoryManager grm) {
+    return new Provider<com.google.gerrit.server.project.ListBranches>() {
+      @Override
+      public com.google.gerrit.server.project.ListBranches get() {
+        return new com.google.gerrit.server.project.ListBranches(grm);
+      }
+    };
+  }
 }
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
new file mode 100644
index 0000000..016222d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.inject.TypeLiteral;
+
+public class BranchResource extends ProjectResource {
+  public static final TypeLiteral<RestView<BranchResource>> BRANCH_KIND =
+      new TypeLiteral<RestView<BranchResource>>() {};
+
+  private final Branch.NameKey branch;
+
+  public BranchResource(ProjectControl control, Branch.NameKey branch) {
+    super(control);
+    this.branch = branch;
+  }
+
+  public Branch.NameKey getBranch() {
+    return branch;
+  }
+}
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
new file mode 100644
index 0000000..fc7343b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class BranchesCollection implements
+    ChildCollection<ProjectResource, BranchResource> {
+  private final DynamicMap<RestView<BranchResource>> views;
+  private final Provider<ListBranches> list;
+
+  @Inject
+  BranchesCollection(DynamicMap<RestView<BranchResource>> views,
+      Provider<ListBranches> list) {
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public RestView<ProjectResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public BranchResource parse(ProjectResource parent, IdString id)
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<BranchResource>> views() {
+    return views;
+  }
+}
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
new file mode 100644
index 0000000..b01c757
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ListBranches implements RestReadView<ProjectResource> {
+
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  public ListBranches(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public List<BranchInfo> apply(ProjectResource rsrc)
+      throws ResourceNotFoundException, IOException {
+    List<BranchInfo> branches = Lists.newArrayList();
+
+    BranchInfo headBranch = null;
+    BranchInfo configBranch = null;
+    final Set<String> targets = Sets.newHashSet();
+
+    final Repository db;
+    try {
+      db = repoManager.openRepository(rsrc.getNameKey());
+    } catch (RepositoryNotFoundException noGitRepository) {
+      throw new ResourceNotFoundException();
+    }
+
+    try {
+      final Map<String, Ref> all = db.getAllRefs();
+
+      if (!all.containsKey(Constants.HEAD)) {
+        // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
+        // filtered it out. If we ask for it individually we can find the
+        // underlying target and put it into the map anyway.
+        //
+        try {
+          Ref head = db.getRef(Constants.HEAD);
+          if (head != null) {
+            all.put(Constants.HEAD, head);
+          }
+        } catch (IOException e) {
+          // Ignore the failure reading HEAD.
+        }
+      }
+
+      for (final Ref ref : all.values()) {
+        if (ref.isSymbolic()) {
+          targets.add(ref.getTarget().getName());
+        }
+      }
+
+      for (final Ref ref : all.values()) {
+        if (ref.isSymbolic()) {
+          // A symbolic reference to another branch, instead of
+          // showing the resolved value, show the name it references.
+          //
+          String target = ref.getTarget().getName();
+          RefControl targetRefControl = rsrc.getControl().controlForRef(target);
+          if (!targetRefControl.isVisible()) {
+            continue;
+          }
+          if (target.startsWith(Constants.R_HEADS)) {
+            target = target.substring(Constants.R_HEADS.length());
+          }
+
+          BranchInfo b = new BranchInfo();
+          b.ref = ref.getName();
+          b.revision = target;
+
+          if (Constants.HEAD.equals(ref.getName())) {
+            b.setCanDelete(false);
+            headBranch = b;
+          } else {
+            b.setCanDelete(targetRefControl.canDelete());
+            branches.add(b);
+          }
+          continue;
+        }
+
+        final RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
+        if (refControl.isVisible()) {
+          if (ref.getName().startsWith(Constants.R_HEADS)) {
+            branches.add(createBranchInfo(ref, refControl, targets));
+          } else if (GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
+            configBranch = createBranchInfo(ref, refControl, targets);
+          }
+        }
+      }
+    } finally {
+      db.close();
+    }
+    Collections.sort(branches, new Comparator<BranchInfo>() {
+      @Override
+      public int compare(final BranchInfo a, final BranchInfo b) {
+        return a.ref.compareTo(b.ref);
+      }
+    });
+    if (configBranch != null) {
+      branches.add(0, configBranch);
+    }
+    if (headBranch != null) {
+      branches.add(0, headBranch);
+    }
+    return branches;
+  }
+
+  private static BranchInfo createBranchInfo(Ref ref, RefControl refControl,
+      Set<String> targets) {
+    BranchInfo b = new BranchInfo();
+    b.ref = ref.getName();
+    if (ref.getObjectId() != null) {
+      b.revision = ref.getObjectId().name();
+    }
+    b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
+    return b;
+  }
+
+  public static class BranchInfo {
+    public String ref;
+    public String revision;
+    public Boolean canDelete;
+
+    void setCanDelete(boolean canDelete) {
+      this.canDelete = canDelete ? true : null;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index d979245..86e9a2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.gerrit.server.project.BranchResource.BRANCH_KIND;
 import static com.google.gerrit.server.project.ChildProjectResource.CHILD_PROJECT_KIND;
 import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.project.CreateProject;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public class Module extends RestApiModule {
@@ -31,6 +31,7 @@
 
     DynamicMap.mapOf(binder(), PROJECT_KIND);
     DynamicMap.mapOf(binder(), CHILD_PROJECT_KIND);
+    DynamicMap.mapOf(binder(), BRANCH_KIND);
     DynamicMap.mapOf(binder(), DASHBOARD_KIND);
 
     put(PROJECT_KIND).to(PutProject.class);
@@ -51,6 +52,8 @@
     get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
     post(PROJECT_KIND, "gc").to(GarbageCollect.class);
 
+    child(PROJECT_KIND, "branches").to(BranchesCollection.class);
+
     child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
     get(DASHBOARD_KIND).to(GetDashboard.class);
     put(DASHBOARD_KIND).to(SetDashboard.class);