Send comments as a separate email when published on push

When a user uses the new publish-comments option, the new patch set
email didn't contain the contents of the comments that were being
published. Actually getting these combined into one email will require a
fair bit of refactoring of templates.

In the meantime, just send the comments in a separate email, which from
the recipients' perspective is the same as if this feature didn't exist
and the author published comments via the web UI immediately after
pushing.

Change-Id: I20e542e1fb044ee678855fc83afcfaa6df65b326
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index b9b6812..2ce0b3b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -69,12 +69,14 @@
 import com.google.gerrit.testutil.TestTimeUtil;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.regex.Pattern;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
@@ -1509,11 +1511,36 @@
         .containsExactly("comment1", "comment2", "comment3");
     assertThat(getLastMessage(r.getChangeId())).isEqualTo("Uploaded patch set 3.\n\n(3 comments)");
 
-    Message m = Iterables.getLast(sender.getMessages());
-    assertThat(m.body()).contains("I'd like you to reexamine a change");
-    // TODO(dborowitz): The template currently doesn't include the ChangeMessage (aka cover letter)
-    // text at all. Update this to look for the (3 commnets) text once that's fixed.
-    assertThat(m.body()).doesNotMatch("[Cc]omment");
+    List<String> messages =
+        sender
+            .getMessages()
+            .stream()
+            .map(m -> m.body())
+            .sorted(Comparator.comparingInt(m -> m.contains("reexamine") ? 0 : 1))
+            .collect(toList());
+    assertThat(messages).hasSize(2);
+
+    assertThat(messages.get(0)).contains("Gerrit-MessageType: newpatchset");
+    assertThat(messages.get(0)).contains("I'd like you to reexamine a change");
+    assertThat(messages.get(0)).doesNotContain("Uploaded patch set 3");
+
+    assertThat(messages.get(1)).contains("Gerrit-MessageType: comment");
+    assertThat(messages.get(1))
+        .containsMatch(
+            Pattern.compile(
+                // A little weird that the comment email contains this text, but it's actually
+                // what's in the ChangeMessage. Really we should fuse the emails into one, but until
+                // then, this test documents the current behavior.
+                "Uploaded patch set 3\\.\n"
+                    + "\n"
+                    + "\\(3 comments\\)\\n.*"
+                    + "PS1, Line 1:.*"
+                    + "comment1\\n.*"
+                    + "PS1, Line 1:.*"
+                    + "comment2\\n.*"
+                    + "PS2, Line 1:.*"
+                    + "comment3\\n",
+                Pattern.DOTALL));
   }
 
   @Test
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index a86e163..382af51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -23,6 +23,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -42,11 +43,13 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
 import com.google.gerrit.server.git.ReceiveCommits.MagicBranchInput;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
@@ -104,6 +107,7 @@
   private final ChangeKindCache changeKindCache;
   private final ChangeMessagesUtil cmUtil;
   private final CommentsUtil commentsUtil;
+  private final EmailReviewComments.Factory emailCommentsFactory;
   private final ExecutorService sendEmailExecutor;
   private final RevisionCreated revisionCreated;
   private final CommentAdded commentAdded;
@@ -127,7 +131,7 @@
   private final MailRecipients recipients = new MailRecipients();
   private RevCommit commit;
   private ReceiveCommand cmd;
-  private Change change;
+  private ChangeNotes notes;
   private PatchSet newPatchSet;
   private ChangeKind changeKind;
   private ChangeMessage msg;
@@ -146,6 +150,7 @@
       ChangeKindCache changeKindCache,
       ChangeMessagesUtil cmUtil,
       CommentsUtil commentsUtil,
+      EmailReviewComments.Factory emailCommentsFactory,
       RevisionCreated revisionCreated,
       CommentAdded commentAdded,
       MergedByPushOp.Factory mergedByPushOpFactory,
@@ -171,6 +176,7 @@
     this.changeKindCache = changeKindCache;
     this.cmUtil = cmUtil;
     this.commentsUtil = commentsUtil;
+    this.emailCommentsFactory = emailCommentsFactory;
     this.revisionCreated = revisionCreated;
     this.commentAdded = commentAdded;
     this.mergedByPushOpFactory = mergedByPushOpFactory;
