Merge changes I3adba5cb,I9ba44609,I7647bd97,I1759c5ba,I50f06e4e

* changes:
  OnCodeOwnerApproval/OnCodeOwnerOverride: Remove wrong error log
  Validation: Log exceptions that happen in dry run mode of as a warning
  Add retrying to add change message on add reviewer
  ChangeFiles: Limit the number of changed files that are written to trace
  Consider only emails that belong to multiple active accounts as ambiguous
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
index 1e76b05..787e452 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
@@ -59,6 +59,8 @@
 public class ChangedFiles {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private static int MAX_CHANGED_FILES_TO_LOG = 25;
+
   private final GitRepositoryManager repoManager;
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final PatchListCache patchListCache;
@@ -203,7 +205,14 @@
       List<DiffEntry> diffEntries = diffFormatter.scan(baseCommit, revCommit);
       ImmutableSet<ChangedFile> changedFiles =
           diffEntries.stream().map(ChangedFile::create).collect(toImmutableSet());
-      logger.atFine().log("changed files = %s", changedFiles);
+      if (changedFiles.size() <= MAX_CHANGED_FILES_TO_LOG) {
+        logger.atFine().log("changed files = %s", changedFiles);
+      } else {
+        logger.atFine().log(
+            "changed files = %s (and %d more)",
+            changedFiles.asList().subList(0, MAX_CHANGED_FILES_TO_LOG),
+            changedFiles.size() - MAX_CHANGED_FILES_TO_LOG);
+      }
       return changedFiles;
     }
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index 5cb187f..cd874ee 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -21,6 +21,7 @@
 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.Account;
 import com.google.gerrit.entities.Project;
@@ -302,24 +303,14 @@
       return OptionalResultWithMessages.createEmpty(messages);
     }
 
