Merge "Adjust to upstream buck and new bucklet version"
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 0f614e6..76f5273 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/CommitData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/CommitData.java
@@ -66,6 +66,7 @@
     LOG_URL,
     MESSAGE,
     PARENTS,
+    PARENT_BLAME_URL,
     SHA,
     SHORT_MESSAGE,
     TAGS,
@@ -144,6 +145,7 @@
         result.tree = c.getTree().copy();
       }
       if (fs.contains(Field.TREE_URL)) {
+        // Tree always implies the root tree.
         result.treeUrl = GitilesView.path().copyFrom(view).setPathPart("/").toUrl();
       }
       if (fs.contains(Field.PARENTS)) {
@@ -175,12 +177,12 @@
       }
     }
 
-    private static String urlFromView(GitilesView view, RevCommit commit,
-        GitilesView.Builder builder) {
+    private static String urlFromView(GitilesView view, RevCommit commit, GitilesView.Builder builder) {
       Revision rev = view.getRevision();
       return builder.copyFrom(view)
+          .setOldRevision(Revision.NULL)
           .setRevision(rev.getId().equals(commit) ? rev.getName() : commit.name(), commit)
-          .setPathPart(null)
+          .setPathPart(view.getPathPart())
           .toUrl();
     }
 
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/CommitSoyData.java b/gitiles-servlet/src/main/java/com/google/gitiles/CommitSoyData.java
index bd03eb2..8127d6d 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/CommitSoyData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/CommitSoyData.java
@@ -52,6 +52,9 @@
       Field.COMMITTER, Field.SHA, Field.TREE, Field.TREE_URL, Field.PARENTS, Field.MESSAGE,
       Field.LOG_URL, Field.ARCHIVE_URL, Field.ARCHIVE_TYPE);
 
+  private static final ImmutableSet<Field> NESTED_FIELDS = Sets.immutableEnumSet(
+      Field.PARENT_BLAME_URL);
+
   private Linkifier linkifier;
   private RevWalk walk;
   private ArchiveFormat archiveFormat;
@@ -111,7 +114,7 @@
       data.put("treeUrl", cd.treeUrl);
     }
     if (cd.parents != null) {
-      data.put("parents", toSoyData(view, cd.parents));
+      data.put("parents", toSoyData(view, fs, cd.parents));
     }
     if (cd.shortMessage != null) {
       data.put("shortMessage", cd.shortMessage);
@@ -132,7 +135,8 @@
     if (cd.diffEntries != null) {
       data.put("diffTree", toSoyData(view, cd.diffEntries));
     }
-    checkState(fs.size() == data.size(), "bad commit data fields: %s != %s", fs, data.keySet());
+    checkState(Sets.difference(fs, NESTED_FIELDS).size() == data.size(),
+        "bad commit data fields: %s != %s", fs, data.keySet());
     return ImmutableMap.copyOf(data);
   }
 
@@ -151,13 +155,16 @@
         "relativeTime", RelativeDateFormatter.format(ident.getWhen()));
   }
 
