Add view parsing for a new /+blame command

Change-Id: Ib0146ed024e2a0d2af23bd45d8f0044070ce5560
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BlameServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/BlameServlet.java
new file mode 100644
index 0000000..53f2213
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/BlameServlet.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2014 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 org.eclipse.jgit.lib.Config;
+
+/** Serves an HTML page with blame data for a commit. */
+public class BlameServlet extends BaseServlet {
+  private static final long serialVersionUID = 1L;
+
+  public BlameServlet(Config cfg, Renderer renderer) {
+    super(cfg, renderer);
+  }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
index c6a0396..dcde5f5 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
@@ -244,6 +244,8 @@
         return new DescribeServlet(config);
       case ARCHIVE:
         return new ArchiveServlet(config);
+      case BLAME:
+        return new BlameServlet(config, renderer);
       default:
         throw new IllegalArgumentException("Invalid view type: " + view);
     }
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 50923bb..fc553c7 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -62,7 +62,8 @@
     DIFF,
     LOG,
     DESCRIBE,
-    ARCHIVE;
+    ARCHIVE,
+    BLAME;
   }
 
   /** Exception thrown when building a view that is invalid. */
@@ -102,6 +103,7 @@
           // Fallthrough.
         case PATH:
         case ARCHIVE:
+        case BLAME:
           path = other.path;
           // Fallthrough.
         case REVISION:
@@ -219,6 +221,7 @@
         case DIFF:
           checkState(path != null, "cannot set null path on %s view", type);
           break;
+        case BLAME:
         case ARCHIVE:
         case DESCRIBE:
         case REFS:
@@ -313,6 +316,9 @@
         case ARCHIVE:
           checkArchive();
           break;
+        case BLAME:
+          checkBlame();
+          break;
       }
       return new GitilesView(type, hostName, servletPath, repositoryName, revision,
           oldRevision, path, extension, params, anchor);
@@ -367,6 +373,10 @@
     private void checkArchive() {
       checkRevision();
     }
+
+    private void checkBlame() {
+      checkPath();
+    }
   }
 
   public static Builder hostIndex() {
@@ -405,6 +415,10 @@
     return new Builder(Type.ARCHIVE);
   }
 
+  public static Builder blame() {
+    return new Builder(Type.BLAME);
+  }
+
   static String maybeTrimLeadingAndTrailingSlash(String str) {
     if (str.startsWith("/")) {
       str = str.substring(1);
@@ -580,6 +594,10 @@
           }
         }
         break;
+      case BLAME:
+        url.append(repositoryName).append("/+blame/").append(revision.getName()).append('/')
+            .append(path);
+        break;
       default:
         throw new IllegalStateException("Unknown view type: " + type);
     }
@@ -651,7 +669,7 @@
     if (path != null) {
       if (type != Type.LOG && type != Type.REFS) {
         // The "." breadcrumb would be no different for LOG or REFS.
-        breadcrumbs.add(breadcrumb(".", copyWithPath().setPathPart("")));
+        breadcrumbs.add(breadcrumb(".", copyWithPath(false).setPathPart("")));
       }
       StringBuilder cur = new StringBuilder();
       List<String> parts = ImmutableList.copyOf(Paths.SPLITTER.omitEmptyStrings().split(path));
@@ -663,7 +681,8 @@
         String part = parts.get(i);
         cur.append(part).append('/');
         String curPath = cur.toString();
-        Builder builder = copyWithPath().setPathPart(curPath);
+        boolean isLeaf = i == parts.size() - 1;
+        Builder builder = copyWithPath(isLeaf).setPathPart(curPath);
         if (hasSingleTree != null && i < parts.size() - 1 && hasSingleTree.get(i)) {
           builder.replaceParam(PathServlet.AUTODIVE_PARAM, PathServlet.NO_AUTODIVE_VALUE);
         }
@@ -677,7 +696,7 @@
     return ImmutableMap.of("text", text, "url", url.toUrl());
   }
 
