Merge "Return code-owners~skip-validation option only if OWNERS files are touched"
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java b/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java
index 2917a90..316da14 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java
@@ -20,13 +20,19 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.plugins.codeowners.backend.ChangedFiles;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy;
 import com.google.gerrit.server.PluginPushOption;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.Path;
 
 /** Push option that allows to skip the code owner config validation. */
 @Singleton
@@ -38,17 +44,20 @@
   private final PermissionBackend permissionBackend;
   private final SkipCodeOwnerConfigValidationCapability skipCodeOwnerConfigValidationCapability;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+  private final ChangedFiles changedFiles;
 
   @Inject
   SkipCodeOwnerConfigValidationPushOption(
       @PluginName String pluginName,
       PermissionBackend permissionBackend,
       SkipCodeOwnerConfigValidationCapability skipCodeOwnerConfigValidationCapability,
-      CodeOwnersPluginConfiguration codeOwnersPluginConfiguration) {
+      CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+      ChangedFiles changedFiles) {
     this.pluginName = pluginName;
     this.permissionBackend = permissionBackend;
     this.skipCodeOwnerConfigValidationCapability = skipCodeOwnerConfigValidationCapability;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+    this.changedFiles = changedFiles;
   }
 
   @Override
@@ -66,7 +75,39 @@
     return !codeOwnersPluginConfiguration
             .getProjectConfig(changeNotes.getProjectName())
             .isDisabled(changeNotes.getChange().getDest().branch())
