Support JSON view of commits in RevisionServlet

Example output:

$ curl 'http://localhost:8080/gitiles/+/1040ab8d5861e2804f725a936ca9b6f3be6f6239?format=JSON'
)]}'
{
  "commit": "1040ab8d5861e2804f725a936ca9b6f3be6f6239",
  "parents": [
    "0ad024fb5aa3e4994ede801428b4e30ba06a837d"
  ],
  "author": {
    "name": "Dave Borowitz",
    "email": "[email protected]",
    "time": "Sun Mar 16 13:49:37 2014 -0700"
  },
  "committer": {
    "name": "Dave Borowitz",
    "email": "[email protected]",
    "time": "Sun Mar 16 13:49:37 2014 -0700"
  },
  "message": "Use CommitData to produce CommitJsonData\n\nChange-Id: I7261b0c40e97119c32627c4ea89038bbe174925f\n",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "87f3456c40f24662d28411fda9b2cf05b82bf007",
      "old_mode": 33188,
      "old_path": "gitiles-servlet/src/main/java/com/google/gitiles/CommitJsonData.java",
      "new_id": "07fbd1e0eb753aa41dee53c758ced595f18f67bf",
      "new_mode": 33188,
      "new_path": "gitiles-servlet/src/main/java/com/google/gitiles/CommitJsonData.java"
    },
    {
      "type": "modify",
      "old_id": "aa6bd34861cf2f49abd357edff0b2f3ff354aa91",
      "old_mode": 33188,
      "old_path": "gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java",
      "new_id": "e6ae4c2062bfe3dfb6f059f282e90af64bbd10d3",
      "new_mode": 33188,
      "new_path": "gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java"
    }
  ]
}

Change-Id: I29b76ce331bd6876476e3ddd56f2f484450b7e87
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/CommitData.java b/gitiles-servlet/src/main/java/com/google/gitiles/CommitData.java
index 1c118dd..0f614e6 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/CommitData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/CommitData.java
@@ -22,7 +22,9 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
 
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffFormatter;
@@ -70,6 +72,10 @@
     TREE,
     TREE_URL,
     URL;
