Add links for specific line numbers to comment emails

When a review comment is associated with a specific line, include a link
in the comment email which will direct the user to that specific line of
the file. If the comment is associated with a range of lines, it will
link to the first line of that range.

Feature: Issue 4370
Change-Id: I21be215707589e3c2ff63c573377cd40b867f233
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 34301ed..f0f4566 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -554,10 +554,12 @@
         + url + "#/c/" + c + "/1/a.txt\n"
         + "File a.txt:\n"
         + "\n"
+        + url + "#/c/12/1/a.txt@a2\n"
         + "PS1, Line 2: \n"
         + "what happened to this?\n"
         + "\n"
         + "\n"
+        + url + "#/c/12/1/a.txt@1\n"
         + "PS1, Line 1: ew\n"
         + "nit: trailing whitespace\n"
         + "\n"
@@ -565,18 +567,22 @@
         + url + "#/c/" + c + "/2/a.txt\n"
         + "File a.txt:\n"
         + "\n"
+        + url + "#/c/12/2/a.txt@a1\n"
         + "PS2, Line 1: \n"
         + "comment 1 on base\n"
         + "\n"
         + "\n"
+        + url + "#/c/12/2/a.txt@a2\n"
         + "PS2, Line 2: \n"
         + "comment 2 on base\n"
         + "\n"
         + "\n"
+        + url + "#/c/12/2/a.txt@1\n"
         + "PS2, Line 1: ew\n"
         + "join lines\n"
         + "\n"
         + "\n"
+        + url + "#/c/12/2/a.txt@2\n"
         + "PS2, Line 2: nten\n"
         + "typo: content\n"
         + "\n"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 3896b31..5e19a43 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -286,7 +286,13 @@
   private String getCommentLinePrefix(Comment comment) {
     int lineNbr = comment.range == null ?
         comment.lineNbr : comment.range.startLine;
-    return "PS" + comment.key.patchSetId + ", Line " + lineNbr + ": ";
+    StringBuilder sb = new StringBuilder();
+    sb.append("PS").append(comment.key.patchSetId);
+    if (lineNbr != 0) {
+      sb.append(", Line ").append(lineNbr);
+    }
+    sb.append(": ");
+    return sb.toString();
   }
 
   /**
@@ -458,18 +464,32 @@
         commentData.put("lines", getLinesOfComment(comment, group.fileData));
         commentData.put("message", comment.message.trim());
 
+        // Set the prefix.
         String prefix = getCommentLinePrefix(comment);
         commentData.put("linePrefix", prefix);
         commentData.put("linePrefixEmpty",
             Strings.padStart(": ", prefix.length(), ' '));
 
+        // Set line numbers.
+        int startLine;
         if (comment.range == null) {
-          commentData.put("startLine", comment.lineNbr);
+          startLine = comment.lineNbr;
         } else {
-          commentData.put("startLine", comment.range.startLine);
+          startLine = comment.range.startLine;
           commentData.put("endLine", comment.range.endLine);
         }
+        commentData.put("startLine", startLine);
 
+        // Set the comment link.
+        if (comment.lineNbr == 0) {
+          commentData.put("link", group.getLink());
+        } else if (comment.side == 0) {
+          commentData.put("link", group.getLink() + "@a" + startLine);
+        } else {
+          commentData.put("link", group.getLink() + '@' + startLine);
+        }
+
+        // Set robot comment data.
         if (comment instanceof RobotComment) {
           RobotComment robotComment = (RobotComment) comment;
           commentData.put("isRobotComment", true);
@@ -480,6 +500,7 @@
           commentData.put("isRobotComment", false);
         }
 
+        // Set parent comment info.
         Optional<Comment> parent = getParent(comment);
         if (parent.isPresent()) {
           commentData.put("parentMessage",
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy
index ed574a8..0e1f153 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy
@@ -51,6 +51,9 @@
 
       {foreach $line in $comment.lines}
         {if isFirst($line)}
+          {if $comment.startLine != 0}
+            {$comment.link}
+          {/if}{\n}
           {$comment.linePrefix}
         {else}
           {$comment.linePrefixEmpty}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
index d55905e..522bcc84 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -83,7 +83,14 @@
 
               <p style="{$commentHeaderStyle}">
                 {if length($comment.lines) > 0}
-                  Patch Set #{$group.patchSetId}, Line {$comment.startLine}:{sp}
+                  <a href="{$comment.link}">
+                    {if $comment.startLine == 0}
+                      Patch Set #{$group.patchSetId}:{sp}
+                    {else}
+                      Patch Set #{$group.patchSetId},{sp}
+                      Line {$comment.startLine}:{sp}
+                    {/if}
+                  </a>
                 {/if}
                 {if length($comment.lines) == 1}
                   <code style="font-size:12px">{$comment.lines[0]}</code>