-        && canSkipCodeOwnerConfigValidation();
+        && canSkipCodeOwnerConfigValidation()
+        && hasModifiedCodeOwnerConfigFiles(changeNotes);
+  }
+
+  private boolean hasModifiedCodeOwnerConfigFiles(ChangeNotes changeNotes) {
+    CodeOwnerBackend codeOwnerBackend =
+        codeOwnersPluginConfiguration
+            .getProjectConfig(changeNotes.getProjectName())
+            .getBackend(changeNotes.getChange().getDest().branch());
+
+    // For merge commits, always do the comparison against the destination branch
+    // (MergeCommitStrategy.ALL_CHANGED_FILES) as this is what CodeOwnerConfigValidator does.
+    try {
+      return changedFiles
+          .getFromDiffCache(
+              changeNotes.getProjectName(),
+              changeNotes.getCurrentPatchSet().commitId(),
+              MergeCommitStrategy.ALL_CHANGED_FILES)
+          .stream()
+          // filter out deletions (files without new path)
+          .filter(changedFile -> changedFile.newPath().isPresent())
+          .anyMatch(
+              changedFile ->
+                  codeOwnerBackend.isCodeOwnerConfigFile(
+                      changeNotes.getProjectName(),
+                      Path.of(changedFile.newPath().get().toString()).getFileName().toString()));
+    } catch (IOException | DiffNotAvailableException e) {
+      throw newInternalServerError(
+          String.format(
+              "Failed to check changed files of change %s in project %s",
+              changeNotes.getChangeId(), changeNotes.getProjectName()),
+          e);
+    }
   }
 
   /**
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/SkipCodeOwnerConfigValidationPushOptionIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/SkipCodeOwnerConfigValidationPushOptionIT.java
index 6c74375..dd2cf9d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/SkipCodeOwnerConfigValidationPushOptionIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/SkipCodeOwnerConfigValidationPushOptionIT.java
@@ -21,30 +21,56 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChangeCreation;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.ValidationOptionInfo;
 import com.google.gerrit.extensions.common.ValidationOptionInfos;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
+import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
+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.plugins.codeowners.validation.SkipCodeOwnerConfigValidationCapability;
 import com.google.gerrit.plugins.codeowners.validation.SkipCodeOwnerConfigValidationPushOption;
 import com.google.inject.Inject;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Before;
 import org.junit.Test;
 
 public class SkipCodeOwnerConfigValidationPushOptionIT extends AbstractCodeOwnersIT {
+  private static final ObjectId TEST_REVISION =
+      ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
   @Inject private ChangeOperations changeOperations;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
+  private FindOwnersCodeOwnerConfigParser findOwnersCodeOwnerConfigParser;
+  private ProtoCodeOwnerConfigParser protoCodeOwnerConfigParser;
+
+  @Before
+  public void setUpCodeOwnersPlugin() throws Exception {
+    backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
+    findOwnersCodeOwnerConfigParser =
+        plugin.getSysInjector().getInstance(FindOwnersCodeOwnerConfigParser.class);
+    protoCodeOwnerConfigParser =
+        plugin.getSysInjector().getInstance(ProtoCodeOwnerConfigParser.class);
+  }
+
   @Test
   public void getCodeOwnersSkipOptionAsAdmin() throws Exception {
     // Use the admin user that has the SkipCodeOwnerConfigValidationCapability global capability
     // implicitly assigned.
     requestScopeOperations.setApiUser(admin.id());
 
-    Change.Id changeId = changeOperations.newChange().project(project).create();
-
+    Change.Id changeId = createChangeWithCodeOwnerConfigFile(project);
     ValidationOptionInfos validationOptionsInfos =
         gApi.changes().id(project.get(), changeId.get()).getValidationOptions();
     assertThat(validationOptionsInfos.validationOptions)
@@ -66,9 +92,7 @@
         .update();
 
     requestScopeOperations.setApiUser(user.id());
-
-    Change.Id changeId = changeOperations.newChange().project(project).create();
-
+    Change.Id changeId = createChangeWithCodeOwnerConfigFile(project);
     ValidationOptionInfos validationOptionsInfos =
         gApi.changes().id(project.get(), changeId.get()).getValidationOptions();
     assertThat(validationOptionsInfos.validationOptions)
@@ -86,7 +110,7 @@
     // capability.
     requestScopeOperations.setApiUser(user.id());
 
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = createChangeWithCodeOwnerConfigFile(project);
     ValidationOptionInfos validationOptionsInfos =
         gApi.changes().id(project.get(), changeId.get()).getValidationOptions();
     assertThat(validationOptionsInfos.validationOptions).isEmpty();
@@ -96,7 +120,7 @@
   @Test
   public void codeOwnersSkipOptionIsOmittedIfCodeOwnersFunctionalityIsDisabledForProject()
       throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = createChangeWithCodeOwnerConfigFile(project);
     ValidationOptionInfos validationOptionsInfos =
         gApi.changes().id(project.get(), changeId.get()).getValidationOptions();
     assertThat(validationOptionsInfos.validationOptions).isEmpty();
@@ -106,9 +130,57 @@
   @Test
   public void codeOwnersSkipOptionIsOmittedIfCodeOwnersFunctionalityIsDisabledForBranch()
       throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).branch("master").create();
+    Change.Id changeId = createChangeWithCodeOwnerConfigFile(project, "master");
     ValidationOptionInfos validationOptionsInfos =
         gApi.changes().id(project.get(), changeId.get()).getValidationOptions();
     assertThat(validationOptionsInfos.validationOptions).isEmpty();
   }
+
+  @Test
+  public void codeOwnersSkipOptionIsOmittedIfChangeDoesNotTouchCodeOwnerConfigs() throws Exception {
+    requestScopeOperations.setApiUser(admin.id());
+    Change.Id changeId = changeOperations.newChange().project(project).create();
+    ValidationOptionInfos validationOptionsInfos =
+        gApi.changes().id(project.get(), changeId.get()).getValidationOptions();
+    assertThat(validationOptionsInfos.validationOptions).isEmpty();
+  }
+
+  private Change.Id createChangeWithCodeOwnerConfigFile(Project.NameKey project) throws Exception {
+    return createChangeWithCodeOwnerConfigFile(project, /* branch= */ null);
+  }
+
+  private Change.Id createChangeWithCodeOwnerConfigFile(
+      Project.NameKey project, @Nullable String branch) throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/");
+    String codeOwnerConfigFile =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath();
+
+    TestChangeCreation.Builder testChangeCreationBuilder =
+        changeOperations.newChange().project(project);
+    if (branch != null) {
+      testChangeCreationBuilder.branch(branch);
+    }
+
+    return testChangeCreationBuilder
+        .file(codeOwnerConfigFile)
+        .content(
+            format(
+                CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
+                    .addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
+                    .build()))
+        .create();
+  }
+
+  private String format(CodeOwnerConfig codeOwnerConfig) throws Exception {
+    if (backendConfig.getDefaultBackend() instanceof FindOwnersBackend) {
+      return findOwnersCodeOwnerConfigParser.formatAsString(codeOwnerConfig);
+    } else if (backendConfig.getDefaultBackend() instanceof ProtoBackend) {
+      return protoCodeOwnerConfigParser.formatAsString(codeOwnerConfig);
+    }
+
+    throw new IllegalStateException(
+        String.format(
+            "unknown code owner backend: %s",
+            backendConfig.getDefaultBackend().getClass().getName()));
+  }
 }