Allow /+archive of any tree-ish

Change-Id: I06b0fe636fd475d9d99392ad7404893e286c1cd9
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ArchiveServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/ArchiveServlet.java
index 2534d82..b0ab8fb 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ArchiveServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ArchiveServlet.java
@@ -22,8 +22,12 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.http.server.ServletUtils;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
 
 import java.io.IOException;
 import java.util.Map;
@@ -49,17 +53,10 @@
     Revision rev = view.getRevision();
     Repository repo = ServletUtils.getRepository(req);
 
-    // Check object type before starting the archive. If we just caught the
-    // exception from cmd.call() below, we wouldn't know whether it was because
-    // the input object is not a tree or something broke later.
-    RevWalk walk = new RevWalk(repo);
-    try {
-      walk.parseTree(rev.getId());
-    } catch (IncorrectObjectTypeException e) {
+    ObjectId treeId = getTree(view, repo, rev);
+    if (treeId.equals(ObjectId.zeroId())) {
       res.sendError(SC_NOT_FOUND);
       return;
-    } finally {
-      walk.release();
     }
 
     ArchiveFormat format = byExt.get(view.getExtension());
@@ -70,7 +67,7 @@
     try {
       new ArchiveCommand(repo)
           .setFormat(format.name())
-          .setTree(rev.getId())
+          .setTree(treeId)
           .setOutputStream(res.getOutputStream())
           .call();
     } catch (GitAPIException e) {
@@ -78,12 +75,35 @@
     }
   }
 
+  private ObjectId getTree(GitilesView view, Repository repo, Revision rev) throws IOException {
+    RevWalk rw = new RevWalk(repo);
+    try {
+      RevTree tree = rw.parseTree(rev.getId());
+      if (view.getPathPart() == null) {
+        return tree;
+      }
+      TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), view.getPathPart(), tree);
+      if (tw.getFileMode(0) != FileMode.TREE) {
+        return ObjectId.zeroId();
+      }
+      return tw.getObjectId(0);
+    } catch (IncorrectObjectTypeException e) {
+      return ObjectId.zeroId();
+    } finally {
+      rw.release();
+    }
+  }
+
   private String getFilename(GitilesView view, Revision rev, String ext) {
-    return new StringBuilder()
+    StringBuilder sb = new StringBuilder()
         .append(Paths.basename(view.getRepositoryName()))
         .append('-')
-        .append(rev.getName())
-        .append(ext)
+        .append(rev.getName());
+    if (view.getPathPart() != null) {
+      sb.append('-')
+          .append(view.getPathPart().replace('/', '-'));
+    }
+    return sb.append(ext)
         .toString();
   }
 }
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 3066462..50923bb 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -101,10 +101,10 @@
           oldRevision = other.oldRevision;
           // Fallthrough.
         case PATH:
+        case ARCHIVE:
           path = other.path;
           // Fallthrough.
         case REVISION:
-        case ARCHIVE:
           revision = other.revision;
           // Fallthrough.
         case DESCRIBE:
@@ -219,6 +219,7 @@
         case DIFF:
           checkState(path != null, "cannot set null path on %s view", type);
           break;
+        case ARCHIVE:
         case DESCRIBE:
         case REFS:
         case LOG:
@@ -547,8 +548,11 @@
         url.append(repositoryName).append("/+/").append(revision.getName());
         break;
       case ARCHIVE:
-        url.append(repositoryName).append("/+archive/").append(revision.getName())
-            .append(Objects.firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
+        url.append(repositoryName).append("/+archive/").append(revision.getName());
+        if (path != null) {
+          url.append('/').append(path);
+        }
+        url.append(Objects.firstNonNull(extension, DEFAULT_ARCHIVE_EXTENSION));
         break;
       case PATH:
         url.append(repositoryName).append("/+/").append(revision.getName()).append('/')
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 582daca..50a4584 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -16,10 +16,18 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
-
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+
+import org.eclipse.jgit.http.server.ServletUtils;
+import org.eclipse.jgit.http.server.glue.WrappedRequest;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
 import java.util.Map;
 import java.util.Set;
@@ -29,14 +37,6 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.eclipse.jgit.http.server.ServletUtils;
-import org.eclipse.jgit.http.server.glue.WrappedRequest;
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.collect.Sets;
-
 /** Filter to parse URLs and convert them to {@link GitilesView}s. */
 public class ViewFilter extends AbstractHttpFilter {
   private static final Logger log = LoggerFactory.getLogger(ViewFilter.class);
@@ -166,7 +166,7 @@
         break;
       }
     }
