Merge changes I2a438734,I437144bd,I3414b0af,I488f0eea,I22a639cb, ...

* changes:
  Read all required approvals that are configured
  Test behaviour with multiple configured required approvals
  Add truth subject for RequiredLabel
  AbstractRequiredApprovalConfig: Merge the get methods
  AbstractRequiredApprovalConfigTest: Improve readability of 2 tests
  Fix comments for boolean and null arguments
  Add REST endpoint to rename an email in code owner config files of one branch
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
index 2f1be86..e06e1bb 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
@@ -40,6 +41,7 @@
 import com.google.inject.Inject;
 import java.nio.file.Path;
 import java.util.Map;
+import java.util.function.Consumer;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -145,9 +147,30 @@
   protected void setCodeOwnersConfig(
       Project.NameKey project, @Nullable String subsection, String key, String value)
       throws Exception {
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig ->
+            codeOwnersConfig.setString(
+                CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS, subsection, key, value));
+  }
+
+  protected void setCodeOwnersConfig(
+      Project.NameKey project,
+      @Nullable String subsection,
+      String key,
+      ImmutableList<String> values)
+      throws Exception {
+    updateCodeOwnersConfig(
+        project,
+        codeOwnersConfig ->
+            codeOwnersConfig.setStringList(
+                CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS, subsection, key, values));
+  }
+
+  private void updateCodeOwnersConfig(Project.NameKey project, Consumer<Config> configUpdater)
+      throws Exception {
     Config codeOwnersConfig = new Config();
-    codeOwnersConfig.setString(
-        CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS, subsection, key, value);
+    configUpdater.accept(codeOwnersConfig);
     try (TestRepository<Repository> testRepo =
         new TestRepository<>(repoManager.openRepository(project))) {
       Ref ref = testRepo.getRepository().exactRef(RefNames.REFS_CONFIG);
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
index 2b81afb..82fb351 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
@@ -66,6 +66,10 @@
     public abstract List<String> paths() throws RestApiException;
   }
 
+  /** Renames an email in the code owner config files of the branch. */
+  RenameEmailResultInfo renameEmailInCodeOwnerConfigFiles(RenameEmailInput input)
+      throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -80,5 +84,11 @@
     public CodeOwnerConfigFilesRequest codeOwnerConfigFiles() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public RenameEmailResultInfo renameEmailInCodeOwnerConfigFiles(RenameEmailInput input)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
index aacc3e2..f6e4804 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerBranchConfig;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerConfigFiles;
+import com.google.gerrit.plugins.codeowners.restapi.RenameEmail;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -33,15 +34,18 @@
 
   private final GetCodeOwnerBranchConfig getCodeOwnerBranchConfig;
   private final Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider;
+  private final RenameEmail renameEmail;
   private final BranchResource branchResource;
 
   @Inject
   public BranchCodeOwnersImpl(
       GetCodeOwnerBranchConfig getCodeOwnerBranchConfig,
       Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider,
+      RenameEmail renameEmail,
       @Assisted BranchResource branchResource) {
     this.getCodeOwnerConfigFilesProvider = getCodeOwnerConfigFilesProvider;
     this.getCodeOwnerBranchConfig = getCodeOwnerBranchConfig;
+    this.renameEmail = renameEmail;
     this.branchResource = branchResource;
   }
 
@@ -66,4 +70,14 @@
       }
     };
   }
