Add config option to exempt pure reverts from code owner approvals

Often reverts have to be done quickly and there is sometimes no time to
gather code owner approvals to submit it. This is why some projects may
prefer to exempt pure reverts from requiring code owner approvals.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I112b65993c16f015c8bf97ffebd59278714626ae
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
index 4dc69b0..2c0128a 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
@@ -86,6 +86,9 @@
   /** Whether code owner config files are read-only. */
   public Boolean readOnly;
 
+  /** Whether pure revert changes are exempted from needing code owner approvals for submit. */
+  public Boolean exemptPureReverts;
+
   /** Policy for validating code owner config files when a commit is received. */
   public CodeOwnerConfigValidationPolicy enableValidationOnCommitReceived;
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 4443910..f980b92 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
@@ -41,6 +42,7 @@
 import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -87,6 +89,7 @@
   private final GitRepositoryManager repoManager;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final ChangedFiles changedFiles;
+  private final PureRevertCache pureRevertCache;
   private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
   private final Provider<CodeOwnerResolver> codeOwnerResolver;
   private final ApprovalsUtil approvalsUtil;
@@ -98,6 +101,7 @@
       GitRepositoryManager repoManager,
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
       ChangedFiles changedFiles,
+      PureRevertCache pureRevertCache,
       CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
       Provider<CodeOwnerResolver> codeOwnerResolver,
       ApprovalsUtil approvalsUtil,
@@ -106,6 +110,7 @@
     this.repoManager = repoManager;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.changedFiles = changedFiles;
+    this.pureRevertCache = pureRevertCache;
     this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
     this.codeOwnerResolver = codeOwnerResolver;
     this.approvalsUtil = approvalsUtil;
@@ -205,6 +210,11 @@
           "compute file statuses (project = %s, change = %d)",
           changeNotes.getProjectName(), changeNotes.getChangeId().get());
 
+      if (codeOwnersPluginConfiguration.arePureRevertsExempted(changeNotes.getProjectName())
+          && isPureRevert(changeNotes)) {
+        return getAllPathsAsApproved(changeNotes, changeNotes.getCurrentPatchSet());
+      }
+
       boolean enableImplicitApprovalFromUploader =
           codeOwnersPluginConfiguration.areImplicitApprovalsEnabled(changeNotes.getProjectName());
       Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader();
@@ -307,23 +317,7 @@
       logger.atFine().log(
           "fallbackCodeOwner = %s, isProjectOwner = %s", fallbackCodeOwners, isProjectOwner);
       if (fallbackCodeOwners.equals(FallbackCodeOwners.PROJECT_OWNERS) && isProjectOwner) {
-        // Return all paths as approved.
-        return changedFiles.compute(changeNotes.getProjectName(), patchSet.commitId()).stream()
-            .map(
-                changedFile ->
-                    FileCodeOwnerStatus.create(
-                        changedFile,
-                        changedFile
-                            .newPath()
-                            .map(
-                                newPath ->
-                                    PathCodeOwnerStatus.create(newPath, CodeOwnerStatus.APPROVED)),
-                        changedFile
-                            .oldPath()
-                            .map(
-                                oldPath ->
-                                    PathCodeOwnerStatus.create(
-                                        oldPath, CodeOwnerStatus.APPROVED))));
+        return getAllPathsAsApproved(changeNotes, patchSet);
       }
 
       return changedFiles.compute(changeNotes.getProjectName(), patchSet.commitId()).stream()
@@ -347,6 +341,39 @@
     }
   }
 
