Merge "Allow custom configuration of missing downstreams message."
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
index 97ce063..df51610 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
@@ -213,6 +213,15 @@
     return downstreamBranches;
   }
 
+  public String getMissingDownstreamsMessage() throws ConfigInvalidException {
+    String message = getConfig().getString("global", null, "missingDownstreamsMessage");
+    if (message == null) {
+      message =
+          "Missing downstream branches ${missingDownstreams}. Please recreate the automerges.";
+    }
+    return message;
+  }
+
   public short getMaxAutomergeVote() throws ConfigInvalidException {
     return (short) getConfig().getInt("global", "maxAutomergeVote", 1);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java
index 2722904..664475d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/MergeValidator.java
@@ -15,6 +15,8 @@
 package com.googlesource.gerrit.plugins.automerger;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -31,8 +33,10 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Repository;
@@ -70,10 +74,7 @@
           gApi.changes().id(changeId).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
       Set<String> missingDownstreams = getMissingDownstreamMerges(upstreamChange);
       if (!missingDownstreams.isEmpty()) {
-        throw new MergeValidationException(
-            "Missing downstream branches for "
-                + missingDownstreams
-                + ". Please recreate the automerges.");
+        throw new MergeValidationException(getMissingDownstreamsMessage(missingDownstreams));
       }
     } catch (RestApiException | IOException | ConfigInvalidException e) {
       log.error("Automerger plugin failed onPreMerge for {}", changeId, e);
@@ -82,6 +83,19 @@
     }
   }
 
+  private String getMissingDownstreamsMessage(Set<String> missingDownstreams)
+      throws ConfigInvalidException {
+    String missingDownstreamsMessage = config.getMissingDownstreamsMessage();
+    ParameterizedString pattern = new ParameterizedString(missingDownstreamsMessage);
+    return pattern.replace(getSubstitutionMap(missingDownstreams));
+  }
+
+  private Map<String, String> getSubstitutionMap(Set<String> missingDownstreams) {
+    Map<String, String> substitutionMap = new HashMap<>();
+    substitutionMap.put("missingDownstreams", Joiner.on(", ").join(missingDownstreams));
+    return substitutionMap;
+  }
+
   @VisibleForTesting
   protected Set<String> getMissingDownstreamMerges(ChangeInfo upstreamChange)
       throws RestApiException, IOException, ConfigInvalidException {
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index ada9bb5..dcb8c3b 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -15,6 +15,7 @@
     alwaysBlankMerge = .*SKIP ME ALWAYS.*
     blankMerge = .*RESTRICT AUTOMERGE.*
     blankMerge = .*SKIP UNLESS MERGEALL SET.*
+    missingDownstreamsMessage = there is no ${missingDownstreams}
 
   [@PLUGIN@ "branch1:branch2"]
     setProjects = some/project
@@ -102,6 +103,22 @@
   Even if mergeAll is set to True for a branch, it will still merge with
   "-s ours".
 
+global.missingDownstreamsMessage
+: Message to display when attempting to submit a change without downstreams.
+
+  If a change has downstream branches configured for a project, but those
+  downstreams have not been uploaded for review or are abandoned, this message
+  will display as an error message on submit.
+
+  The message can be custom configured to include the missing downstream
+  branches.
+
+  For example, you could configure the @PLUGIN@.config to include:
+
+  ```
+    missingDownstreamsMessage = Missing downstreams ${missingDownstreams}
+  ```
+
 @PLUGIN@.branch1:branch2.setProjects
 : Projects to automerge for.
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
index 126dc02..dcbc4e8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
@@ -74,11 +74,29 @@
     PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
     pushConfig("automerger.config", result.getChange().project().get());
     result.assertOkStatus();
+    int changeNumber = result.getChange().getId().id;
     // Assert we are missing downstreams
     exception.expect(ResourceConflictException.class);
     exception.expectMessage(
-        "Failed to submit 1 change due to the following problems:\n"
-            + "Change 1: Missing downstream branches for [ds_one]. Please recreate the automerges.");
+        "Failed to submit 1 change due to the following problems:\nChange "
+            + changeNumber
+            + ": Missing downstream branches ds_one. Please recreate the automerges.");
+    merge(result);
+  }
+
+  @Test
+  public void testMissingDownstreamMerges_custom() throws Exception {
+    // Create initial change
+    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    pushConfig("alternate.config", result.getChange().project().get());
+    result.assertOkStatus();
+    int changeNumber = result.getChange().getId().id;
+    // Assert we are missing downstreams
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(
+        "Failed to submit 1 change due to the following problems:\nChange "
+            + changeNumber
+            + ": there is no ds_one");
     merge(result);
   }
 }
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
index 1016e89..3f0ed7d 100644
--- a/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
@@ -18,4 +18,5 @@
   conflictMessage = line1\n\
 line2\n\
 line3 ${branch}\n\
-line4
\ No newline at end of file
+line4
+  missingDownstreamsMessage = there is no ${missingDownstreams}
\ No newline at end of file