+
+  @Override
+  public RenameEmailResultInfo renameEmailInCodeOwnerConfigFiles(RenameEmailInput input)
+      throws RestApiException {
+    try {
+      return renameEmail.apply(branchResource, input).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot rename email", e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/RenameEmailInput.java b/java/com/google/gerrit/plugins/codeowners/api/RenameEmailInput.java
new file mode 100644
index 0000000..cf38e59
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/RenameEmailInput.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.api;
+
+/**
+ * The input for the {@link com.google.gerrit.plugins.codeowners.restapi.RenameEmail} REST endpoint.
+ */
+public class RenameEmailInput {
+  /**
+   * Optional commit message that should be used for the commit that renames the email in the code
+   * owner config files.
+   */
+  public String message;
+
+  /** The old email that should be replaced with the new email. */
+  public String oldEmail;
+
+  /** The new email that should be used to replace the old email. */
+  public String newEmail;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/RenameEmailResultInfo.java b/java/com/google/gerrit/plugins/codeowners/api/RenameEmailResultInfo.java
new file mode 100644
index 0000000..6400734
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/RenameEmailResultInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.api;
+
+import com.google.gerrit.extensions.common.CommitInfo;
+
+/**
+ * The result of the {@link com.google.gerrit.plugins.codeowners.restapi.RenameEmail} REST endpoint.
+ */
+public class RenameEmailResultInfo {
+  /** The commit that did the email rename, not set if no update was performed. */
+  public CommitInfo commit;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
index 0f762e1..be5318d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerBackend.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.server.IdentifiedUser;
 import java.nio.file.Path;
 import java.util.Optional;
@@ -52,11 +53,7 @@
    */
   default Optional<CodeOwnerConfig> getCodeOwnerConfig(
       CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) {
-    return getCodeOwnerConfig(
-        codeOwnerConfigKey,
-        /** revWalk = */
-        null,
-        revision);
+    return getCodeOwnerConfig(codeOwnerConfigKey, /* revWalk= */ null, revision);
   }
 
   /**
@@ -118,4 +115,19 @@
   default Optional<PathExpressionMatcher> getPathExpressionMatcher() {
     return Optional.empty();
   }
+
+  /**
+   * Replaces the old email in the given code owner config file content with the new email.
+   *
+   * @param codeOwnerConfigFileContent the code owner config file content in which the old email
+   *     should be replaced with the new email
+   * @param oldEmail the email that should be replaced by the new email
+   * @param newEmail the email that should replace the old email
+   * @return the updated content
+   * @throws NotImplementedException if the backend doesn't support replacing emails in code owner
+   *     config files
+   */
+  default String replaceEmail(String codeOwnerConfigFileContent, String oldEmail, String newEmail) {
+    throw new NotImplementedException();
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
index 0e116a0..d01c4a3 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfig.java
@@ -350,11 +350,7 @@
      * @return the code owner config key
      */
     public static Key create(BranchNameKey branchNameKey, Path folderPath) {
-      return create(
-          branchNameKey,
-          folderPath,
-          /** fileName = */
-          null);
+      return create(branchNameKey, folderPath, /* fileName= */ null);
     }
 
     /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportMode.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportMode.java
index c712b27..1e598df 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportMode.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportMode.java
@@ -30,14 +30,10 @@
    * <p>Imports of the referenced code owner config are resolved.
    */
   ALL(
-      /** importIgnoreParentCodeOwners = */
-      true,
-      /** importGlobalCodeOwnerSets = */
-      true,
-      /** importPerFileCodeOwnerSets = */
-      true,
-      /** resolveImportsOfImport = */
-      true),
+      /* importIgnoreParentCodeOwners= */ true,
+      /* importGlobalCodeOwnerSets= */ true,
+      /* importPerFileCodeOwnerSets= */ true,
+      /* resolveImportsOfImport= */ true),
 
   /**
    * Only global code owner sets (code owner sets without path expressions) should be imported from
@@ -53,14 +49,10 @@
    * <p>Imports of the referenced code owner config are resolved.
    */
   GLOBAL_CODE_OWNER_SETS_ONLY(
-      /** importIgnoreParentCodeOwners = */
-      false,
-      /** importGlobalCodeOwnerSets = */
-      true,
-      /** importPerFileCodeOwnerSets = */
-      false,
-      /** resolveImportsOfImport = */
-      true);
+      /* importIgnoreParentCodeOwners= */ false,
+      /* importGlobalCodeOwnerSets= */ true,
+      /* importPerFileCodeOwnerSets= */ false,
+      /* resolveImportsOfImport= */ true);
 
   private final boolean importIgnoreParentCodeOwners;
   private final boolean importGlobalCodeOwnerSets;
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index 9b77316..c8c96ab 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -106,8 +106,7 @@
         branchNameKey,
         codeOwnerConfigVisitor,
         invalidCodeOwnerConfigCallback,
-        /** pathGlob = */
-        null);
+        /* pathGlob= */ null);
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
index 07648ef..2165fda 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwners.java
@@ -70,10 +70,7 @@
     requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey");
     CodeOwnerBackend codeOwnerBackend =
         codeOwnersPluginConfiguration.getBackend(codeOwnerConfigKey.branchNameKey());
-    return codeOwnerBackend.getCodeOwnerConfig(
-        codeOwnerConfigKey,
-        /** revision = */
-        null);
+    return codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
index 0aa9ca8..ab08201 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersBackend.java
@@ -63,4 +63,10 @@
   public Optional<PathExpressionMatcher> getPathExpressionMatcher() {
     return Optional.of(GlobMatcher.INSTANCE);
   }
+
+  @Override
+  public String replaceEmail(String codeOwnerConfigFileContent, String oldEmail, String newEmail) {
+    return FindOwnersCodeOwnerConfigParser.replaceEmail(
+        codeOwnerConfigFileContent, oldEmail, newEmail);
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
index 7e8fff8..561c3b1 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
@@ -20,6 +20,7 @@
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -77,6 +78,12 @@
   // Artifical owner token for "set noparent" when used in per-file.
   private static final String TOK_SET_NOPARENT = "set noparent";
 
+  /**
+   * Any Unicode linebreak sequence, is equivalent to {@code
+   * \u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]}.
+   */
+  private static final String LINEBREAK_MATCHER = "\\R";
+
   @Override
   public CodeOwnerConfig parse(
       ObjectId revision, CodeOwnerConfig.Key codeOwnerConfigKey, String codeOwnerConfigAsString)
@@ -98,6 +105,35 @@
     return Formatter.formatAsString(requireNonNull(codeOwnerConfig, "codeOwnerConfig"));
   }
 
+  public static String replaceEmail(
+      String codeOwnerConfigFileContent, String oldEmail, String newEmail) {
+    requireNonNull(codeOwnerConfigFileContent, "codeOwnerConfigFileContent");
+    requireNonNull(oldEmail, "oldEmail");
+    requireNonNull(newEmail, "newEmail");
+
+    String charsThatCanAppearBeforeOrAfterEmail = "[\\s=,#]";
+    Pattern pattern =
+        Pattern.compile(
+            "(^|.*"
+                + charsThatCanAppearBeforeOrAfterEmail
+                + "+)"
+                + "("
+                + Pattern.quote(oldEmail)
+                + ")"
+                + "($|"
+                + charsThatCanAppearBeforeOrAfterEmail
+                + "+.*)");
+
+    List<String> updatedLines = new ArrayList<>();
+    for (String line : Splitter.onPattern(LINEBREAK_MATCHER).split(codeOwnerConfigFileContent)) {
+      while (pattern.matcher(line).matches()) {
+        line = pattern.matcher(line).replaceFirst("$1" + newEmail + "$3");
+      }
+      updatedLines.add(line);
+    }
+    return Joiner.on("\n").join(updatedLines);
+  }
+
   private static class Parser implements ValidationError.Sink {
     private static final String COMMA = "[\\s]*,[\\s]*";
 
@@ -158,7 +194,7 @@
       CodeOwnerSet.Builder globalCodeOwnerSetBuilder = CodeOwnerSet.builder();
       List<CodeOwnerSet> perFileCodeOwnerSet = new ArrayList<>();
 
-      for (String line : Splitter.onPattern("\\R").split(codeOwnerConfigAsString)) {
+      for (String line : Splitter.onPattern(LINEBREAK_MATCHER).split(codeOwnerConfigAsString)) {
         parseLine(codeOwnerConfigBuilder, globalCodeOwnerSetBuilder, perFileCodeOwnerSet, line);
       }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfig.java b/java/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfig.java
index dcf50e7..260d9d9 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfig.java
@@ -17,13 +17,13 @@
 import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.project.ProjectState;
-import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -50,45 +50,60 @@
 
   protected abstract String getConfigKey();
 
-  Optional<RequiredApproval> getForProject(ProjectState projectState, Config pluginConfig) {
+  /**
+   * Reads the required approvals for the specified project from the given plugin config with
+   * fallback to {@code gerrit.config}.
+   *
+   * @param projectState state of the project for which the required approvals should be read
+   * @param pluginConfig the plugin config from which the required approvals should be read
+   * @return the required approvals, an empty list if none was configured
+   */
+  ImmutableList<RequiredApproval> get(ProjectState projectState, Config pluginConfig) {
     requireNonNull(projectState, "projectState");
     requireNonNull(pluginConfig, "pluginConfig");
-    String requiredApproval = pluginConfig.getString(SECTION_CODE_OWNERS, null, getConfigKey());
-    if (requiredApproval == null) {
-      return Optional.empty();
+
+    ImmutableList.Builder<RequiredApproval> requiredApprovalList = ImmutableList.builder();
+    String[] requiredApprovals =
+        pluginConfig.getStringList(SECTION_CODE_OWNERS, /* subsection= */ null, getConfigKey());
+    if (requiredApprovals.length > 0) {
+      for (String requiredApproval : requiredApprovals) {
+        try {
+          requiredApprovalList.add(RequiredApproval.parse(projectState, requiredApproval));
+        } catch (IllegalStateException | IllegalArgumentException e) {
+          throw new InvalidPluginConfigurationException(
+              pluginName,
+              String.format(
+                  "Required approval '%s' that is configured in %s.config"
+                      + " (parameter %s.%s) is invalid: %s",
+                  requiredApproval,
+                  pluginName,
+                  SECTION_CODE_OWNERS,
+                  getConfigKey(),
+                  e.getMessage()));
+        }
+      }
+      return requiredApprovalList.build();
     }
 
-    try {
-      return Optional.of(RequiredApproval.parse(projectState, requiredApproval));
-    } catch (IllegalStateException | IllegalArgumentException e) {
-      throw new InvalidPluginConfigurationException(
-          pluginName,
-          String.format(
-              "Required approval '%s' that is configured in %s.config"
-                  + " (parameter %s.%s) is invalid: %s",
-              requiredApproval, pluginName, SECTION_CODE_OWNERS, getConfigKey(), e.getMessage()));
-    }
-  }
-
-  Optional<RequiredApproval> getFromGlobalPluginConfig(ProjectState projectState) {
-    requireNonNull(projectState, "projectState");
-
-    String requiredApproval =
-        pluginConfigFactory.getFromGerritConfig(pluginName).getString(getConfigKey());
-    if (requiredApproval == null) {
-      return Optional.empty();
+    requiredApprovals =
+        pluginConfigFactory.getFromGerritConfig(pluginName).getStringList(getConfigKey());
+    if (requiredApprovals.length > 0) {
+      for (String requiredApproval : requiredApprovals) {
+        try {
+          requiredApprovalList.add(RequiredApproval.parse(projectState, requiredApproval));
+        } catch (IllegalStateException | IllegalArgumentException e) {
+          throw new InvalidPluginConfigurationException(
+              pluginName,
+              String.format(
+                  "Required approval '%s' that is configured in gerrit.config"
+                      + " (parameter plugin.%s.%s) is invalid: %s",
+                  requiredApproval, pluginName, getConfigKey(), e.getMessage()));
+        }
+      }
+      return requiredApprovalList.build();
     }
 
-    try {
-      return Optional.of(RequiredApproval.parse(projectState, requiredApproval));
-    } catch (IllegalStateException | IllegalArgumentException e) {
-      throw new InvalidPluginConfigurationException(
-          pluginName,
-          String.format(
-              "Required approval '%s' that is configured in gerrit.config"
-                  + " (parameter plugin.%s.%s) is invalid: %s",
-              requiredApproval, pluginName, getConfigKey(), e.getMessage()));
-    }
+    return ImmutableList.of();
   }
 
   /**
@@ -99,19 +114,22 @@
    * @return list of validation messages for validation errors, empty list if there are no
    *     validation errors
    */
-  Optional<CommitValidationMessage> validateProjectLevelConfig(
+  ImmutableList<CommitValidationMessage> validateProjectLevelConfig(
       ProjectState projectState, String fileName, ProjectLevelConfig.Bare projectLevelConfig) {
     requireNonNull(projectState, "projectState");
     requireNonNull(fileName, "fileName");
     requireNonNull(projectLevelConfig, "projectLevelConfig");
 
-    String requiredApproval =
-        projectLevelConfig.getConfig().getString(SECTION_CODE_OWNERS, null, getConfigKey());
-    if (requiredApproval != null) {
+    String[] requiredApprovals =
+        projectLevelConfig
+            .getConfig()
+            .getStringList(SECTION_CODE_OWNERS, /* subsection= */ null, getConfigKey());
+    ImmutableList.Builder<CommitValidationMessage> validationMessages = ImmutableList.builder();
+    for (String requiredApproval : requiredApprovals) {
       try {
         RequiredApproval.parse(projectState, requiredApproval);
       } catch (IllegalArgumentException | IllegalStateException e) {
-        return Optional.of(
+        validationMessages.add(
             new CommitValidationMessage(
                 String.format(
                     "Required approval '%s' that is configured in %s (parameter %s.%s) is invalid: %s",
@@ -123,6 +141,6 @@
                 ValidationMessage.Type.ERROR));
       }
     }
-    return Optional.empty();
+    return validationMessages.build();
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
index dba7bb2..2a29766 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigValidator.java
@@ -158,12 +158,10 @@
     validationMessages.addAll(backendConfig.validateProjectLevelConfig(fileName, cfg));
     validationMessages.addAll(generalConfig.validateProjectLevelConfig(fileName, cfg));
     validationMessages.addAll(statusConfig.validateProjectLevelConfig(fileName, cfg));
-    requiredApprovalConfig
-        .validateProjectLevelConfig(projectState, fileName, cfg)
-        .ifPresent(validationMessages::add);
-    overrideApprovalConfig
-        .validateProjectLevelConfig(projectState, fileName, cfg)
-        .ifPresent(validationMessages::add);
+    validationMessages.addAll(
+        requiredApprovalConfig.validateProjectLevelConfig(projectState, fileName, cfg));
+    validationMessages.addAll(
+        overrideApprovalConfig.validateProjectLevelConfig(projectState, fileName, cfg));
     if (!validationMessages.isEmpty()) {
       throw new CommitValidationException(
           exceptionMessage(fileName, cfg.getRevision()), validationMessages);
diff --git a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
index a881e1c..9f088a7 100644
--- a/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
+++ b/java/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfiguration.java
@@ -18,7 +18,9 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
@@ -322,16 +324,22 @@
    *   <li>hard-coded default required approval
    * </ul>
    *
-   * <p>The first required code owner approval that exists counts and the evaluation is stopped.
+   * <p>The first required code owner approval configuration that exists counts and the evaluation
+   * is stopped.
+   *
+   * <p>If the code owner configuration contains multiple required approvals values, the last value
+   * is used.
    *
    * @param project project for which the required approval should be returned
    * @return the required code owner approval that should be used for the given project
    */
   public RequiredApproval getRequiredApproval(Project.NameKey project) {
-    Optional<RequiredApproval> configuredRequiredApprovalConfig =
+    ImmutableList<RequiredApproval> configuredRequiredApprovalConfig =
         getConfiguredRequiredApproval(requiredApprovalConfig, project);
-    if (configuredRequiredApprovalConfig.isPresent()) {
-      return configuredRequiredApprovalConfig.get();
+    if (!configuredRequiredApprovalConfig.isEmpty()) {
+      // There can be only one required approval. If multiple ones are configured just use the last
+      // one, this is also what Config#getString(String, String, String) does.
+      return Iterables.getLast(configuredRequiredApprovalConfig);
     }
 
     // fall back to hard-coded default required approval
@@ -353,7 +361,9 @@
    *   <li>globally configured override approval
    * </ul>
    *
-   * <p>The first override approval that exists counts and the evaluation is stopped.
+   * <p>The first override approval configuration that exists counts and the evaluation is stopped.
+   *
+   * <p>If the code owner configuration contains multiple override values, the last value is used.
    *
    * @param project project for which the override approval should be returned
    * @return the override approval that should be used for the given project, {@link
@@ -362,10 +372,12 @@
    */
   public Optional<RequiredApproval> getOverrideApproval(Project.NameKey project) {
     try {
-      Optional<RequiredApproval> configuredOverrideApprovalConfig =
+      ImmutableList<RequiredApproval> configuredOverrideApprovalConfig =
           getConfiguredRequiredApproval(overrideApprovalConfig, project);
-      if (configuredOverrideApprovalConfig.isPresent()) {
-        return configuredOverrideApprovalConfig;
+      if (!configuredOverrideApprovalConfig.isEmpty()) {
+        // There can be only one override approval. If multiple ones are configured just use the
+        // last one, this is also what Config#getString(String, String, String) does.
+        return Optional.of(Iterables.getLast(configuredOverrideApprovalConfig));
       }
     } catch (InvalidPluginConfigurationException e) {
       logger.atWarning().withCause(e).log(
@@ -378,33 +390,18 @@
   }
 
   /**
-   * Gets the required approval that is configured for the given project.
+   * Gets the required approvals that are configured for the given project.
    *
-   * @param requiredApprovalConfig the config from which the required approval should be read
-   * @param project the project for which the configured required approval should be returned
-   * @return the required approval that is configured for the given project, {@link
-   *     Optional#empty()} if no required approval is configured
+   * @param requiredApprovalConfig the config from which the required approvals should be read
+   * @param project the project for which the configured required approvals should be returned
+   * @return the required approvals that is configured for the given project, an empty list if no
+   *     required approvals are configured
    */
-  private Optional<RequiredApproval> getConfiguredRequiredApproval(
+  private ImmutableList<RequiredApproval> getConfiguredRequiredApproval(
       AbstractRequiredApprovalConfig requiredApprovalConfig, Project.NameKey project) {
     Config pluginConfig = getPluginConfig(project);
-
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-
-    // check if a project specific required approval is configured
-    Optional<RequiredApproval> requiredApproval =
-        requiredApprovalConfig.getForProject(projectState, pluginConfig);
-    if (requiredApproval.isPresent()) {
-      return requiredApproval;
-    }
-
-    // check if a required approval is globally configured
-    requiredApproval = requiredApprovalConfig.getFromGlobalPluginConfig(projectState);
-    if (requiredApproval.isPresent()) {
-      return requiredApproval;
-    }
-
-    return Optional.empty();
+    return requiredApprovalConfig.get(projectState, pluginConfig);
   }
 
   /**
@@ -423,10 +420,7 @@
     try {
       return pluginConfigFactory
           .getFromGerritConfig(pluginName)
-          .getBoolean(
-              KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS,
-              /** defaultValue = */
-              false);
+          .getBoolean(KEY_ENABLE_EXPERIMENTAL_REST_ENDPOINTS, /* defaultValue= */ false);
     } catch (IllegalArgumentException e) {
       logger.atWarning().withCause(e).log(
           "Value '%s' in gerrit.config (parameter plugin.%s.%s) is invalid.",
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RenameEmail.java b/java/com/google/gerrit/plugins/codeowners/restapi/RenameEmail.java
new file mode 100644
index 0000000..8df9a34
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/RenameEmail.java
@@ -0,0 +1,202 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.restapi;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.codeowners.api.RenameEmailInput;
+import com.google.gerrit.plugins.codeowners.api.RenameEmailResultInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFileUpdateScanner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.BranchResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+@Singleton
+public class RenameEmail implements RestModifyView<BranchResource, RenameEmailInput> {
+  @VisibleForTesting
+  public static String DEFAULT_COMMIT_MESSAGE = "Rename email in code owner config files";
+
+  private final Provider<CurrentUser> currentUser;
+  private final PermissionBackend permissionBackend;
+  private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+  private final CodeOwnerResolver codeOwnerResolver;
+  private final CodeOwnerConfigFileUpdateScanner codeOwnerConfigFileUpdateScanner;
+
+  @Inject
+  public RenameEmail(
+      Provider<CurrentUser> currentUser,
+      PermissionBackend permissionBackend,
+      CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+      CodeOwnerResolver codeOwnerResolver,
+      CodeOwnerConfigFileUpdateScanner codeOwnerConfigFileUpdateScanner) {
+    this.currentUser = currentUser;
+    this.permissionBackend = permissionBackend;
+    this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+    this.codeOwnerResolver = codeOwnerResolver;
+    this.codeOwnerConfigFileUpdateScanner = codeOwnerConfigFileUpdateScanner;
+  }
+
+  @Override
+  public Response<RenameEmailResultInfo> apply(
+      BranchResource branchResource, RenameEmailInput input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+          MethodNotAllowedException, UnprocessableEntityException, PermissionBackendException,
+          IOException {
+    if (!currentUser.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    // caller needs to be project owner or have direct push permissions for the branch
+    if (!permissionBackend
+        .currentUser()
+        .project(branchResource.getNameKey())
+        .test(ProjectPermission.WRITE_CONFIG)) {
+      permissionBackend
+          .currentUser()
+          .ref(branchResource.getBranchKey())
+          .check(RefPermission.UPDATE);
+    }
+
+    validateInput(input);
+
+    CodeOwnerBackend codeOwnerBackend =
+        codeOwnersPluginConfiguration.getBackend(branchResource.getBranchKey());
+
+    Account.Id accountOwningOldEmail = resolveEmail(input.oldEmail);
+    Account.Id accountOwningNewEmail = resolveEmail(input.newEmail);
+    if (!accountOwningOldEmail.equals(accountOwningNewEmail)) {
+      throw new BadRequestException(
+          String.format(
+              "emails must belong to the same account"
+                  + " (old email %s is owned by account %d, new email %s is owned by account %d)",
+              input.oldEmail,
+              accountOwningOldEmail.get(),
+              input.newEmail,
+              accountOwningNewEmail.get()));
+    }
+
+    String inputMessage = Strings.nullToEmpty(input.message).trim();
+    String commitMessage = !inputMessage.isEmpty() ? inputMessage : DEFAULT_COMMIT_MESSAGE;
+
+    try {
+      Optional<RevCommit> commitId =
+          codeOwnerConfigFileUpdateScanner.update(
+              branchResource.getBranchKey(),
+              commitMessage,
+              (codeOwnerConfigFilePath, codeOwnerConfigFileContent) ->
+                  renameEmailInCodeOwnerConfig(
+                      codeOwnerBackend,
+                      codeOwnerConfigFileContent,
+                      input.oldEmail,
+                      input.newEmail));
+
+      RenameEmailResultInfo result = new RenameEmailResultInfo();
+      if (commitId.isPresent()) {
+        result.commit = CommitUtil.toCommitInfo(commitId.get());
+      }
+      return Response.ok(result);
+    } catch (NotImplementedException e) {
+      throw new MethodNotAllowedException(
+          String.format(
+              "rename email not supported by %s backend",
+              CodeOwnerBackendId.getBackendId(codeOwnerBackend.getClass())),
+          e);
+    }
+  }
+
+  private void validateInput(RenameEmailInput input) throws BadRequestException {
+    if (input.oldEmail == null) {
+      throw new BadRequestException("old email is required");
+    }
+    if (input.newEmail == null) {
+      throw new BadRequestException("new email is required");
+    }
+    if (input.oldEmail.equals(input.newEmail)) {
+      throw new BadRequestException("old and new email must differ");
+    }
+  }
+
+  private Account.Id resolveEmail(String email)
+      throws ResourceConflictException, UnprocessableEntityException {
+    requireNonNull(email, "email");
+
+    ImmutableSet<CodeOwner> codeOwners =
+        codeOwnerResolver.resolve(CodeOwnerReference.create(email)).collect(toImmutableSet());
+    if (codeOwners.isEmpty()) {
+      throw new UnprocessableEntityException(String.format("cannot resolve email %s", email));
+    }
+    if (codeOwners.size() > 1) {
+      throw new ResourceConflictException(String.format("email %s is ambigious", email));
+    }
+    return Iterables.getOnlyElement(codeOwners).accountId();
+  }
+
+  /**
+   * Renames an email in the given code owner config.
+   *
+   * @param codeOwnerBackend the code owner backend that is being used
+   * @param codeOwnerConfigFileContent the content of the code owner config file
+   * @param oldEmail the old email that should be replaced by the new email
+   * @param newEmail the new email that should replace the old email
+   * @return the updated code owner config file content if an update was performed, {@link
+   *     Optional#empty()} if no update was done
+   */
+  private Optional<String> renameEmailInCodeOwnerConfig(
+      CodeOwnerBackend codeOwnerBackend,
+      String codeOwnerConfigFileContent,
+      String oldEmail,
+      String newEmail) {
+    requireNonNull(codeOwnerConfigFileContent, "codeOwnerConfigFileContent");
+    requireNonNull(oldEmail, "oldEmail");
+    requireNonNull(newEmail, "newEmail");
+
+    String updatedCodeOwnerConfigFileContent =
+        codeOwnerBackend.replaceEmail(codeOwnerConfigFileContent, oldEmail, newEmail);
+    if (codeOwnerConfigFileContent.equals(updatedCodeOwnerConfigFileContent)) {
+      return Optional.empty();
+    }
+    return Optional.of(updatedCodeOwnerConfigFileContent);
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
index 38fe259..03d8858 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
@@ -31,6 +31,7 @@
         .to(GetCodeOwnerConfigForPathInBranch.class);
     get(BRANCH_KIND, "code_owners.config_files").to(GetCodeOwnerConfigFiles.class);
     get(BRANCH_KIND, "code_owners.branch_config").to(GetCodeOwnerBranchConfig.class);
+    post(BRANCH_KIND, "code_owners.rename").to(RenameEmail.class);
 
     factory(CodeOwnerJson.Factory.class);
     DynamicMap.mapOf(binder(), CodeOwnersInBranchCollection.PathResource.PATH_KIND);
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/RequiredApprovalSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/RequiredApprovalSubject.java
new file mode 100644
index 0000000..27d2b20
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/RequiredApprovalSubject.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.config.RequiredApproval;
+import com.google.gerrit.truth.ListSubject;
+import com.google.gerrit.truth.OptionalSubject;
+import java.util.Optional;
+
+/** {@link Subject} for doing assertions on {@link RequiredApproval}s. */
+public class RequiredApprovalSubject extends Subject {
+  /**
+   * Starts a fluent chain to do assertions on a {@link RequiredApproval}.
+   *
+   * @param requiredApproval the required approval on which assertions should be done
+   * @return the created {@link RequiredApprovalSubject}
+   */
+  public static RequiredApprovalSubject assertThat(RequiredApproval requiredApproval) {
+    return assertAbout(requiredApprovals()).that(requiredApproval);
+  }
+
+  /**
+   * Starts a fluent chain to do assertions on an {@link Optional} {@link RequiredApproval}.
+   *
+   * @param requiredApproval optional required approval on which assertions should be done
+   * @return the created {@link OptionalSubject}
+   */
+  public static OptionalSubject<RequiredApprovalSubject, RequiredApproval> assertThat(
+      Optional<RequiredApproval> requiredApproval) {
+    return OptionalSubject.assertThat(requiredApproval, requiredApprovals());
+  }
+
+  /**
+   * Starts a fluent chain to do assertions on a list of {@link RequiredApproval}s.
+   *
+   * @param requiredApprovals list of required approvals on which assertions should be done
+   * @return the created {@link ListSubject}
+   */
+  public static ListSubject<RequiredApprovalSubject, RequiredApproval> assertThat(
+      ImmutableList<RequiredApproval> requiredApprovals) {
+    return ListSubject.assertThat(requiredApprovals, requiredApprovals());
+  }
+
+  /**
+   * Creates a subject factory for mapping {@link RequiredApproval}s to {@link
+   * RequiredApprovalSubject}s.
+   */
+  private static Subject.Factory<RequiredApprovalSubject, RequiredApproval> requiredApprovals() {
+    return RequiredApprovalSubject::new;
+  }
+
+  private final RequiredApproval requiredApproval;
+
+  private RequiredApprovalSubject(FailureMetadata metadata, RequiredApproval requiredApproval) {
+    super(metadata, requiredApproval);
+    this.requiredApproval = requiredApproval;
+  }
+
+  /** Returns a subject for the label name. */
+  public StringSubject hasLabelNameThat() {
+    return check("labelName()").that(requiredApproval().labelType().getName());
+  }
+
+  /** Returns a subject for the value. */
+  public IntegerSubject hasValueThat() {
+    return check("value()").that((int) requiredApproval().value());
+  }
+
+  private RequiredApproval requiredApproval() {
+    isNotNull();
+    return requiredApproval;
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
index 94bd27d..daf08e2 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
@@ -17,7 +17,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
@@ -75,11 +77,9 @@
     Config cfg = new Config();
     cfg.setBoolean(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         StatusConfig.KEY_DISABLED,
-        /** value = */
-        true);
+        /* value= */ true);
     setCodeOwnersConfig(cfg);
 
     PushResult r = pushRefsMetaConfig();
@@ -94,8 +94,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         StatusConfig.KEY_DISABLED_BRANCH,
         "refs/heads/master");
     setCodeOwnersConfig(cfg);
@@ -113,8 +112,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         StatusConfig.KEY_DISABLED,
         "INVALID");
     setCodeOwnersConfig(cfg);
@@ -135,8 +133,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         StatusConfig.KEY_DISABLED_BRANCH,
         "^refs/heads/[");
     setCodeOwnersConfig(cfg);
@@ -157,8 +154,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         BackendConfig.KEY_BACKEND,
         CodeOwnerBackendId.PROTO.getBackendId());
     setCodeOwnersConfig(cfg);
@@ -194,8 +190,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         BackendConfig.KEY_BACKEND,
         "INVALID");
     setCodeOwnersConfig(cfg);
@@ -237,8 +232,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
         "Code-Review+2");
     setCodeOwnersConfig(cfg);
@@ -246,8 +240,8 @@
     PushResult r = pushRefsMetaConfig();
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(2);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
   }
 
   @Test
@@ -257,8 +251,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
         "INVALID");
     setCodeOwnersConfig(cfg);
@@ -276,14 +269,40 @@
   }
 
   @Test
+  public void allRequiredApprovalsAreValidated() throws Exception {
+    fetchRefsMetaConfig();
+
+    ImmutableList<String> invalidValues = ImmutableList.of("INVALID", "ALSO_INVALID");
+    Config cfg = new Config();
+    cfg.setStringList(
+        CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
+        invalidValues);
+    setCodeOwnersConfig(cfg);
+
+    PushResult r = pushRefsMetaConfig();
+    assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus())
+        .isEqualTo(Status.REJECTED_OTHER_REASON);
+    for (String invalidValue : invalidValues) {
+      assertThat(r.getMessages())
+          .contains(
+              String.format(
+                  "Required approval '%s' that is configured in code-owners.config (parameter"
+                      + " codeOwners.%s) is invalid: Invalid format, expected"
+                      + " '<label-name>+<label-value>'.",
+                  invalidValue, RequiredApprovalConfig.KEY_REQUIRED_APPROVAL));
+    }
+  }
+
+  @Test
   public void configureOverrideApproval() throws Exception {
     fetchRefsMetaConfig();
 
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
         "Code-Review+2");
     setCodeOwnersConfig(cfg);
@@ -292,8 +311,9 @@
     assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK);
     Optional<RequiredApproval> overrideApproval =
         codeOwnersPluginConfiguration.getOverrideApproval(project);
-    assertThat(overrideApproval.get().labelType().getName()).isEqualTo("Code-Review");
-    assertThat(overrideApproval.get().value()).isEqualTo(2);
+    assertThat(overrideApproval).isPresent();
+    assertThat(overrideApproval).value().hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(overrideApproval).value().hasValueThat().isEqualTo(2);
   }
 
   @Test
@@ -303,8 +323,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
         "INVALID");
     setCodeOwnersConfig(cfg);
@@ -322,14 +341,40 @@
   }
 
   @Test
+  public void allOverrideApprovalsAreValidated() throws Exception {
+    fetchRefsMetaConfig();
+
+    ImmutableList<String> invalidValues = ImmutableList.of("INVALID", "ALSO_INVALID");
+    Config cfg = new Config();
+    cfg.setStringList(
+        CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
+        invalidValues);
+    setCodeOwnersConfig(cfg);
+
+    PushResult r = pushRefsMetaConfig();
+    assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus())
+        .isEqualTo(Status.REJECTED_OTHER_REASON);
+    for (String invalidValue : invalidValues) {
+      assertThat(r.getMessages())
+          .contains(
+              String.format(
+                  "Required approval '%s' that is configured in code-owners.config (parameter"
+                      + " codeOwners.%s) is invalid: Invalid format, expected"
+                      + " '<label-name>+<label-value>'.",
+                  invalidValue, OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL));
+    }
+  }
+
+  @Test
   public void configureMergeCommitStrategy() throws Exception {
     fetchRefsMetaConfig();
 
     Config cfg = new Config();
     cfg.setEnum(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         GeneralConfig.KEY_MERGE_COMMIT_STRATEGY,
         MergeCommitStrategy.ALL_CHANGED_FILES);
     setCodeOwnersConfig(cfg);
@@ -347,8 +392,7 @@
     Config cfg = new Config();
     cfg.setString(
         CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         GeneralConfig.KEY_MERGE_COMMIT_STRATEGY,
         "INVALID");
     setCodeOwnersConfig(cfg);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
index 9b05857..d5a840c 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInChangeIT.java
@@ -62,11 +62,7 @@
   public void createTestChange() throws Exception {
     changeOwner =
         accountCreator.create(
-            "changeOwner",
-            "changeOwner@example.com",
-            "ChangeOwner",
-            /** displayName = */
-            null);
+            "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null);
     TestRepository<InMemoryRepository> testRepo = cloneProject(project, changeOwner);
     // Create a change that contains files for all paths that are used in the tests. This is
     // necessary since CodeOwnersInChangeCollection rejects requests for paths that are not present
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java
new file mode 100644
index 0000000..ec0a368
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java
@@ -0,0 +1,735 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigSubject.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.api.RenameEmailInput;
+import com.google.gerrit.plugins.codeowners.api.RenameEmailResultInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFileUpdateScanner;
+import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
+import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.plugins.codeowners.restapi.RenameEmail;
+import com.google.inject.Inject;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Acceptance test for the {@link com.google.gerrit.plugins.codeowners.restapi.RenameEmail} REST
+ * endpoint.
+ *
+ * <p>Further tests for the {@link com.google.gerrit.plugins.codeowners.restapi.RenameEmail} REST
+ * endpoint that require using the REST API are implemented in {@link
+ * com.google.gerrit.plugins.codeowners.acceptance.restapi.RenameEmailRestIT}.
+ */
+public class RenameEmailIT extends AbstractCodeOwnersIT {
+  @Inject private AccountOperations accountOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  private BackendConfig backendConfig;
+  private CodeOwnerConfigFileUpdateScanner codeOwnerConfigFileUpdateScanner;
+
+  @Before
+  public void setup() throws Exception {
+    backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
+    codeOwnerConfigFileUpdateScanner =
+        plugin.getSysInjector().getInstance(CodeOwnerConfigFileUpdateScanner.class);
+  }
+
+  @Test
+  public void oldEmailIsRequired() throws Exception {
+    RenameEmailInput input = new RenameEmailInput();
+    input.newEmail = "new@example.com";
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> renameEmail(project, "master", input));
+    assertThat(exception).hasMessageThat().isEqualTo("old email is required");
+  }
+
+  @Test
+  public void newEmailIsRequired() throws Exception {
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = "old@example.com";
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> renameEmail(project, "master", input));
+    assertThat(exception).hasMessageThat().isEqualTo("new email is required");
+  }
+
+  @Test
+  public void oldEmailNotResolvable() throws Exception {
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = "unknown@example.com";
+    input.newEmail = admin.email();
+    UnprocessableEntityException exception =
+        assertThrows(
+            UnprocessableEntityException.class, () -> renameEmail(project, "master", input));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(String.format("cannot resolve email %s", input.oldEmail));
+  }
+
+  @Test
+  public void newEmailNotResolvable() throws Exception {
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = admin.email();
+    input.newEmail = "unknown@example.com";
+    UnprocessableEntityException exception =
+        assertThrows(
+            UnprocessableEntityException.class, () -> renameEmail(project, "master", input));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(String.format("cannot resolve email %s", input.newEmail));
+  }
+
+  @Test
+  public void emailsMustBelongToSameAccount() throws Exception {
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = admin.email();
+    input.newEmail = user.email();
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> renameEmail(project, "master", input));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "emails must belong to the same account"
+                    + " (old email %s is owned by account %d, new email %s is owned by account %d)",
+                admin.email(), admin.id().get(), user.email(), user.id().get()));
+  }
+
+  @Test
+  public void oldAndNewEmailMustDiffer() throws Exception {
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = admin.email();
+    input.newEmail = admin.email();
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> renameEmail(project, "master", input));
+    assertThat(exception).hasMessageThat().isEqualTo("old and new email must differ");
+  }
+
+  @Test
+  public void renameEmailRequiresDirectPushPermissionsForNonProjectOwner() throws Exception {
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    requestScopeOperations.setApiUser(user.id());
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    AuthException exception =
+        assertThrows(AuthException.class, () -> renameEmail(project, "master", input));
+    assertThat(exception).hasMessageThat().isEqualTo("not permitted: update on refs/heads/master");
+  }
+
+  @Test
+  public void renameEmail_noCodeOwnerConfig() throws Exception {
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNull();
+  }
+
+  @Test
+  public void renameEmail_noUpdateIfEmailIsNotContainedInCodeOwnerConfigs() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/foo/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNull();
+  }
+
+  @Test
+  public void renameOwnEmailWithDirectPushPermission() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(admin.email())
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    // grant all users direct push permissions
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    requestScopeOperations.setApiUser(user.id());
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail, admin.email());
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail);
+  }
+
+  @Test
+  public void renameOtherEmailWithDirectPushPermission() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(admin.email())
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    // grant all users direct push permissions
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    // Allow all users to see secondary emails.
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
+        .update();
+
+    String secondaryEmail = "admin-foo@example.com";
+    accountOperations.account(admin.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    requestScopeOperations.setApiUser(user.id());
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = admin.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail, user.email());
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail);
+  }
+
+  @Test
+  public void renameOwnEmailAsProjectOwner() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    String secondaryEmail = "admin-foo@example.com";
+    accountOperations.account(admin.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = admin.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail, user.email());
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail);
+  }
+
+  @Test
+  public void renameOtherEmailAsProjectOwner() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey1 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    CodeOwnerConfig.Key codeOwnerConfigKey2 =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/foo/")
+            .addCodeOwnerEmail(user.email())
+            .create();
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail, admin.email());
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail);
+  }
+
+  @Test
+  public void renameEmail_callingUserBecomesCommitAuthor() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+    assertThat(result.commit.author.email).isEqualTo(admin.email());
+  }
+
+  @Test
+  public void renameEmailWithDefaultCommitMessage() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+    assertThat(result.commit.message).isEqualTo(RenameEmail.DEFAULT_COMMIT_MESSAGE);
+  }
+
+  @Test
+  public void renameEmailWithSpecifiedCommitMessage() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    input.message = "Update email with custom message";
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+    assertThat(result.commit.message).isEqualTo(input.message);
+  }
+
+  @Test
+  public void renameEmail_specifiedCommitMessageIsTrimmed() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    String message = "Update email with custom message";
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    input.message = "  " + message + "\t";
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+    assertThat(result.commit.message).isEqualTo(message);
+  }
+
+  @Test
+  public void renameEmail_lineCommentsArePreserved() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    // insert some comments
+    codeOwnerConfigFileUpdateScanner.update(
+        BranchNameKey.create(project, "master"),
+        "Insert comments",
+        (codeOwnerConfigFilePath, codeOwnerConfigFileContent) -> {
+          StringBuilder b = new StringBuilder();
+          // insert comment line at the top of the file
+          b.append("# top comment\n");
+
+          String[] lines = codeOwnerConfigFileContent.split("\\n");
+          b.append(lines[0] + "\n");
+
+          // insert comment line in the middle of the file
+          b.append("# middle comment\n");
+
+          for (int n = 1; n < lines.length; n++) {
+            b.append(lines[n] + "\n");
+          }
+
+          // insert comment line at the bottom of the file
+          b.append("# bottom comment\n");
+
+          return Optional.of(b.toString());
+        });
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    renameEmail(project, "master", input);
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail, admin.email());
+
+    // verify that the comments are still present
+    String codeOwnerConfigFileContent =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getContent();
+    String[] lines = codeOwnerConfigFileContent.split("\\n");
+    assertThat(lines[0]).isEqualTo("# top comment");
+    assertThat(lines[2]).isEqualTo("# middle comment");
+    assertThat(lines[lines.length - 1]).isEqualTo("# bottom comment");
+  }
+
+  @Test
+  public void renameEmail_inlineCommentsArePreserved() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    // insert some inline comments
+    codeOwnerConfigFileUpdateScanner.update(
+        BranchNameKey.create(project, "master"),
+        "Insert comments",
+        (codeOwnerConfigFilePath, codeOwnerConfigFileContent) -> {
+          StringBuilder b = new StringBuilder();
+          for (String line : codeOwnerConfigFileContent.split("\\n")) {
+            if (line.contains(user.email())) {
+              b.append(line + "# some comment\n");
+              continue;
+            }
+            if (line.contains(admin.email())) {
+              b.append(line + "# other comment\n");
+              continue;
+            }
+            b.append(line + "\n");
+          }
+
+          return Optional.of(b.toString());
+        });
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    renameEmail(project, "master", input);
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail, admin.email());
+
+    // verify that the inline comments are still present
+    String codeOwnerConfigFileContent =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getContent();
+    for (String line : codeOwnerConfigFileContent.split("\\n")) {
+      if (line.contains(secondaryEmail)) {
+        assertThat(line).endsWith("# some comment");
+      } else if (line.contains(admin.email())) {
+        assertThat(line).endsWith("# other comment");
+      }
+    }
+  }
+
+  @Test
+  public void renameEmail_emailInCommentIsReplaced() throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerEmail(admin.email())
+            .create();
+
+    // insert some comments
+    codeOwnerConfigFileUpdateScanner.update(
+        BranchNameKey.create(project, "master"),
+        "Insert comments",
+        (codeOwnerConfigFilePath, codeOwnerConfigFileContent) ->
+            Optional.of("# foo " + user.email() + " bar\n" + codeOwnerConfigFileContent));
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    renameEmail(project, "master", input);
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(secondaryEmail, admin.email());
+
+    // verify that the comments are still present
+    String codeOwnerConfigFileContent =
+        codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getContent();
+    assertThat(codeOwnerConfigFileContent.split("\\n")[0])
+        .endsWith("# foo " + secondaryEmail + " bar");
+  }
+
+  @Test
+  public void renameEmail_emailThatContainsEmailToBeReplacesAsSubstringStaysIntact()
+      throws Exception {
+    // renaming email is not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    TestAccount otherUser1 =
+        accountCreator.create(
+            "otherUser1", "foo" + user.email(), "Other User 1", /* displayName= */ null);
+    TestAccount otherUser2 =
+        accountCreator.create(
+            "otherUser2", user.email() + "bar", "Other User 2", /* displayName= */ null);
+    TestAccount otherUser3 =
+        accountCreator.create(
+            "otherUser3", "foo" + user.email() + "bar", "Other User 3", /* displayName= */ null);
+
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath("/")
+            .addCodeOwnerEmail(user.email())
+            .addCodeOwnerEmail(otherUser1.email())
+            .addCodeOwnerEmail(otherUser2.email())
+            .addCodeOwnerEmail(otherUser3.email())
+            .create();
+
+    // grant all users direct push permissions
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    String secondaryEmail = "user-new@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    requestScopeOperations.setApiUser(user.id());
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RenameEmailResultInfo result = renameEmail(project, "master", input);
+    assertThat(result.commit).isNotNull();
+
+    assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get())
+        .hasCodeOwnerSetsThat()
+        .onlyElement()
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(
+            secondaryEmail, otherUser1.email(), otherUser2.email(), otherUser3.email());
+  }
+
+  private RenameEmailResultInfo renameEmail(
+      Project.NameKey projectName, String branchName, RenameEmailInput input)
+      throws RestApiException {
+    return projectCodeOwnersApiFactory
+        .project(projectName)
+        .branch(branchName)
+        .renameEmailInCodeOwnerConfigFiles(input);
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
index aa43cc1..d70be9f 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
@@ -52,7 +52,8 @@
   private static final ImmutableList<RestCall> BRANCH_ENDPOINTS =
       ImmutableList.of(
           RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.config_files"),
-          RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.branch_config"));
+          RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.branch_config"),
+          RestCall.post("/projects/%s/branches/%s/code-owners~code_owners.rename"));
 
   private static final ImmutableList<RestCall> BRANCH_CODE_OWNER_CONFIGS_ENDPOINTS =
       ImmutableList.of(RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.config/%s"));
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/GetCodeOwnersForPathInChangeRestIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/GetCodeOwnersForPathInChangeRestIT.java
index 5583fdc..3c88f2d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/GetCodeOwnersForPathInChangeRestIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/GetCodeOwnersForPathInChangeRestIT.java
@@ -40,11 +40,7 @@
   public void createTestChange() throws Exception {
     TestAccount changeOwner =
         accountCreator.create(
-            "changeOwner",
-            "changeOwner@example.com",
-            "ChangeOwner",
-            /** displayName = */
-            null);
+            "changeOwner", "changeOwner@example.com", "ChangeOwner", /* displayName= */ null);
     // Create a change that contains the file that is used in the tests. This is necessary since
     // CodeOwnersInChangeCollection rejects requests for paths that are not present in the change.
     changeId =
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/RenameEmailRestIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/RenameEmailRestIT.java
new file mode 100644
index 0000000..5978272
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/RenameEmailRestIT.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.acceptance.restapi;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.api.RenameEmailInput;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
+import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
+import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.inject.Inject;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Acceptance test for the {@link com.google.gerrit.plugins.codeowners.restapi.RenameEmail} REST
+ * endpoint. that require using via REST.
+ *
+ * <p>Acceptance test for the {@link com.google.gerrit.plugins.codeowners.restapi.RenameEmail} REST
+ * endpoint that can use the Java API are implemented in {@link
+ * com.google.gerrit.plugins.codeowners.acceptance.api.RenameEmailIT}.
+ */
+public class RenameEmailRestIT extends AbstractCodeOwnersIT {
+  @Inject private AccountOperations accountOperations;
+
+  private BackendConfig backendConfig;
+
+  @Before
+  public void setup() throws Exception {
+    backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
+  }
+
+  @Test
+  public void cannotRenameEmailsAnonymously() throws Exception {
+    RestResponse r =
+        anonymousRestSession.post(
+            String.format(
+                "/projects/%s/branches/%s/code_owners.rename",
+                IdString.fromDecoded(project.get()), "master"));
+    r.assertForbidden();
+    assertThat(r.getEntityContent()).contains("Authentication required");
+  }
+
+  @Test
+  public void renameEmailNotSupported() throws Exception {
+    // renaming email is only unsupported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isInstanceOf(ProtoBackend.class);
+
+    String secondaryEmail = "user-foo@example.com";
+    accountOperations.account(user.id()).forUpdate().addSecondaryEmail(secondaryEmail).update();
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/")
+        .addCodeOwnerEmail(user.email())
+        .create();
+
+    RenameEmailInput input = new RenameEmailInput();
+    input.oldEmail = user.email();
+    input.newEmail = secondaryEmail;
+    RestResponse r =
+        adminRestSession.post(
+            String.format(
+                "/projects/%s/branches/%s/code_owners.rename",
+                IdString.fromDecoded(project.get()), "master"),
+            input);
+    r.assertMethodNotAllowed();
+    assertThat(r.getEntityContent())
+        .contains(
+            String.format(
+                "rename email not supported by %s backend",
+                CodeOwnerBackendId.getBackendId(backendConfig.getDefaultBackend().getClass())));
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java
index 278dcb3..17e3ccd 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java
@@ -334,9 +334,7 @@
   @Test
   public void setIgnoreParentCodeOwners() throws Exception {
     CodeOwnerConfig codeOwnerConfig =
-        createCodeOwnerConfig(
-            /** ignoreParentCodeOwners = */
-            false, admin.email());
+        createCodeOwnerConfig(/* ignoreParentCodeOwners= */ false, admin.email());
     codeOwnerConfigOperations
         .codeOwnerConfig(codeOwnerConfig.key())
         .forUpdate()
@@ -350,9 +348,7 @@
   @Test
   public void unsetIgnoreParentCodeOwners() throws Exception {
     CodeOwnerConfig codeOwnerConfig =
-        createCodeOwnerConfig(
-            /** ignoreParentCodeOwners = */
-            true, admin.email());
+        createCodeOwnerConfig(/* ignoreParentCodeOwners= */ true, admin.email());
     codeOwnerConfigOperations
         .codeOwnerConfig(codeOwnerConfig.key())
         .forUpdate()
@@ -429,8 +425,8 @@
     // Create a code owner config that contains only a single code owner set.
     CodeOwnerConfig codeOwnerConfig =
         createCodeOwnerConfig(
-            /** ignoreParentCodeOwners = */
-            false, CodeOwnerSetModification.set(ImmutableList.of(codeOwnerSet)));
+            /* ignoreParentCodeOwners= */ false,
+            CodeOwnerSetModification.set(ImmutableList.of(codeOwnerSet)));
 
     // Remove all code owners so that the code owner set becomes empty.
     codeOwnerConfigOperations
@@ -713,9 +709,7 @@
   }
 
   private CodeOwnerConfig createCodeOwnerConfig(String... emails) {
-    return createCodeOwnerConfig(
-        /** ignoreParentCodeOwners = */
-        false, emails);
+    return createCodeOwnerConfig(/* ignoreParentCodeOwners= */ false, emails);
   }
 
   private CodeOwnerConfig createCodeOwnerConfig(boolean ignoreParentCodeOwners, String... emails) {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
index 563ed57..da58fca 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -70,10 +70,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwnerApprovalCheck.getFileStatuses(
-                    /** changeNotes = */
-                    null));
+            () -> codeOwnerApprovalCheck.getFileStatuses(/* changeNotes= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("changeNotes");
   }
 
@@ -601,16 +598,14 @@
   @Test
   public void getStatusForFileAddition_noImplicitApprovalByPatchSetUploader() throws Exception {
     testImplicitApprovalByPatchSetUploaderOnGetStatusForFileAddition(
-        /** implicitApprovalsEnabled = */
-        false);
+        /* implicitApprovalsEnabled= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void getStatusForFileAddition_withImplicitApprovalByPatchSetUploader() throws Exception {
     testImplicitApprovalByPatchSetUploaderOnGetStatusForFileAddition(
-        /** implicitApprovalsEnabled = */
-        true);
+        /* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitApprovalByPatchSetUploaderOnGetStatusForFileAddition(
@@ -649,8 +644,7 @@
   @Test
   public void getStatusForFileModification_noImplicitApprovalByPatchSetUploader() throws Exception {
     testImplicitApprovalByPatchSetUploaderOnGetStatusForFileModification(
-        /** implicitApprovalsEnabled = */
-        false);
+        /* implicitApprovalsEnabled= */ false);
   }
 
   @Test
