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/main/java/com/google/gitiles/GitilesView.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
index 42079ee..67040fe 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -205,6 +205,7 @@
case DIFF:
this.path = maybeTrimLeadingAndTrailingSlash(checkNotNull(path));
return this;
+ case REFS:
case LOG:
this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null;
return this;
@@ -348,7 +349,7 @@
return new Builder(Type.LOG);
}
- private static String maybeTrimLeadingAndTrailingSlash(String str) {
+ static String maybeTrimLeadingAndTrailingSlash(String str) {
if (str.startsWith("/")) {
str = str.substring(1);
}
@@ -522,6 +523,8 @@
* auto-diving into one-entry subtrees.
*/
public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) {
+ checkArgument(type != Type.REFS || Strings.isNullOrEmpty(path),
+ "breadcrumbs for REFS view with path not supported");
checkArgument(hasSingleTree == null || type == Type.PATH,
"hasSingleTree must be null for %s view", type);
String path = this.path;
@@ -551,7 +554,8 @@
breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this)));
}
if (path != null) {
- if (type != Type.LOG) { // The "." breadcrumb would be no different for LOG.
+ if (type != Type.LOG && type != Type.REFS) {
+ // The "." breadcrumb would be no different for LOG or REFS.
breadcrumbs.add(breadcrumb(".", copyWithPath().setTreePath("")));
}
StringBuilder cur = new StringBuilder();
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java
index a046ff1..84cfdb2 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java
@@ -15,7 +15,6 @@
package com.google.gitiles;
import static com.google.common.base.Preconditions.checkNotNull;
-
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
index b75965c..f3dcc43 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
@@ -15,7 +15,6 @@
package com.google.gitiles;
import static com.google.common.base.Preconditions.checkNotNull;
-
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java
index bb51f04..fd5e793 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java
@@ -15,6 +15,7 @@
package com.google.gitiles;
import static com.google.common.base.Preconditions.checkNotNull;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
@@ -24,13 +25,16 @@
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.eclipse.jgit.http.server.ServletUtils;
+import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefComparator;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefAdvertiser;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -51,25 +55,41 @@
}
@Override
- protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ protected void doGetHtml(HttpServletRequest req, HttpServletResponse res)
throws IOException {
+ if (!ViewFilter.getView(req).getTreePath().isEmpty()) {
+ res.setStatus(SC_NOT_FOUND);
+ return;
+ }
RevWalk walk = new RevWalk(ServletUtils.getRepository(req));
List<Map<String, Object>> tags;
try {
- tags = getTags(req, timeCache, walk, 0);
+ tags = getTagsSoyData(req, timeCache, walk, 0);
} finally {
walk.release();
}
renderHtml(req, res, "gitiles.refsDetail",
- ImmutableMap.of("branches", getBranches(req, 0), "tags", tags));
+ ImmutableMap.of("branches", getBranchesSoyData(req, 0), "tags", tags));
}
- static List<Map<String, Object>> getBranches(HttpServletRequest req, int limit)
+ @Override
+ protected void doGetText(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ GitilesView view = ViewFilter.getView(req);
+ Map<String, Ref> refs = getRefs(ServletUtils.getRepository(req).getRefDatabase(),
+ view.getTreePath());
+ TextRefAdvertiser adv = new TextRefAdvertiser(startRenderText(req, res));
+ adv.setDerefTags(true);
+ adv.send(refs);
+ adv.end();
+ }
+
+ static List<Map<String, Object>> getBranchesSoyData(HttpServletRequest req, int limit)
throws IOException {
RefDatabase refdb = ServletUtils.getRepository(req).getRefDatabase();
Ref head = refdb.getRef(Constants.HEAD);
Ref headLeaf = head != null && head.isSymbolic() ? head.getLeaf() : null;
- return getRefs(
+ return getRefsSoyData(
refdb,
ViewFilter.getView(req),
Constants.R_HEADS,
@@ -97,9 +117,9 @@
}.compound(RefComparator.INSTANCE);
}
- static List<Map<String, Object>> getTags(HttpServletRequest req,
+ static List<Map<String, Object>> getTagsSoyData(HttpServletRequest req,
TimeCache timeCache, RevWalk walk, int limit) throws IOException {
- return getRefs(
+ return getRefsSoyData(
ServletUtils.getRepository(req).getRefDatabase(),
ViewFilter.getView(req),
Constants.R_TAGS,
@@ -121,7 +141,7 @@
}).reverse().compound(RefComparator.INSTANCE);
}
- private static List<Map<String, Object>> getRefs(
+ private static List<Map<String, Object>> getRefsSoyData(
RefDatabase refdb,
GitilesView view,
String prefix,
@@ -146,4 +166,46 @@
}
return result;
}
+
+ private static String sanitizeRefForText(String refName) {
+ return refName.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">");
+ }
+
+ private static Map<String, Ref> getRefs(RefDatabase refdb, String path) throws IOException {
+ path = GitilesView.maybeTrimLeadingAndTrailingSlash(path);
+ if (path.isEmpty()) {
+ return refdb.getRefs(RefDatabase.ALL);
+ }
+ path = Constants.R_REFS + path;
+ Ref singleRef = refdb.getRef(path);
+ if (singleRef != null) {
+ return ImmutableMap.of(singleRef.getName(), singleRef);
+ }
+ return refdb.getRefs(path + '/');
+ }
+
+ private static class TextRefAdvertiser extends RefAdvertiser {
+ private final PrintWriter writer;
+
+ private TextRefAdvertiser(PrintWriter writer) {
+ this.writer = writer;
+ }
+
+ @Override
+ public void advertiseId(AnyObjectId id, String refName) throws IOException {
+ super.advertiseId(id, sanitizeRefForText(refName));
+ }
+
+ @Override
+ protected void writeOne(CharSequence line) throws IOException {
+ writer.print(line);
+ }
+
+ @Override
+ public void end() throws IOException {
+ writer.close();
+ }
+ }
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
index 9672815..5a92dea 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
@@ -67,7 +67,7 @@
List<Map<String, Object>> tags;
Map<String, Object> data;
try {
- tags = RefServlet.getTags(req, timeCache, walk, REF_LIMIT);
+ tags = RefServlet.getTagsSoyData(req, timeCache, walk, REF_LIMIT);
ObjectId headId = repo.resolve(Constants.HEAD);
if (headId != null) {
RevObject head = walk.parseAny(headId);
@@ -88,7 +88,7 @@
if (!data.containsKey("entries")) {
data.put("entries", ImmutableList.of());
}
- List<Map<String, Object>> branches = RefServlet.getBranches(req, REF_LIMIT);
+ List<Map<String, Object>> branches = RefServlet.getBranchesSoyData(req, REF_LIMIT);
data.put("cloneUrl", desc.cloneUrl);
data.put("mirroredFromUrl", Strings.nullToEmpty(desc.mirroredFromUrl));
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
index 4f629be..c24b4f0 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -102,8 +102,8 @@
// Non-path cases.
if (repoName.isEmpty()) {
return GitilesView.hostIndex();
- } else if (command.equals(CMD_REFS) && path.isEmpty()) {
- return GitilesView.refs().setRepositoryName(repoName);
+ } else if (command.equals(CMD_REFS)) {
+ return GitilesView.refs().setRepositoryName(repoName).setTreePath(path);
} else if (command.equals(CMD_LOG) && (path.isEmpty() || path.equals("/"))) {
return GitilesView.log().setRepositoryName(repoName);
} else if (command.isEmpty()) {
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/<script>window.close();</script>/&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 {