Merge "Include link to validation documentation when issues in OWNERS files are found"
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index f763a2c..0083497 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.plugins.codeowners.backend.ChangedFiles;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
@@ -53,6 +54,7 @@
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.events.RefReceivedEvent;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -151,6 +153,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final SkipCodeOwnerConfigValidationPushOption skipCodeOwnerConfigValidationPushOption;
   private final CodeOwnerMetrics codeOwnerMetrics;
+  private final DynamicItem<UrlFormatter> urlFormatter;
 
   @Inject
   CodeOwnerConfigValidator(
@@ -165,7 +168,8 @@
       PatchSetUtil patchSetUtil,
       IdentifiedUser.GenericFactory userFactory,
       SkipCodeOwnerConfigValidationPushOption skipCodeOwnerConfigValidationPushOption,
-      CodeOwnerMetrics codeOwnerMetrics) {
+      CodeOwnerMetrics codeOwnerMetrics,
+      DynamicItem<UrlFormatter> urlFormatter) {
     this.pluginName = pluginName;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.repoManager = repoManager;
@@ -178,6 +182,7 @@
     this.userFactory = userFactory;
     this.skipCodeOwnerConfigValidationPushOption = skipCodeOwnerConfigValidationPushOption;
     this.codeOwnerMetrics = codeOwnerMetrics;
+    this.urlFormatter = urlFormatter;
   }
 
   @Override