@@ -658,8 +652,7 @@
   public void getStatusForFileModification_withImplicitApprovalByPatchSetUploader()
       throws Exception {
     testImplicitApprovalByPatchSetUploaderOnGetStatusForFileModification(
-        /** implicitApprovalsEnabled = */
-        true);
+        /* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitApprovalByPatchSetUploaderOnGetStatusForFileModification(
@@ -700,16 +693,14 @@
   @Test
   public void getStatusForFileDeletion_noImplicitApprovalByPatchSetUploader() throws Exception {
     testImplicitApprovalByPatchSetUploaderOnGetStatusForFileDeletion(
-        /** implicitApprovalsEnabled = */
-        false);
+        /* implicitApprovalsEnabled= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void getStatusForFileDeletion_withImplicitApprovalByPatchSetUploader() throws Exception {
     testImplicitApprovalByPatchSetUploaderOnGetStatusForFileDeletion(
-        /** implicitApprovalsEnabled = */
-        true);
+        /* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitApprovalByPatchSetUploaderOnGetStatusForFileDeletion(
@@ -748,8 +739,7 @@
   public void getStatusForFileRename_noImplicitApprovalByPatchSetUploaderOnOldPath()
       throws Exception {
     testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnOldPath(
-        /** implicitApprovalsEnabled = */
-        false);
+        /* implicitApprovalsEnabled= */ false);
   }
 
   @Test