-    OptionalResultWithMessages<Account.Id> lookupEmailResult = lookupEmail(email);
-    messages.addAll(lookupEmailResult.messages());
-    if (lookupEmailResult.isEmpty()) {
+    OptionalResultWithMessages<AccountState> activeAccountResult =
+        lookupActiveAccountForEmail(email);
+    messages.addAll(activeAccountResult.messages());
+    if (activeAccountResult.isEmpty()) {
       return OptionalResultWithMessages.createEmpty(messages);
     }
 
-    Account.Id accountId = lookupEmailResult.get();
-    OptionalResultWithMessages<AccountState> lookupAccountResult = lookupAccount(accountId, email);
-    messages.addAll(lookupAccountResult.messages());
-    if (lookupAccountResult.isEmpty()) {
-      return OptionalResultWithMessages.createEmpty(messages);
-    }
-
-    AccountState accountState = lookupAccountResult.get();
-    if (!accountState.account().isActive()) {
-      messages.add(String.format("account %s for email %s is inactive", accountId, email));
-      return OptionalResultWithMessages.createEmpty(messages);
-    }
+    AccountState accountState = activeAccountResult.get();
     if (enforceVisibility) {
       OptionalResultWithMessages<Boolean> isVisibleResult = isVisible(accountState, email);
       messages.addAll(isVisibleResult.messages());
@@ -343,15 +334,15 @@
   }
 
   /**
-   * Looks up an email and returns the ID of the account to which it belongs.
+   * Looks up an email and returns the ID of the active account to which it belongs.
    *
-   * <p>If the email is ambiguous (it belongs to multiple accounts) it is considered as
-   * non-resolvable and {@link Optional#empty()} is returned.
+   * <p>If the email is ambiguous (it belongs to multiple active accounts) it is considered as
+   * non-resolvable and empty result is returned.
    *
    * @param email the email that should be looked up
    * @return the ID of the account to which the email belongs if was found
    */
-  private OptionalResultWithMessages<Account.Id> lookupEmail(String email) {
+  private OptionalResultWithMessages<AccountState> lookupActiveAccountForEmail(String email) {
     ImmutableSet<ExternalId> extIds;
     try {
       extIds = externalIds.byEmail(email);
@@ -366,12 +357,51 @@
               "cannot resolve code owner email %s: no account with this email exists", email));
     }
 
-    if (extIds.stream().map(ExternalId::accountId).distinct().count() > 1) {
-      return OptionalResultWithMessages.createEmpty(
-          String.format("cannot resolve code owner email %s: email is ambiguous", email));
+    List<String> messages = new ArrayList<>();
+    OptionalResultWithMessages<ImmutableSet<AccountState>> activeAccountsResult =
+        lookupActiveAccounts(extIds, email);
+    ImmutableSet<AccountState> activeAccounts = activeAccountsResult.get();
+    messages.addAll(activeAccountsResult.messages());
+
+    if (activeAccounts.isEmpty()) {
+      messages.add(
+          String.format(
+              "cannot resolve code owner email %s: no active account with this email found",
+              email));
+      return OptionalResultWithMessages.createEmpty(messages);
     }
 
-    return OptionalResultWithMessages.create(extIds.stream().findFirst().get().accountId());
+    if (activeAccounts.size() > 1) {
+      messages.add(String.format("cannot resolve code owner email %s: email is ambiguous", email));
+      return OptionalResultWithMessages.createEmpty(messages);
+    }
+
+    return OptionalResultWithMessages.create(Iterables.getOnlyElement(activeAccounts));
+  }
+
+  private OptionalResultWithMessages<ImmutableSet<AccountState>> lookupActiveAccounts(
+      ImmutableSet<ExternalId> extIds, String email) {
+    ImmutableSet<OptionalResultWithMessages<AccountState>> accountStateResults =
+        extIds.stream()
+            .map(externalId -> lookupAccount(externalId.accountId(), externalId.email()))
+            .collect(toImmutableSet());
+
+    ImmutableSet.Builder<AccountState> activeAccounts = ImmutableSet.builder();
+    List<String> messages = new ArrayList<>();
+    for (OptionalResultWithMessages<AccountState> accountStateResult : accountStateResults) {
+      messages.addAll(accountStateResult.messages());
+      if (accountStateResult.isPresent()) {
+        AccountState accountState = accountStateResult.get();
+        if (accountState.account().isActive()) {
+          activeAccounts.add(accountState);
+        } else {
+          messages.add(
+              String.format(
+                  "account %s for email %s is inactive", accountState.account().id(), email));
+        }
+      }
+    }
+    return OptionalResultWithMessages.create(activeAccounts.build(), messages);
   }
 
   /**
@@ -388,9 +418,7 @@
     if (!accountState.isPresent()) {
       return OptionalResultWithMessages.createEmpty(
           String.format(
-              "cannot resolve code owner email %s: email belongs to account %s,"
-                  + " but no account with this ID exists",
-              email, accountId));
+              "cannot resolve account %s for email %s: account does not exists", accountId, email));
     }
     return OptionalResultWithMessages.create(accountState.get());
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
index 92db861..8e57412 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -35,7 +35,7 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -61,7 +61,7 @@
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
   private final Provider<CurrentUser> userProvider;
-  private final BatchUpdate.Factory batchUpdateFactory;
+  private final RetryHelper retryHelper;
   private final ChangeNotes.Factory changeNotesFactory;
   private final AccountCache accountCache;
   private final ChangeMessagesUtil changeMessageUtil;
@@ -71,14 +71,14 @@
       CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
       CodeOwnerApprovalCheck codeOwnerApprovalCheck,
       Provider<CurrentUser> userProvider,
-      BatchUpdate.Factory batchUpdateFactory,
+      RetryHelper retryHelper,
       ChangeNotes.Factory changeNotesFactory,
       AccountCache accountCache,
       ChangeMessagesUtil changeMessageUtil) {
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
     this.codeOwnerApprovalCheck = codeOwnerApprovalCheck;
     this.userProvider = userProvider;
-    this.batchUpdateFactory = batchUpdateFactory;
+    this.retryHelper = retryHelper;
     this.changeNotesFactory = changeNotesFactory;
     this.accountCache = accountCache;
     this.changeMessageUtil = changeMessageUtil;
@@ -95,11 +95,20 @@
       return;
     }
 
-    try (BatchUpdate batchUpdate =
-        batchUpdateFactory.create(projectName, userProvider.get(), TimeUtil.nowTs())) {
-      batchUpdate.addOp(changeId, new Op(event.getReviewers()));
-      batchUpdate.execute();
-    } catch (RestApiException | UpdateException e) {
+    try {
+      retryHelper
+          .changeUpdate(
+              "addCodeOwnersMessageOnAddReviewer",
+              updateFactory -> {
+                try (BatchUpdate batchUpdate =
+                    updateFactory.create(projectName, userProvider.get(), TimeUtil.nowTs())) {
+                  batchUpdate.addOp(changeId, new Op(event.getReviewers()));
+                  batchUpdate.execute();
+                }
+                return null;
+              })
+          .call();
+    } catch (Exception e) {
       logger.atSevere().withCause(e).log(
           String.format(
               "Failed to post code-owners change message for reviewer on change %s in project %s.",
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
index dff2876..1cf59ce 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -199,16 +199,7 @@
                 newVote, user.getName()));
       }
     } else {
-      logger.atSevere().log(
-          "code owner approval was neither newly applied, removed, upgraded nor downgraded:"
-              + " project = %s, change = %s, patch set = %d, oldApprvals = %s, approvals = %s,"
-              + " requiredApproval = %s",
-          changeNotes.getProjectName(),
-          changeNotes.getChangeId(),
-          patchSet.id().get(),
-          oldApprovals,
-          approvals,
-          requiredApproval);
+      // non-approval was downgraded (e.g. -1 to -2)
       return Optional.empty();
     }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
index 52ca037..8a8c702 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
@@ -20,7 +20,6 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
@@ -44,8 +43,6 @@
  */
 @Singleton
 class OnCodeOwnerOverride implements OnPostReview {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
 
   @Inject
@@ -82,8 +79,7 @@
         continue;
       }
 
-      buildMessageForCodeOwnerOverride(
-              user, changeNotes, patchSet, oldApprovals, approvals, overrideApproval)
+      buildMessageForCodeOwnerOverride(user, patchSet, oldApprovals, approvals, overrideApproval)
           .ifPresent(messages::add);
     }
 