-    if (ext == null) {
+    if (ext == null || path.endsWith("/")) {
       return null;
     }
     RevisionParser.Result result = parseRevision(req, path);
@@ -176,6 +176,7 @@
     return GitilesView.archive()
         .setRepositoryName(repoName)
         .setRevision(result.getRevision())
+        .setPathPart(Strings.emptyToNull(result.getPath()))
         .setExtension(ext);
   }
 
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
index 3cc846e..44e69c3 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
@@ -516,6 +516,49 @@
         view.getBreadcrumbs());
   }
 
+  public void testArchiveWithNoPath() throws Exception {
+    ObjectId id = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    GitilesView view = GitilesView.archive()
+        .copyFrom(HOST)
+        .setRepositoryName("foo/bar")
+        .setRevision(Revision.unpeeled("master", id))
+        .setExtension(".tar.bz2")
+        .build();
+
+    assertEquals("/b", view.getServletPath());
+    assertEquals(Type.ARCHIVE, view.getType());
+    assertEquals("host", view.getHostName());
+    assertEquals("foo/bar", view.getRepositoryName());
+    assertEquals(id, view.getRevision().getId());
+    assertEquals("master", view.getRevision().getName());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertNull(view.getPathPart());
+    assertTrue(HOST.getParameters().isEmpty());
+    assertEquals("/b/foo/bar/+archive/master.tar.bz2", view.toUrl());
+  }
+
+  public void testArchiveWithPath() throws Exception {
+    ObjectId id = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    GitilesView view = GitilesView.archive()
+        .copyFrom(HOST)
+        .setRepositoryName("foo/bar")
+        .setRevision(Revision.unpeeled("master", id))
+        .setPathPart("/path/to/a/dir")
+        .setExtension(".tar.bz2")
+        .build();
+
+    assertEquals("/b", view.getServletPath());
+    assertEquals(Type.ARCHIVE, view.getType());
+    assertEquals("host", view.getHostName());
+    assertEquals("foo/bar", view.getRepositoryName());
+    assertEquals(id, view.getRevision().getId());
+    assertEquals("master", view.getRevision().getName());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertEquals("path/to/a/dir", view.getPathPart());
+    assertTrue(HOST.getParameters().isEmpty());
+    assertEquals("/b/foo/bar/+archive/master/path/to/a/dir.tar.bz2", view.toUrl());
+  }
+
   public void testEscaping() throws Exception {
     ObjectId id = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     ObjectId parent = ObjectId.fromString("efab5678efab5678efab5678efab5678efab5678");
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 201ac9d..4fddf0f 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
@@ -19,14 +19,9 @@
 import static com.google.gitiles.GitilesFilter.REPO_REGEX;
 import static com.google.gitiles.GitilesFilter.ROOT_REGEX;
 
-import java.io.IOException;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.regex.Pattern;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Atomics;
+import com.google.gitiles.GitilesView.Type;
 
 import junit.framework.TestCase;
 
@@ -39,9 +34,14 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Atomics;
-import com.google.gitiles.GitilesView.Type;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
 /** Tests for the view filter. */
 public class ViewFilterTest extends TestCase {
@@ -391,6 +391,8 @@
     assertNull(getView("/repo/+archive/master..branch"));
     assertNull(getView("/repo/+archive/master.foo"));
     assertNull(getView("/repo/+archive/master.zip"));
+    assertNull(getView("/repo/+archive/master/.tar.gz"));
+    assertNull(getView("/repo/+archive/master/foo/.tar.gz"));
 
     view = getView("/repo/+archive/master.tar.gz");
     assertEquals(Type.ARCHIVE, view.getType());
@@ -409,6 +411,15 @@
     assertEquals(Revision.NULL, view.getOldRevision());
     assertEquals(".tar.bz2", view.getExtension());
     assertNull(view.getPathPart());
+
+    view = getView("/repo/+archive/master/foo/bar.tar.gz");
+    assertEquals(Type.ARCHIVE, view.getType());
+    assertEquals("repo", view.getRepositoryName());
+    assertEquals("master", view.getRevision().getName());
+    assertEquals(master, view.getRevision().getId());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertEquals(".tar.gz", view.getExtension());
+    assertEquals("foo/bar", view.getPathPart());
   }
 
   private GitilesView getView(String pathAndQuery) throws ServletException, IOException {