@@ -521,6 +526,7 @@
       // validate the code owner config files
       return Optional.of(
           ValidationResult.create(
+              urlFormatter,
               pluginName,
               codeOwnerConfigFilesToValidate.stream()
                   .flatMap(
@@ -1326,12 +1332,23 @@
     }
 
     static ValidationResult create(
-        String pluginName, Stream<CommitValidationMessage> validationMessagesStream) {
+        DynamicItem<UrlFormatter> urlFormatter,
+        String pluginName,
+        Stream<CommitValidationMessage> validationMessagesStream) {
       ImmutableList<CommitValidationMessage> validationMessages =
           validationMessagesStream.collect(toImmutableList());
+      Optional<String> helpPage =
+          urlFormatter
+              .get()
+              .getPluginDocUrl(
+                  pluginName, "validation.html", "validation-checks-for-code-owner-config-files");
       return new AutoValue_CodeOwnerConfigValidator_ValidationResult(
           pluginName,
-          validationMessages.isEmpty() ? NO_ISSUES_MSG : INVALID_MSG,
+          validationMessages.isEmpty()
+              ? NO_ISSUES_MSG
+              : (helpPage.isPresent()
+                  ? String.format("%s (see %s for help)", INVALID_MSG, helpPage.get())
+                  : INVALID_MSG),
           validationMessages);
     }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
index 0800de1..51b52da 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.git.ObjectIds;
@@ -68,6 +69,7 @@
 import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
 import com.google.gerrit.plugins.codeowners.validation.SkipCodeOwnerConfigValidationCapability;
 import com.google.gerrit.plugins.codeowners.validation.SkipCodeOwnerConfigValidationPushOption;
+import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.submit.IntegrationConflictException;
 import com.google.inject.Inject;
 import java.nio.file.Path;
@@ -92,6 +94,7 @@
 
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ProjectOperations projectOperations;
+  @Inject private DynamicItem<UrlFormatter> urlFormatter;
 
   private FindOwnersCodeOwnerConfigParser findOwnersCodeOwnerConfigParser;
   private ProtoCodeOwnerConfigParser protoCodeOwnerConfigParser;
@@ -731,9 +734,11 @@
         .isEqualTo(
             String.format(
                 "Failed to submit 1 change due to the following problems:\n"
-                    + "Change %d: [code-owners] invalid code owner config files:\n"
+                    + "Change %d: [code-owners] invalid code owner config files"
+                    + " (see %s for help):\n"
                     + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
                 r.getChange().getId().get(),
+                getHelpPage(),
                 unknownEmail,
                 codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
                 identifiedUserFactory.create(admin.id()).getLoggableName()));
@@ -2521,8 +2526,9 @@
         .hasMessageThat()
         .isEqualTo(
             String.format(
-                "[code-owners] invalid code owner config files:\n"
+                "[code-owners] invalid code owner config files (see %s for help):\n"
                     + "  [code-owners] code owner email '%s' in '%s' cannot be resolved for %s",
+                getHelpPage(),
                 unknownEmail,
                 codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
                 admin.username()));
@@ -2613,10 +2619,11 @@
         .isEqualTo(
             String.format(
                 "Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
-                    + "[code-owners] invalid code owner config files:\n"
+                    + "[code-owners] invalid code owner config files (see %s for help):\n"
                     + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s\n"
                     + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
                 project,
+                getHelpPage(),
                 unknownEmail,
                 codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath(),
                 identifiedUserFactory.create(admin.id()).getLoggableName(),
@@ -2648,7 +2655,9 @@
             () -> gApi.projects().name(project.get()).branch(input.ref).create(input));
     assertThat(exception)
         .hasMessageThat()
-        .contains("[code-owners] invalid code owner config files:");
+        .contains(
+            String.format(
+                "[code-owners] invalid code owner config files (see %s for help):", getHelpPage()));
 
     input.validationOptions =
         ImmutableMap.of(
@@ -2743,10 +2752,11 @@
         "refs/heads/new",
         String.format(
             "Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
-                + "[code-owners] invalid code owner config files:\n"
+                + "[code-owners] invalid code owner config files (see %s for help):\n"
                 + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s\n"
                 + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
             project,
+            getHelpPage(),
             unknownEmail,
             codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath(),
             identifiedUserFactory.create(admin.id()).getLoggableName(),
@@ -2785,9 +2795,10 @@
         "refs/heads/new",
         String.format(
             "Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
-                + "[code-owners] invalid code owner config files:\n"
+                + "[code-owners] invalid code owner config files (see %s for help):\n"
                 + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
             project,
+            getHelpPage(),
             unknownEmail,
             codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
             identifiedUserFactory.create(admin.id()).getLoggableName()));
@@ -2905,9 +2916,10 @@
     assertThat(r.getMessages())
         .contains(
             String.format(
-                "ERROR: [code-owners] invalid code owner config files\n"
+                "ERROR: [code-owners] invalid code owner config files (see %s for help)\n"
                     + "ERROR: [code-owners] code owner email '%s' in '%s' cannot be resolved for"
                     + " %s",
+                getHelpPage(),
                 unknownEmail,
                 codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
                 identifiedUserFactory.create(admin.id()).getLoggableName()));
@@ -3316,4 +3328,10 @@
     pushResult.assertNotMessage("warning");
     pushResult.assertNotMessage("hint");
   }
+
+  private String getHelpPage() {
+    return urlFormatter.get().getWebUrl().get()
+        + "plugins/code-owners/Documentation/validation.html"
+        + "#validation-checks-for-code-owner-config-files";
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java
index b92d0e0..75843f3 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
@@ -35,6 +36,7 @@
 import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersCodeOwnerConfigParser;
 import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
 import com.google.gerrit.plugins.codeowners.backend.proto.ProtoCodeOwnerConfigParser;
+import com.google.gerrit.server.config.UrlFormatter;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
@@ -50,6 +52,7 @@
 
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ProjectOperations projectOperations;
+  @Inject private DynamicItem<UrlFormatter> urlFormatter;
 
   private FindOwnersCodeOwnerConfigParser findOwnersCodeOwnerConfigParser;
   private ProtoCodeOwnerConfigParser protoCodeOwnerConfigParser;
@@ -126,9 +129,11 @@
         .isEqualTo(
             String.format(
                 "Failed to submit 1 change due to the following problems:\n"
-                    + "Change %d: [code-owners] invalid code owner config files:\n"
+                    + "Change %d: [code-owners] invalid code owner config files"
+                    + " (see %s for help):\n"
                     + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
                 r.getChange().getId().get(),
+                getHelpPage(),
                 unknownEmail,
                 codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
                 identifiedUserFactory.create(admin.id()).getLoggableName()));
@@ -181,9 +186,11 @@
         .isEqualTo(
             String.format(
                 "Failed to submit 1 change due to the following problems:\n"
-                    + "Change %d: [code-owners] invalid code owner config files:\n"
+                    + "Change %d: [code-owners] invalid code owner config files"
+                    + " (see %s for help):\n"
                     + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
                 r.getChange().getId().get(),
+                getHelpPage(),
                 admin.email(),
                 codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
                 identifiedUserFactory.create(user2.id()).getLoggableName()));
@@ -309,9 +316,11 @@
         .isEqualTo(
             String.format(
                 "Failed to submit 1 change due to the following problems:\n"
-                    + "Change %d: [code-owners] invalid code owner config files:\n"
+                    + "Change %d: [code-owners] invalid code owner config files"
+                    + " (see %s for help):\n"
                     + "  ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
                 r.getChange().getId().get(),
+                getHelpPage(),
                 unknownEmail,
                 codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
                 identifiedUserFactory.create(admin.id()).getLoggableName()));
@@ -375,4 +384,10 @@
             "unknown code owner backend: %s",
             backendConfig.getDefaultBackend().getClass().getName()));
   }
+
+  private String getHelpPage() {
+    return urlFormatter.get().getWebUrl().get()
+        + "plugins/code-owners/Documentation/validation.html"
+        + "#validation-checks-for-code-owner-config-files";
+  }
 }