Factor out common walking logic in PathServlet

We can't just use a TreeWalk for this in all cases because it has no
concept of being positioned just before a root tree. Plus, there are
various other pieces that we pass around with TreeWalk that make sense
to encapsulate together.

Change-Id: Ia82d4df558aceef4c2ddd8a0b17972fbd3db01e0
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BlobSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/BlobSoyData.java
index 02cb54c..857fa91 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/BlobSoyData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/BlobSoyData.java
@@ -29,6 +29,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
@@ -55,11 +56,16 @@
   private static final int MAX_FILE_SIZE = 10 << 20;
 
   private final GitilesView view;
-  private final RevWalk walk;
+  private final ObjectReader reader;
 
+  // TODO(dborowitz): Remove this constructor.
   public BlobSoyData(RevWalk walk, GitilesView view) {
+    this(walk.getObjectReader(), view);
+  }
+
+  public BlobSoyData(ObjectReader reader, GitilesView view) {
+    this.reader = reader;
     this.view = view;
-    this.walk = walk;
   }
 
   public Map<String, Object> toSoyData(ObjectId blobId)
@@ -72,7 +78,7 @@
     Map<String, Object> data = Maps.newHashMapWithExpectedSize(4);
     data.put("sha", ObjectId.toString(blobId));
 
