Merge "Add properties to specify where an issue got associated"
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java
index 8030d78..017fbb9 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java
@@ -12,6 +12,7 @@
 
 import com.googlesource.gerrit.plugins.hooks.its.ItsName;
 
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.lib.Config;
 
 import org.slf4j.Logger;
@@ -71,19 +72,24 @@
   }
 
   /**
-   * Adds an issue to the map returned by {@link #getIssueIds(String, String)}.
+   * Helper funcion for {@link #getIssueIds(String, String)}.
+   * <p>
+   * Adds a text's issues for a given occurrence to the map returned by
+   * {@link #getIssueIds(String, String)}.
    *
-   * @param issue The issue to add.
-   * @param map The map that the issue should get added to.
-   * @param occurrence The occurrence the issue get added at in {@code map}.
+   * @param text The text to extract issues from.
+   * @param occurrence The occurrence the issues get added at in {@code map}.
+   * @param map The map that the issues should get added to.
    */
-  private void addIssueOccurrence(String issue, Map<String,Set<String>> map, String occurrence) {
-    Set<String> occurrences = map.get(issue);
-    if (occurrences == null) {
-      occurrences = Sets.newLinkedHashSet();
-      map.put(issue, occurrences);
+  private void addIssuesOccurrence(String text, String occurrence, Map<String,Set<String>> map) {
+    for (String issue : getIssueIds(text)) {
+      Set<String> occurrences = map.get(issue);
+      if (occurrences == null) {
+        occurrences = Sets.newLinkedHashSet();
+        map.put(issue, occurrences);
+      }
+      occurrences.add(occurrence);
     }
-    occurrences.add(occurrence);
   }
 
   /**
@@ -93,15 +99,61 @@
    * @param commitId The commit id to fetch issues for.
    * @return A mapping, whose keys are issue ids and whose values is a set of
    *    places where the issue occurs. Each issue occurs at least in
-   *    "somewhere".
+   *    "somewhere". Issues from the first line get tagged with an occurrence
+   *    "subject". Issues in the last block get tagged with "footer". Issues
+   *    occurring between "subject" and "footer" get tagged with "body".
    */
   public Map<String,Set<String>> getIssueIds(String projectName,
       String commitId) {
     Map<String,Set<String>> ret = Maps.newHashMap();
     String commitMessage = commitMessageFetcher.fetchGuarded(projectName,
         commitId);
-    for (String issue : getIssueIds(commitMessage)) {
-      addIssueOccurrence(issue, ret, "somewhere");
+
+    addIssuesOccurrence(commitMessage, "somewhere", ret);
+
+    String[] lines = commitMessage.split("\n");
+    if (lines.length > 0) {
+      // Parsing for "subject"
+      addIssuesOccurrence(lines[0], "subject", ret);
+
+      // Determining footer line numbers
+      int currentLine = lines.length-1;
+      while (currentLine >=0 && lines[currentLine].isEmpty()) {
+        currentLine--;
+      }
+      int footerEnd = currentLine + 1;
+      while (currentLine >=0 && !lines[currentLine].isEmpty()) {
+        currentLine--;
+      }
+      int footerStart = currentLine + 1;
+
+      if (footerStart == 0) {
+        // The first block of non-blank lines is not considered a footer, so
+        // we adjust that.
+        footerStart = -1;
+      }
+
+      // Parsing for "body", and "footer"
+      String body = null;
+      String footer = null;
+      if (footerStart == -1) {
+        // No footer could be found. So all lines after the first one (that's
+        // the subject) is the body.
+        //body = String[] templateParameters =
+          //  Arrays.copyOfRange(allParameters, 1, allParameters.length);
+        if (lines.length > 0) {
+          body = StringUtils.join(lines, "\n", 1, lines.length);
+        }
+      } else {
+        body = StringUtils.join(lines, "\n", 1, footerStart - 1);
+        footer = StringUtils.join(lines, "\n", footerStart, footerEnd);
+      }
+      if (body != null) {
+        addIssuesOccurrence(body, "body", ret);
+      }
+      if (footer != null) {
+        addIssuesOccurrence(footer, "footer", ret);
+      }
     }
     return ret;
   }
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
index 33705a3..323205f 100644
--- a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
@@ -193,10 +193,13 @@
         "1234567891123456789212345678931234567894");
 
     Map<String,Set<String>> expected = Maps.newHashMap();
-    expected.put("42", Sets.newHashSet("somewhere"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
     assertEquals("Extracted issues do not match", expected, actual);
 
     assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
   }
 
   public void testIssueIdsCommitMultipleIssues() {
@@ -216,11 +219,14 @@
         "1234567891123456789212345678931234567894");
 
     Map<String,Set<String>> expected = Maps.newHashMap();
-    expected.put("42", Sets.newHashSet("somewhere"));
-    expected.put("4711", Sets.newHashSet("somewhere"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("4711", Sets.newHashSet("somewhere", "subject"));
     assertEquals("Extracted issues do not match", expected, actual);
 
     assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
   }
 
   public void testIssueIdsCommitMultipleIssuesMultipleTimes() {
@@ -240,11 +246,395 @@
         "1234567891123456789212345678931234567894");
 
     Map<String,Set<String>> expected = Maps.newHashMap();
-    expected.put("42", Sets.newHashSet("somewhere"));
-    expected.put("4711", Sets.newHashSet("somewhere"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("4711", Sets.newHashSet("somewhere", "subject"));
     assertEquals("Extracted issues do not match", expected, actual);
 
     assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitSingleIssueBody() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject does not reference a bug\n" +
+            "Body references bug#42\n" +
+            "\n" +
+            "Footer: does not reference a bug\n" +
+            "Change-Id: I1234567891123456789212345678931234567894");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("42", Sets.newHashSet("somewhere", "body"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitSingleIssueFooter() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject does not reference a bug\n" +
+            "Body does not reference a bug\n" +
+            "\n" +
+            "Footer: references bug#42\n" +
+            "Change-Id: I1234567891123456789212345678931234567894");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("42", Sets.newHashSet("somewhere", "footer"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitDifferentParts() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject references bug#42.\n" +
+            "Body references bug#16.\n" +
+            "Body also references bug#176.\n" +
+            "\n" +
+            "Bug: bug#4711 in footer\n" +
+            "Change-Id: I1234567891123456789212345678931234567894");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "body"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("176", Sets.newHashSet("somewhere", "body"));
+    expected.put("4711", Sets.newHashSet("somewhere", "footer"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitDifferentPartsEmptySubject() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "\n" +
+            "Body references bug#16.\n" +
+            "Body also references bug#176.\n" +
+            "\n" +
+            "Bug: bug#4711 in footer\n" +
+            "Change-Id: I1234567891123456789212345678931234567894");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "body"));
+    expected.put("176", Sets.newHashSet("somewhere", "body"));
+    expected.put("4711", Sets.newHashSet("somewhere", "footer"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitDifferentPartsLinePastFooter() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject references bug#42.\n" +
+            "Body references bug#16.\n" +
+            "Body also references bug#176.\n" +
+            "\n" +
+            "Bug: bug#4711 in footer\n" +
+            "Change-Id: I1234567891123456789212345678931234567894\n");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "body"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("176", Sets.newHashSet("somewhere", "body"));
+    expected.put("4711", Sets.newHashSet("somewhere", "footer"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitDifferentPartsLinesPastFooter() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject references bug#42.\n" +
+            "Body references bug#16.\n" +
+            "Body also references bug#176.\n" +
+            "\n" +
+            "Bug: bug#4711 in footer\n" +
+            "Change-Id: I1234567891123456789212345678931234567894\n" +
+            "\n");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "body"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("176", Sets.newHashSet("somewhere", "body"));
+    expected.put("4711", Sets.newHashSet("somewhere", "footer"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitDifferentPartsNoFooter() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject references bug#42.\n" +
+            "Body references bug#16.\n" +
+            "Body also references bug#176.");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "body"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("176", Sets.newHashSet("somewhere", "body"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitDifferentPartsNoFooterTrailingLine() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject references bug#42.\n" +
+            "Body references bug#16.\n" +
+            "Body also references bug#176.\n");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "body"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("176", Sets.newHashSet("somewhere", "body"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitDifferentPartsNoFooterTrailingLines() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject references bug#42.\n" +
+            "Body references bug#16.\n" +
+            "Body also references bug#176.\n" +
+            "\n");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "body"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("176", Sets.newHashSet("somewhere", "body"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitEmpty() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn("");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitBlankLine() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn("\n");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitBlankLines() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn("\n\n");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitMoreBlankLines() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn("\n\n\n");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsCommitMixed() {
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+    .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(commitMessageFetcher.fetchGuarded("testProject",
+        "1234567891123456789212345678931234567894")).andReturn(
+            "Subject bug#42, bug#1984, and bug#16\n" +
+            "\n" +
+            "bug#4711 in body,\n" +
+            "along with bug#1984, and bug#5150.\n" +
+            "bug#4711 in body again, along with bug#16\n" +
+            "\n" +
+            "Bug: bug#176, bug#1984, and bug#5150\n" +
+            "Change-Id: I1234567891123456789212345678931234567894");
+
+    replayMocks();
+
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    Map<String,Set<String>> actual = issueExtractor.getIssueIds("testProject",
+        "1234567891123456789212345678931234567894");
+
+    Map<String,Set<String>> expected = Maps.newHashMap();
+    expected.put("16", Sets.newHashSet("somewhere", "subject", "body"));
+    expected.put("42", Sets.newHashSet("somewhere", "subject"));
+    expected.put("176", Sets.newHashSet("somewhere", "footer"));
+    expected.put("1984", Sets.newHashSet("somewhere", "subject", "body",
+        "footer"));
+    expected.put("4711", Sets.newHashSet("somewhere", "body"));
+    expected.put("5150", Sets.newHashSet("somewhere", "body", "footer"));
+    assertEquals("Extracted issues do not match", expected, actual);
+
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
+    assertLogMessageContains("Matching");
   }
 
   @Override