GetCodeOwnerConfigFiles: Allow to filter by email

Add a new parameter that allows to limit the returned code owner config
files to those that contain the given email. This is useful if an email
should be updated, as it allows you to find the code owner config files
that you need to update for this.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Ifc94f68ba9957fcad2db7612ec0137aeee771c07
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
index 2b054b3..c4dfaac 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.api;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import java.util.List;
 
@@ -27,6 +28,24 @@
   CodeOwnerConfigFilesRequest codeOwnerConfigFiles() throws RestApiException;
 
   abstract class CodeOwnerConfigFilesRequest {
+    private String email;
+
+    /**
+     * Limits the returned code owner config files to those that contain the given email.
+     *
+     * @param email the email that should appear in the returned code owner config files
+     */
+    public CodeOwnerConfigFilesRequest withEmail(String email) {
+      this.email = email;
+      return this;
+    }
+
+    /** Returns the email that should appear in the returned code owner config files/ */
+    @Nullable
+    public String getEmail() {
+      return email;
+    }
+
     /** Executes the request and retrieves the paths of the requested code owner config file */
     public abstract List<String> paths() throws RestApiException;
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
index 3383eb1..189b0d1 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerConfigFiles;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.util.List;
 
@@ -27,13 +28,14 @@
     BranchCodeOwnersImpl create(BranchResource branchResource);
   }
 
-  private final GetCodeOwnerConfigFiles getCodeOwnerConfigFiles;
+  private final Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider;
   private final BranchResource branchResource;
 
   @Inject
   public BranchCodeOwnersImpl(
-      GetCodeOwnerConfigFiles getCodeOwnerConfigFiles, @Assisted BranchResource branchResource) {
-    this.getCodeOwnerConfigFiles = getCodeOwnerConfigFiles;
+      Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider,
+      @Assisted BranchResource branchResource) {
+    this.getCodeOwnerConfigFilesProvider = getCodeOwnerConfigFilesProvider;
     this.branchResource = branchResource;
   }
 
@@ -42,6 +44,8 @@
     return new CodeOwnerConfigFilesRequest() {
       @Override
       public List<String> paths() throws RestApiException {
+        GetCodeOwnerConfigFiles getCodeOwnerConfigFiles = getCodeOwnerConfigFilesProvider.get();
+        getCodeOwnerConfigFiles.setEmail(getEmail());
         return getCodeOwnerConfigFiles.apply(branchResource).value();
       }
     };
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
index 26f31fc..008f682 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnerConfigFiles.java
@@ -15,18 +15,22 @@
 package com.google.gerrit.plugins.codeowners.restapi;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigScanner;
 import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import java.nio.file.Path;
 import java.util.List;
+import org.kohsuke.args4j.Option;
 
 /**
  * REST endpoint that lists the code owner config files in a branch.
@@ -38,11 +42,22 @@
  * branch. This means the expected performance of this REST endpoint is rather low and it should not
  * be used in any critical path where performance matters.
  */
-@Singleton
 public class GetCodeOwnerConfigFiles implements RestReadView<BranchResource> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final CodeOwnerConfigScanner codeOwnerConfigScanner;
 
+  private String email;
+
+  @Option(
+      name = "--email",
+      metaVar = "EMAIL",
+      usage = "limits the returned code owner config files to those that contain this email")
+  public void setEmail(@Nullable String email) {
+    this.email = email;
+  }
+
   @Inject
   public GetCodeOwnerConfigFiles(
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
@@ -56,13 +71,46 @@
     CodeOwnerBackend codeOwnerBackend =
         codeOwnersPluginConfiguration.getBackend(resource.getBranchKey());
     ImmutableList.Builder<Path> codeOwnerConfigs = ImmutableList.builder();
+
+    if (email != null) {
+      logger.atFine().log(
+          "limiting the returned code owner config files to those that contain the email %s",
+          email);
+    }
+
     codeOwnerConfigScanner.visit(
         resource.getBranchKey(),
         codeOwnerConfig -> {
-          codeOwnerConfigs.add(codeOwnerBackend.getFilePath(codeOwnerConfig.key()));
+          Path codeOwnerConfigPath = codeOwnerBackend.getFilePath(codeOwnerConfig.key());
+          if (email == null || containsEmail(codeOwnerConfig, codeOwnerConfigPath, email)) {
+            codeOwnerConfigs.add(codeOwnerConfigPath);
+          }
           return true;
         });
     return Response.ok(
         codeOwnerConfigs.build().stream().map(Path::toString).collect(toImmutableList()));
   }