@@ -757,8 +747,7 @@
   public void getStatusForFileRename_withImplicitApprovalByPatchSetUploaderOnOldPath()
       throws Exception {
     testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnOldPath(
-        /** implicitApprovalsEnabled = */
-        true);
+        /* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnOldPath(
@@ -803,8 +792,7 @@
   public void getStatusForFileRename_noImplicitApprovalByPatchSetUploaderOnNewPath()
       throws Exception {
     testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnNewPath(
-        /** implicitApprovalsEnabled = */
-        false);
+        /* implicitApprovalsEnabled= */ false);
   }
 
   @Test
@@ -812,8 +800,7 @@
   public void getStatusForFileRename_withImplicitApprovalByPatchSetUploaderOnNewPath()
       throws Exception {
     testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnNewPath(
-        /** implicitApprovalsEnabled = */
-        true);
+        /* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitApprovalByPatchSetUploaderOnStatusForFileRenameOnNewPath(
@@ -967,17 +954,13 @@
 
   @Test
   public void everyoneIsCodeOwner_noImplicitApproval() throws Exception {
-    testImplicitlyApprovedWhenEveryoneIsCodeOwner(
-        /** implicitApprovalsEnabled = */
-        false);
+    testImplicitlyApprovedWhenEveryoneIsCodeOwner(/* implicitApprovalsEnabled= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void everyoneIsCodeOwner_withImplicitApproval() throws Exception {
-    testImplicitlyApprovedWhenEveryoneIsCodeOwner(
-        /** implicitApprovalsEnabled = */
-        true);
+    testImplicitlyApprovedWhenEveryoneIsCodeOwner(/* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitlyApprovedWhenEveryoneIsCodeOwner(boolean implicitApprovalsEnabled)
@@ -1057,28 +1040,19 @@
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com")
   public void approvedByGlobalCodeOwner() throws Exception {
-    testApprovedByGlobalCodeOwner(
-        /** bootstrappingMode = */
-        false);
+    testApprovedByGlobalCodeOwner(/* bootstrappingMode= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com")
   public void approvedByGlobalCodeOwner_bootstrappingMode() throws Exception {
-    testApprovedByGlobalCodeOwner(
-        /** bootstrappingMode = */
-        true);
+    testApprovedByGlobalCodeOwner(/* bootstrappingMode= */ true);
   }
 
   private void testApprovedByGlobalCodeOwner(boolean bootstrappingMode) throws Exception {
     // Create a bot user that is a global code owner.
     TestAccount bot =
-        accountCreator.create(
-            "bot",
-            "bot@example.com",
-            "Bot",
-            /** displayName = */
-            null);
+        accountCreator.create("bot", "bot@example.com", "Bot", /* displayName= */ null);
 
     if (!bootstrappingMode) {
       // Create a code owner config file so that we are not in the bootstrapping mode.
@@ -1134,10 +1108,7 @@
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com")
   public void globalCodeOwner_noImplicitApproval() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        false,
-        /** bootstrappingMode = */
-        false);
+        /* implicitApprovalsEnabled= */ false, /* bootstrappingMode= */ false);
   }
 
   @Test