-  private Builder copyWithPath() {
+  private Builder copyWithPath(boolean isLeaf) {
     Builder copy;
     switch (type) {
       case DIFF:
@@ -686,6 +705,9 @@
       case LOG:
         copy = log();
         break;
+      case BLAME:
+        copy = isLeaf ? blame() : path();
+        break;
       default:
         copy = path();
         break;
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 50a4584..aece87b 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -49,6 +49,7 @@
 
   private static final String CMD_ARCHIVE = "+archive";
   private static final String CMD_AUTO = "+";
+  private static final String CMD_BLAME = "+blame";
   private static final String CMD_DESCRIBE = "+describe";
   private static final String CMD_DIFF = "+diff";
   private static final String CMD_LOG = "+log";
@@ -137,6 +138,8 @@
       return parseArchiveCommand(req, repoName, path);
     } else if (command.equals(CMD_AUTO)) {
       return parseAutoCommand(req, repoName, path);
+    } else if (command.equals(CMD_BLAME)) {
+      return parseBlameCommand(req, repoName, path);
     } else if (command.equals(CMD_DESCRIBE)) {
       return parseDescribeCommand(repoName, path);
     } else if (command.equals(CMD_DIFF)) {
@@ -198,6 +201,21 @@
     }
   }
 
+  private GitilesView.Builder parseBlameCommand(
+      HttpServletRequest req, String repoName, String path) throws IOException {
+    if (path.isEmpty()) {
+      return null;
+    }
+    RevisionParser.Result result = parseRevision(req, path);
+    if (result == null || result.getOldRevision() != null || result.getPath().isEmpty()) {
+      return null;
+    }
+    return GitilesView.blame()
+        .setRepositoryName(repoName)
+        .setRevision(result.getRevision())
+        .setPathPart(result.getPath());
+  }
+
   private GitilesView.Builder parseDescribeCommand(String repoName, String path) {
     if (isEmptyOrSlash(path)) {
       return null;
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 44e69c3..2e1e005 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/GitilesViewTest.java
@@ -559,6 +559,36 @@
     assertEquals("/b/foo/bar/+archive/master/path/to/a/dir.tar.bz2", view.toUrl());
   }
 
+  public void testBlame() throws Exception {
+    ObjectId id = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+    GitilesView view = GitilesView.blame()
+        .copyFrom(HOST)
+        .setRepositoryName("foo/bar")
+        .setRevision(Revision.unpeeled("master", id))
+        .setPathPart("/dir/file")
+        .build();
+
+    assertEquals("/b", view.getServletPath());
+    assertEquals(Type.BLAME, 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("dir/file", view.getPathPart());
+    assertTrue(HOST.getParameters().isEmpty());
+    assertEquals("/b/foo/bar/+blame/master/dir/file", view.toUrl());
+    assertEquals(
+        ImmutableList.of(
+            breadcrumb("host", "/b/?format=HTML"),
+            breadcrumb("foo/bar", "/b/foo/bar/"),
+            breadcrumb("master", "/b/foo/bar/+/master"),
+            breadcrumb(".", "/b/foo/bar/+/master/"),
+            breadcrumb("dir", "/b/foo/bar/+/master/dir"),
+            breadcrumb("file", "/b/foo/bar/+blame/master/dir/file")),
+        view.getBreadcrumbs());
+  }
+
   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 4fddf0f..47013d6 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
@@ -422,6 +422,25 @@
     assertEquals("foo/bar", view.getPathPart());
   }
 
+  public void testBlame() throws Exception {
+    RevCommit master = repo.branch("refs/heads/master").commit().create();
+    repo.branch("refs/heads/branch").commit().create();
+    GitilesView view;
+
+    assertNull(getView("/repo/+blame"));
+    assertNull(getView("/repo/+blame/"));
+    assertNull(getView("/repo/+blame/master"));
+    assertNull(getView("/repo/+blame/master..branch"));
+
+    view = getView("/repo/+blame/master/foo/bar");
+    assertEquals(Type.BLAME, view.getType());
+    assertEquals("repo", view.getRepositoryName());
+    assertEquals("master", view.getRevision().getName());
+    assertEquals(master, view.getRevision().getId());
+    assertEquals(Revision.NULL, view.getOldRevision());
+    assertEquals("foo/bar", view.getPathPart());
+  }
+
   private GitilesView getView(String pathAndQuery) throws ServletException, IOException {
     final AtomicReference<GitilesView> view = Atomics.newReference();
     HttpServlet testServlet = new HttpServlet() {