Allow to configure an info URL that is shown if an OWNERS file is invalid

If an operation fails because an OWNERS file is invalid / non-parsable
we return '409 Conflict' with an error message telling which OWNERS file
is invalid and which issues it has. Since OWNERS files are maintained by
the project team, it's the responsibility of the project team, project
owners or host admins to fix the OWNERS file, but it depends on the
project who exactly is responsible for this. The new info URL that can
be configured will be included into the error message so that users can
find project-specific information about how they can report / fix
invalid OWNERS files.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I4f14842a283e478cf503b8f18d18a3643c994373
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
index 0cd47fa..8bc182e 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
@@ -86,6 +86,12 @@
    */
   public String overrideInfoUrl;
 
+  /**
+   * URL for a page that provides project/host-specific information about how to deal with invalid
+   * code owner config files.
+   */
+  public String invalidCodeOwnerConfigInfoUrl;
+
   /** Whether code owner config files are read-only. */
   public Boolean readOnly;
 
diff --git a/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java b/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java
index 2f4a133..d0c123f 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/GeneralInfo.java
@@ -47,6 +47,12 @@
    */
   public String overrideInfoUrl;
 
+  /**
+   * Optional URL for a page that provides project/host-specific information about how to deal with
+   * invalid code owner config files.
+   */
+  public String invalidCodeOwnerConfigInfoUrl;
+
   /** Policy that controls who should own paths that have no code owners defined. */
   public FallbackCodeOwners fallbackCodeOwners;
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
index 1d9542a..4f0d371 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
@@ -224,7 +224,8 @@
                   codeOwnerConfigParser.parse(
                       revision, codeOwnerConfigKey, codeOwnerConfigFileContent.get()));
         } catch (CodeOwnerConfigParseException e) {
-          throw new InvalidCodeOwnerConfigException(e.getFullMessage(defaultFileName), e);
+          throw new InvalidCodeOwnerConfigException(
+              e.getFullMessage(defaultFileName), projectName, e);
         }
       }
     }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
index af4083d..142b273 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
@@ -17,8 +17,10 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.backend.config.InvalidPluginConfigurationException;
 import com.google.gerrit.server.ExceptionHook;
+import com.google.inject.Inject;
 import java.nio.file.InvalidPathException;
 import java.util.Optional;
 