@@ -1145,20 +1116,14 @@
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void globalCodeOwner_withImplicitApproval() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        true,
-        /** bootstrappingMode = */
-        false);
+        /* implicitApprovalsEnabled= */ true, /* bootstrappingMode= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com")
   public void globalCodeOwner_noImplicitApproval_bootstrappingMode() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        false,
-        /** bootstrappingMode = */
-        true);
+        /* implicitApprovalsEnabled= */ false, /* bootstrappingMode= */ true);
   }
 
   @Test
@@ -1166,21 +1131,13 @@
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void globalCodeOwner_withImplicitApproval_bootstrappingMode() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        true,
-        /** bootstrappingMode = */
-        true);
+        /* implicitApprovalsEnabled= */ true, /* bootstrappingMode= */ true);
   }
 
   private void testImplicitlyApprovedByGlobalCodeOwner(
       boolean implicitApprovalsEnabled, boolean bootstrappingMode) throws Exception {
     TestAccount bot =
-        accountCreator.create(
-            "bot",
-            "bot@example.com",
-            "Bot",
-            /** displayName = */
-            null);
+        accountCreator.create("bot", "bot@example.com", "Bot", /* displayName= */ null);
 
     if (!bootstrappingMode) {
       codeOwnerConfigOperations
@@ -1215,28 +1172,19 @@
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com")
   public void globalCodeOwnerAsReviewer() throws Exception {
-    testGlobalCodeOwnerAsReviewer(
-        /** bootstrappingMode = */
-        false);
+    testGlobalCodeOwnerAsReviewer(/* bootstrappingMode= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com")
   public void globalCodeOwnerAsReviewer_bootstrappingMode() throws Exception {
-    testGlobalCodeOwnerAsReviewer(
-        /** bootstrappingMode = */
-        true);
+    testGlobalCodeOwnerAsReviewer(/* bootstrappingMode= */ true);
   }
 
   private void testGlobalCodeOwnerAsReviewer(boolean bootstrappingMode) throws Exception {
     // Create a bot user that is a global code owner.
     TestAccount bot =
-        accountCreator.create(
-            "bot",
-            "bot@example.com",
-            "Bot",
-            /** displayName = */
-            null);
+        accountCreator.create("bot", "bot@example.com", "Bot", /* displayName= */ null);
 
     if (!bootstrappingMode) {
       // Create a code owner config file so that we are not in the bootstrapping mode.
@@ -1305,17 +1253,13 @@
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
   public void approvedByAnyoneWhenEveryoneIsGlobalCodeOwner() throws Exception {
-    testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(
-        /** bootstrappingMode = */
-        false);
+    testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(/* bootstrappingMode= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
   public void approvedByAnyoneWhenEveryoneIsGlobalCodeOwner_bootstrappingMode() throws Exception {
-    testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(
-        /** bootstrappingMode = */
-        true);
+    testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(/* bootstrappingMode= */ true);
   }
 
   private void testApprovedByAnyoneWhenEveryoneIsGlobalCodeOwner(boolean bootstrappingMode)
@@ -1369,10 +1313,7 @@
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
   public void everyoneIsGlobalCodeOwner_noImplicitApproval() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        false,
-        /** bootstrappingMode = */
-        false);
+        /* implicitApprovalsEnabled= */ false, /* bootstrappingMode= */ false);
   }
 
   @Test
@@ -1380,20 +1321,14 @@
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void everyoneIsGlobalCodeOwner_withImplicitApproval() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        true,
-        /** bootstrappingMode = */
-        false);
+        /* implicitApprovalsEnabled= */ true, /* bootstrappingMode= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
   public void everyoneIsGlobalCodeOwner_noImplicitApproval_bootstrappingMode() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        false,
-        /** bootstrappingMode = */
-        true);
+        /* implicitApprovalsEnabled= */ false, /* bootstrappingMode= */ true);
   }
 
   @Test
@@ -1401,10 +1336,7 @@
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void everyoneIsGlobalCodeOwner_withImplicitApproval_bootstrappingMode() throws Exception {
     testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
-        /** implicitApprovalsEnabled = */
-        true,
-        /** bootstrappingMode = */
-        true);
+        /* implicitApprovalsEnabled= */ true, /* bootstrappingMode= */ true);
   }
 
   private void testImplicitlyApprovedByGlobalCodeOwnerWhenEveryoneIsGlobalCodeOwner(
@@ -1442,17 +1374,13 @@
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
   public void anyReviewerWhenEveryoneIsGlobalCodeOwner() throws Exception {
-    testAnyReviewerWhenEveryoneIsGlobalCodeOwner(
-        /** bootstrappingMode = */
-        false);
+    testAnyReviewerWhenEveryoneIsGlobalCodeOwner(/* bootstrappingMode= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*")
   public void anyReviewerWhenEveryoneIsGlobalCodeOwner_bootstrappingMode() throws Exception {
-    testAnyReviewerWhenEveryoneIsGlobalCodeOwner(
-        /** bootstrappingMode = */
-        true);
+    testAnyReviewerWhenEveryoneIsGlobalCodeOwner(/* bootstrappingMode= */ true);
   }
 
   private void testAnyReviewerWhenEveryoneIsGlobalCodeOwner(boolean bootstrappingMode)
@@ -1506,12 +1434,7 @@
   public void parentCodeOwnerConfigsAreConsidered() throws Exception {
     TestAccount user2 = accountCreator.user2();
     TestAccount user3 =
-        accountCreator.create(
-            "user3",
-            "user3@example.com",
-            "User3",
-            /** displayName = */
-            null);
+        accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
 
     codeOwnerConfigOperations
         .newCodeOwnerConfig()
@@ -1617,10 +1540,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwnerApprovalCheck.isSubmittable(
-                    /** changeNotes = */
-                    null));
+            () -> codeOwnerApprovalCheck.isSubmittable(/* changeNotes= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("changeNotes");
   }
 
@@ -1710,12 +1630,7 @@
 
     TestAccount user2 = accountCreator.user2();
     TestAccount user3 =
-        accountCreator.create(
-            "user3",
-            "user3@example.com",
-            "User3",
-            /** displayName = */
-            null);
+        accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
 
     // Create change with a user that is not a project owner.
     Path path = Paths.get("/foo/bar.baz");
@@ -1813,16 +1728,14 @@
   @Test
   public void bootstrappingGetStatus_noImplicitApprovalByPatchSetUploader() throws Exception {
     testImplicitApprovalByPatchSetUploaderOnBootstrappingGetStatus(
-        /** implicitApprovalsEnabled = */
-        false);
+        /* implicitApprovalsEnabled= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void bootstrappingGetStatus_withImplicitApprovalByPatchSetUploader() throws Exception {
     testImplicitApprovalByPatchSetUploaderOnBootstrappingGetStatus(
-        /** implicitApprovalsEnabled = */
-        true);
+        /* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitApprovalByPatchSetUploaderOnBootstrappingGetStatus(
@@ -2206,17 +2119,13 @@
 
   @Test
   public void defaultCodeOwner_noImplicitApproval() throws Exception {
-    testImplicitlyApprovedByDefaultCodeOwner(
-        /** implicitApprovalsEnabled = */
-        false);
+    testImplicitlyApprovedByDefaultCodeOwner(/* implicitApprovalsEnabled= */ false);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true")
   public void defaultCodeOwner_withImplicitApproval() throws Exception {
-    testImplicitlyApprovedByDefaultCodeOwner(
-        /** implicitApprovalsEnabled = */
-        true);
+    testImplicitlyApprovedByDefaultCodeOwner(/* implicitApprovalsEnabled= */ true);
   }
 
   private void testImplicitlyApprovedByDefaultCodeOwner(boolean implicitApprovalsEnabled)
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScannerTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScannerTest.java
index 16ec682..839c77b 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScannerTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScannerTest.java
@@ -71,8 +71,7 @@
             NullPointerException.class,
             () ->
                 codeOwnerConfigFileUpdateScanner.update(
-                    /** branchNameKey = */
-                    null,
+                    /* branchNameKey= */ null,
                     "Update code owner configs",
                     (codeOwnerConfigFilePath, codeOwnerConfigFileContent) -> Optional.empty()));
     assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
