Warn when a code owner config directly imports itself

It's never useful if an OWNERS file directly imports itself. If this is
done, it's almost certainly a mistake and the intention was to import
another OWNERS file. Report a warning if we see this.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I843389909edbf70325d262f6a18942c81c474643
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index e4714bd..cef6d52 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -855,6 +855,14 @@
         PathCodeOwners.createKeyForImportedCodeOwnerConfig(
             keyOfImportingCodeOwnerConfig, codeOwnerConfigReference);
 
+    if (isSelfImport(keyOfImportingCodeOwnerConfig, keyOfImportedCodeOwnerConfig)) {
+      return nonResolvableImport(
+          importType,
+          codeOwnerConfigFilePath,
+          "code owner config imports itself",
+          ValidationMessage.Type.WARNING);
+    }
+
     Optional<ProjectState> projectState = projectCache.get(keyOfImportedCodeOwnerConfig.project());
     if (!projectState.isPresent() || !isProjectReadable(keyOfImportedCodeOwnerConfig)) {
       // we intentionally use the same error message for non-existing and non-readable projects so
@@ -944,6 +952,21 @@
     return Optional.empty();
   }
 
+  /** Whether the importing code owner config is the same as the imported code owner config. */
+  private boolean isSelfImport(
+      CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
+      CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig) {
+    return keyOfImportingCodeOwnerConfig.project().equals(keyOfImportedCodeOwnerConfig.project())
+        && keyOfImportingCodeOwnerConfig.ref().equals(keyOfImportedCodeOwnerConfig.ref())
+        && codeOwnersPluginConfiguration
+            .getBackend(keyOfImportingCodeOwnerConfig.branchNameKey())
+            .getFilePath(keyOfImportingCodeOwnerConfig)
+            .equals(
+                codeOwnersPluginConfiguration
+                    .getBackend(keyOfImportedCodeOwnerConfig.branchNameKey())
+                    .getFilePath(keyOfImportedCodeOwnerConfig));
+  }
+
   private boolean isProjectReadable(CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig) {
     try {
       return permissionBackend
@@ -994,14 +1017,26 @@
       CodeOwnerConfigImportType importType,
       Path codeOwnerConfigFilePath,
       String message) {
+    return nonResolvableImport(
+        importType,
+        codeOwnerConfigFilePath,
+        message,
+        codeOwnersPluginConfiguration.rejectNonResolvableImports(project)
+            ? ValidationMessage.Type.ERROR
+            : ValidationMessage.Type.WARNING);
+  }
+
+  private Optional<CommitValidationMessage> nonResolvableImport(
+      CodeOwnerConfigImportType importType,
+      Path codeOwnerConfigFilePath,
+      String message,
+      ValidationMessage.Type validationMessageType) {
     return Optional.of(
         new CommitValidationMessage(
             String.format(
                 "invalid %s import in '%s': %s",
                 importType.getType(), codeOwnerConfigFilePath, message),
-            codeOwnersPluginConfiguration.rejectNonResolvableImports(project)
-                ? ValidationMessage.Type.ERROR
-                : ValidationMessage.Type.WARNING));
+            validationMessageType));
   }
 
   private Optional<CommitValidationMessage> nonResolvableCodeOwner(
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 12b3360..3dfa9db 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -1023,6 +1023,102 @@
   }
 
   @Test
+  public void uploadConfigWithGlobalSelfImportReportsAWarning() throws Exception {
+    testUploadConfigWithSelfImport(CodeOwnerConfigImportType.GLOBAL);
+  }
+
+  @Test
+  public void uploadConfigWithPerFileSelfImportReportsAWarning() throws Exception {
+    testUploadConfigWithSelfImport(CodeOwnerConfigImportType.PER_FILE);
+  }
+
+  private void testUploadConfigWithSelfImport(CodeOwnerConfigImportType importType)
+      throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    // create a code owner config that imports itself
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        CodeOwnerConfigReference.builder(
+                CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+                codeOwnerConfigOperations
+                    .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+                    .getFilePath())
+            .setProject(project)
+            .build();
+    CodeOwnerConfig codeOwnerConfig =
+        createCodeOwnerConfigWithImport(
+            keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
+
+    PushOneCommit.Result r =
+        createChange(
+            "Add code owners",
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+                .getJGitFilePath(),
+            format(codeOwnerConfig));
+    assertOkWithWarnings(
+        r,
+        "invalid code owner config files",
+        String.format(
+            "invalid %s import in '%s': code owner config imports itself",
+            importType.getType(),
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+                .getFilePath()));
+  }
+
+  @Test
+  public void canUploadConfigWithGlobalImportOfFileWithExtensionFromSameFolder() throws Exception {
+    testUploadConfigWithImportOfFileWithExtensionFromSameFolder(CodeOwnerConfigImportType.GLOBAL);
+  }
+
+  @Test
+  public void canUploadConfigWithPerFileImportOfFileWithExtensionFromSameFolder() throws Exception {
+    testUploadConfigWithImportOfFileWithExtensionFromSameFolder(CodeOwnerConfigImportType.PER_FILE);
+  }
+
+  private void testUploadConfigWithImportOfFileWithExtensionFromSameFolder(
+      CodeOwnerConfigImportType importType) throws Exception {
+    skipTestIfImportsNotSupportedByCodeOwnersBackend();
+
+    // create a code owner config that imports a code owner config from the same folder but with an
+    // extension in the file name
+    CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
+    CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .fileName(getCodeOwnerConfigFileName() + "_extension")
+            .addCodeOwnerEmail(user.email())
+            .create();
+    GitUtil.fetch(testRepo, "refs/*:refs/*");
+    testRepo.reset(projectOperations.project(project).getHead("master"));
+    CodeOwnerConfigReference codeOwnerConfigReference =
+        CodeOwnerConfigReference.builder(
+                CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
+                codeOwnerConfigOperations
+                    .codeOwnerConfig(keyOfImportedCodeOwnerConfig)
+                    .getFilePath())
+            .setProject(project)
+            .build();
+    CodeOwnerConfig codeOwnerConfig =
+        createCodeOwnerConfigWithImport(
+            keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
+
+    PushOneCommit.Result r =
+        createChange(
+            "Add code owners",
+            codeOwnerConfigOperations
+                .codeOwnerConfig(keyOfImportingCodeOwnerConfig)
+                .getJGitFilePath(),
+            format(codeOwnerConfig));
+    r.assertOkStatus();
+  }
+
+  @Test
   public void cannotUploadConfigWithGlobalImportFromNonExistingProject() throws Exception {
     testUploadConfigWithImportFromNonExistingProject(CodeOwnerConfigImportType.GLOBAL);
   }
@@ -1764,6 +1860,16 @@
     return ObjectIds.abbreviateName(id, testRepo.getRevWalk().getObjectReader());
   }
 
+  private String getCodeOwnerConfigFileName() {
+    CodeOwnerBackend backend = backendConfig.getDefaultBackend();
+    if (backend instanceof FindOwnersBackend) {
+      return FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME;
+    } else if (backend instanceof ProtoBackend) {
+      return ProtoBackend.CODE_OWNER_CONFIG_FILE_NAME;
+    }
+    throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
+  }
+
   private static void assertOkWithoutMessages(PushOneCommit.Result pushResult) {
     pushResult.assertOkStatus();
     pushResult.assertNotMessage("fatal");