Support format=TEXT for /+refs

List refs in the same format in an /info/refs request for the git
protocol, plus some minor sanitization to deal with XSS. Support scoping
of refs by adding additional path components (e.g. /heads) after /+refs.
This means allowing refs views with paths, but note that this only
applies to the TEXT (and soon JSON) format, not HTML.

Change-Id: I7c7074544af366f38791d5a90d0a024d2555c92e
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/RefServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/RefServletTest.java
new file mode 100644
index 0000000..9e8bac4
--- /dev/null
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/RefServletTest.java
@@ -0,0 +1,177 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// 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.gitiles;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.storage.dfs.DfsRepository;
+import org.eclipse.jgit.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.storage.dfs.InMemoryRepository;
+
+import java.io.IOException;
+
+/** Tests for {@link Linkifier}. */
+public class RefServletTest extends TestCase {
+  private TestRepository<DfsRepository> repo;;
+  private GitilesServlet servlet;
+
+  @Override
+  protected void setUp() throws Exception {
+    DfsRepository r = new InMemoryRepository(new DfsRepositoryDescription("test"));
+    repo = new TestRepository<DfsRepository>(r);
+
+    RevCommit commit = repo.branch("refs/heads/master").commit().create();
+    repo.update("refs/heads/branch", commit);
+    repo.update("refs/tags/ctag", commit);
+    RevTag tag = repo.tag("atag", commit);
+    repo.update("refs/tags/atag", tag);
+    r.updateRef("HEAD").link("refs/heads/master");
+
+    servlet = TestGitilesServlet.create(repo);
+  }
+
+  private String id(String refName) throws IOException {
+    return ObjectId.toString(repo.getRepository().getRef(refName).getObjectId());
+  }
+
+  private String peeled(String refName) throws IOException {
+    return ObjectId.toString(repo.getRepository().peel(
+          repo.getRepository().getRef(refName)).getPeeledObjectId());
+  }
+
+  public void testEvilRefName() throws Exception {
+    String evilRefName = "refs/evil/<script>window.close();</script>/&foo";
+    assertTrue(Repository.isValidRefName(evilRefName));
+    repo.branch(evilRefName).commit().create();
+
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs/evil");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(
+        id(evilRefName) + " refs/evil/&lt;script&gt;window.close();&lt;/script&gt;/&amp;foo\n",
+        res.getActualBodyString());
+  }
+
+  public void testGetRefsTextAll() throws Exception {
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(200, res.getStatus());
+    assertEquals(
+        id("HEAD") + " HEAD\n"
+        + id("refs/heads/branch") + " refs/heads/branch\n"
+        + id("refs/heads/master") + " refs/heads/master\n"
+        + id("refs/tags/atag") + " refs/tags/atag\n"
+        + peeled("refs/tags/atag") + " refs/tags/atag^{}\n"
+        + id("refs/tags/ctag") + " refs/tags/ctag\n",
+        res.getActualBodyString());
+  }
+
+  public void testGetRefsTextAllTrailingSlash() throws Exception {
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(200, res.getStatus());
+    assertEquals(
+        id("HEAD") + " HEAD\n"
+        + id("refs/heads/branch") + " refs/heads/branch\n"
+        + id("refs/heads/master") + " refs/heads/master\n"
+        + id("refs/tags/atag") + " refs/tags/atag\n"
+        + peeled("refs/tags/atag") + " refs/tags/atag^{}\n"
+        + id("refs/tags/ctag") + " refs/tags/ctag\n",
+        res.getActualBodyString());
+  }
+
+  public void testGetRefsHeadsText() throws Exception {
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs/heads");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(200, res.getStatus());
+    assertEquals(
+        id("refs/heads/branch") + " refs/heads/branch\n"
+        + id("refs/heads/master") + " refs/heads/master\n",
+        res.getActualBodyString());
+  }
+
+  public void testGetRefsHeadsTextTrailingSlash() throws Exception {
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs/heads/");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(200, res.getStatus());
+    assertEquals(
+        id("refs/heads/branch") + " refs/heads/branch\n"
+        + id("refs/heads/master") + " refs/heads/master\n",
+        res.getActualBodyString());
+  }
+
+  public void testNoHeadText() throws Exception {
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs/HEAD");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(200, res.getStatus());
+    // /+refs/foo means refs/foo(/*), so this is empty.
+    assertEquals("", res.getActualBodyString());
+  }
+
+  public void testSingleHeadText() throws Exception {
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs/heads/master");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(200, res.getStatus());
+    assertEquals(
+        id("refs/heads/master") + " refs/heads/master\n",
+        res.getActualBodyString());
+  }
+
+  public void testSinglePeeledTagText() throws Exception {
+    FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+    req.setPathInfo("/test/+refs/tags/atag");
+    req.setQueryString("format=TEXT");
+    FakeHttpServletResponse res = new FakeHttpServletResponse();
+    servlet.service(req, res);
+
+    assertEquals(200, res.getStatus());
+    assertEquals(
+        id("refs/tags/atag") + " refs/tags/atag\n"
+        + peeled("refs/tags/atag") + " refs/tags/atag^{}\n",
+        res.getActualBodyString());
+  }
+}
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
index 2c3ae2a..1088692 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
@@ -97,12 +97,42 @@
   }
 
   public void testRefs() throws Exception {
-    GitilesView view = getView("/repo/+refs");
+    GitilesView view;
+
+    view = getView("/repo/+refs");
     assertEquals(Type.REFS, view.getType());
     assertEquals("repo", view.getRepositoryName());
     assertEquals(Revision.NULL, view.getRevision());
     assertEquals(Revision.NULL, view.getOldRevision());
-    assertNull(view.getTreePath());
+    assertEquals("", view.getTreePath());
+
+    view = getView("/repo/+refs/");
+    assertEquals(Type.REFS, view.getType());
+    assertEquals("repo", view.getRepositoryName());
+    assertEquals(Revision.NULL, view.getRevision());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertEquals("", view.getTreePath());
+
+    view = getView("/repo/+refs/heads");
+    assertEquals(Type.REFS, view.getType());
+    assertEquals("repo", view.getRepositoryName());
+    assertEquals(Revision.NULL, view.getRevision());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertEquals("heads", view.getTreePath());
+
+    view = getView("/repo/+refs/heads/");
+    assertEquals(Type.REFS, view.getType());
+    assertEquals("repo", view.getRepositoryName());
+    assertEquals(Revision.NULL, view.getRevision());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertEquals("heads", view.getTreePath());
+
+    view = getView("/repo/+refs/heads/master");
+    assertEquals(Type.REFS, view.getType());
+    assertEquals("repo", view.getRepositoryName());
+    assertEquals(Revision.NULL, view.getRevision());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertEquals("heads/master", view.getTreePath());
   }
 
   public void testBranches() throws Exception {