@@ -87,8 +86,7 @@
             () ->
                 codeOwnerConfigFileUpdateScanner.update(
                     branchNameKey,
-                    /** commitMessage = */
-                    null,
+                    /* commitMessage= */ null,
                     (codeOwnerConfigFilePath, codeOwnerConfigFileContent) -> Optional.empty()));
     assertThat(npe).hasMessageThat().isEqualTo("commitMessage");
   }
@@ -103,8 +101,7 @@
                 codeOwnerConfigFileUpdateScanner.update(
                     branchNameKey,
                     "Update code owner configs",
-                    /** codeOwnerConfigFileUpdater = */
-                    null));
+                    /* codeOwnerConfigFileUpdater= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigFileUpdater");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java
index 18ba78f..2b30264 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchyTest.java
@@ -75,8 +75,7 @@
             NullPointerException.class,
             () ->
                 codeOwnerConfigHierarchy.visit(
-                    /** branchNameKey = */
-                    null,
+                    /* branchNameKey= */ null,
                     getCurrentRevision(BranchNameKey.create(project, "master")),
                     Paths.get("/foo/bar/baz.md"),
                     visitor));
@@ -91,8 +90,7 @@
             () ->
                 codeOwnerConfigHierarchy.visit(
                     BranchNameKey.create(project, "master"),
-                    /** revision = */
-                    null,
+                    /* revision= */ null,
                     Paths.get("/foo/bar/baz.md"),
                     visitor));
     assertThat(npe).hasMessageThat().isEqualTo("revision");
@@ -108,8 +106,7 @@
                 codeOwnerConfigHierarchy.visit(
                     branchNameKey,
                     getCurrentRevision(branchNameKey),
-                    /** absolutePath = */
-                    null,
+                    /* absolutePath= */ null,
                     visitor));
     assertThat(npe).hasMessageThat().isEqualTo("absolutePath");
   }
@@ -143,8 +140,7 @@
                     branchNameKey,
                     getCurrentRevision(branchNameKey),
                     Paths.get("/foo/bar/baz.md"),
-                    /** codeOwnerConfigVisitor = */
-                    null));
+                    /* codeOwnerConfigVisitor= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigVisitor");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
index 4a9faa7..c26f000 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScannerTest.java
@@ -70,9 +70,7 @@
             () ->
                 codeOwnerConfigScannerFactory
                     .create()
-                    .visit(
-                        /** branchNameKey = */
-                        null, visitor, invalidCodeOwnerConfigCallback));
+                    .visit(/* branchNameKey= */ null, visitor, invalidCodeOwnerConfigCallback));
     assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
   }
 
@@ -87,8 +85,7 @@
                     .create()
                     .visit(
                         branchNameKey,
-                        /** codeOwnerConfigVisitor = */
-                        null,
+                        /* codeOwnerConfigVisitor= */ null,
                         invalidCodeOwnerConfigCallback));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigVisitor");
   }
@@ -102,11 +99,7 @@
             () ->
                 codeOwnerConfigScannerFactory
                     .create()
-                    .visit(
-                        branchNameKey,
-                        visitor,
-                        /** invalidCodeOwnerConfigCallback = */
-                        null));
+                    .visit(branchNameKey, visitor, /* invalidCodeOwnerConfigCallback= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("invalidCodeOwnerConfigCallback");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java
index 433ff34..e3e9e6a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTest.java
@@ -109,10 +109,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwnerConfigKey.filePath(
-                    /** defaultCodeOwnerConfigFileName = */
-                    null));
+            () -> codeOwnerConfigKey.filePath(/* defaultCodeOwnerConfigFileName= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigFileName");
   }
 
@@ -121,11 +118,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                createCodeOwnerBuilder()
-                    .addCodeOwnerSet(
-                        /** codeOwnerSet = */
-                        null));
+            () -> createCodeOwnerBuilder().addCodeOwnerSet(/* codeOwnerSet= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerSet");
   }
 
@@ -134,11 +127,7 @@
     CodeOwnerConfig codeOwnerConfig = createCodeOwnerBuilder().build();
     NullPointerException npe =
         assertThrows(
-            NullPointerException.class,
-            () ->
-                codeOwnerConfig.relativize(
-                    /** path = */
-                    null));
+            NullPointerException.class, () -> codeOwnerConfig.relativize(/* path= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("path");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
index dc4646d..6e19e28 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -67,9 +67,7 @@
             () ->
                 codeOwnerResolver
                     .get()
-                    .resolve(
-                        /** codeOwnerReference = */
-                        (CodeOwnerReference) null));
+                    .resolve(/* codeOwnerReference= */ (CodeOwnerReference) null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerReference");
 
     npe =
@@ -78,9 +76,7 @@
             () ->
                 codeOwnerResolver
                     .get()
-                    .resolve(
-                        /** codeOwnerReferences = */
-                        (Set<CodeOwnerReference>) null));
+                    .resolve(/* codeOwnerReferences= */ (Set<CodeOwnerReference>) null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerReferences");
   }
 
@@ -118,12 +114,7 @@
             (a, u) ->
                 u.addExternalId(
                     ExternalId.create(
-                        "foo",
-                        "bar",
-                        user.id(),
-                        admin.email(),
-                        /** hashedPassword = */
-                        null)));
+                        "foo", "bar", user.id(), admin.email(), /* hashedPassword= */ null)));
 
     assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(admin.email()))).isEmpty();
   }
@@ -252,9 +243,7 @@
             () ->
                 codeOwnerResolver
                     .get()
-                    .resolvePathCodeOwners(
-                        /** codeOwnerConfig = */
-                        null, Paths.get("/README.md")));
+                    .resolvePathCodeOwners(/* codeOwnerConfig= */ null, Paths.get("/README.md")));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfig");
   }
 
@@ -270,10 +259,7 @@
             () ->
                 codeOwnerResolver
                     .get()
-                    .resolvePathCodeOwners(
-                        codeOwnerConfig,
-                        /** absolutePath = */
-                        null));
+                    .resolvePathCodeOwners(codeOwnerConfig, /* absolutePath= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("absolutePath");
   }
 
@@ -354,12 +340,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwnerResolver
-                    .get()
-                    .isEmailDomainAllowed(
-                        /** email = */
-                        null));
+            () -> codeOwnerResolver.get().isEmailDomainAllowed(/* email= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("email");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoringTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoringTest.java
index 3deb240..0cab290 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoringTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerScoringTest.java
@@ -32,10 +32,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                builder.putValueForCodeOwner(
-                    /** codeOwner = */
-                    null, 50));
+            () -> builder.putValueForCodeOwner(/* codeOwner= */ null, 50));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwner");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSetTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSetTest.java
index fccd87c..3b6304f 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSetTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSetTest.java
@@ -45,11 +45,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                CodeOwnerSet.builder()
-                    .addCodeOwner(
-                        /** codeOwnerReference = */
-                        null));
+            () -> CodeOwnerSet.builder().addCodeOwner(/* codeOwnerReference= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerReference");
   }
 
@@ -83,11 +79,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                CodeOwnerSet.builder()
-                    .addCodeOwnerEmail(
-                        /** email = */
-                        null));
+            () -> CodeOwnerSet.builder().addCodeOwnerEmail(/* email= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerEmail");
   }
 
@@ -121,11 +113,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                CodeOwnerSet.builder()
-                    .addPathExpression(
-                        /** pathExpression = */
-                        null));
+            () -> CodeOwnerSet.builder().addPathExpression(/* pathExpression= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("pathExpression");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRuleTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRuleTest.java
index 25705d9..8e03935 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRuleTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRuleTest.java
@@ -115,11 +115,7 @@
   @Test
   public void ruleErrorWhenChangeDataIsNull() throws Exception {
     SubmitRecordSubject submitRecordSubject =
-        assertThatOptional(
-                codeOwnerSubmitRule.evaluate(
-                    /** changeData = */
-                    null))
-            .value();
+        assertThatOptional(codeOwnerSubmitRule.evaluate(/* changeData= */ null)).value();
     submitRecordSubject.hasStatusThat().isRuleError();
     submitRecordSubject.hasErrorMessageThat().isEqualTo("Failed to evaluate code owner statuses.");
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersTest.java
index 78835dc..cb065a0 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersTest.java
@@ -53,10 +53,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwners.get(
-                    /** codeOwnerConfigKey = */
-                    null, TEST_REVISION));
+            () -> codeOwners.get(/* codeOwnerConfigKey= */ null, TEST_REVISION));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigKey");
   }
 
@@ -67,9 +64,7 @@
             NullPointerException.class,
             () ->
                 codeOwners.get(
-                    CodeOwnerConfig.Key.create(project, "master", "/"),
-                    /** folderPath = */
-                    null));
+                    CodeOwnerConfig.Key.create(project, "master", "/"), /* folderPath= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("revision");
   }
 
@@ -79,10 +74,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwners.getFromCurrentRevision(
-                    /** codeOwnerConfigKey = */
-                    null));
+            () -> codeOwners.getFromCurrentRevision(/* codeOwnerConfigKey= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigKey");
   }
 
@@ -95,20 +87,13 @@
             .addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
             .build();
     CodeOwnerBackend codeOwnerBackendMock = mock(CodeOwnerBackend.class);
-    when(codeOwnerBackendMock.getCodeOwnerConfig(
-            codeOwnerConfigKey,
-            /** revision = */
-            null))
+    when(codeOwnerBackendMock.getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null))
         .thenReturn(Optional.of(expectedCodeOwnersConfig));
     try (AutoCloseable registration = registerTestBackend("test-backend", codeOwnerBackendMock)) {
       Optional<CodeOwnerConfig> codeOwnerConfig =
           codeOwners.getFromCurrentRevision(codeOwnerConfigKey);
       assertThat(codeOwnerConfig).value().isEqualTo(expectedCodeOwnersConfig);
-      verify(codeOwnerBackendMock)
-          .getCodeOwnerConfig(
-              codeOwnerConfigKey,
-              /** revision = */
-              null);
+      verify(codeOwnerBackendMock).getCodeOwnerConfig(codeOwnerConfigKey, /* revision= */ null);
     }
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdateTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdateTest.java
index 813f44f..ec7ec15 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdateTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersUpdateTest.java
@@ -66,9 +66,7 @@
   @GerritConfig(name = "plugin.code-owners.backend", value = "test-backend")
   public void codeOwnerUpdateIsForwardedToConfiguredBackendServerInitiated() throws Exception {
     testCodeOwnerUpdateIsForwardedToConfiguredBackend(
-        serverInitiatedCodeOwnersUpdate.get(),
-        /** currentUser = */
-        null);
+        serverInitiatedCodeOwnersUpdate.get(), /* currentUser= */ null);
   }
 
   private void testCodeOwnerUpdateIsForwardedToConfiguredBackend(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
index b92fc0c..9fbc92c 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
@@ -16,10 +16,12 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.joining;
 
+import com.google.common.base.Joiner;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.plugins.codeowners.backend.AbstractCodeOwnerConfigParserTest;
@@ -34,6 +36,7 @@
 import com.google.gerrit.plugins.codeowners.testing.CodeOwnerSetSubject;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.regex.Pattern;
 import org.junit.Test;
 
 /** Tests for {@link FindOwnersCodeOwnerConfigParser}. */
@@ -603,4 +606,125 @@
               .isEqualTo(CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY);
         });
   }