-  private List<Map<String, String>> toSoyData(GitilesView view, List<RevCommit> parents) {
+  private List<Map<String, String>> toSoyData(GitilesView view, Set<Field> fs,
+      List<RevCommit> parents) {
     List<Map<String, String>> result = Lists.newArrayListWithCapacity(parents.size());
     int i = 1;
     // TODO(dborowitz): Render something slightly different when we're actively
     // viewing a diff against one of the parents.
     for (RevCommit parent : parents) {
       String name = parent.name();
+      // Clear path on parent diff view, since this parent may not have a diff
+      // for the path in question.
       GitilesView.Builder diff = GitilesView.diff().copyFrom(view).setPathPart("");
       String parentName;
       if (parents.size() == 1) {
@@ -165,13 +172,21 @@
       } else {
         parentName = view.getRevision().getName() + "^" + (i++);
       }
-      result.add(ImmutableMap.of(
-          "sha", name,
-          "url", GitilesView.revision()
-              .copyFrom(view)
-              .setRevision(parentName, parent)
-              .toUrl(),
-          "diffUrl", diff.setOldRevision(parentName, parent).toUrl()));
+      Map<String, String> e = Maps.newHashMapWithExpectedSize(4);
+      e.put("sha", name);
+      e.put("url", GitilesView.revision()
+          .copyFrom(view)
+          .setRevision(parentName, parent)
+          .toUrl());
+      e.put("diffUrl", diff.setOldRevision(parentName, parent).toUrl());
+      if (fs.contains(Field.PARENT_BLAME_URL)) {
+        // Assumes caller has ensured path is a file.
+        e.put("blameUrl", GitilesView.blame()
+            .copyFrom(view)
+            .setRevision(Revision.peeled(parentName, parent))
+            .toUrl());
+      }
+      result.add(e);
     }
     return result;
   }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java
index 2a18afb..ebe89e2 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DiffServlet.java
@@ -18,11 +18,13 @@
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 
 import com.google.common.base.Charsets;
+import com.google.gitiles.CommitData.Field;
 
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.http.server.ServletUtils;
+import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -30,6 +32,7 @@
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.EmptyTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.eclipse.jgit.util.GitDateFormatter;
 import org.eclipse.jgit.util.GitDateFormatter.Format;
@@ -38,6 +41,7 @@
 import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.Map;
+import java.util.Set;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -62,19 +66,17 @@
 
     RevWalk walk = new RevWalk(repo);
     try {
-      boolean showCommit;
+      boolean showCommit, isFile;
       AbstractTreeIterator oldTree;
       AbstractTreeIterator newTree;
       try {
         // If we are viewing the diff between a commit and one of its parents,
         // include the commit detail in the rendered page.
         showCommit = isParentOf(walk, view.getOldRevision(), view.getRevision());
+        isFile = showCommit ? isFile(walk, view) : false;
         oldTree = getTreeIterator(walk, view.getOldRevision().getId());
         newTree = getTreeIterator(walk, view.getRevision().getId());
-      } catch (MissingObjectException e) {
-        res.setStatus(SC_NOT_FOUND);
-        return;
-      } catch (IncorrectObjectTypeException e) {
+      } catch (MissingObjectException | IncorrectObjectTypeException e) {
         res.setStatus(SC_NOT_FOUND);
         return;
       }
@@ -82,11 +84,15 @@
       Map<String, Object> data = getData(req);
       data.put("title", "Diff - " + view.getRevisionRange());
       if (showCommit) {
+        Set<Field> fs = CommitSoyData.DEFAULT_FIELDS;
+        if (isFile) {
+          fs = Field.setOf(fs, Field.PARENT_BLAME_URL);
+        }
         GitDateFormatter df = new GitDateFormatter(Format.DEFAULT);
         data.put("commit", new CommitSoyData()
             .setLinkifier(linkifier)
             .setArchiveFormat(getArchiveFormat(getAccess(req)))
-            .toSoyData(req, walk.parseCommit(view.getRevision().getId()), df));
+            .toSoyData(req, walk.parseCommit(view.getRevision().getId()), fs, df));
       }
       if (!data.containsKey("repositoryName") && (view.getRepositoryName() != null)) {
         data.put("repositoryName", view.getRepositoryName());
@@ -104,7 +110,7 @@
       OutputStream out = res.getOutputStream();
       try {
         out.write(html[0].getBytes(Charsets.UTF_8));
-        formatHtmlDiff(out, repo, oldTree, newTree, view.getPathPart());
+        formatHtmlDiff(out, view, repo, oldTree, newTree, view.getPathPart());
         out.write(html[1].getBytes(Charsets.UTF_8));
       } finally {
         out.close();
@@ -124,6 +130,21 @@
     }
   }
 
+  private static boolean isFile(RevWalk walk, GitilesView view) throws IOException {
+    if (view.getPathPart().equals("")) {
+      return false;
+    }
+    TreeWalk tw = TreeWalk.forPath(
+        walk.getObjectReader(),
+        view.getPathPart(),
+        walk.parseTree(view.getRevision().getId()));
+    try {
+      return (tw.getRawMode(0) & FileMode.TYPE_FILE) > 0;
+    } finally {
+      tw.release();
+    }
+  }
+
   private String[] renderAndSplit(Map<String, Object> data) {
     String html = renderer.newRenderer("gitiles.diffDetail")
         .setData(data)
@@ -138,11 +159,11 @@
     return new String[] {html.substring(0, lt), html.substring(gt + 1)};
   }
 
-  private void formatHtmlDiff(OutputStream out,
+  private void formatHtmlDiff(OutputStream out, GitilesView view,
       Repository repo, AbstractTreeIterator oldTree,
       AbstractTreeIterator newTree, String path)
       throws IOException {
-    DiffFormatter diff = new HtmlDiffFormatter(renderer, out);
+    DiffFormatter diff = new HtmlDiffFormatter(renderer, view, out);
     try {
       if (!path.equals("")) {
         diff.setPathFilter(PathFilter.create(path));
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java b/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java
index 993197d..98a8be3 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java
@@ -15,12 +15,15 @@
 package com.google.gitiles;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.eclipse.jgit.util.QuotedString.GIT_PATH;
 
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
 
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.patch.FileHeader;
@@ -30,6 +33,7 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
+import java.util.Map;
 
 /** Formats a unified format patch as UTF-8 encoded HTML. */
 final class HtmlDiffFormatter extends DiffFormatter {
@@ -45,17 +49,21 @@
   private static final byte[] LINE_END = "</span>\n".getBytes(Charsets.UTF_8);
 
   private final Renderer renderer;
+  private final GitilesView view;
   private int fileIndex;
+  private DiffEntry entry;
 
-  HtmlDiffFormatter(Renderer renderer, OutputStream out) {
+  HtmlDiffFormatter(Renderer renderer, GitilesView view, OutputStream out) {
     super(out);
     this.renderer = checkNotNull(renderer, "renderer");
+    this.view = checkNotNull(view, "view");
   }
 
   @Override
   public void format(List<? extends DiffEntry> entries) throws IOException {
     for (fileIndex = 0; fileIndex < entries.size(); fileIndex++) {
-      format(entries.get(fileIndex));
+      entry = entries.get(fileIndex);
+      format(entry);
     }
   }
 
@@ -79,21 +87,43 @@
   private void renderHeader(String header)
       throws IOException {
     int lf = header.indexOf('\n');
-    String first;
-    String rest;
-    if (0 <= lf) {
-      first = header.substring(0, lf);
-      rest = header.substring(lf + 1);
+    String rest = 0 <= lf ?  header.substring(lf + 1) : "";
+
+    // Based on DiffFormatter.formatGitDiffFirstHeaderLine.
+    List<Map<String, String>> parts = Lists.newArrayListWithCapacity(3);
+    parts.add(ImmutableMap.of("text", "diff --git"));
+    if (entry.getChangeType() != ChangeType.ADD) {
+      parts.add(ImmutableMap.of(
+          "text", GIT_PATH.quote(getOldPrefix() + entry.getOldPath()),
+          "url", revisionUrl(view.getOldRevision(), entry.getOldPath())));
     } else {
-      first = header;
-      rest = "";
+      parts.add(ImmutableMap.of(
+          "text", GIT_PATH.quote(getOldPrefix() + entry.getNewPath())));
     }
+    if (entry.getChangeType() != ChangeType.DELETE) {
+      parts.add(ImmutableMap.of(
+          "text", GIT_PATH.quote(getNewPrefix() + entry.getNewPath()),
+          "url", revisionUrl(view.getRevision(), entry.getNewPath())));
+    } else {
+      parts.add(ImmutableMap.of(
+          "text", GIT_PATH.quote(getNewPrefix() + entry.getOldPath())));
+    }
+
     getOutputStream().write(renderer.newRenderer("gitiles.diffHeader")
-        .setData(ImmutableMap.of("first", first, "rest", rest, "fileIndex", fileIndex))
+        .setData(ImmutableMap.of("firstParts", parts, "rest", rest, "fileIndex", fileIndex))
         .render()
         .getBytes(Charsets.UTF_8));
   }
 
+  private String revisionUrl(Revision rev, String path) {
+    return GitilesView.path()
+        .copyFrom(view)
+        .setOldRevision(Revision.NULL)
+        .setRevision(Revision.named(rev.getId().name()))
+        .setPathPart(path)
+        .toUrl();
+  }
+
   @Override
   protected void writeHunkHeader(int aStartLine, int aEndLine,
       int bStartLine, int bEndLine) throws IOException {
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/blame/BlameServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/blame/BlameServlet.java
index 3d88ffc..8259a9d 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/blame/BlameServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/blame/BlameServlet.java
@@ -130,14 +130,22 @@
           abbrevSha = reader.abbreviate(r.getSourceCommit()).name();
           abbrevShas.put(r.getSourceCommit(), abbrevSha);
         }
-        result.add(ImmutableMap.of(
-            "abbrevSha", abbrevSha,
-            "url", GitilesView.blame().copyFrom(view)
-                .setRevision(r.getSourceCommit().name())
-                .setPathPart(r.getSourcePath())
-                .toUrl(),
-            "author", CommitSoyData.toSoyData(r.getSourceAuthor(), df),
-            "count", r.getCount()));
+        Map<String, Object> e = Maps.newHashMapWithExpectedSize(6);
+        e.put("abbrevSha", abbrevSha);
+        e.put("blameUrl", GitilesView.blame().copyFrom(view)
+            .setRevision(r.getSourceCommit().name())
+            .setPathPart(r.getSourcePath())
+            .toUrl());
+        e.put("commitUrl", GitilesView.revision().copyFrom(view)
+            .setRevision(r.getSourceCommit().name())
+            .toUrl());
+        e.put("diffUrl", GitilesView.diff().copyFrom(view)
+            .setRevision(r.getSourceCommit().name())
+            .setPathPart(r.getSourcePath())
+            .toUrl());
+        e.put("author", CommitSoyData.toSoyData(r.getSourceAuthor(), df));
+        e.put("count", r.getCount());
+        result.add(e);
       }
     }
     return result;
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css b/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
index 19668ce..0dbd477 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/gitiles.css
@@ -343,10 +343,12 @@
   padding: 0;
 }
 #regions {
-  padding-top: 0px;
+  padding-top: 2px;
   padding-right: 2px;
   padding-bottom: 5px;
   padding-left: 2px;
+  white-space: nowrap;
+  list-style-type: none;
 
   /* Matching pre.git-blob below. */
   font-family: monospace;
@@ -370,6 +372,31 @@
   border-top: 0;
 }
 
+#regions li {
+  position: relative;
+}
+#regions li.line:hover {
+  background-color: #eee;
+}
+#regions li .regionMenu {
+  position: absolute;
+  top: 13px;
+  left: 0;
+  visibility: hidden;
+  padding-top: 4px;
+  padding-bottom: 4px;
+  padding-left: 8px;
+  padding-right: 8px;
+
+  background-color: #eee;
+  font-size: 8pt;
+  font-family: arial,sans-serif;
+  z-index: 101;
+}
+#regions li:hover .regionMenu {
+  visibility: visible;
+}
+
 /* Override some styles from the default prettify.css. */
 
 /* Line numbers on all lines. */
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/BlameDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/BlameDetail.soy
index 52486cf..0118d92 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/BlameDetail.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/BlameDetail.soy
@@ -26,11 +26,13 @@
  *     following keys:
  *       abbrevSha: abbreviated SHA-1 of revision for this line; if missing,
  *           assume blame info is missing.
