Add recursive option to gitiles tree JSON

In recursive mode, the tree entries are walked so the final JSON only
contains non-tree objects.

The name is analogous to git-ls-tree -r[ecurse].

Change-Id: I61fa14214280822b36ba28b53d1393fa93749645
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
index d570dbc..e0cc53e 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
@@ -133,7 +133,7 @@
     Repository repo = ServletUtils.getRepository(req);
 
     try (RevWalk rw = new RevWalk(repo);
-        WalkResult wr = WalkResult.forPath(rw, view)) {
+        WalkResult wr = WalkResult.forPath(rw, view, false)) {
       if (wr == null) {
         res.setStatus(SC_NOT_FOUND);
         return;
@@ -168,7 +168,7 @@
     Repository repo = ServletUtils.getRepository(req);
 
     try (RevWalk rw = new RevWalk(repo);
-        WalkResult wr = WalkResult.forPath(rw, view)) {
+        WalkResult wr = WalkResult.forPath(rw, view, false)) {
       if (wr == null) {
         res.setStatus(SC_NOT_FOUND);
         return;
@@ -246,8 +246,14 @@
         (longStr != null)
             && (longStr.isEmpty() || Boolean.TRUE.equals(StringUtils.toBooleanOrNull(longStr)));
 
+    String recursiveStr = req.getParameter("recursive");
+    boolean recursive =
+        (recursiveStr != null)
+            && (recursiveStr.isEmpty()
+                || Boolean.TRUE.equals(StringUtils.toBooleanOrNull(recursiveStr)));
+
     try (RevWalk rw = new RevWalk(repo);
-        WalkResult wr = WalkResult.forPath(rw, view)) {
+        WalkResult wr = WalkResult.forPath(rw, view, recursive)) {
       if (wr == null) {
         res.setStatus(SC_NOT_FOUND);
         return;
@@ -257,7 +263,7 @@
           renderJson(
               req,
               res,
-              TreeJsonData.toJsonData(wr.id, wr.tw, includeSizes),
+              TreeJsonData.toJsonData(wr.id, wr.tw, includeSizes, recursive),
               TreeJsonData.Tree.class);
           break;
         default:
@@ -297,6 +303,7 @@
     @Override
     public boolean include(TreeWalk tw)
         throws MissingObjectException, IncorrectObjectTypeException, IOException {
+
       count++;
       int cmp = tw.isPathPrefix(pathRaw, pathRaw.length);
       if (cmp > 0) {
@@ -352,7 +359,42 @@
    * Includes information to help the auto-dive routine as well.
    */
   private static class WalkResult implements AutoCloseable {
-    private static WalkResult forPath(RevWalk rw, GitilesView view) throws IOException {
+    private static WalkResult recursivePath(RevWalk rw, GitilesView view) throws IOException {
+      RevTree root = getRoot(view, rw);
+      String path = view.getPathPart();
+
+      TreeWalk tw;
+      if (!path.isEmpty()) {
+        try (TreeWalk toRoot = TreeWalk.forPath(rw.getObjectReader(), path, root)) {
+          if (toRoot == null) {
+            return null;
+          }
+
+          ObjectId treeSHA = toRoot.getObjectId(0);
+
+          ObjectLoader treeLoader = rw.getObjectReader().open(treeSHA);
+          if (treeLoader.getType() != Constants.OBJ_TREE) {
+            return null;
+          }
+
+          tw = new TreeWalk(rw.getObjectReader());
+          tw.addTree(treeSHA);
+        }
+      } else {
+        tw = new TreeWalk(rw.getObjectReader());
+        tw.addTree(root);
+      }
+
+      tw.setRecursive(true);
+      return new WalkResult(tw, path, root, root, FileType.TREE, ImmutableList.<Boolean>of());
+    }
+
+    private static WalkResult forPath(RevWalk rw, GitilesView view, boolean recursive)
+        throws IOException {
+      if (recursive) {
+        return recursivePath(rw, view);
+      }
+
       RevTree root = getRoot(view, rw);
       String path = view.getPathPart();
       TreeWalk tw = new TreeWalk(rw.getObjectReader());
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/TreeJsonData.java b/gitiles-servlet/src/main/java/com/google/gitiles/TreeJsonData.java
index 6fb4a6c..70dcf9a 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/TreeJsonData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/TreeJsonData.java
@@ -46,7 +46,8 @@
     @Nullable Long size;
   }
 
-  static Tree toJsonData(ObjectId id, TreeWalk tw, boolean includeSizes) throws IOException {
+  static Tree toJsonData(ObjectId id, TreeWalk tw, boolean includeSizes, boolean recursive)
+      throws IOException {
     Tree tree = new Tree();
     tree.id = id.name();
     tree.entries = Lists.newArrayList();
@@ -56,7 +57,7 @@
       e.mode = mode.getBits();
       e.type = Constants.typeString(mode.getObjectType());
       e.id = tw.getObjectId(0).name();
-      e.name = tw.getNameString();
+      e.name = recursive ? tw.getPathString() : tw.getNameString();
 
       if (includeSizes) {
         if ((mode.getBits() & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) {
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
index 1eac476..696b446 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
@@ -268,6 +268,33 @@
   }
 
   @Test
+  public void treeJsonRecursive() throws Exception {
+    RevCommit c =
+        repo.parseBody(
+            repo.branch("master")
+                .commit()
+                .add("foo/baz/bar/a", "bar contents")
+                .add("foo/baz/bar/b", "bar contents")
+                .add("baz", "baz contents")
+                .create());
+    Tree tree = buildJson(Tree.class, "/repo/+/master/", "recursive=1");
+
+    assertThat(tree.id).isEqualTo(c.getTree().name());
+    assertThat(tree.entries).hasSize(3);
+
+    assertThat(tree.entries.get(0).name).isEqualTo("baz");
+    assertThat(tree.entries.get(1).name).isEqualTo("foo/baz/bar/a");
+    assertThat(tree.entries.get(2).name).isEqualTo("foo/baz/bar/b");
+
+    tree = buildJson(Tree.class, "/repo/+/master/foo/baz", "recursive=1");
+
+    assertThat(tree.entries).hasSize(2);
+
+    assertThat(tree.entries.get(0).name).isEqualTo("bar/a");
+    assertThat(tree.entries.get(1).name).isEqualTo("bar/b");
+  }
+
+  @Test
   public void treeJson() throws Exception {
     RevCommit c =
         repo.parseBody(