Add text support for tree paths

Match the output of git ls-tree, base64-encoded.

Change-Id: I5a949e69920dbc942f3f12e690213dfc3bdeaf9a
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 bcbb1a5..1ba2477 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
@@ -50,6 +50,7 @@
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.QuotedString;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -173,19 +174,18 @@
         return;
       }
 
+      // Write base64 as plain text without modifying any other headers, under
+      // the assumption that any hint we can give to a browser that this is
+      // base64 data might cause it to try to decode it and render as HTML,
+      // which would be bad.
       switch (wr.type) {
         case SYMLINK:
         case REGULAR_FILE:
         case EXECUTABLE_FILE:
-          // Write base64 as plain text without modifying any other headers,
-          // under the assumption that any hint we can give to a browser that
-          // this is base64 data might cause it to try to decode it and render
-          // as HTML, which would be bad.
-          res.setHeader(MODE_HEADER, String.format("%06o", wr.type.mode.getBits()));
-          try (Writer writer = startRenderText(req, res, null);
-              OutputStream out = BaseEncoding.base64().encodingStream(writer)) {
-            rw.getObjectReader().open(wr.id).copyTo(out);
-          }
+          writeBlobText(req, res, wr);
+          break;
+        case TREE:
+          writeTreeText(req, res, wr);
           break;
         default:
           renderTextError(req, res, SC_NOT_FOUND, "Not a file");
@@ -201,6 +201,40 @@
     }
   }
 
+  private void setModeHeader(HttpServletResponse res, FileType type) {
+    res.setHeader(MODE_HEADER, String.format("%06o", type.mode.getBits()));
+  }
+
+  private void writeBlobText(HttpServletRequest req, HttpServletResponse res, WalkResult wr)
+      throws IOException {
+    setModeHeader(res, wr.type);
+    try (Writer writer = startRenderText(req, res, null);
+        OutputStream out = BaseEncoding.base64().encodingStream(writer)) {
+      wr.getObjectReader().open(wr.id).copyTo(out);
+    }
+  }
+
+  private void writeTreeText(HttpServletRequest req, HttpServletResponse res, WalkResult wr)
+      throws IOException {
+    setModeHeader(res, wr.type);
+
+    try (Writer writer = startRenderText(req, res, null);
+        OutputStream out = BaseEncoding.base64().encodingStream(writer)) {
+      // Match git ls-tree format.
+      while (wr.tw.next()) {
+        FileMode mode = wr.tw.getFileMode(0);
+        out.write(Constants.encode(String.format("%06o", mode.getBits())));
+        out.write(' ');
+        out.write(Constants.encode(Constants.typeString(mode.getObjectType())));
+        out.write(' ');
+        wr.tw.getObjectId(0).copyTo(out);
+        out.write('\t');
+        out.write(Constants.encode(QuotedString.GIT_PATH.quote(wr.tw.getNameString())));
+        out.write('\n');
+      }
+    }
+  }
+
   private static RevTree getRoot(GitilesView view, RevWalk rw) throws IOException {
     RevObject obj = rw.peel(rw.parseAny(view.getRevision().getId()));
     switch (obj.getType()) {
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 a6d29b6..02d7463 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
@@ -33,6 +33,7 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -161,7 +162,7 @@
   public void blobText() throws Exception {
     repo.branch("master").commit().add("foo", "contents").create();
     String text = buildText("/repo/+/master/foo?format=TEXT", "100644");
-    assertEquals("contents", new String(BaseEncoding.base64().decode(text), UTF_8));
+    assertEquals("contents", decodeBase64(text));
   }
 
   @Test
@@ -176,7 +177,30 @@
           }
         }).create();
     String text = buildText("/repo/+/master/baz?format=TEXT", "120000");
-    assertEquals("foo", new String(BaseEncoding.base64().decode(text), UTF_8));
+    assertEquals("foo", decodeBase64(text));
+  }
+
+  @Test
+  public void treeText() throws Exception {
+    RevBlob blob = repo.blob("contents");
+    RevTree tree = repo.tree(repo.file("foo/bar", blob));
+    repo.branch("master").commit().setTopLevelTree(tree).create();
+
+    String expected = "040000 tree " + repo.get(tree, "foo").name() + "\tfoo\n";
+    assertEquals(expected, decodeBase64(buildText("/repo/+/master/?format=TEXT", "040000")));
+
+    expected = "100644 blob " + blob.name() + "\tbar\n";
+    assertEquals(expected, decodeBase64(buildText("/repo/+/master/foo?format=TEXT", "040000")));
+    assertEquals(expected, decodeBase64(buildText("/repo/+/master/foo/?format=TEXT", "040000")));
+  }
+
+  @Test
+  public void treeTextEscaped() throws Exception {
+    RevBlob blob = repo.blob("contents");
+    repo.branch("master").commit().add("foo\nbar\rbaz", blob).create();
+
+    assertEquals("100644 blob " + blob.name() + "\t\"foo\\nbar\\rbaz\"\n",
+        decodeBase64(buildText("/repo/+/master/?format=TEXT", "040000")));
   }
 
   @Test
@@ -197,9 +221,6 @@
         }).create();
 
     assertNotFound("/repo/+/master/nonexistent?format=TEXT");
-    assertNotFound("/repo/+/master/?format=TEXT");
-    assertNotFound("/repo/+/master/foo?format=TEXT");
-    assertNotFound("/repo/+/master/foo/?format=TEXT");
     assertNotFound("/repo/+/master/gitiles?format=TEXT");
   }
 
@@ -235,4 +256,8 @@
     // the Soy data for introspection.
     return BaseServlet.getData(service(pathAndQuery).getRequest());
   }
+
+  private static String decodeBase64(String in) {
+    return new String(BaseEncoding.base64().decode(in), UTF_8);
+  }
 }