- *       url: URL for detail about the revision
  *       author: author information with at least "name" and "relativeTime"
  *           keys.
  *       relativeTime: relative time of the revision
  *       count: line count
+ *       blameUrl: URL for a blame of this file at this commit
+ *       commitUrl: URL for detail about the commit
+ *       diffUrl: URL for a diff of this file at this commit
  */
 {template .blameDetail}
 {if $regions}
@@ -48,35 +50,32 @@
   <table id="blame">
     <tr>
       <td>
-        <pre id="regions">
-          <ul>
-            {foreach $region in $regions}
-              <li>
-                {if $region.abbrevSha}
-                  <a href="{$region.url}">
-                    <span class="sha1">{$region.abbrevSha}</span>
-                    {sp}<span class="author">{$region.author.name}</span>
-                    {sp}<span class="time">- {$region.author.relativeTime}</span>
-                  </a>
-                {else}
-                  &nbsp;
-                {/if}
-              </li>
-              {for $i in range($region.count - 1)}
-                <li>&nbsp;</li>
-              {/for}
-            {/foreach}
-          </ul>
-        </pre>
+        <ul id="regions">
+          {foreach $region in $regions}
+            <li class="line">
+              {if $region.abbrevSha}
+                <a href="{$region.commitUrl}">
+                  <span class="sha1">{$region.abbrevSha}</span>
+                  {sp}<span class="author">{$region.author.name}</span>
+                  {sp}<span class="time">- {$region.author.relativeTime}</span>
+                </a>
+                <div class="regionMenu">
+                  [<a href="{$region.commitUrl}">commit</a>]
+                  {sp}[<a href="{$region.diffUrl}">diff</a>]
+                  {sp}[<a href="{$region.blameUrl}">blame</a>]
+                </div>
+              {else}
+                &nbsp;
+              {/if}
+            </li>
+            {for $i in range($region.count - 1)}
+              <li>&nbsp;</li>
+            {/for}
+          {/foreach}
+        </ul>
       </td>
       <td>
