Add link to file view for creating a Gerrit change/edit

Bug: issue 40011246
Change-Id: I726260840191f50eda9e1235bd2abd40eed11af8
diff --git a/java/com/google/gitiles/BlobSoyData.java b/java/com/google/gitiles/BlobSoyData.java
index a6879ae..c90b434 100644
--- a/java/com/google/gitiles/BlobSoyData.java
+++ b/java/com/google/gitiles/BlobSoyData.java
@@ -26,6 +26,7 @@
 import com.google.template.soy.data.SoyListData;
 import com.google.template.soy.data.SoyMapData;
 import java.io.IOException;
+import java.net.URI;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -79,6 +80,11 @@
 
   public Map<String, Object> toSoyData(String path, ObjectId blobId)
       throws MissingObjectException, IOException {
+    return toSoyData(path, blobId, null);
+  }
+
+  public Map<String, Object> toSoyData(String path, ObjectId blobId, @Nullable URI editUrl)
+      throws MissingObjectException, IOException {
     Map<String, Object> data = Maps.newHashMapWithExpectedSize(4);
     data.put("sha", ObjectId.toString(blobId));
 
@@ -119,6 +125,9 @@
       data.put("fileUrl", GitilesView.path().copyFrom(view).toUrl());
       data.put("logUrl", GitilesView.log().copyFrom(view).toUrl());
       data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl());
+      if (editUrl != null) {
+        data.put("editUrl", editUrl.toString());
+      }
       if (imageBlob != null) {
         data.put("imgBlob", imageBlob);
       }
diff --git a/java/com/google/gitiles/PathServlet.java b/java/com/google/gitiles/PathServlet.java
index 87d2680..fc0af05 100644
--- a/java/com/google/gitiles/PathServlet.java
+++ b/java/com/google/gitiles/PathServlet.java
@@ -15,6 +15,7 @@
 package com.google.gitiles;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gitiles.GitilesUrls.escapeName;
 import static com.google.gitiles.TreeSoyData.resolveTargetUrl;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
@@ -31,6 +32,8 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.Writer;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -49,6 +52,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -61,11 +66,14 @@
 import org.eclipse.jgit.util.QuotedString;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Serves an HTML page with detailed information about a path within a tree. */
 // TODO(dborowitz): Handle non-UTF-8 names.
 public class PathServlet extends BaseServlet {
   private static final long serialVersionUID = 1L;
+  private static final Logger log = LoggerFactory.getLogger(PathServlet.class);
 
   static final String MODE_HEADER = "X-Gitiles-Path-Mode";
   static final String TYPE_HEADER = "X-Gitiles-Object-Type";
@@ -532,10 +540,34 @@
     return p.eof() ? p : null;
   }
 
+  private @Nullable URI createEditUrl(HttpServletRequest req, GitilesView view)
+      throws IOException {
+      String baseGerritUrl = this.urls.getBaseGerritUrl(req);
+      if (baseGerritUrl == null) {
+        return null;
+      }
+      String commitish = view.getRevision().getName();
+      if (commitish == null || !commitish.startsWith("refs/heads/")) {
+        return null;
+      }
+      try {
+        URI editUrl = new URI(baseGerritUrl);
+        String path =
+          String.format("/admin/repos/edit/repo/%s/branch/%s/file/%s",
+            view.getRepositoryName(),
+            commitish,
+            view.getPathPart());
+        return editUrl.resolve(escapeName(path));
+      } catch (URISyntaxException use) {
+        log.warn("Malformed Edit URL", use);
+      }
+      return null;
+  }
+
   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);
+    Map<String, ?> data = new BlobSoyData(wr.getObjectReader(), view).toSoyData(wr.path, wr.id, createEditUrl(req, view));
     // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
     renderHtml(
         req,
diff --git a/javatests/com/google/gitiles/PathServletTest.java b/javatests/com/google/gitiles/PathServletTest.java
index 5002948..7531242 100644
--- a/javatests/com/google/gitiles/PathServletTest.java
+++ b/javatests/com/google/gitiles/PathServletTest.java
@@ -103,6 +103,18 @@
   }
 
   @Test
+  public void editUrl() throws Exception {
+    repo.branch("master").commit().add("editFoo", "Content").create();
+    repo.reset("refs/heads/master");
+
+    Map<String, ?> data = buildData("/repo/+/refs/heads/master/editFoo");
+
+    String editUrl = (String) getBlobData(data).get("editUrl");
+    String testUrl = "http://test-host-review/admin/repos/edit/repo/repo/branch/refs/heads/master/file/editFoo";
+    assertThat(editUrl).isEqualTo(testUrl);
+  }
+
+  @Test
   public void fileWithMaxLines() throws Exception {
     int MAX_LINE_COUNT = 50000;
     StringBuilder contentBuilder = new StringBuilder();
diff --git a/resources/com/google/gitiles/templates/ObjectDetail.soy b/resources/com/google/gitiles/templates/ObjectDetail.soy
index 1dd3a40..f7c0b0c 100644
--- a/resources/com/google/gitiles/templates/ObjectDetail.soy
+++ b/resources/com/google/gitiles/templates/ObjectDetail.soy
@@ -242,12 +242,14 @@
   {@param? logUrl: ?}  /** optional URL to a log for this file. */
   {@param? blameUrl: ?}  /** optional URL to a blame for this file. */
   {@param? docUrl: ?}  /** optional URL to view rendered file. */
+  {@param? editUrl: ?} /** optional URL to create a change in Gerrit. */
 <div class="u-sha1 u-monospace BlobSha1">
   {msg desc="SHA-1 for the file's blob"}blob: {$sha}{/msg}
   {if $fileUrl}{sp}[<a href="{$fileUrl}">{msg desc="detail view of a file"}file{/msg}</a>]{/if}
   {if $logUrl}{sp}[<a href="{$logUrl}">{msg desc="history for a file"}log{/msg}</a>]{/if}
   {if $blameUrl}{sp}[<a href="{$blameUrl}">{msg desc="blame for a file"}blame{/msg}</a>]{/if}
   {if $docUrl}{sp}[<a href="{$docUrl}">{msg desc="view rendered file"}view{/msg}</a>]{/if}
+  {if $editUrl}{sp}[<a href="{$editUrl}">{msg desc="edit file in Gerrit"}edit{/msg}</a>]{/if}
 </div>
 {/template}