+  private boolean isPureRevert(ChangeNotes changeNotes) throws IOException {
+    try {
+      return changeNotes.getChange().getRevertOf() != null
+          && pureRevertCache.isPureRevert(changeNotes);
+    } catch (BadRequestException e) {
+      throw new StorageException(
+          String.format(
+              "failed to check if change %s in project %s is a pure revert",
+              changeNotes.getChangeId(), changeNotes.getProjectName()),
+          e);
+    }
+  }
+
+  private Stream<FileCodeOwnerStatus> getAllPathsAsApproved(
+      ChangeNotes changeNotes, PatchSet patchSet)
+      throws IOException, PatchListNotAvailableException {
+    return changedFiles.compute(changeNotes.getProjectName(), patchSet.commitId()).stream()
+        .map(
+            changedFile ->
+                FileCodeOwnerStatus.create(
+                    changedFile,
+                    changedFile
+                        .newPath()
+                        .map(
+                            newPath ->
+                                PathCodeOwnerStatus.create(newPath, CodeOwnerStatus.APPROVED)),
+                    changedFile
+                        .oldPath()
+                        .map(
+                            oldPath ->
+                                PathCodeOwnerStatus.create(oldPath, CodeOwnerStatus.APPROVED))));
+  }
+
   private FileCodeOwnerStatus getFileStatus(
       BranchNameKey branch,
       ObjectId revision,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
index cc62a94..8b9407f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
@@ -118,6 +118,18 @@
   }
 
   /**
+   * Checks whether pure revert changes are exempted from needing code owner approvals for submit.
+   *
+   * @param project the project for which it should be checked whether pure revert changes are
+   *     exempted from needing code owner approvals for submit
+   * @return whether pure revert changes are exempted from needing code owner approvals for submit
+   */
+  public boolean arePureRevertsExempted(Project.NameKey project) {
+    requireNonNull(project, "project");
+    return generalConfig.getExemptPureReverts(getPluginConfig(project));
+  }
+
+  /**
    * Checks whether newly added non-resolvable code owners should be rejected on commit received and
    * submit.
    *
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 187a7ab..2c07267 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -60,6 +60,7 @@
   @VisibleForTesting public static final String KEY_ALLOWED_EMAIL_DOMAIN = "allowedEmailDomain";
   @VisibleForTesting public static final String KEY_FILE_EXTENSION = "fileExtension";
   @VisibleForTesting public static final String KEY_READ_ONLY = "readOnly";
+  @VisibleForTesting public static final String KEY_EXEMPT_PURE_REVERTS = "exemptPureReverts";
   @VisibleForTesting public static final String KEY_FALLBACK_CODE_OWNERS = "fallbackCodeOwners";
 
   @VisibleForTesting
@@ -217,6 +218,20 @@
   }
 
   /**
+   * Gets the exempt-pure-reverts configuration from the given plugin config with fallback to {@code
+   * gerrit.config}.
+   *
+   * <p>The exempt-pure-reverts configuration controls whether pure revert changes are exempted from
+   * needing code owner approvals for submit.
+   *
+   * @param pluginConfig the plugin config from which the read-only configuration should be read.
+   * @return whether pure reverts are exempted from needing code owner approvals for submit
+   */
+  boolean getExemptPureReverts(Config pluginConfig) {
+    return getBooleanConfig(pluginConfig, KEY_EXEMPT_PURE_REVERTS, /* defaultValue= */ false);
+  }
+
+  /**
    * Gets the reject-non-resolvable-code-owners configuration from the given plugin config with
    * fallback to {@code gerrit.config}.
    *
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
index 597b694..308c3ac 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_IMPLICIT_APPROVALS;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_SUBMIT;
+import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_EXEMPT_PURE_REVERTS;
 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;
@@ -197,6 +198,14 @@
             SECTION_CODE_OWNERS, /* subsection= */ null, KEY_READ_ONLY, input.readOnly);
       }
 
+      if (input.exemptPureReverts != null) {
+        codeOwnersConfig.setBoolean(
+            SECTION_CODE_OWNERS,
+            /* subsection= */ null,
+            KEY_EXEMPT_PURE_REVERTS,
+            input.exemptPureReverts);
+      }
+
       if (input.enableValidationOnCommitReceived != null) {
         codeOwnersConfig.setEnum(
             SECTION_CODE_OWNERS,
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 6ce59bc..c09a8ae 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
@@ -403,6 +403,20 @@
   }
 
   @Test
+  public void setExemptPureReverts() throws Exception {
+    assertThat(codeOwnersPluginConfiguration.arePureRevertsExempted(project)).isFalse();
+
+    CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
+    input.exemptPureReverts = true;
+    projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(codeOwnersPluginConfiguration.arePureRevertsExempted(project)).isTrue();
+
+    input.exemptPureReverts = false;
+    projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(codeOwnersPluginConfiguration.arePureRevertsExempted(project)).isFalse();
+  }
+
+  @Test
   public void setEnableValidationOnCommitReceived() throws Exception {
     assertThat(
             codeOwnersPluginConfiguration.getCodeOwnerConfigValidationPolicyForCommitReceived(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
index ce8b10a..27167c6 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -1911,6 +1911,96 @@
         .isEqualTo(CodeOwnerStatus.APPROVED);
   }
 
+  @Test
+  public void pureRevertsAreNotExemptedByDefault() throws Exception {
+    setAsRootCodeOwners(admin);
+
+    Path path = Paths.get("/foo/bar.baz");
+    String changeId =
+        createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+    approve(changeId);
+    gApi.changes().id(changeId).current().submit();
+
+    // Revert the change
+    String changeIdOfRevert = gApi.changes().id(changeId).revert().get().changeId;
+
+    // Check that the file is not approved.
+    Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+        codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeIdOfRevert));
+    FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+        assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasOldPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasOldPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.exemptPureReverts", value = "true")
+  public void pureRevertsAreExemptedIfConfigured() throws Exception {
+    setAsRootCodeOwners(admin);
+
+    Path path = Paths.get("/foo/bar.baz");
+    String changeId =
+        createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+    approve(changeId);
+    gApi.changes().id(changeId).current().submit();
+
+    // Revert the change
+    String changeIdOfRevert = gApi.changes().id(changeId).revert().get().changeId;
+
+    // Check that the file is approved since it's a pure revert.
+    Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+        codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeIdOfRevert));
+    FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+        assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasOldPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasOldPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.APPROVED);
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.exemptPureReverts", value = "true")
+  public void nonPureRevertsAreNotExempted() throws Exception {
+    setAsRootCodeOwners(admin);
+
+    Path path = Paths.get("/foo/bar.baz");
+    String changeId =
+        createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId();
+    approve(changeId);
+    gApi.changes().id(changeId).current().submit();
+
+    // Revert the change
+    String changeIdOfRevert = gApi.changes().id(changeId).revert().get().changeId;
+
+    // Amend change to make it a non-pure revert change.
+    amendChange(
+        changeIdOfRevert,
+        "refs/for/master",
+        admin,
+        testRepo,
+        "Revert change",
+        JgitPath.of(path).get(),
+        "other content");
+
+    // Check that the file is not approved.
+    Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses =
+        codeOwnerApprovalCheck.getFileStatuses(getChangeNotes(changeIdOfRevert));
+    FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject =
+        assertThatStream(fileCodeOwnerStatuses).onlyElement();
+    fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path);
+    fileCodeOwnerStatusSubject
+        .hasNewPathStatus()
+        .value()
+        .hasStatusThat()
+        .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS);
+  }
+
   private ChangeNotes getChangeNotes(String changeId) throws Exception {
     return changeNotesFactory.create(project, Change.id(gApi.changes().id(changeId).get()._number));
   }
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 a80fe35..4e43414 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_IMPLICIT_APPROVALS;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_SUBMIT;
+import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_EXEMPT_PURE_REVERTS;
 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;