+
+  @Test
+  public void replaceEmail_contentCannotBeNull() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                FindOwnersCodeOwnerConfigParser.replaceEmail(
+                    /* content= */ null, admin.email(), user.email()));
+    assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigFileContent");
+  }
+
+  @Test
+  public void replaceEmail_oldEmailCannotBeNull() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                FindOwnersCodeOwnerConfigParser.replaceEmail(
+                    "content", /* oldEmail= */ null, user.email()));
+    assertThat(npe).hasMessageThat().isEqualTo("oldEmail");
+  }
+
+  @Test
+  public void replaceEmail_newEmailCannotBeNull() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                FindOwnersCodeOwnerConfigParser.replaceEmail(
+                    "content", admin.email(), /* newEmail= */ null));
+    assertThat(npe).hasMessageThat().isEqualTo("newEmail");
+  }
+
+  @Test
+  public void replaceEmail() throws Exception {
+    String oldEmail = "old@example.com";
+    String newEmail = "new@example.com";
+
+    // In the following test lines '${email} is used as placefolder for the email that we expect to
+    // be replaced.
+    String[] testLines = {
+      // line = email
+      "${email}",
+      // line = email with leading whitespace
+      " ${email}",
+      // line = email with trailing whitespace
+      "${email} ",
+      // line = email with leading and trailing whitespace
+      " ${email} ",
+      // line = email with comment
+      "${email}# comment",
+      // line = email with trailing whitespace and comment
+      "${email} # comment",
+      // line = email with leading whitespace and comment
+      " ${email}# comment",
+      // line = email with leading and trailing whitespace and comment
+      " ${email} # comment",
+      // line = email that ends with oldEmail
+      "foo" + oldEmail,
+      // line = email that starts with oldEmail
+      oldEmail + "bar",
+      // line = email that contains oldEmail
+      "foo" + oldEmail + "bar",
+      // line = email that contains oldEmail with leading and trailing whitespace
+      " foo" + oldEmail + "bar ",
+      // line = email that contains oldEmail with leading and trailing whitespace and comment
+      " foo" + oldEmail + "bar # comment",
+      // email in comment
+      "foo@example.com # ${email}",
+      // email in comment that contains oldEmail
+      "foo@example.com # foo" + oldEmail + "bar",
+      // per-file line with email
+      "per-file *.md=${email}",
+      // per-file line with email and whitespace
+      "per-file *.md = ${email} ",
+      // per-file line with multiple emails and old email at first position
+      "per-file *.md=${email},foo@example.com,bar@example.com",
+      // per-file line with multiple emails and old email at middle position
+      "per-file *.md=foo@example.com,${email},bar@example.com",
+      // per-file line with multiple emails and old email at last position
+      "per-file *.md=foo@example.com,bar@example.com,${email}",
+      // per-file line with multiple emails and old email at last position and comment
+      "per-file *.md=foo@example.com,bar@example.com,${email}# comment",
+      // per-file line with multiple emails and old email at last position and comment with
+      // whitespace
+      "per-file *.md = foo@example.com, bar@example.com , ${email} # comment",
+      // per-file line with multiple emails and old email appearing multiple times
+      "per-file *.md=${email},${email}",
+      "per-file *.md=${email},${email},${email}",
+      "per-file *.md=${email},foo@example.com,${email},bar@example.com,${email}",
+      // per-file line with multiple emails and old email appearing multiple times and comment
+      "per-file *.md=${email},foo@example.com,${email},bar@example.com,${email}# comment",
+      // per-file line with multiple emails and old email appearing multiple times and comment and
+      // whitespace
+      "per-file *.md= ${email} , foo@example.com , ${email} , bar@example.com , ${email} # comment",
+      // per-file line with email that contains old email
+      "per-file *.md=for" + oldEmail + "bar",
+      // per-file line with multiple emails and one email that contains the old email
+      "per-file *.md=for" + oldEmail + "bar,${email}",
+    };
+
+    for (String testLine : testLines) {
+      String content = testLine.replaceAll(Pattern.quote("${email}"), oldEmail);
+      String expectedContent = testLine.replaceAll(Pattern.quote("${email}"), newEmail);
+      assertWithMessage(testLine)
+          .that(FindOwnersCodeOwnerConfigParser.replaceEmail(content, oldEmail, newEmail))
+          .isEqualTo(expectedContent);
+    }
+
+    // join all test lines and replace email in all of them at once
+    String testContent = Joiner.on("\n").join(testLines);
+    String content = testContent.replaceAll(Pattern.quote("${email}"), oldEmail);
+    String expectedContent = testContent.replaceAll(Pattern.quote("${email}"), newEmail);
+    assertThat(FindOwnersCodeOwnerConfigParser.replaceEmail(content, oldEmail, newEmail))
+        .isEqualTo(expectedContent);
+
+    // test that trailing new line is preserved
+    assertThat(FindOwnersCodeOwnerConfigParser.replaceEmail(content + "\n", oldEmail, newEmail))
+        .isEqualTo(expectedContent + "\n");
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfigTest.java
index 7ab8924..42a32dd 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/AbstractRequiredApprovalConfigTest.java
@@ -16,16 +16,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
+import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.project.ProjectState;
-import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
@@ -34,78 +34,76 @@
   /** Must return the {@link AbstractRequiredApprovalConfig} that should be tested. */
   protected abstract AbstractRequiredApprovalConfig getRequiredApprovalConfig();
 
-  protected void testGetFromGlobalPluginConfig() throws Exception {
-    ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-    Optional<RequiredApproval> requiredApproval =
-        getRequiredApprovalConfig().getFromGlobalPluginConfig(projectState);
-    assertThat(requiredApproval).isPresent();
-    assertThat(requiredApproval.get().labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.get().value()).isEqualTo(2);
-  }
-
-  protected void testCannotGetFromGlobalPluginConfigIfConfigIsInvalid() throws Exception {
+  protected void testCannotGetIfGlobalConfigIsInvalid(String invalidValue) throws Exception {
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
     InvalidPluginConfigurationException exception =
         assertThrows(
             InvalidPluginConfigurationException.class,
-            () -> getRequiredApprovalConfig().getFromGlobalPluginConfig(projectState));
+            () -> getRequiredApprovalConfig().get(projectState, new Config()));
     assertThat(exception)
         .hasMessageThat()
         .isEqualTo(
             String.format(
-                "Invalid configuration of the code-owners plugin. Required approval 'INVALID' that is"
+                "Invalid configuration of the code-owners plugin. Required approval '%s' that is"
                     + " configured in gerrit.config (parameter plugin.code-owners.%s) is"
                     + " invalid: Invalid format, expected '<label-name>+<label-value>'.",
-                getRequiredApprovalConfig().getConfigKey()));
+                invalidValue, getRequiredApprovalConfig().getConfigKey()));
   }
 
   @Test
-  public void cannotGetForProjectForNullProjectState() throws Exception {
+  public void cannotGetForNullProjectState() throws Exception {
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> getRequiredApprovalConfig().getForProject(null, new Config()));
+            () -> getRequiredApprovalConfig().get(/* projectState= */ null, new Config()));
     assertThat(npe).hasMessageThat().isEqualTo("projectState");
   }
 
   @Test
-  public void cannotGetForProjectForNullConfig() throws Exception {
+  public void cannotGetForNullConfig() throws Exception {
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> getRequiredApprovalConfig().getForProject(projectState, null));
+            () -> getRequiredApprovalConfig().get(projectState, /* pluginConfig= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
   }
 
   @Test
-  public void getForProjectWhenRequiredApprovalIsNotSet() throws Exception {
+  public void getWhenRequiredApprovalIsNotSet() throws Exception {
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-    assertThat(getRequiredApprovalConfig().getForProject(projectState, new Config())).isEmpty();
+    assertThat(getRequiredApprovalConfig().get(projectState, new Config())).isEmpty();
   }
 
   @Test
-  public void getForProject() throws Exception {
+  public void getFromPluginConfig() throws Exception {
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
     Config cfg = new Config();
     cfg.setString(
-        SECTION_CODE_OWNERS, null, getRequiredApprovalConfig().getConfigKey(), "Code-Review+2");
-    Optional<RequiredApproval> requiredApproval =
-        getRequiredApprovalConfig().getForProject(projectState, cfg);
-    assertThat(requiredApproval).isPresent();
-    assertThat(requiredApproval.get().labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.get().value()).isEqualTo(2);
+        SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        getRequiredApprovalConfig().getConfigKey(),
+        "Code-Review+2");
+    ImmutableList<RequiredApproval> requiredApproval =
+        getRequiredApprovalConfig().get(projectState, cfg);
+    assertThat(requiredApproval).hasSize(1);
+    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(2);
   }
 
   @Test
-  public void cannotGetForProjectIfConfigIsInvalid() throws Exception {
+  public void cannotGetFromPluginConfigIfConfigIsInvalid() throws Exception {
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
     Config cfg = new Config();
-    cfg.setString(SECTION_CODE_OWNERS, null, getRequiredApprovalConfig().getConfigKey(), "INVALID");
+    cfg.setString(
+        SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        getRequiredApprovalConfig().getConfigKey(),
+        "INVALID");
     InvalidPluginConfigurationException exception =
         assertThrows(
             InvalidPluginConfigurationException.class,
-            () -> getRequiredApprovalConfig().getForProject(projectState, cfg));
+            () -> getRequiredApprovalConfig().get(projectState, cfg));
     assertThat(exception)
         .hasMessageThat()
         .isEqualTo(
@@ -117,21 +115,6 @@
   }
 
   @Test
-  public void cannotGetFromGlobalPluginConfigForNullProjectState() throws Exception {
-    NullPointerException npe =
-        assertThrows(
-            NullPointerException.class,
-            () -> getRequiredApprovalConfig().getFromGlobalPluginConfig(null));
-    assertThat(npe).hasMessageThat().isEqualTo("projectState");
-  }
-
-  @Test
-  public void getFromGlobalPluginConfigWhenRequiredApprovalIsNotSet() throws Exception {
-    ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-    assertThat(getRequiredApprovalConfig().getFromGlobalPluginConfig(projectState)).isEmpty();
-  }
-
-  @Test
   public void cannotValidateProjectLevelConfigWithNullProjectState() throws Exception {
     NullPointerException npe =
         assertThrows(
@@ -139,7 +122,7 @@
             () ->
                 getRequiredApprovalConfig()
                     .validateProjectLevelConfig(
-                        null,
+                        /* projectState= */ null,
                         "code-owners.config",
                         new ProjectLevelConfig.Bare("code-owners.config")));
     assertThat(npe).hasMessageThat().isEqualTo("projectState");
@@ -154,7 +137,9 @@
             () ->
                 getRequiredApprovalConfig()
                     .validateProjectLevelConfig(
-                        projectState, null, new ProjectLevelConfig.Bare("code-owners.config")));
+                        projectState,
+                        /* fileName= */ null,
+                        new ProjectLevelConfig.Bare("code-owners.config")));
     assertThat(npe).hasMessageThat().isEqualTo("fileName");
   }
 
@@ -166,14 +151,15 @@
             NullPointerException.class,
             () ->
                 getRequiredApprovalConfig()
-                    .validateProjectLevelConfig(projectState, "code-owners.config", null));
+                    .validateProjectLevelConfig(
+                        projectState, "code-owners.config", /* projectLevelConfig= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("projectLevelConfig");
   }
 
   @Test
   public void validateEmptyProjectLevelConfig() throws Exception {
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-    Optional<CommitValidationMessage> commitValidationMessage =
+    ImmutableList<CommitValidationMessage> commitValidationMessage =
         getRequiredApprovalConfig()
             .validateProjectLevelConfig(
                 projectState,
@@ -188,8 +174,11 @@
     ProjectLevelConfig.Bare cfg = new ProjectLevelConfig.Bare("code-owners.config");
     cfg.getConfig()
         .setString(
-            SECTION_CODE_OWNERS, null, getRequiredApprovalConfig().getConfigKey(), "Code-Review+2");
-    Optional<CommitValidationMessage> commitValidationMessage =
+            SECTION_CODE_OWNERS,
+            /* subsection= */ null,
+            getRequiredApprovalConfig().getConfigKey(),
+            "Code-Review+2");
+    ImmutableList<CommitValidationMessage> commitValidationMessage =
         getRequiredApprovalConfig()
             .validateProjectLevelConfig(projectState, "code-owners.config", cfg);
     assertThat(commitValidationMessage).isEmpty();
@@ -201,13 +190,16 @@
     ProjectLevelConfig.Bare cfg = new ProjectLevelConfig.Bare("code-owners.config");
     cfg.getConfig()
         .setString(
-            SECTION_CODE_OWNERS, null, getRequiredApprovalConfig().getConfigKey(), "INVALID");
-    Optional<CommitValidationMessage> commitValidationMessage =
+            SECTION_CODE_OWNERS,
+            /* subsection= */ null,
+            getRequiredApprovalConfig().getConfigKey(),
+            "INVALID");
+    ImmutableList<CommitValidationMessage> commitValidationMessage =
         getRequiredApprovalConfig()
             .validateProjectLevelConfig(projectState, "code-owners.config", cfg);
-    assertThat(commitValidationMessage).isPresent();
-    assertThat(commitValidationMessage.get().getType()).isEqualTo(ValidationMessage.Type.ERROR);
-    assertThat(commitValidationMessage.get().getMessage())
+    assertThat(commitValidationMessage).hasSize(1);
+    assertThat(commitValidationMessage.get(0).getType()).isEqualTo(ValidationMessage.Type.ERROR);
+    assertThat(commitValidationMessage.get(0).getMessage())
         .isEqualTo(
             String.format(
                 "Required approval 'INVALID' that is configured in code-owners.config (parameter"
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/BUILD b/javatests/com/google/gerrit/plugins/codeowners/config/BUILD
index 9f115b5..7f1985a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/BUILD
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/BUILD
@@ -15,6 +15,7 @@
         ":testbases",
         "//plugins/code-owners:code-owners__plugin",
         "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/acceptance",
+        "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/testing",
     ],
 )
 
@@ -25,9 +26,11 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/com/google/gerrit/truth",
+        "//lib:guava",
         "//lib:jgit",
         "//lib/truth",
         "//plugins/code-owners:code-owners__plugin",
         "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/acceptance",
+        "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/testing",
     ],
 )
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
index 723836d..dab2b0d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/CodeOwnersPluginConfigurationTest.java
@@ -15,9 +15,11 @@
 package com.google.gerrit.plugins.codeowners.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
@@ -66,10 +68,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwnersPluginConfiguration.isDisabled(
-                    /** project = */
-                    (Project.NameKey) null));
+            () -> codeOwnersPluginConfiguration.isDisabled(/* project= */ (Project.NameKey) null));
     assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
@@ -80,8 +79,7 @@
             NullPointerException.class,
             () ->
                 codeOwnersPluginConfiguration.isDisabled(
-                    /** branchNameKey = */
-                    (BranchNameKey) null));
+                    /* branchNameKey= */ (BranchNameKey) null));
     assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
   }
 
@@ -464,17 +462,16 @@
   @Test
   public void getDefaultRequiredApprovalWhenNoRequiredApprovalIsConfigured() throws Exception {
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName())
-        .isEqualTo(RequiredApprovalConfig.DEFAULT_LABEL);
-    assertThat(requiredApproval.value()).isEqualTo(RequiredApprovalConfig.DEFAULT_VALUE);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo(RequiredApprovalConfig.DEFAULT_LABEL);
+    assertThat(requiredApproval).hasValueThat().isEqualTo(RequiredApprovalConfig.DEFAULT_VALUE);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+2")
   public void getConfiguredDefaultRequireApproval() throws Exception {
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(2);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
   }
 
   @Test