+
+  /**
+   * Checks whether the given code owner config contains the given email.
+   *
+   * @param codeOwnerConfig the code owner config for which it should be checked if it contains the
+   *     email
+   * @param codeOwnerConfigPath the path of the code owner config
+   * @param email the email
+   * @return whether the given code owner config contains the given email
+   */
+  private boolean containsEmail(
+      CodeOwnerConfig codeOwnerConfig, Path codeOwnerConfigPath, String email) {
+    requireNonNull(email, "email");
+    boolean containsEmail =
+        codeOwnerConfig.codeOwnerSets().stream()
+            .flatMap(codeOwnerSet -> codeOwnerSet.codeOwners().stream())
+            .anyMatch(codeOwnerReference -> email.equals(codeOwnerReference.email()));
+    if (!containsEmail) {
+      logger.atFine().log(
+          "Filtering out %s since it doesn't contain the email", codeOwnerConfigPath);
+    }
+    return containsEmail;
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
index 1c0c145..cf8ddea 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigFilesIT.java
@@ -16,9 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
 import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
 import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
 import com.google.gerrit.plugins.codeowners.config.BackendConfig;
@@ -179,6 +181,107 @@
         .isEmpty();
   }
 
+  @Test
+  public void filterByEmail() throws Exception {
+    TestAccount user2 = accountCreator.user2();
+    TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey3 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/bar/")
+            .addCodeOwnerSet(
+                CodeOwnerSet.builder()
+                    .addPathExpression("foo")
+                    .addCodeOwnerEmail(user.email())
+                    .build())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey4 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/baz/")
+            .addCodeOwnerEmail(admin.email())
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey5 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/xyz/")
+            .addCodeOwnerEmail(admin.email())
+            .addCodeOwnerEmail(user2.email())
+            .create();
+
+    assertThat(
+            projectCodeOwnersApiFactory
+                .project(project)
+                .branch("master")
+                .codeOwnerConfigFiles()
+                .withEmail(admin.email())
+                .paths())
+        .containsExactly(
+            getCodeOwnerConfigFilePath(codeOwnerConfigKey2),
+            getCodeOwnerConfigFilePath(codeOwnerConfigKey4),
+            getCodeOwnerConfigFilePath(codeOwnerConfigKey5))
+        .inOrder();
+
+    assertThat(
+            projectCodeOwnersApiFactory
+                .project(project)
+                .branch("master")
+                .codeOwnerConfigFiles()
+                .withEmail(user.email())
+                .paths())
+        .containsExactly(
+            getCodeOwnerConfigFilePath(codeOwnerConfigKey1),
+            getCodeOwnerConfigFilePath(codeOwnerConfigKey3),
+            getCodeOwnerConfigFilePath(codeOwnerConfigKey4))
+        .inOrder();
+
+    assertThat(
+            projectCodeOwnersApiFactory
+                .project(project)
+                .branch("master")
+                .codeOwnerConfigFiles()
+                .withEmail(user2.email())
+                .paths())
+        .containsExactly(getCodeOwnerConfigFilePath(codeOwnerConfigKey5));
+
+    assertThat(
+            projectCodeOwnersApiFactory
+                .project(project)
+                .branch("master")
+                .codeOwnerConfigFiles()
+                .withEmail(user3.email())
+                .paths())
+        .isEmpty();
+  }
+
   private String getCodeOwnerConfigFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) {
     return backendConfig.getDefaultBackend().getFilePath(codeOwnerConfigKey).toString();
   }
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index b3ea9b9..7c9b896 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -68,6 +68,13 @@
 why this REST endpoint must not be used in any critical paths where performance
 matters.
 
+The following request parameters can be specified:
+
+| Field Name  |          | Description |
+| ----------- | -------- | ----------- |
+| `email`     | optional | Code owner email that must appear in the returned
+code owner config files.
+
 #### Request
 
 ```