@@ -135,6 +136,38 @@
   }
 
   @Test
+  public void cannotGetExemptPureRevertsForNullPluginConfig() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> generalConfig.getExemptPureReverts(/* pluginConfig= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
+  }
+
+  @Test
+  public void noExemptPureRevertsConfiguration() throws Exception {
+    assertThat(generalConfig.getExemptPureReverts(new Config())).isFalse();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.exemptPureReverts", value = "true")
+  public void
+      exemptPureRevertsConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
+          throws Exception {
+    assertThat(generalConfig.getExemptPureReverts(new Config())).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.exemptPureReverts", value = "true")
+  public void
+      exemptPureRevertsConfigurationInPluginConfigOverridesExemptPureRevertsConfigurationInGerritConfig()
+          throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, null, KEY_EXEMPT_PURE_REVERTS, "false");
+    assertThat(generalConfig.getExemptPureReverts(cfg)).isFalse();
+  }
+
+  @Test
   public void cannotGetRejectNonResolvableCodeOwnersForNullPluginConfig() throws Exception {
     NullPointerException npe =
         assertThrows(
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index b40e939..2f7c30e 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -143,6 +143,18 @@
         `@PLUGIN@.config`.\
         By default `false`.
 
+<a id="pluginCodeOwnersExemptPureReverts">plugin.@PLUGIN@.exemptPureReverts</a>
+:       Whether pure revert changes are exempted from needing code owner
+        approvals for submit.\
+        Only works for pure reverts which have been created through the Gerrit
+        [REST API](../../../Documentation/rest-api-change.html#revert-change)
+        (but not for pure reverts which were done locally and then pushed to
+        Gerrit).\
+        Can be overridden per project by setting
+        [codeOwners.exemptPureReverts](#codeOwnersExemptPureReverts) in
+        `@PLUGIN@.config`.\
+        By default `false`.
+
 <a id="pluginCodeOwnersEnableValidationOnCommitReceived">plugin.@PLUGIN@.enableValidationOnCommitReceived</a>
 :       Policy for validating code owner config files when a commit is received.
         Allowed values are `true` (the code owner config file validation is
@@ -514,6 +526,21 @@
         [plugin.@PLUGIN@.readOnly](#pluginCodeOwnersReadOnly) in
         `gerrit.config` is used.
 
+<a id="codeOwnersExemptPureReverts">codeOwners.exemptPureReverts</a>
+:       Whether pure revert changes are exempted from needing code owner
+        approvals for submit.\
+        Only works for pure reverts which have been created through the Gerrit
+        [REST API](../../../Documentation/rest-api-change.html#revert-change)
+        (but not for pure reverts which were done locally and then pushed to
+        Gerrit).\
+        Overrides the global setting
+        [plugin.@PLUGIN@.exemptPureReverts](#pluginCodeOwnersExemptPureReverts)
+        in `gerrit.config` and the `codeOwners.exemptPureReverts` setting from
+        parent projects.\
+        If not set, the global setting
+        [plugin.@PLUGIN@.exemptPureReverts](#pluginCodeOwnersExemptPureReverts)
+        in `gerrit.config` is used.
+
 <a id="codeOwnersEnableValidationOnCommitReceived">codeOwners.enableValidationOnCommitReceived</a>
 :       Policy for validating code owner config files when a commit is received.
         Allowed values are `true` (the code owner config file validation is
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index a4dc690..68d8f50 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -884,6 +884,7 @@
 | `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.
 | `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).
 | `enable_validation_on_submit` | optional | Policy for validating code owner config files when a change is submitted. Allowed values are `true` (the code owner config file validation is enabled and the submission 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).
 | `reject_non_resolvable_code_owners` | optional | Whether modifications of code owner config files that newly add non-resolvable code owners should be rejected on commit received and submit.