@@ -534,8 +531,22 @@
   public void getRequiredApprovalConfiguredOnProjectLevel() throws Exception {
     configureRequiredApproval(project, "Code-Review+2");
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(2);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void getRequiredApprovalMultipleConfiguredOnProjectLevel() throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
+        ImmutableList.of("Code-Review+2", "Code-Review+1"));
+
+    // If multiple values are set for a key, the last value wins.
+    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
   }
 
   @Test
@@ -544,16 +555,16 @@
       throws Exception {
     configureRequiredApproval(project, "Code-Review+2");
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(2);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
   }
 
   @Test
   public void requiredApprovalIsInheritedFromParentProject() throws Exception {
     configureRequiredApproval(allProjects, "Code-Review+2");
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(2);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
   }
 
   @Test
@@ -561,8 +572,8 @@
   public void inheritedRequiredApprovalOverridesDefaultRequiredApproval() throws Exception {
     configureRequiredApproval(allProjects, "Code-Review+2");
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(2);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
   }
 
   @Test
@@ -570,8 +581,8 @@
     configureRequiredApproval(allProjects, "Code-Review+1");
     configureRequiredApproval(project, "Code-Review+2");
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(2);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
   }
 
   @Test
@@ -635,8 +646,8 @@
     Project.NameKey otherProject = projectOperations.newProject().create();
     configureRequiredApproval(otherProject, "Code-Review+2");
     RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
-    assertThat(requiredApproval.labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.value()).isEqualTo(1);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
   }
 
   @Test
@@ -664,8 +675,8 @@
     Optional<RequiredApproval> requiredApproval =
         codeOwnersPluginConfiguration.getOverrideApproval(project);
     assertThat(requiredApproval).isPresent();
-    assertThat(requiredApproval.get().labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.get().value()).isEqualTo(2);
+    assertThat(requiredApproval).value().hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).value().hasValueThat().isEqualTo(2);
   }
 
   @Test
@@ -674,8 +685,24 @@
     Optional<RequiredApproval> requiredApproval =
         codeOwnersPluginConfiguration.getOverrideApproval(project);
     assertThat(requiredApproval).isPresent();
-    assertThat(requiredApproval.get().labelType().getName()).isEqualTo("Code-Review");
-    assertThat(requiredApproval.get().value()).isEqualTo(2);
+    assertThat(requiredApproval).value().hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).value().hasValueThat().isEqualTo(2);
+  }
+
+  @Test
+  public void getOverrideApprovalMultipleConfiguredOnProjectLevel() throws Exception {
+    setCodeOwnersConfig(
+        project,
+        /* subsection= */ null,
+        OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
+        ImmutableList.of("Code-Review+2", "Code-Review+1"));
+
+    // If multiple values are set for a key, the last value wins.
+    Optional<RequiredApproval> requiredApproval =
+        codeOwnersPluginConfiguration.getOverrideApproval(project);
+    assertThat(requiredApproval).isPresent();
+    assertThat(requiredApproval).value().hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).value().hasValueThat().isEqualTo(1);
   }
 
   @Test
@@ -725,10 +752,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwnersPluginConfiguration.getFileExtension(
-                    /** project = */
-                    null));
+            () -> codeOwnersPluginConfiguration.getFileExtension(/* project= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
@@ -769,10 +793,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                codeOwnersPluginConfiguration.getMergeCommitStrategy(
-                    /** project = */
-                    null));
+            () -> codeOwnersPluginConfiguration.getMergeCommitStrategy(/* project= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
@@ -821,39 +842,21 @@
   }
 
   private void configureDisabled(Project.NameKey project, String disabled) throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /** subsection = */
-        null,
-        StatusConfig.KEY_DISABLED,
-        disabled);
+    setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED, disabled);
   }
 
   private void configureDisabledBranch(Project.NameKey project, String disabledBranch)
       throws Exception {
     setCodeOwnersConfig(
-        project,
-        /** subsection = */
-        null,
-        StatusConfig.KEY_DISABLED_BRANCH,
-        disabledBranch);
+        project, /* subsection= */ null, StatusConfig.KEY_DISABLED_BRANCH, disabledBranch);
   }
 
   private void enableCodeOwnersForAllBranches(Project.NameKey project) throws Exception {
-    setCodeOwnersConfig(
-        project,
-        /** subsection = */
-        null,
-        StatusConfig.KEY_DISABLED_BRANCH,
-        "");
+    setCodeOwnersConfig(project, /* subsection= */ null, StatusConfig.KEY_DISABLED_BRANCH, "");
   }
 
   private void configureBackend(Project.NameKey project, String backendName) throws Exception {
-    configureBackend(
-        project,
-        /** branch = */
-        null,
-        backendName);
+    configureBackend(project, /* branch= */ null, backendName);
   }
 
   private void configureBackend(
@@ -865,8 +868,7 @@
       throws Exception {
     setCodeOwnersConfig(
         project,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         RequiredApprovalConfig.KEY_REQUIRED_APPROVAL,
         requiredApproval);
   }
@@ -875,8 +877,7 @@
       throws Exception {
     setCodeOwnersConfig(
         project,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL,
         requiredApproval);
   }
@@ -884,19 +885,14 @@
   private void configureFileExtension(Project.NameKey project, String fileExtension)
       throws Exception {
     setCodeOwnersConfig(
-        project,
-        /** subsection = */
-        null,
-        GeneralConfig.KEY_FILE_EXTENSION,
-        fileExtension);
+        project, /* subsection= */ null, GeneralConfig.KEY_FILE_EXTENSION, fileExtension);
   }
 
   private void configureMergeCommitStrategy(
       Project.NameKey project, MergeCommitStrategy mergeCommitStrategy) throws Exception {
     setCodeOwnersConfig(
         project,
-        /** subsection = */
-        null,
+        /* subsection= */ null,
         GeneralConfig.KEY_MERGE_COMMIT_STRATEGY,
         mergeCommitStrategy.name());
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/OverrideApprovalConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/OverrideApprovalConfigTest.java
index 343ee30..3ea4e18 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/OverrideApprovalConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/OverrideApprovalConfigTest.java
@@ -14,7 +14,13 @@
 
 package com.google.gerrit.plugins.codeowners.config;
 
+import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.server.project.ProjectState;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -33,14 +39,21 @@
   }
 
   @Test
-  @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Code-Review+2")
+  @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
   public void getFromGlobalPluginConfig() throws Exception {
-    testGetFromGlobalPluginConfig();
+    createOwnersOverrideLabel();
+
+    ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
+    ImmutableList<RequiredApproval> requiredApproval =
+        getRequiredApprovalConfig().get(projectState, new Config());
+    assertThat(requiredApproval).hasSize(1);
+    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Owners-Override");
+    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(1);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "INVALID")
-  public void cannotGetFromGlobalPluginConfigIfConfigIsInvalid() throws Exception {
-    testCannotGetFromGlobalPluginConfigIfConfigIsInvalid();
+  public void cannotGetIfGlobalConfigIsInvalid() throws Exception {
+    testCannotGetIfGlobalConfigIsInvalid("INVALID");
   }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/config/RequiredApprovalConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/config/RequiredApprovalConfigTest.java
index 53c0fa2..37da12d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/config/RequiredApprovalConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/config/RequiredApprovalConfigTest.java
@@ -15,11 +15,14 @@
 package com.google.gerrit.plugins.codeowners.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.server.project.ProjectState;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -40,13 +43,18 @@
   @Test
   @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+2")
   public void getFromGlobalPluginConfig() throws Exception {
-    testGetFromGlobalPluginConfig();
+    ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
+    ImmutableList<RequiredApproval> requiredApproval =
+        getRequiredApprovalConfig().get(projectState, new Config());
+    assertThat(requiredApproval).hasSize(1);
+    assertThat(requiredApproval).element(0).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).element(0).hasValueThat().isEqualTo(2);
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "INVALID")
-  public void cannotGetFromGlobalPluginConfigIfConfigIsInvalid() throws Exception {
-    testCannotGetFromGlobalPluginConfigIfConfigIsInvalid();
+  public void cannotIfGlobalConfigIsInvalid() throws Exception {
+    testCannotGetIfGlobalConfigIsInvalid("INVALID");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerJsonTest.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerJsonTest.java
index cb7ede8..2aa5b6f 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerJsonTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerJsonTest.java
@@ -89,9 +89,7 @@
             () ->
                 codeOwnerJsonFactory
                     .create(EnumSet.of(FillOptions.ID))
-                    .format(
-                        /** codeOwners = */
-                        null));
+                    .format(/* codeOwners= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("codeOwners");
   }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
index 28d05fe..30f8454 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerProjectConfigJsonTest.java
@@ -97,10 +97,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                CodeOwnerProjectConfigJson.formatRequiredApproval(
-                    /** requiredApproval = */
-                    null));
+            () -> CodeOwnerProjectConfigJson.formatRequiredApproval(/* requiredApproval= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("requiredApproval");
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerStatusInfoJsonTest.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerStatusInfoJsonTest.java
index 93aeeca..75aaeef 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerStatusInfoJsonTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerStatusInfoJsonTest.java
@@ -50,8 +50,7 @@
             NullPointerException.class,
             () ->
                 CodeOwnerStatusInfoJson.format(
-                    /** pathCodeOwnerStatus = */
-                    (PathCodeOwnerStatus) null));
+                    /* pathCodeOwnerStatus= */ (PathCodeOwnerStatus) null));
     assertThat(npe).hasMessageThat().isEqualTo("pathCodeOwnerStatus");
   }
 
@@ -72,8 +71,7 @@
             NullPointerException.class,
             () ->
                 CodeOwnerStatusInfoJson.format(
-                    /** fileCodeOwnerStatus = */
-                    (FileCodeOwnerStatus) null));
+                    /* fileCodeOwnerStatus= */ (FileCodeOwnerStatus) null));
     assertThat(npe).hasMessageThat().isEqualTo("fileCodeOwnerStatus");
   }
 
@@ -192,9 +190,7 @@
             NullPointerException.class,
             () ->
                 CodeOwnerStatusInfoJson.format(
-                    PatchSet.id(Change.id(1), 1),
-                    /** fileCodeOwnerStatuses = */
-                    null));
+                    PatchSet.id(Change.id(1), 1), /* fileCodeOwnerStatuses= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("fileCodeOwnerStatuses");
   }
 
@@ -203,10 +199,7 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () ->
-                CodeOwnerStatusInfoJson.format(
-                    /** patchSetId = */
-                    null, ImmutableSet.of()));
+            () -> CodeOwnerStatusInfoJson.format(/* patchSetId= */ null, ImmutableSet.of()));
     assertThat(npe).hasMessageThat().isEqualTo("patchSetId");
   }
 
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 1978bd0..3fd9f84 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -214,6 +214,70 @@
   ]
 ```
 
+### <a id="rename-email-in-code-owner-config-files">Rename Email In Code Owner Config Files
+_'POST /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/branches/[\{branch-id\}](../../../Documentation/rest-api-projects.html#branch-id)/code_owners.rename/'_
+
+Renames an email in all code owner config files in the branch.
+
+The old and new email must be specified in the request body as
+[RenameEmailInput](#rename-email-input).
+
+The old and new email must both belong to the same Gerrit account.
+
+All updates are done atomically within one commit. The calling user will be the
+author of this commit.
+
+Requires that the calling user is a project owner
+([Owner](../../../Documentation/access-control.html#category_owner) permission
+on ‘refs/*’) or has
+[direct push](../../../Documentation/access-control.html#category_push)
+permissions for the branch.
+
+#### Request
+
+```
+  POST /projects/foo%2Fbar/branches/master/code_owners.rename HTTP/1.0
+```
+
+#### Response
+
+As response a [RenameEmailResultInfo](#rename-email-result-info) entity is
+returned.
+
+```
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "commit": {
+      "commit": "",
+      "parents": [
+        {
+          "commit": "1efe2c9d8f352483781e772f35dc586a69ff5646",
+          "subject": "Fix Foo Bar"
+        }
+      ],
+      "author": {
+        "name": "John Doe",
+        "email": "john.doe@example.com",
+        "date": "2020-03-30 18:08:08.000000000",
+        "tz": -420
+      },
+      "committer": {
+        "name": "Gerrit Code Review",
+        "email": "no-reply@gerritcodereview.com",
+        "date": "2020-03-30 18:08:08.000000000",
+        "tz": -420
+      },
+      "subject": "Rename email in code owner config files",
+      "message": "Rename email in code owner config files"
+    }
+  }
+```
+
+
 ### <a id="get-code-owner-config">[EXPERIMENTAL] Get Code Owner Config
 _'GET /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/branches/[\{branch-id\}](../../../Documentation/rest-api-projects.html#branch-id)/code_owners.config/[\{path\}](#path)'_
 
@@ -633,6 +697,27 @@
 
 ---
 
+### <a id="rename-email-input"> RenameEmailInput
+The `RenameEmailInput` entity specifies how an email should be renamed.
+
+| Field Name  |          | Description |
+| ----------- | -------- | ----------- |
+| `message`   | optional | Commit message that should be used for the commit that renames the email in the code owner config files. If not set the following default commit message is used: "Rename email in code owner config files"
+| `old_email` || The old email that should be replaced with the new email.
+| `new_email` || The new email that should be used to replace the old email.
+
+---
+
+### <a id="rename-email-result-info"> RenameEmailResultInfo
+The `RenameEmailResultInfo` entity describes the result of the rename email REST
+endpoint.
+
+| Field Name  |          | Description |
+| ----------- | -------- | ----------- |
+| `commit`    | optional | The commit that did the email rename. Not set, if no update was necessary.
+
+---
+
 ### <a id="required-approval-info"> RequiredApprovalInfo
 The `RequiredApprovalInfo` entity describes an approval that is required for an
 action.