-        {call .blobBox}
-          {param data: $data.data /}
-          {param size: $data.size /}
-          // No pretty printing; line spacing is different with/without, and
-          // JS rendering is a performance pain point.
-          {param lang: null /}
-        {/call}
+        {call .blobBox data="$data" /}
       </td>
     </tr>
   </table>
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy
index d888d0a..5222fa0 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/DiffDetail.soy
@@ -38,13 +38,22 @@
 /**
  * File header for a single unified diff patch.
  *
- * @param first the first line of the header, with no trailing LF.
+ * @param firstParts parts of the first line of the header, with "text" and
+ *     optional "url" fields.
  * @param rest remaining lines of the header, if any.
  * @param fileIndex position of the file within the difference.
  */
 {template .diffHeader}
 <pre class="diff-header">
-<a name="F{$fileIndex}" class="diff-git">{$first}</a>{\n}
+<a name="F{$fileIndex}" class="diff-git"></a>
+{foreach $part in $firstParts}
+  {if not isFirst($part)}{sp}{/if}
+  {if $part.url}
+    <a href="{$part.url}">{$part.text}</a>
+  {else}
+    {$part.text}
+  {/if}
+{/foreach}{\n}
 {$rest}
 </pre>
 {/template}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy
index 78ea913..66a0d75 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy
@@ -25,6 +25,7 @@
  *     sha: SHA-1.
  *     url: URL to view the parent commit.
  *     diffUrl: URL to display diffs relative to this parent.
+ *     blameUrl: optional URL to display blame of a file at this parent.
  * @param message list of commit message parts, where each part contains:
  *     text: raw text of the part.
  *     url: optional URL that should be linked to from the part.
@@ -74,6 +75,9 @@
           <a href="{$parent.url}">{$parent.sha}</a>
           <span class="diff-link">
             [<a href="{$parent.diffUrl}">{msg desc="text for the parent diff link"}diff{/msg}</a>]
+            {if isNonnull($parent.blameUrl)}
+              {sp}[<a href="{$parent.blameUrl}">{msg desc="text for the parent blame link"}blame{/msg}</a>]
+            {/if}
           </span>
         </td>
       </tr>
@@ -234,10 +238,10 @@
 {template .blobBox}
   {if $data != null}
     {if $data}
-      {if $lang != null}
+      {if $lang}
         <pre class="git-blob prettyprint linenums lang-{$lang}">{$data}</pre>
       {else}
-        <pre class="git-blob">{$data}</pre>
+        <pre class="git-blob prettyprint linenums">{$data}</pre>
       {/if}
     {else}
       <div class="file-empty">Empty file</div>