@@ -36,6 +38,13 @@
  * </ul>
  */
 public class CodeOwnersExceptionHook implements ExceptionHook {
+  private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+
+  @Inject
+  CodeOwnersExceptionHook(CodeOwnersPluginConfiguration codeOwnersPluginConfiguration) {
+    this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+  }
+
   @Override
   public boolean skipRetryWithTrace(String actionType, String actionName, Throwable throwable) {
     return isInvalidPluginConfigurationException(throwable)
@@ -54,7 +63,15 @@
     Optional<InvalidCodeOwnerConfigException> invalidCodeOwnerConfigException =
         CodeOwners.getInvalidCodeOwnerConfigCause(throwable);
     if (invalidCodeOwnerConfigException.isPresent()) {
-      return ImmutableList.of(invalidCodeOwnerConfigException.get().getMessage());
+      ImmutableList.Builder<String> messages = ImmutableList.builder();
+      messages.add(invalidCodeOwnerConfigException.get().getMessage());
+      codeOwnersPluginConfiguration
+          .getProjectConfig(invalidCodeOwnerConfigException.get().getProjectName())
+          .getInvalidCodeOwnerConfigInfoUrl()
+          .ifPresent(
+              invalidCodeOwnerConfigInfoUrl ->
+                  messages.add(String.format("For help check %s", invalidCodeOwnerConfigInfoUrl)));
+      return messages.build();
     }
 
     Optional<InvalidPathException> invalidPathException = getInvalidPathException(throwable);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigException.java b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigException.java
index cd1395e..d2df76d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigException.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/InvalidCodeOwnerConfigException.java
@@ -14,17 +14,31 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.entities.Project;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Exception that is thrown if there is an invalid code owner config file. */
 public class InvalidCodeOwnerConfigException extends ConfigInvalidException {
   private static final long serialVersionUID = 1L;
 
-  public InvalidCodeOwnerConfigException(String message) {
+  private final Project.NameKey projectName;
+
+  public InvalidCodeOwnerConfigException(String message, Project.NameKey projectName) {
     super(message);
+
+    this.projectName = requireNonNull(projectName, "projectName");
   }
 
-  public InvalidCodeOwnerConfigException(String message, Throwable cause) {
+  public InvalidCodeOwnerConfigException(
+      String message, Project.NameKey projectName, Throwable cause) {
     super(message, cause);
+
+    this.projectName = requireNonNull(projectName, "projectName");
+  }
+
+  public Project.NameKey getProjectName() {
+    return projectName;
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
index 52eeba4..04046ac 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfigSnapshot.java
@@ -242,6 +242,11 @@
     return generalConfig.getOverrideInfoUrl(pluginConfig);
   }
 
+  /** Gets the invalid code owner config info URL that is configured. */
+  public Optional<String> getInvalidCodeOwnerConfigInfoUrl() {
+    return generalConfig.getInvalidCodeOwnerConfigInfoUrl(pluginConfig);
+  }
+
   /**
    * Whether the code owners functionality is disabled for the given branch.
    *
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
index 0639003..72e2022 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -77,6 +77,8 @@
   public static final String KEY_EXEMPTED_USER = "exemptedUser";
   public static final String KEY_ENABLE_IMPLICIT_APPROVALS = "enableImplicitApprovals";
   public static final String KEY_OVERRIDE_INFO_URL = "overrideInfoUrl";
+  public static final String KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL =
+      "invalidCodeOwnerConfigInfoUrl";
   public static final String KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS =
       "rejectNonResolvableCodeOwners";
   public static final String KEY_REJECT_NON_RESOLVABLE_IMPORTS = "rejectNonResolvableImports";
@@ -824,6 +826,21 @@
     return getStringValue(pluginConfig, KEY_OVERRIDE_INFO_URL);
   }
 
+  /**
+   * Gets an URL that leads to an information page about invalid code owner config files.
+   *
+   * <p>The URL is retrieved from the given plugin config, with fallback to the {@code
+   * gerrit.config}.
+   *
+   * @param pluginConfig the plugin config from which the invalid code owner config info URL should
+   *     be read.
+   * @return URL that leads to an information page about invalid code owner config files, {@link
+   *     Optional#empty()} if no such URL is configured
+   */
+  Optional<String> getInvalidCodeOwnerConfigInfoUrl(Config pluginConfig) {
+    return getStringValue(pluginConfig, KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL);
+  }
+
   private Optional<String> getStringValue(Config pluginConfig, String key) {
     requireNonNull(pluginConfig, "pluginConfig");
 
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
index 8140fac..49053ec 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJson.java
@@ -110,6 +110,8 @@
     generalInfo.mergeCommitStrategy = codeOwnersConfig.getMergeCommitStrategy();
     generalInfo.implicitApprovals = codeOwnersConfig.areImplicitApprovalsEnabled() ? true : null;
     generalInfo.overrideInfoUrl = codeOwnersConfig.getOverrideInfoUrl().orElse(null);
+    generalInfo.invalidCodeOwnerConfigInfoUrl =
+        codeOwnersConfig.getInvalidCodeOwnerConfigInfoUrl().orElse(null);
     generalInfo.fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners();
     return generalInfo;
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
index ed130c8..96b467e 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
@@ -23,6 +23,7 @@
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FALLBACK_CODE_OWNERS;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FILE_EXTENSION;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_GLOBAL_CODE_OWNER;
+import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_MERGE_COMMIT_STRATEGY;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_OVERRIDE_INFO_URL;
@@ -199,6 +200,14 @@
             input.overrideInfoUrl);
       }
 
+      if (input.invalidCodeOwnerConfigInfoUrl != null) {
+        codeOwnersConfig.setString(
+            SECTION_CODE_OWNERS,
+            /* subsection= */ null,
+            KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL,
+            input.invalidCodeOwnerConfigInfoUrl);
+      }
+
       if (input.readOnly != null) {
         codeOwnersConfig.setBoolean(
             SECTION_CODE_OWNERS, /* subsection= */ null, KEY_READ_ONLY, input.readOnly);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
index f92d566..5834499 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
@@ -426,6 +426,36 @@
   }
 
   @Test
+  public void setInvalidCodeOwnerConfigInfoUrl() throws Exception {
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getInvalidCodeOwnerConfigInfoUrl())
+        .isEmpty();
+
+    CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
+    input.invalidCodeOwnerConfigInfoUrl = "http://foo.bar";
+    CodeOwnerProjectConfigInfo updatedConfig =
+        projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(updatedConfig.general.invalidCodeOwnerConfigInfoUrl).isEqualTo("http://foo.bar");
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getInvalidCodeOwnerConfigInfoUrl())
+        .value()
+        .isEqualTo("http://foo.bar");
+
+    input.invalidCodeOwnerConfigInfoUrl = "";
+    updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(updatedConfig.general.invalidCodeOwnerConfigInfoUrl).isNull();
+    assertThat(
+            codeOwnersPluginConfiguration
+                .getProjectConfig(project)
+                .getInvalidCodeOwnerConfigInfoUrl())
+        .isEmpty();
+  }
+
+  @Test
   public void setReadOnly() throws Exception {
     assertThat(
             codeOwnersPluginConfiguration.getProjectConfig(project).areCodeOwnerConfigsReadOnly())
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
index b28d523..5d81963 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.plugins.codeowners.backend.config.InvalidPluginConfigurationException;
 import com.google.gerrit.server.ExceptionHook.Status;
@@ -92,6 +93,21 @@
   }
 
   @Test
+  @GerritConfig(name = "plugin.code-owners.invalidCodeOwnerConfigInfoUrl", value = "http://foo.bar")
+  public void getUserMessages_withInvalidCodeOwnerConfigInfoUrl() throws Exception {
+    InvalidCodeOwnerConfigException invalidCodeOwnerConfigException =
+        newInvalidCodeOwnerConfigException();
+    assertThat(getUserMessages(invalidCodeOwnerConfigException))
+        .containsExactly(
+            invalidCodeOwnerConfigException.getMessage(), "For help check http://foo.bar")
+        .inOrder();
+    assertThat(getUserMessages(newExceptionWithCause(invalidCodeOwnerConfigException)))
+        .containsExactly(
+            invalidCodeOwnerConfigException.getMessage(), "For help check http://foo.bar")
+        .inOrder();
+  }
+
+  @Test
   public void getStatus() throws Exception {
     Status conflictStatus = Status.create(409, "Conflict");
     assertThat(getStatus(newInvalidPluginConfigurationException()))
@@ -140,7 +156,7 @@
   }
 
   private InvalidCodeOwnerConfigException newInvalidCodeOwnerConfigException() {
-    return new InvalidCodeOwnerConfigException("message");
+    return new InvalidCodeOwnerConfigException("message", project);
   }
 
   private InvalidPathException newInvalidPathException() {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
index dc6692f..9220d02 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -25,6 +25,7 @@
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FALLBACK_CODE_OWNERS;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FILE_EXTENSION;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_GLOBAL_CODE_OWNER;
+import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_MAX_PATHS_IN_CHANGE_MESSAGES;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_MERGE_COMMIT_STRATEGY;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_OVERRIDE_INFO_URL;
@@ -1500,6 +1501,48 @@
   }
 
   @Test
+  public void cannotGetInvalidCodeOwnerConfigInfoUrlForNullPluginConfig() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> generalConfig.getInvalidCodeOwnerConfigInfoUrl(/* pluginConfig= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+  }
+
+  @Test
+  public void noInvalidCodeOwnerConfigInfoUrlConfigured() throws Exception {
+    assertThat(generalConfig.getInvalidCodeOwnerConfigInfoUrl(new Config())).isEmpty();
+  }
+
+  @Test
+  @GerritConfig(
+      name = "plugin.code-owners.invalidCodeOwnerConfigInfoUrl",
+      value = "http://foo.example.com")
+  public void invalidCodeOwnerConfigInfoIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
+      throws Exception {
+    assertThat(generalConfig.getInvalidCodeOwnerConfigInfoUrl(new Config()))
+        .value()
+        .isEqualTo("http://foo.example.com");
+  }
+
+  @Test
+  @GerritConfig(
+      name = "plugin.code-owners.invalidCodeOwnerConfigInfoUrl",
+      value = "http://foo.example.com")
+  public void invalidCodeOwnerConfigInfoUrlInPluginConfigOverridesOverrideInfoUrlInGerritConfig()
+      throws Exception {
+    Config cfg = new Config();
+    cfg.setString(
+        SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL,
+        "http://bar.example.com");
+    assertThat(generalConfig.getInvalidCodeOwnerConfigInfoUrl(cfg))
+        .value()
+        .isEqualTo("http://bar.example.com");
+  }
+
+  @Test
   public void cannotGetFallbackCodeOwnersForNullProject() throws Exception {
     NullPointerException npe =
         assertThrows(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
index ffecde7..bec1973 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
@@ -157,6 +157,8 @@
         .thenReturn(FallbackCodeOwners.ALL_USERS);
     when(codeOwnersPluginConfigSnapshot.getOverrideInfoUrl())
         .thenReturn(Optional.of("http://foo.example.com"));
+    when(codeOwnersPluginConfigSnapshot.getInvalidCodeOwnerConfigInfoUrl())
+        .thenReturn(Optional.of("http://bar.example.com"));
     when(codeOwnersPluginConfigSnapshot.isDisabled()).thenReturn(false);
     when(codeOwnersPluginConfigSnapshot.isDisabled(any(String.class))).thenReturn(false);
     when(codeOwnersPluginConfigSnapshot.getBackend()).thenReturn(findOwnersBackend);
@@ -184,6 +186,8 @@
     assertThat(codeOwnerProjectConfigInfo.general.fileExtension).isEqualTo("foo");
     assertThat(codeOwnerProjectConfigInfo.general.overrideInfoUrl)
         .isEqualTo("http://foo.example.com");
+    assertThat(codeOwnerProjectConfigInfo.general.invalidCodeOwnerConfigInfoUrl)
+        .isEqualTo("http://bar.example.com");
     assertThat(codeOwnerProjectConfigInfo.general.mergeCommitStrategy)
         .isEqualTo(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
     assertThat(codeOwnerProjectConfigInfo.general.fallbackCodeOwners)
@@ -278,6 +282,8 @@
         .thenReturn(FallbackCodeOwners.ALL_USERS);
     when(codeOwnersPluginConfigSnapshot.getOverrideInfoUrl())
         .thenReturn(Optional.of("http://foo.example.com"));
+    when(codeOwnersPluginConfigSnapshot.getInvalidCodeOwnerConfigInfoUrl())
+        .thenReturn(Optional.of("http://bar.example.com"));
     when(codeOwnersPluginConfigSnapshot.isDisabled(any(String.class))).thenReturn(false);
     when(codeOwnersPluginConfigSnapshot.getBackend("refs/heads/master"))
         .thenReturn(findOwnersBackend);
@@ -298,6 +304,8 @@
     assertThat(codeOwnerBranchConfigInfo.general.fileExtension).isEqualTo("foo");
     assertThat(codeOwnerBranchConfigInfo.general.overrideInfoUrl)
         .isEqualTo("http://foo.example.com");
+    assertThat(codeOwnerBranchConfigInfo.general.invalidCodeOwnerConfigInfoUrl)
+        .isEqualTo("http://bar.example.com");
     assertThat(codeOwnerBranchConfigInfo.general.mergeCommitStrategy)
         .isEqualTo(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION);
     assertThat(codeOwnerBranchConfigInfo.general.fallbackCodeOwners)
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index bc04b21..652fe2f 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -170,6 +170,16 @@
         `@PLUGIN@.config`.\
         By default unset (no override info URL).
 
+<a id="pluginCodeOwnersInvalidCodeOwnerConfigInfoUrl">plugin.@PLUGIN@.invalidCodeOwnerConfigInfoUrl</a>
+:       A URL for a page that provides host-specific information about how to
+        deal with invalid code owner config files.\
+        This URL is included into error messages that indicate invalid code
+        owner config files.\
+        Can be overridden per project by setting
+        [codeOwners.invalidCodeOwnerConfigInfoUrl](#codeOwnersInvalidCodeOwnerConfigInfoUrl)
+        in `@PLUGIN@.config`.\
+        By default unset (no invalid code owner config info URL).
+
 <a id="pluginCodeOwnersEnableImplicitApprovals">plugin.@PLUGIN@.enableImplicitApprovals</a>
 :       Whether an implicit code owner approval from the last uploader is
         assumed.\
@@ -632,6 +642,19 @@
         [plugin.@PLUGIN@.overrideInfoUrl](#pluginCodeOwnersOverrideInfoUrl) in
         `gerrit.config` is used.
 
+<a id="codeOwnersInvalidCodeOwnerConfigInfoUrl">codeOwners.invalidCodeOwnerConfigInfoUrl</a>
+:       A URL for a page that provides project-specific information about how
+        to deal with invalid code owner config files.\
+        This URL is included into error messages that indicate invalid code
+        owner config files.\
+        Overrides the global setting
+        [plugin.@PLUGIN@.invalidCodeOwnerConfigInfoUrl](#pluginCodeOwnersInvalidCodeOwnerConfigInfoUrl)
+        in `gerrit.config` and the `codeOwners.invalidCodeOwnerConfigInfoUrl`
+        setting from parent projects.\
+        If not set, the global setting
+        [plugin.@PLUGIN@.invalidCodeOwnerConfigInfoUrl](#pluginCodeOwnersInvalidCodeOwnerConfigInfoUrl)
+        in `gerrit.config` is used.
+
 <a id="codeOwnersEnableImplicitApprovals">codeOwners.enableImplicitApprovals</a>
 :       Whether an implicit code owner approval from the last uploader is
         assumed.\
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 232b619..40fb545 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -880,6 +880,7 @@
 | `merge_commit_strategy` | optional | Strategy that defines for merge commits which files require code owner approvals. Can be `ALL_CHANGED_FILES` or `FILES_WITH_CONFLICT_RESOLUTION` (see [mergeCommitStrategy](config.html#pluginCodeOwnersMergeCommitStrategy) for an explanation of these values).
 | `implicit_approvals` | optional | Whether an implicit code owner approval from the last uploader is assumed.
 | `override_info_url` | optional | URL for a page that provides project/host-specific information about how to request a code owner override.
+| `invalid_code_owner_config_info_url` | optional | URL for a page that provides project/host-specific information about how to deal with invalid code owner config files.
 | `read_only` | optional | Whether code owner config files are read-only.
 | `exempt_pure_reverts` | optional | Whether pure revert changes are exempted from needing code owner approvals for submit.
 | `enable_validation_on_commit_received` | optional | Policy for validating code owner config files when a commit is received. Allowed values are `true` (the code owner config file validation is enabled and the upload of invalid code owner config files is rejected), `false` (the code owner config file validation is disabled, invalid code owner config files are not rejected) and `dry_run` (code owner config files are validated, but invalid code owner config files are not rejected).
@@ -952,6 +953,7 @@
 | `merge_commit_strategy` || Strategy that defines for merge commits which files require code owner approvals. Can be `ALL_CHANGED_FILES` or `FILES_WITH_CONFLICT_RESOLUTION` (see [mergeCommitStrategy](config.html#pluginCodeOwnersMergeCommitStrategy) for an explanation of these values).
 | `implicit_approvals` | optional |  Whether an implicit code owner approval from the last uploader is assumed (see [enableImplicitApprovals](config.html#pluginCodeOwnersEnableImplicitApprovals) for details). When unset, `false`.
 | `override_info_url` | optional | Optional URL for a page that provides project/host-specific information about how to request a code owner override.
+| `invalid_code_owner_config_info_url` | optional | Optional URL for a page that provides project/host-specific information about how to deal with invalid code owner config files.
 |`fallback_code_owners` || Policy that controls who should own paths that have no code owners defined. Possible values are: `NONE`: Paths for which no code owners are defined are owned by no one. `PROJECT_OWNERS`: Paths for which no code owners are defined are owned by the project owners. `ALL_USERS`: Paths for which no code owners are defined are owned by all users.
 
 ### <a id="owned-paths-info"> OwnedPathsInfo