Merge "validaton.md: Link to the config options that disable some of the checks"
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index eee9cfb..6e59621 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -833,32 +833,38 @@
       RevWalk revWalk,
       Path codeOwnerConfigFilePath,
       CodeOwnerConfig codeOwnerConfig) {
-    return Streams.concat(
-            codeOwnerConfig.imports().stream()
-                .map(
-                    codeOwnerConfigReference ->
-                        validateCodeOwnerConfigReference(
-                            branchNameKey,
-                            revWalk,
-                            codeOwnerConfigFilePath,
-                            codeOwnerConfig.key(),
-                            codeOwnerConfig.revision(),
-                            CodeOwnerConfigImportType.GLOBAL,
-                            codeOwnerConfigReference)),
-            codeOwnerConfig.codeOwnerSets().stream()
-                .flatMap(codeOwnerSet -> codeOwnerSet.imports().stream())
-                .map(
-                    codeOwnerConfigReference ->
-                        validateCodeOwnerConfigReference(
-                            branchNameKey,
-                            revWalk,
-                            codeOwnerConfigFilePath,
-                            codeOwnerConfig.key(),
-                            codeOwnerConfig.revision(),
-                            CodeOwnerConfigImportType.PER_FILE,
-                            codeOwnerConfigReference)))
-        .filter(Optional::isPresent)
-        .map(Optional::get);
+    try {
+      RevCommit codeOwnerConfigRevision = revWalk.parseCommit(codeOwnerConfig.revision());
+      return Streams.concat(
+              codeOwnerConfig.imports().stream()
+                  .map(
+                      codeOwnerConfigReference ->
+                          validateCodeOwnerConfigReference(
+                              branchNameKey,
+                              revWalk,
+                              codeOwnerConfigFilePath,
+                              codeOwnerConfig.key(),
+                              codeOwnerConfigRevision,
+                              CodeOwnerConfigImportType.GLOBAL,
+                              codeOwnerConfigReference)),
+              codeOwnerConfig.codeOwnerSets().stream()
+                  .flatMap(codeOwnerSet -> codeOwnerSet.imports().stream())
+                  .map(
+                      codeOwnerConfigReference ->
+                          validateCodeOwnerConfigReference(
+                              branchNameKey,
+                              revWalk,
+                              codeOwnerConfigFilePath,
+                              codeOwnerConfig.key(),
+                              codeOwnerConfigRevision,
+                              CodeOwnerConfigImportType.PER_FILE,
+                              codeOwnerConfigReference)))
+          .filter(Optional::isPresent)
+          .map(Optional::get);
+    } catch (IOException e) {
+      throw new CodeOwnersInternalServerErrorException(
+          String.format("Failed to validate imports for %s in ", codeOwnerConfig.key()), e);
+    }
   }
 
   /**
@@ -881,7 +887,7 @@
       RevWalk revWalk,
       Path codeOwnerConfigFilePath,
       CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
-      ObjectId codeOwnerConfigRevision,
+      RevCommit codeOwnerConfigRevision,
       CodeOwnerConfigImportType importType,
       CodeOwnerConfigReference codeOwnerConfigReference) {
     CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
@@ -902,16 +908,20 @@
       // that uploaders cannot probe for the existence of projects (e.g. deduce from the error
       // message whether a project exists)
       return nonResolvableImport(
+          codeOwnerConfigRevision,
           branchNameKey,
           importType,
+          codeOwnerConfigReference,
           codeOwnerConfigFilePath,
           String.format("project '%s' not found", keyOfImportedCodeOwnerConfig.project().get()));
     }
 
     if (!projectState.get().statePermitsRead()) {
       return nonResolvableImport(
+          codeOwnerConfigRevision,
           branchNameKey,
           importType,
+          codeOwnerConfigReference,
           codeOwnerConfigFilePath,
           String.format(
               "project '%s' has state '%s' that doesn't permit read",
@@ -927,8 +937,10 @@
       // that uploaders cannot probe for the existence of branches (e.g. deduce from the error
       // message whether a branch exists)
       return nonResolvableImport(
+          codeOwnerConfigRevision,
           branchNameKey,
           importType,
+          codeOwnerConfigReference,
           codeOwnerConfigFilePath,
           String.format(
               "branch '%s' not found in project '%s'",
@@ -943,8 +955,10 @@
     if (!codeOwnerBackend.isCodeOwnerConfigFile(
         keyOfImportedCodeOwnerConfig.project(), codeOwnerConfigReference.fileName())) {
       return nonResolvableImport(
+          codeOwnerConfigRevision,
           branchNameKey,
           importType,
+          codeOwnerConfigReference,
           codeOwnerConfigFilePath,
           String.format(
               "'%s' is not a code owner config file", codeOwnerConfigReference.filePath()));
@@ -961,8 +975,10 @@
               : codeOwnerBackend.getCodeOwnerConfig(keyOfImportedCodeOwnerConfig, revision.get());
       if (!importedCodeOwnerConfig.isPresent()) {
         return nonResolvableImport(
+            codeOwnerConfigRevision,
             branchNameKey,
             importType,
+            codeOwnerConfigReference,
             codeOwnerConfigFilePath,
             String.format(
                 "'%s' does not exist (project = %s, branch = %s, revision = %s)",
@@ -975,8 +991,10 @@
       if (getInvalidConfigCause(codeOwnersInternalServerErrorException).isPresent()) {
         // The imported code owner config is non-parseable.
         return nonResolvableImport(
+            codeOwnerConfigRevision,
             branchNameKey,
             importType,
+            codeOwnerConfigReference,
             codeOwnerConfigFilePath,
             String.format(
                 "'%s' is not parseable (project = %s, branch = %s)",
@@ -1057,19 +1075,54 @@
   }
 
   private Optional<CommitValidationMessage> nonResolvableImport(
+      RevCommit codeOwnerConfigRevision,
       BranchNameKey branchNameKey,
       CodeOwnerConfigImportType importType,
+      CodeOwnerConfigReference codeOwnerConfigReference,
       Path codeOwnerConfigFilePath,
       String message) {
-    return nonResolvableImport(
-        importType,
-        codeOwnerConfigFilePath,
-        message,
-        codeOwnersPluginConfiguration
-                .getProjectConfig(branchNameKey.project())
-                .rejectNonResolvableImports(branchNameKey.branch())
-            ? ValidationMessage.Type.ERROR
-            : ValidationMessage.Type.WARNING);
+    ValidationMessage.Type validationMessageType;
+    if (codeOwnerConfigRevision.getParentCount() > 1
+        && !codeOwnerConfigReference.branch().isPresent()
+        && codeOwnerConfigReference.project().isPresent()
+        && !codeOwnerConfigReference.project().get().equals(branchNameKey.project())) {
+      // For merge commits, imports from other projects, that implicitly assume the same branch as
+      // the importing code owner config, should not be rejected if they cannot be resolved. Hence
+      // issues with them are always reported as warnings (rather than errors which would cause a
+      // rejection).
+      //
+      // We do not reject such non-resolvable imports because that can require landing merge
+      // commits in a certain order or even make the landing of merge commits impossible.
+      //
+      // Example 1:
+      // 1. project A adds foo/OWNERS
+      // 2. project B imports A:foo/OWNERS
+      // => If these changes should be merged into another branch (e.g. a release branch) the
+      // validation of the code owner config files only succeeds if the merges are done in the
+      // correct order (1. do merge for project A, 2. do merge for project B). If the merges are
+      // done in the opposite order (1. do merge for project B, 2. do merge for project A) the code
+      // owner config file validation for the merge in project B would fail since A:foo/OWNERS
+      // doesn't exist in the target branch yet.
+      //
+      // Example 2:
+      // 1. project A adds foo/OWNERS
+      // 2. project B imports A:foo/OWNERS
+      // 3. project B adds bar/OWNERS
+      // 4. project A imports B:bar/OWNERS
+      // => If the merge for project A is done first the code owner config file validation would
+      // fail because B:bar/OWNERS doesn't exist yet. If the merge for project B is done first the
+      // code owner config file validation would fail because A:foo/OWNERS doesn't exist yet.
+      validationMessageType = ValidationMessage.Type.WARNING;
+    } else {
+      validationMessageType =
+          codeOwnersPluginConfiguration
+                  .getProjectConfig(branchNameKey.project())
+                  .rejectNonResolvableImports(branchNameKey.branch())
+              ? ValidationMessage.Type.ERROR
+              : ValidationMessage.Type.WARNING;
+    }
+
+    return nonResolvableImport(importType, codeOwnerConfigFilePath, message, validationMessageType);
   }
 
   private Optional<CommitValidationMessage> nonResolvableImport(
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 d2ce328..7f0e207 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -32,6 +33,7 @@
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ProjectState;
@@ -43,6 +45,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
@@ -67,6 +70,7 @@
 import java.util.Optional;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
@@ -1633,6 +1637,208 @@
   }
 
   @Test
+  public void
+      forMergeCommitsNonResolvableGlobalImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch()
+          throws Exception {
+    testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch(
+        CodeOwnerConfigImportType.GLOBAL);
+  }
+
+  @Test
+  public void
+      forMergeCommitsNonResolvablePerFileImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch()
+          throws Exception {
+    testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch(
+        CodeOwnerConfigImportType.PER_FILE);
+  }
+
+  private void
+      testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch(
+          CodeOwnerConfigImportType importType) throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    // Create a second project from which we will import a code owner config.
+    Project.NameKey otherProject = projectOperations.newProject().create();
+
+    // Create a target branch for into which we will merge later.
+    String targetBranchName = "target";
+    BranchInput branchInput = new BranchInput();
+    branchInput.ref = targetBranchName;
+    branchInput.revision = projectOperations.project(project).getHead("master").name();
+    gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+    branchInput.revision = projectOperations.project(otherProject).getHead("master").name();
+    gApi.projects().name(otherProject.get()).branch(branchInput.ref).create(branchInput);
+
+    // Create the code owner config file in the second project that we will import.
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(otherProject)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    // Create a code owner config that imports the code owner config from the other project, without
+    // specifying the branch for the import (if the branch is not specified the code owner config is
+    // imported from the same branch that contains the importing code owner config).
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        CodeOwnerConfigReference.builder(
+                CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+                codeOwnerConfigOperations
+                    .codeOwnerConfig(keyOfImportedCodeOwnerConfig)
+                    .getFilePath())
+            .setProject(otherProject)
+            .build();
+    TestCodeOwnerConfigCreation.Builder codeOwnerConfigBuilder =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/");
+    switch (importType) {
+      case GLOBAL:
+        codeOwnerConfigBuilder.addImport(codeOwnerConfigReference);
+        break;
+      case PER_FILE:
+        codeOwnerConfigBuilder.addCodeOwnerSet(
+            CodeOwnerSet.builder()
+                .addPathExpression("foo")
+                .addImport(codeOwnerConfigReference)
+                .build());
+        break;
+      default:
+        throw new IllegalStateException("unknown import type: " + importType);
+    }
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = codeOwnerConfigBuilder.create();
+    GitUtil.fetch(testRepo, "refs/*:refs/*");
+
+    // Create the merge commit.
+    RevCommit parent1 = projectOperations.project(project).getHead(targetBranchName);
+    RevCommit parent2 = projectOperations.project(project).getHead("master");
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "merge",
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+                .getJGitFilePath(),
+            format(codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get()));
+    m.setParents(ImmutableList.of(parent1, parent2));
+    PushOneCommit.Result r = m.to("refs/for/" + targetBranchName);
+    assertOkWithWarnings(
+        r,
+        "invalid code owner config files",
+        String.format(
+            "invalid %s import in '%s': '%s' does not exist (project = %s, branch = %s,"
+                + " revision = %s)",
+            importType.getType(),
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath(),
+            otherProject.get(),
+            targetBranchName,
+            parent1.name()));
+  }
+
+  @Test
+  public void
+      forMergeCommitsNonResolvableGlobalImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch()
+          throws Exception {
+    testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch(
+        CodeOwnerConfigImportType.GLOBAL);
+  }
+
+  @Test
+  public void
+      forMergeCommitsNonResolvablePerFileImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch()
+          throws Exception {
+    testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch(
+        CodeOwnerConfigImportType.PER_FILE);
+  }
+
+  private void
+      testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch(
+          CodeOwnerConfigImportType importType) throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    // Create a second project from which we will import a non-existing code owner config.
+    Project.NameKey otherProject = projectOperations.newProject().create();
+
+    // Create a target branch for into which we will merge later.
+    String targetBranchName = "target";
+    BranchInput branchInput = new BranchInput();
+    branchInput.ref = targetBranchName;
+    branchInput.revision = projectOperations.project(project).getHead("master").name();
+    gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+    // Create a code owner config that imports a non-existing code owner config from the other
+    // project, with specifying the branch for the import. When this code owner config is merged
+    // into another branch later we expect that it is rejected by the validation.
+    CodeOwnerConfig.Key keyOfNonExistingCodeOwnerConfig =
+        CodeOwnerConfig.Key.create(project, "master", "/foo/");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        CodeOwnerConfigReference.builder(
+                CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+                codeOwnerConfigOperations
+                    .codeOwnerConfig(keyOfNonExistingCodeOwnerConfig)
+                    .getFilePath())
+            .setProject(otherProject)
+            .setBranch("master")
+            .build();
+    TestCodeOwnerConfigCreation.Builder codeOwnerConfigBuilder =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/");
+    switch (importType) {
+      case GLOBAL:
+        codeOwnerConfigBuilder.addImport(codeOwnerConfigReference);
+        break;
+      case PER_FILE:
+        codeOwnerConfigBuilder.addCodeOwnerSet(
+            CodeOwnerSet.builder()
+                .addPathExpression("foo")
+                .addImport(codeOwnerConfigReference)
+                .build());
+        break;
+      default:
+        throw new IllegalStateException("unknown import type: " + importType);
+    }
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = codeOwnerConfigBuilder.create();
+    GitUtil.fetch(testRepo, "refs/*:refs/*");
+
+    // Create the merge commit.
+    RevCommit parent1 = projectOperations.project(project).getHead(targetBranchName);
+    RevCommit parent2 = projectOperations.project(project).getHead("master");
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "merge",
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+                .getJGitFilePath(),
+            format(codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get()));
+    m.setParents(ImmutableList.of(parent1, parent2));
+    PushOneCommit.Result r = m.to("refs/for/" + targetBranchName);
+    assertErrorWithMessages(
+        r,
+        "invalid code owner config files",
+        String.format(
+            "invalid %s import in '%s': '%s' does not exist (project = %s, branch = master,"
+                + " revision = %s)",
+            importType.getType(),
+            codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfNonExistingCodeOwnerConfig)
+                .getFilePath(),
+            otherProject.get(),
+            parent1.name()));
+  }
+
+  @Test
   public void cannotUploadConfigWithGlobalImportOfNonParseableCodeOwnerConfig() throws Exception {
     testUploadConfigWithImportOfNonParseableCodeOwnerConfig(CodeOwnerConfigImportType.GLOBAL);
   }