Merge "Allow ITS comments for a change's patch sets that first associate an issue"
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java
index f5611fa..4d2ed8d 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 public class GerritHookFilter implements ChangeListener {
@@ -56,7 +57,8 @@
     }
   }
 
-  public void doFilter(PatchSetCreatedEvent hook) throws IOException {
+  public void doFilter(PatchSetCreatedEvent hook) throws IOException,
+      OrmException {
   }
 
   public void doFilter(CommentAddedEvent hook) throws IOException {
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java
index c3ad727..3a8314b 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java
@@ -16,13 +16,21 @@
 
 import java.io.IOException;
 import java.net.URL;
+import java.util.Iterator;
+import java.util.List;
 
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
 import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor;
@@ -43,8 +51,52 @@
   @Inject
   private IssueExtractor issueExtractor;
 
+  @Inject
+  private ReviewDb db;
+
+  /**
+   * Filter issues to those that occur for the first time in a change
+   *
+   * @param issues The issues to filter.
+   * @param patchSet Filter for this patch set.
+   * @return the issues that occur for the first time.
+   * @throws IOException
+   * @throws OrmException
+   */
+  private List<String> filterForFirstLinkedIssues(String[] issues,
+      PatchSetCreatedEvent patchSet) throws IOException, OrmException {
+    List<String> ret = Lists.newArrayList(issues);
+    int patchSetNumberCurrent = Integer.parseInt(patchSet.patchSet.number);
+
+    if (patchSetNumberCurrent > 1) {
+      String project = patchSet.change.project;
+      int changeNumber = Integer.parseInt(patchSet.change.number);
+      Change.Id changeId = new Change.Id(changeNumber);
+
+      // It would be nice to get patch sets directly via
+      //   patchSetCreated.change.patchSets
+      // but it turns out that it's null for our events. So we fetch the patch
+      // sets from the db instead.
+      ResultSet<PatchSet> patchSets = db.patchSets().byChange(changeId);
+      Iterator<PatchSet> patchSetIter = patchSets.iterator();
+
+      while (!ret.isEmpty() && patchSetIter.hasNext()) {
+        PatchSet previousPatchSet = patchSetIter.next();
+        if (previousPatchSet.getPatchSetId() < patchSetNumberCurrent) {
+          String commitMessage = getComment(project,
+              previousPatchSet.getRevision().get());
+          for (String issue : issueExtractor.getIssueIds(commitMessage)) {
+            ret.remove(issue);
+          }
+        }
+      }
+    }
+    return ret;
+  }
+
   @Override
-  public void doFilter(PatchSetCreatedEvent patchsetCreated) throws IOException {
+  public void doFilter(PatchSetCreatedEvent patchsetCreated)
+      throws IOException, OrmException {
     boolean addPatchSetComment = gerritConfig.getBoolean(its.name(), null,
         "commentOnPatchSetCreated", true);
 
@@ -52,13 +104,21 @@
         gerritConfig.getBoolean(its.name(), null, "commentOnChangeCreated",
             false);
 
-    if (addPatchSetComment || addChangeComment) {
+    boolean addFirstLinkedPatchSetComment = gerritConfig.getBoolean(its.name(),
+        null, "commentOnFirstLinkedPatchSetCreated", false);
+
+    if (addPatchSetComment || addFirstLinkedPatchSetComment || addChangeComment) {
       String gitComment =
           getComment(patchsetCreated.change.project,
               patchsetCreated.patchSet.revision);
 
       String[] issues = issueExtractor.getIssueIds(gitComment);
 
+      List<String> firstLinkedIssues = null;
+      if (addFirstLinkedPatchSetComment) {
+        firstLinkedIssues = filterForFirstLinkedIssues(issues, patchsetCreated);
+      }
+
       for (String issue : issues) {
         if (addChangeComment) {
           its.addRelatedLink(issue, new URL(patchsetCreated.change.url),
@@ -70,6 +130,12 @@
               "Gerrit Patch-Set " + patchsetCreated.change.id + "/"
                   + patchsetCreated.patchSet.number);
         }
+
+        if (addFirstLinkedPatchSetComment && firstLinkedIssues.contains(issue)) {
+          its.addRelatedLink(issue, new URL(patchsetCreated.change.url),
+              "Gerrit Patch-Set " + patchsetCreated.change.id + "/"
+                  + patchsetCreated.patchSet.number);
+        }
       }
     }
   }
diff --git a/hooks-its/src/main/resources/Documentation/config.md b/hooks-its/src/main/resources/Documentation/config.md
index adaaf47..f817ccd 100644
--- a/hooks-its/src/main/resources/Documentation/config.md
+++ b/hooks-its/src/main/resources/Documentation/config.md
@@ -54,6 +54,14 @@
 +
 Default is `true`.
 
+[[itsName.commentOnFirstLinkedPatchSetCreated]]itsName.commentOnFirstLinkedPatchSetCreated::
++
+If true, creating a patch set for a change adds an ITS comment to the change's
+associated issue, if the issue has not been mentioned in previous patch sets of
+the same change.
++
+Default is `false`.
+
 [[itsName.commentOnPatchSetCreated]]itsName.commentOnPatchSetCreated::
 +
 If true, creating a patch set for a change adds an ITS comment to the change's