@@ -95,7 +91,6 @@
 
   private Optional<String> buildMessageForCodeOwnerOverride(
       IdentifiedUser user,
-      ChangeNotes changeNotes,
       PatchSet patchSet,
       Map<String, Short> oldApprovals,
       Map<String, Short> approvals,
@@ -137,16 +132,7 @@
               "By voting %s the code-owners submit requirement is still overridden by %s",
               newVote, user.getName()));
     }
-    logger.atSevere().log(
-        "code owner override was neither newly applied, removed, upgraded nor downgraded:"
-            + " project = %s, change = %s, patch set = %d, oldApprvals = %s, approvals = %s,"
-            + " requiredApproval = %s",
-        changeNotes.getProjectName(),
-        changeNotes.getChangeId(),
-        patchSet.id().get(),
-        oldApprovals,
-        approvals,
-        overrideApproval);
+    // non-approval was downgraded (e.g. -1 to -2)
     return Optional.empty();
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index 74dae62..f230eb9 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -202,7 +202,7 @@
 
           // The validation was executed as dry-run and failures during the validation should not
           // cause an error. Hence we swallow the exception here.
-          logger.atFine().withCause(e).log(
+          logger.atWarning().withCause(e).log(
               "ignoring failure during validation of code owner config files in revision %s"
                   + " (project = %s, branch = %s) because the validation was performed as dry-run",
               receiveEvent.commit.getName(),
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
index 709797a..8b56cd5 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -344,9 +344,11 @@
                 "found email %s as code owner in %s",
                 orphanedEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
             String.format(
-                "cannot resolve code owner email %s: email belongs to account %s,"
-                    + " but no account with this ID exists",
-                orphanedEmail, accountId));
+                "cannot resolve account %s for email %s: account does not exists",
+                accountId, orphanedEmail),
+            String.format(
+                "cannot resolve code owner email %s: no active account with this email found",
+                orphanedEmail));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
index 79b3924..971f8e9 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
@@ -1061,4 +1061,28 @@
                     + "* %s\n",
                 user.fullName(), path));
   }