-    ObjectLoader loader = walk.getObjectReader().open(blobId, Constants.OBJ_BLOB);
+    ObjectLoader loader = reader.open(blobId, Constants.OBJ_BLOB);
     String content;
     try {
       byte[] raw = loader.getCachedBytes(MAX_FILE_SIZE);
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 e48ea15..d134460 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
@@ -40,6 +40,7 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -122,57 +123,38 @@
     Repository repo = ServletUtils.getRepository(req);
 
     RevWalk rw = new RevWalk(repo);
+    WalkResult wr = null;
     try {
-      RevTree root = getRoot(view, rw);
-      TreeWalk tw = new TreeWalk(rw.getObjectReader());
-      tw.addTree(root);
-      tw.setRecursive(false);
-      FileType type;
-      String path = view.getPathPart();
-      List<Boolean> hasSingleTree;
-
-      if (path.isEmpty()) {
-        type = FileType.TREE;
-        hasSingleTree = ImmutableList.<Boolean> of();
-      } else {
-        hasSingleTree = walkToPath(tw, path);
-        if (hasSingleTree == null) {
-          res.setStatus(SC_NOT_FOUND);
-          return;
-        }
-        type = FileType.forEntry(tw);
+      wr = WalkResult.forPath(rw, view);
+      if (wr == null) {
+        res.setStatus(SC_NOT_FOUND);
+        return;
       }
-
-      switch (type) {
+      switch (wr.type) {
         case TREE:
-          ObjectId treeId;
-          if (path.isEmpty()) {
-            treeId = root;
-          } else {
-            treeId = tw.getObjectId(0);
-            tw.enterSubtree();
-            tw.setRecursive(false);
-          }
-          showTree(req, res, rw, tw, treeId, hasSingleTree);
+          showTree(req, res, wr);
           break;
         case SYMLINK:
-          showSymlink(req, res, rw, tw, hasSingleTree);
+          showSymlink(req, res, wr);
           break;
         case REGULAR_FILE:
         case EXECUTABLE_FILE:
-          showFile(req, res, rw, tw, hasSingleTree);
+          showFile(req, res, wr);
           break;
         case GITLINK:
-          showGitlink(req, res, tw, root);
+          showGitlink(req, res, wr);
           break;
         default:
-          log.error("Bad file type: {}", type);
+          log.error("Bad file type: {}", wr.type);
           res.setStatus(SC_NOT_FOUND);
           break;
       }
     } catch (LargeObjectException e) {
       res.setStatus(SC_INTERNAL_SERVER_ERROR);
     } finally {
+      if (wr != null) {
+        wr.release();
+      }
       rw.release();
     }
   }
@@ -183,23 +165,15 @@
     Repository repo = ServletUtils.getRepository(req);
 
     RevWalk rw = new RevWalk(repo);
+    WalkResult wr = null;
     try {
-      RevTree root = getRoot(view, rw);
-      TreeWalk tw = new TreeWalk(rw.getObjectReader());
-      tw.addTree(root);
-      tw.setRecursive(false);
-
-      String path = view.getPathPart();
-      if (path.isEmpty()) {
-        res.setStatus(SC_NOT_FOUND);
-        return;
-      }
-      if (walkToPath(tw, path) == null) {
+      wr = WalkResult.forPath(rw, view);
+      if (wr == null) {
         res.setStatus(SC_NOT_FOUND);
         return;
       }
 
-      switch (FileType.forEntry(tw)) {
+      switch (wr.type) {
         case SYMLINK:
         case REGULAR_FILE:
         case EXECUTABLE_FILE:
@@ -208,9 +182,9 @@
           // this is base64 data might cause it to try to decode it and render
           // as HTML, which would be bad.
           PrintWriter writer = startRenderText(req, res, null);
-          res.setHeader(MODE_HEADER, String.format("%06o", tw.getRawMode(0)));
+          res.setHeader(MODE_HEADER, String.format("%06o", wr.type.mode.getBits()));
           try (OutputStream out = BaseEncoding.base64().encodingStream(writer)) {
-            rw.getObjectReader().open(tw.getObjectId(0)).copyTo(out);
+            rw.getObjectReader().open(wr.id).copyTo(out);
           }
           break;
         default:
@@ -220,6 +194,9 @@
     } catch (LargeObjectException e) {
       res.setStatus(SC_INTERNAL_SERVER_ERROR);
     } finally {
+      if (wr != null) {
+        wr.release();
+      }
       rw.release();
     }
   }
@@ -300,31 +277,84 @@
     }
   }
 
-  private List<Boolean> walkToPath(TreeWalk tw, String pathString) throws IOException {
-    AutoDiveFilter f = new AutoDiveFilter(pathString);
-    tw.setFilter(f);
-    while (tw.next()) {
-      if (f.isDone(tw)) {
-        return f.hasSingleTree;
-      } else if (tw.isSubtree()) {
-        tw.enterSubtree();
+  /**
+   * Encapsulate the result of walking to a single tree.
+   * <p>
+   * Unlike {@link TreeWalk} itself, supports positioning at the root tree.
+   * Includes information to help the auto-dive routine as well.
+   */
+  private static class WalkResult {
+    private static WalkResult forPath(RevWalk rw, GitilesView view) throws IOException {
+      RevTree root = getRoot(view, rw);
+      String path = view.getPathPart();
+      TreeWalk tw = new TreeWalk(rw.getObjectReader());
+      try {
+        tw.addTree(root);
+        tw.setRecursive(false);
+        if (path.isEmpty()) {
+          return new WalkResult(tw, path, root, root, FileType.TREE, ImmutableList.<Boolean> of());
+        }
+        AutoDiveFilter f = new AutoDiveFilter(path);
+        tw.setFilter(f);
+        while (tw.next()) {
+          if (f.isDone(tw)) {
+            FileType type = FileType.forEntry(tw);
+            ObjectId id = tw.getObjectId(0);
+            if (type == FileType.TREE) {
+              tw.enterSubtree();
+              tw.setRecursive(false);
+            }
+            return new WalkResult(tw, path, root, id, type, f.hasSingleTree);
+          } else if (tw.isSubtree()) {
+            tw.enterSubtree();
+          }
+        }
+      } catch (IOException | RuntimeException e) {
+        // Fallthrough.
       }
+      tw.release();
+      return null;
     }
-    return null;
+
+    private final TreeWalk tw;
+    private final String path;
+    private final RevTree root;
+    private final ObjectId id;
+    private final FileType type;
+    private final List<Boolean> hasSingleTree;
+
+    private WalkResult(TreeWalk tw, String path, RevTree root, ObjectId objectId, FileType type,
+        List<Boolean> hasSingleTree) {
+      this.tw = tw;
+      this.path = path;
+      this.root = root;
+      this.id = objectId;
+      this.type = type;
+      this.hasSingleTree = hasSingleTree;
+    }
+
+    private ObjectReader getObjectReader() {
+      return tw.getObjectReader();
+    }
+
+    private void release() {
+      tw.release();
+    }
   }
 
-  private void showTree(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
-      ObjectId id, List<Boolean> hasSingleTree) throws IOException {
+  private void showTree(HttpServletRequest req, HttpServletResponse res, WalkResult wr)
+      throws IOException {
     GitilesView view = ViewFilter.getView(req);
     List<String> autodive = view.getParameters().get(AUTODIVE_PARAM);
     if (autodive.size() != 1 || !NO_AUTODIVE_VALUE.equals(autodive.get(0))) {
       byte[] path = Constants.encode(view.getPathPart());
-      CanonicalTreeParser child = getOnlyChildSubtree(rw, id, path);
+      ObjectReader reader = wr.getObjectReader();
+      CanonicalTreeParser child = getOnlyChildSubtree(reader, wr.id, path);
       if (child != null) {
         while (true) {
           path = new byte[child.getEntryPathLength()];
           System.arraycopy(child.getEntryPathBuffer(), 0, path, 0, child.getEntryPathLength());
-          CanonicalTreeParser next = getOnlyChildSubtree(rw, child.getEntryObjectId(), path);
+          CanonicalTreeParser next = getOnlyChildSubtree(reader, child.getEntryObjectId(), path);
           if (next == null) {
             break;
           }
@@ -340,16 +370,16 @@
     // TODO(sop): Allow caching trees by SHA-1 when no S cookie is sent.
     renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
         "title", !view.getPathPart().isEmpty() ? view.getPathPart() : "/",
-        "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
+        "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
         "type", FileType.TREE.toString(),
-        "data", new TreeSoyData(rw, view)
+        "data", new TreeSoyData(wr.getObjectReader(), view)
             .setArchiveFormat(getArchiveFormat(getAccess(req)))
-            .toSoyData(id, tw)));
+            .toSoyData(wr.id, wr.tw)));
   }
 
-  private CanonicalTreeParser getOnlyChildSubtree(RevWalk rw, ObjectId id, byte[] prefix)
+  private CanonicalTreeParser getOnlyChildSubtree(ObjectReader reader, ObjectId id, byte[] prefix)
       throws IOException {
-    CanonicalTreeParser p = new CanonicalTreeParser(prefix, rw.getObjectReader(), id);
+    CanonicalTreeParser p = new CanonicalTreeParser(prefix, reader, id);
     if (p.eof() || p.getEntryFileMode() != FileMode.TREE) {
       return null;
     }
@@ -357,36 +387,37 @@
     return p.eof() ? p : null;
   }
 
-  private void showFile(HttpServletRequest req, HttpServletResponse res, RevWalk rw, TreeWalk tw,
-      List<Boolean> hasSingleTree) throws IOException {
+  private void showFile(HttpServletRequest req, HttpServletResponse res, WalkResult wr)
+      throws IOException {
     GitilesView view = ViewFilter.getView(req);
+    Map<String, ?> data = new BlobSoyData(wr.getObjectReader(), view)
+        .toSoyData(wr.path, wr.id);
     // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
     renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
         "title", ViewFilter.getView(req).getPathPart(),
-        "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
-        "type", FileType.forEntry(tw).toString(),
-        "data", new BlobSoyData(rw, view).toSoyData(tw.getPathString(), tw.getObjectId(0))));
+        "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
+        "type", wr.type.toString(),
+        "data", data));
   }
 
-  private void showSymlink(HttpServletRequest req, HttpServletResponse res, RevWalk rw,
-      TreeWalk tw, List<Boolean> hasSingleTree) throws IOException {
+  private void showSymlink(HttpServletRequest req, HttpServletResponse res, WalkResult wr)
+      throws IOException {
     GitilesView view = ViewFilter.getView(req);
-    ObjectId id = tw.getObjectId(0);
     Map<String, Object> data = Maps.newHashMap();
 
-    ObjectLoader loader = rw.getObjectReader().open(id, OBJ_BLOB);
+    ObjectLoader loader = wr.getObjectReader().open(wr.id, OBJ_BLOB);
     String target;
     try {
       target = RawParseUtils.decode(loader.getCachedBytes(TreeSoyData.MAX_SYMLINK_SIZE));
     } catch (LargeObjectException.OutOfMemory e) {
       throw e;
     } catch (LargeObjectException e) {
-      data.put("sha", ObjectId.toString(id));
+      data.put("sha", ObjectId.toString(wr.id));
       data.put("data", null);
       data.put("size", Long.toString(loader.getSize()));
       renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
           "title", ViewFilter.getView(req).getPathPart(),
-          "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
+          "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
           "type", FileType.REGULAR_FILE.toString(),
           "data", data));
       return;
@@ -407,7 +438,7 @@
     // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
     renderHtml(req, res, "gitiles.pathDetail", ImmutableMap.of(
         "title", ViewFilter.getView(req).getPathPart(),
-        "breadcrumbs", view.getBreadcrumbs(hasSingleTree),
+        "breadcrumbs", view.getBreadcrumbs(wr.hasSingleTree),
         "type", FileType.SYMLINK.toString(),
         "data", data));
   }
@@ -426,10 +457,10 @@
     }
   }
 