+
+    static ImmutableSet<Field> setOf(Iterable<Field> base, Field... fields) {
+      return Sets.immutableEnumSet(Iterables.concat(base, Arrays.asList(fields)));
+    }
   }
 
   static class DiffList {
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/CommitJsonData.java b/gitiles-servlet/src/main/java/com/google/gitiles/CommitJsonData.java
index 10a8c24..e887441 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/CommitJsonData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/CommitJsonData.java
@@ -17,8 +17,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.gitiles.CommitData.DiffList;
 import com.google.gitiles.CommitData.Field;
 
+import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -26,12 +28,13 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Set;
 
 import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 
 class CommitJsonData {
-  private static final ImmutableSet<Field> DEFAULT_FIELDS = Sets.immutableEnumSet(
+  static final ImmutableSet<Field> DEFAULT_FIELDS = Sets.immutableEnumSet(
       Field.SHA, Field.PARENTS, Field.AUTHOR, Field.COMMITTER, Field.MESSAGE);
 
   static class Commit {
@@ -40,6 +43,8 @@
     Ident author;
     Ident committer;
     String message;
+
+    List<Diff> treeDiff;
   }
 
   static class Ident {
@@ -48,6 +53,18 @@
     String time;
   }
 
+  /** @see DiffEntry */
+  static class Diff {
+    String type;
+    String oldId;
+    int oldMode;
+    String oldPath;
+    String newId;
+    int newMode;
+    String newPath;
+    Integer score;
+  }
+
   private RevWalk walk;
 
   CommitJsonData setRevWalk(@Nullable RevWalk walk) {
@@ -55,11 +72,15 @@
     return this;
   }
 
-  Commit toJsonData(HttpServletRequest req, RevCommit c, GitDateFormatter df)
+  Commit toJsonData(HttpServletRequest req, RevCommit c, GitDateFormatter df) throws IOException {
+    return toJsonData(req, c, DEFAULT_FIELDS, df);
+  }
+
+  Commit toJsonData(HttpServletRequest req, RevCommit c, Set<Field> fs, GitDateFormatter df)
       throws IOException {
     CommitData cd = new CommitData.Builder()
         .setRevWalk(walk)
-        .build(req, c, DEFAULT_FIELDS);
+        .build(req, c, fs);
 
     Commit result = new Commit();
     if (cd.sha != null) {
@@ -80,6 +101,9 @@
     if (cd.message != null) {
       result.message = cd.message;
     }
+    if (cd.diffEntries != null) {
+      result.treeDiff = toJsonData(cd.diffEntries);
+    }
     return result;
   }
 
@@ -90,4 +114,30 @@
     result.time = df.formatDate(ident);
     return result;
   }
+
+  private static List<Diff> toJsonData(DiffList dl) {
+    List<Diff> result = Lists.newArrayListWithCapacity(dl.entries.size());
+    for (DiffEntry de : dl.entries) {
+      Diff d = new Diff();
+      d.type = de.getChangeType().name().toLowerCase();
+      d.oldId = de.getOldId().name();
+      d.oldMode = de.getOldMode().getBits();
+      d.oldPath = de.getOldPath();
+      d.newId = de.getNewId().name();
+      d.newMode = de.getNewMode().getBits();
+      d.newPath = de.getNewPath();
+
+      switch (de.getChangeType()) {
+        case COPY:
+        case RENAME:
+          d.score = de.getScore();
+          break;
+        default:
+          break;
+      }
+
+      result.add(d);
+    }
+    return result;
+  }
 }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RevisionServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RevisionServlet.java
index ccdaa53..5ac2ad0 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/RevisionServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/RevisionServlet.java
@@ -21,13 +21,11 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_TAG;
 import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.gitiles.CommitData.Field;
+import com.google.gitiles.CommitJsonData.Commit;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -53,8 +51,10 @@
 
 /** Serves an HTML page with detailed information about a ref. */
 public class RevisionServlet extends BaseServlet {
-  private static final ImmutableSet<Field> COMMIT_FIELDS = Sets.immutableEnumSet(
-      Iterables.concat(CommitSoyData.DEFAULT_FIELDS, ImmutableList.of(Field.DIFF_TREE)));
+  private static final ImmutableSet<Field> COMMIT_SOY_FIELDS =
+      Field.setOf(CommitSoyData.DEFAULT_FIELDS, Field.DIFF_TREE);
+  private static final ImmutableSet<Field> COMMIT_JSON_FIELDS =
+      Field.setOf(CommitJsonData.DEFAULT_FIELDS, Field.DIFF_TREE);
 
   private static final long serialVersionUID = 1L;
   private static final Logger log = LoggerFactory.getLogger(RevisionServlet.class);
@@ -70,7 +70,7 @@
   }
 
   @Override
-  protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+  protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException {
     GitilesView view = ViewFilter.getView(req);
     Repository repo = ServletUtils.getRepository(req);
 
@@ -92,7 +92,7 @@
                       .setLinkifier(linkifier)
                       .setRevWalk(walk)
                       .setArchiveFormat(getArchiveFormat(accessFactory.forRequest(req)))
-                      .toSoyData(req, (RevCommit) obj, COMMIT_FIELDS, df)));
+                      .toSoyData(req, (RevCommit) obj, COMMIT_SOY_FIELDS, df)));
               break;
             case OBJ_TREE:
               soyObjects.add(ImmutableMap.of(
@@ -135,6 +135,32 @@
     }
   }
 
+  @Override
+  protected void doGetJson(HttpServletRequest req, HttpServletResponse res) throws IOException {
+    GitilesView view = ViewFilter.getView(req);
+    Repository repo = ServletUtils.getRepository(req);
+
+    RevWalk walk = new RevWalk(repo);
+    try {
+      GitDateFormatter df = new GitDateFormatter(Format.DEFAULT);
+      RevObject obj = walk.parseAny(view.getRevision().getId());
+      switch (obj.getType()) {
+        case OBJ_COMMIT:
+          renderJson(req, res, new CommitJsonData()
+                .setRevWalk(walk)
+                .toJsonData(req, (RevCommit) obj, COMMIT_JSON_FIELDS, df),
+              Commit.class);
+          break;
+        default:
+          // TODO(dborowitz): Support showing other types.
+          res.setStatus(SC_NOT_FOUND);
+          break;
+      }
+    } finally {
+      walk.release();
+    }
+  }
+
   // TODO(dborowitz): Extract this.
   static List<RevObject> listObjects(RevWalk walk, Revision rev)
       throws MissingObjectException, IOException {