Add follow support to LogServlet

Currently just adds a TreeFilter to the walk; there is not yet any
indication in the HTML view that a rename occurred.

Change-Id: Idacd5713155df6f616f1a72e09bce14e0463a80e
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 ce9d38a..bcc09eb 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
@@ -30,6 +30,7 @@
 import com.google.gitiles.DateFormatter.Format;
 import com.google.gson.reflect.TypeToken;
 
+import org.eclipse.jgit.diff.DiffConfig;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RevWalkException;
@@ -40,6 +41,7 @@
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FollowFilter;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
@@ -72,8 +74,11 @@
 
   static final String LIMIT_PARAM = "n";
   static final String START_PARAM = "s";
-  private static final String PRETTY_PARAM = "pretty";
+
+  private static final String FOLLOW_PARAM = "follow";
   private static final String NAME_STATUS_PARAM = "name-status";
+  private static final String PRETTY_PARAM = "pretty";
+
   private static final int DEFAULT_LIMIT = 100;
   private static final int MAX_LIMIT = 10000;
 
@@ -88,14 +93,15 @@
   protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException {
     Repository repo = ServletUtils.getRepository(req);
     GitilesView view = getView(req, repo);
-    Paginator paginator = newPaginator(repo, view);
-    if (paginator == null) {
-      res.setStatus(SC_NOT_FOUND);
-      return;
-    }
 
+    Paginator paginator = null;
     try {
       GitilesAccess access = getAccess(req);
+      paginator = newPaginator(repo, view, getAccess(req));
+      if (paginator == null) {
+        res.setStatus(SC_NOT_FOUND);
+        return;
+      }
       DateFormatter df = new DateFormatter(access, Format.DEFAULT);
 
       // Allow the user to select a logView variant with the "pretty" param.
@@ -134,7 +140,9 @@
       res.setStatus(SC_INTERNAL_SERVER_ERROR);
       return;
     } finally {
-      paginator.getWalk().close();
+      if (paginator != null) {
+        paginator.getWalk().close();
+      }
     }
   }
 
@@ -142,11 +150,6 @@
   protected void doGetJson(HttpServletRequest req, HttpServletResponse res) throws IOException {
     Repository repo = ServletUtils.getRepository(req);
     GitilesView view = getView(req, repo);
-    Paginator paginator = newPaginator(repo, view);
-    if (paginator == null) {
-      res.setStatus(SC_NOT_FOUND);
-      return;
-    }
 
     Set<Field> fs = Sets.newEnumSet(CommitJsonData.DEFAULT_FIELDS, Field.class);
     String nameStatus = Iterables.getFirst(view.getParameters().get(NAME_STATUS_PARAM), null);
@@ -154,7 +157,13 @@
       fs.add(Field.DIFF_TREE);
     }
 
+    Paginator paginator = null;
     try {
+      paginator = newPaginator(repo, view, getAccess(req));
+      if (paginator == null) {
+        res.setStatus(SC_NOT_FOUND);
+        return;
+      }
       DateFormatter df = new DateFormatter(getAccess(req), Format.DEFAULT);
       CommitJsonData.Log result = new CommitJsonData.Log();
       List<CommitJsonData.Commit> entries = Lists.newArrayListWithCapacity(paginator.getLimit());
@@ -172,7 +181,9 @@
       }
       renderJson(req, res, result, new TypeToken<CommitJsonData.Log>() {}.getType());
     } finally {
-      paginator.getWalk().close();
+      if (paginator != null) {
+        paginator.getWalk().close();
+      }
     }
   }
 
@@ -210,19 +221,14 @@
     }
   }
 
-  private static RevWalk newWalk(Repository repo, GitilesView view)
+  private static RevWalk newWalk(Repository repo, GitilesView view, GitilesAccess access)
       throws MissingObjectException, IncorrectObjectTypeException, IOException {
     RevWalk walk = new RevWalk(repo);
     walk.markStart(walk.parseCommit(view.getRevision().getId()));
     if (view.getOldRevision() != Revision.NULL) {
       walk.markUninteresting(walk.parseCommit(view.getOldRevision().getId()));
     }
-    if (!Strings.isNullOrEmpty(view.getPathPart())) {
-      walk.setRewriteParents(false);
-      walk.setTreeFilter(AndTreeFilter.create(
-          PathFilterGroup.createFromStrings(view.getPathPart()),
-          TreeFilter.ANY_DIFF));
-    }
+    setTreeFilter(walk, view, access);
     List<RevFilter> filters = new ArrayList<>(3);
     if (isTrue(Iterables.getFirst(view.getParameters().get("no-merges"), null))) {
       filters.add(RevFilter.NO_MERGES);
@@ -243,6 +249,22 @@
     return walk;
   }
 
+  private static void setTreeFilter(RevWalk walk, GitilesView view, GitilesAccess access)
+      throws IOException {
+    if (Strings.isNullOrEmpty(view.getPathPart())) {
+      return;
+    }
+    walk.setRewriteParents(false);
+    String path = view.getPathPart();
+    if (isTrue(Iterables.getFirst(view.getParameters().get(FOLLOW_PARAM), null))) {
+      walk.setTreeFilter(FollowFilter.create(path, access.getConfig().get(DiffConfig.KEY)));
+    } else {
+      walk.setTreeFilter(AndTreeFilter.create(
+          PathFilterGroup.createFromStrings(view.getPathPart()),
+          TreeFilter.ANY_DIFF));
+    }
+  }
+
   private static boolean isTrue(String v) {
     if (v == null) {
       return false;
@@ -252,14 +274,15 @@
     return Boolean.TRUE.equals(StringUtils.toBooleanOrNull(v));
   }
 
-  private static Paginator newPaginator(Repository repo, GitilesView view) throws IOException {
+  private static Paginator newPaginator(Repository repo, GitilesView view, GitilesAccess access)
+      throws IOException {
     if (view == null) {
       return null;
     }
 
     RevWalk walk = null;
     try {
-      walk = newWalk(repo, view);
+      walk = newWalk(repo, view, access);
     } catch (IncorrectObjectTypeException e) {
       return null;
     }
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/LogServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/LogServletTest.java
index 893e259..56f1c29 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/LogServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/LogServletTest.java
@@ -68,6 +68,31 @@
     assertThat(jc1.treeDiff.get(0).newPath).isEqualTo("foo");
   }
 
+  @Test
+  public void follow() throws Exception {
+    String contents = "contents";
+    RevCommit c1 = repo.branch("master")
+        .commit()
+        .add("foo", contents)
+        .create();
+    RevCommit c2 = repo.branch("master")
+        .commit()
+        .rm("foo")
+        .add("bar", contents)
+        .create();
+    repo.getRevWalk().parseBody(c1);
+    repo.getRevWalk().parseBody(c2);
+
+    Log response = buildJson(LOG, "/repo/+log/master/bar");
+    assertThat(response.log).hasSize(1);
+    verifyJsonCommit(response.log.get(0), c2);
+
+    response = buildJson(LOG, "/repo/+log/master/bar", "follow=1");
+    assertThat(response.log).hasSize(2);
+    verifyJsonCommit(response.log.get(0), c2);
+    verifyJsonCommit(response.log.get(1), c1);
+  }
+
   private void verifyJsonCommit(Commit jsonCommit, RevCommit commit) throws Exception {
     repo.getRevWalk().parseBody(commit);
     GitilesAccess access = new TestGitilesAccess(repo.getRepository()).forRequest(null);