-  private void showGitlink(HttpServletRequest req, HttpServletResponse res, TreeWalk tw,
-      RevTree root) throws IOException {
+  private void showGitlink(HttpServletRequest req, HttpServletResponse res, WalkResult wr)
+      throws IOException {
     GitilesView view = ViewFilter.getView(req);
-    SubmoduleWalk sw = SubmoduleWalk.forPath(ServletUtils.getRepository(req), root,
+    SubmoduleWalk sw = SubmoduleWalk.forPath(ServletUtils.getRepository(req), wr.root,
         view.getPathPart());
 
     String modulesUrl;
@@ -451,7 +482,7 @@
     }
 
     Map<String, Object> data = Maps.newHashMap();
-    data.put("sha", ObjectId.toString(tw.getObjectId(0)));
+    data.put("sha", ObjectId.toString(wr.id));
     data.put("remoteUrl", remoteUrl != null ? remoteUrl : modulesUrl);
 
     // TODO(dborowitz): Guess when we can put commit SHAs in the URL.
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java
index 0a828c2..428de98 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/TreeSoyData.java
@@ -25,6 +25,7 @@
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 
@@ -69,12 +70,17 @@
     }
   }
 
-  private final RevWalk rw;
+  private final ObjectReader reader;
   private final GitilesView view;
   private ArchiveFormat archiveFormat;
 
+  // TODO(dborowitz): Remove this constructor.
   public TreeSoyData(RevWalk rw, GitilesView view) {
-    this.rw = rw;
+    this(rw.getObjectReader(), view);
+  }
+
+  public TreeSoyData(ObjectReader reader, GitilesView view) {
+    this.reader = reader;
     this.view = view;
   }
 
@@ -115,7 +121,7 @@
       entry.put("url", url);
       if (type == FileType.SYMLINK) {
         String target = new String(
-            rw.getObjectReader().open(tw.getObjectId(0)).getCachedBytes(),
+            reader.open(tw.getObjectId(0)).getCachedBytes(),
             Charsets.UTF_8);
         entry.put("targetName", getTargetDisplayName(target));
         String targetUrl = resolveTargetUrl(view, target);
@@ -145,7 +151,7 @@
   }
 
   public Map<String, Object> toSoyData(ObjectId treeId) throws MissingObjectException, IOException {
-    TreeWalk tw = new TreeWalk(rw.getObjectReader());
+    TreeWalk tw = new TreeWalk(reader);
     tw.addTree(treeId);
     tw.setRecursive(false);
     return toSoyData(treeId, tw);