@@ -218,13 +224,14 @@
   @Override
   public boolean updateChange(ChangeContext ctx)
       throws RestApiException, OrmException, IOException {
-    change = ctx.getChange();
+    notes = ctx.getNotes();
+    Change change = notes.getChange();
     if (change == null || change.getStatus().isClosed()) {
       rejectMessage = CHANGE_IS_CLOSED;
       return false;
     }
     if (groups.isEmpty()) {
-      PatchSet prevPs = psUtil.current(ctx.getDb(), ctx.getNotes());
+      PatchSet prevPs = psUtil.current(ctx.getDb(), notes);
       groups = prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of();
     }
 
@@ -240,7 +247,7 @@
       approvals.putAll(magicBranch.labels);
       Set<String> hashtags = magicBranch.hashtags;
       if (hashtags != null && !hashtags.isEmpty()) {
-        hashtags.addAll(ctx.getNotes().getHashtags());
+        hashtags.addAll(notes.getHashtags());
         update.setHashtags(hashtags);
       }
       if (magicBranch.topic != null && !magicBranch.topic.equals(ctx.getChange().getTopic())) {
@@ -260,7 +267,7 @@
         change.setWorkInProgress(true);
         update.setWorkInProgress(true);
       }
-      if (magicBranch.shouldPublishComments()) {
+      if (shouldPublishComments()) {
         comments = publishComments(ctx);
       }
     }
@@ -425,47 +432,35 @@
   @Override
   public void postUpdate(Context ctx) throws Exception {
     if (changeKind != ChangeKind.TRIVIAL_REBASE) {
-      Runnable sender =
-          new Runnable() {
-            @Override
-            public void run() {
-              try {
-                ReplacePatchSetSender cm =
-                    replacePatchSetFactory.create(
-                        projectControl.getProject().getNameKey(), change.getId());
-                cm.setFrom(ctx.getAccountId());
-                cm.setPatchSet(newPatchSet, info);
-                cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
-                if (magicBranch != null) {
-                  cm.setNotify(magicBranch.notify);
-                  cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
-                }
-                cm.addReviewers(recipients.getReviewers());
-                cm.addExtraCC(recipients.getCcOnly());
-                cm.send();
-              } catch (Exception e) {
-                log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
-              }
-            }
-
-            @Override
-            public String toString() {
-              return "send-email newpatchset";
-            }
-          };
-
+      // TODO(dborowitz): Merge email templates so we only have to send one.
+      Runnable e = new ReplaceEmailTask(ctx);
       if (requestScopePropagator != null) {
         @SuppressWarnings("unused")
-        Future<?> possiblyIgnoredError =
-            sendEmailExecutor.submit(requestScopePropagator.wrap(sender));
+        Future<?> possiblyIgnoredError = sendEmailExecutor.submit(requestScopePropagator.wrap(e));
       } else {
-        sender.run();
+        e.run();
       }
     }
 
     NotifyHandling notify =
         magicBranch != null && magicBranch.notify != null ? magicBranch.notify : NotifyHandling.ALL;
-    revisionCreated.fire(change, newPatchSet, ctx.getAccount(), ctx.getWhen(), notify);
+
+    if (shouldPublishComments()) {
+      emailCommentsFactory
+          .create(
+              notify,
+              magicBranch != null ? magicBranch.getAccountsToNotify() : ImmutableListMultimap.of(),
+              notes,
+              newPatchSet,
+              ctx.getUser().asIdentifiedUser(),
+              msg,
+              comments,
+              msg.getMessage(),
+              ImmutableList.of()) // TODO(dborowitz): Include labels.
+          .sendAsync();
+    }
+
+    revisionCreated.fire(notes.getChange(), newPatchSet, ctx.getAccount(), ctx.getWhen(), notify);
     try {
       fireCommentAddedEvent(ctx);
     } catch (Exception e) {
@@ -476,6 +471,40 @@
     }
   }
 
+  private class ReplaceEmailTask implements Runnable {
+    private final Context ctx;
+
+    private ReplaceEmailTask(Context ctx) {
+      this.ctx = ctx;
+    }
+
+    @Override
+    public void run() {
+      try {
+        ReplacePatchSetSender cm =
+            replacePatchSetFactory.create(
+                projectControl.getProject().getNameKey(), notes.getChangeId());
+        cm.setFrom(ctx.getAccount().getId());
+        cm.setPatchSet(newPatchSet, info);
+        cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
+        if (magicBranch != null) {
+          cm.setNotify(magicBranch.notify);
+          cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
+        }
+        cm.addReviewers(recipients.getReviewers());
+        cm.addExtraCC(recipients.getCcOnly());
+        cm.send();
+      } catch (Exception e) {
+        log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+      }
+    }
+
+    @Override
+    public String toString() {
+      return "send-email newpatchset";
+    }
+  }
+
   private void fireCommentAddedEvent(Context ctx) throws OrmException {
     if (approvals.isEmpty()) {
       return;
@@ -487,7 +516,7 @@
      * show a transition from an oldValue of 0 to the new value.
      */
     ChangeControl changeControl =
-        changeControlFactory.controlFor(ctx.getDb(), change, ctx.getUser());
+        changeControlFactory.controlFor(ctx.getDb(), notes.getChange(), ctx.getUser());
     List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes();
     Map<String, Short> allApprovals = new HashMap<>();
     Map<String, Short> oldApprovals = new HashMap<>();
@@ -503,7 +532,13 @@
     }
 
     commentAdded.fire(
-        change, newPatchSet, ctx.getAccount(), null, allApprovals, oldApprovals, ctx.getWhen());
+        notes.getChange(),
+        newPatchSet,
+        ctx.getAccount(),
+        null,
+        allApprovals,
+        oldApprovals,
+        ctx.getWhen());
   }
 
   public PatchSet getPatchSet() {
@@ -511,7 +546,7 @@
   }
 
   public Change getChange() {
-    return change;
+    return notes.getChange();
   }
 
   public String getRejectMessage() {
@@ -546,4 +581,8 @@
       return null;
     }
   }
+
+  private boolean shouldPublishComments() {
+    return magicBranch != null && magicBranch.shouldPublishComments();
+  }
 }