+
+  @Test
+  public void changeMessageNotExtendedIfNonApprovalIsDowngraded() throws Exception {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/foo/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    String path = "foo/bar.baz";
+    String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+    // Apply the Code-Review-1.
+    gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+    // Change Code-Review-1 to Code-Review-2
+    gApi.changes().id(changeId).current().review(ReviewInput.reject());
+
+    Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+
+    assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Code-Review-2");
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java
index 5f17972..1265466 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerOverrrideIT.java
@@ -599,4 +599,45 @@
                     + "By voting Owners-Override+1 the code-owners submit requirement is overridden by %s\n",
                 user.fullName()));
   }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1")
+  public void changeMessageNotExtendedIfNonApprovalIsDowngraded() throws Exception {
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values =
+        ImmutableMap.of("+1", "Override", " 0", "No Override", "-1", "Dislike", "-2", "Blocked");
+    gApi.projects().name(project.get()).label("Owners-Override").create(input);
+
+    // Allow to vote on the Owners-Override label.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            TestProjectUpdate.allowLabel("Owners-Override")
+                .range(-2, 1)
+                .ref("refs/*")
+                .group(REGISTERED_USERS)
+                .build())
+        .update();
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/foo/")
+        .addCodeOwnerEmail(admin.email())
+        .create();
+
+    String path = "foo/bar.baz";
+    String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+    gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", -1));
+
+    // Downgrade the vote from -1 to -2.
+    gApi.changes().id(changeId).current().review(new ReviewInput().label("Owners-Override", -2));
+
+    Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+
+    assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Owners-Override-2");
+  }
 }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
index 0297426..6a4ea64 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -121,6 +121,28 @@
   }
 
   @Test
+  public void resolveCodeOwnerReferenceForAmbiguousEmailIfOtherAccountIsInactive()
+      throws Exception {
+    // Create an external ID for 'user' account that has the same email as the 'admin' account.
+    accountsUpdate
+        .get()
+        .update(
+            "Test update",
+            user.id(),
+            (a, u) ->
+                u.addExternalId(
+                    ExternalId.create(
+                        "foo", "bar", user.id(), admin.email(), /* hashedPassword= */ null)));
+
+    // Deactivate the 'user' account.
+    accountOperations.account(user.id()).forUpdate().inactive().update();
+
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(admin.email()));
+    assertThat(result.get()).hasAccountIdThat().isEqualTo(admin.id());
+  }
+
+  @Test
   public void resolveCodeOwnerReferenceForAmbiguousEmail() throws Exception {
     // Create an external ID for 'user' account that has the same email as the 'admin' account.
     accountsUpdate
@@ -159,10 +181,13 @@
     assertThat(result).isEmpty();
     assertThat(result)
         .hasMessagesThat()
-        .contains(
+        .containsAnyOf(
             String.format(
-                "cannot resolve code owner email %s: email belongs to account %s, but no account with this ID exists",
-                email, accountId));
+                "cannot resolve account %s for email %s: account does not exists",
+                accountId, email),
+            String.format(
+                "cannost resolve code owner email %s: no active account with this email found",
+                email));
   }
 
   @Test
diff --git a/resources/Documentation/validation.md b/resources/Documentation/validation.md
index 3b0dc95..69929f4 100644
--- a/resources/Documentation/validation.md
+++ b/resources/Documentation/validation.md
@@ -102,7 +102,7 @@
       setting)
     * it is a non-visible secondary email
     * there is no account that has this email assigned
-    * it is ambiguous (the same email is assigned to multiple accounts)
+    * it is ambiguous (the same email is assigned to multiple active accounts)
     * it has an email domain that is disallowed (see
       [allowedEmailDomain](config.html#pluginCodeOwnersAllowedEmailDomain))
       configuration