Merge "Diff against root commits: use ObjectId#zeroId instead of EMPTY_TREE_ID"
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonModule.java b/java/com/google/gerrit/server/change/FileInfoJsonModule.java
index f90bd14..de116bb 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonModule.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonModule.java
@@ -14,12 +14,31 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.AbstractModule;
+import org.eclipse.jgit.lib.Config;
 
 public class FileInfoJsonModule extends AbstractModule {
+  /** Use the new diff cache implementation {@link FileInfoJsonNewImpl}. */
+  private final boolean useNewDiffCache;
+
+  /** Used to dark launch the new diff cache with the list files endpoint. */
+  private final boolean runNewDiffCacheAsync;
+
+  public FileInfoJsonModule(@GerritServerConfig Config cfg) {
+    this.useNewDiffCache =
+        cfg.getBoolean("cache", "diff_cache", "runNewDiffCache_ListFiles", false);
+    this.runNewDiffCacheAsync =
+        cfg.getBoolean("cache", "diff_cache", "runNewDiffCacheAsync_listFiles", false);
+  }
 
   @Override
   public void configure() {
-    bind(FileInfoJson.class).to(FileInfoJsonComparingImpl.class);
+    if (runNewDiffCacheAsync) {
+      bind(FileInfoJson.class).to(FileInfoJsonComparingImpl.class);
+      return;
+    }
+    bind(FileInfoJson.class)
+        .to(useNewDiffCache ? FileInfoJsonNewImpl.class : FileInfoJsonOldImpl.class);
   }
 }
diff --git a/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java b/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
index e12b538..80b4fee 100644
--- a/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
+++ b/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
@@ -99,25 +99,26 @@
       Iterable<CommentContextKey> inputKeys) {
     ImmutableMap.Builder<CommentContextKey, CommentContext> result = ImmutableMap.builder();
 
-    List<CommentContextKey> adjustedKeys =
+    // We do two transformations to the input keys: first we adjust the max context padding, and
+    // second we hash the file path. The transformed keys are used to request context from the
+    // cache. Keeping a map of the original inputKeys to the transformed keys
+    Map<CommentContextKey, CommentContextKey> inputKeysToCacheKeys =
         Streams.stream(inputKeys)
-            .map(CommentContextCacheImpl::adjustMaxContextPadding)
-            .collect(ImmutableList.toImmutableList());
-
-    // Convert the input keys to the same keys but with their file paths hashed
-    Map<CommentContextKey, CommentContextKey> keysToCacheKeys =
-        adjustedKeys.stream()
             .collect(
                 Collectors.toMap(
                     Function.identity(),
-                    k -> k.toBuilder().path(Loader.hashPath(k.path())).build()));
+                    k ->
+                        adjustMaxContextPadding(k)
+                            .toBuilder()
+                            .path(Loader.hashPath(k.path()))
+                            .build()));
 
     try {
       ImmutableMap<CommentContextKey, CommentContext> allContext =
-          contextCache.getAll(keysToCacheKeys.values());
+          contextCache.getAll(inputKeysToCacheKeys.values());
 
       for (CommentContextKey inputKey : inputKeys) {
-        CommentContextKey cacheKey = keysToCacheKeys.get(adjustMaxContextPadding(inputKey));
+        CommentContextKey cacheKey = inputKeysToCacheKeys.get(inputKey);
         result.put(inputKey, allContext.get(cacheKey));
       }
       return result.build();
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 076ba46..5a74c78 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -271,7 +271,7 @@
     install(new IgnoreSelfApprovalRule.Module());
     install(new ReceiveCommitsModule());
     install(new SshAddressesModule());
-    install(new FileInfoJsonModule());
+    install(new FileInfoJsonModule(cfg));
     install(ThreadLocalRequestContext.module());
     install(new ApprovalModule());
 
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index 7fff232..258c9af 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
@@ -95,6 +96,7 @@
   final OutgoingEmailValidator validator;
   final boolean addInstanceNameInSubject;
   final Provider<String> instanceNameProvider;
+  final Provider<CurrentUser> currentUserProvider;
 
   @Inject
   EmailArguments(
@@ -126,7 +128,8 @@
       Provider<InternalAccountQuery> accountQueryProvider,
       OutgoingEmailValidator validator,
       @GerritInstanceName Provider<String> instanceNameProvider,
-      @GerritServerConfig Config cfg) {
+      @GerritServerConfig Config cfg,
+      Provider<CurrentUser> currentUserProvider) {
     this.server = server;
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
@@ -157,5 +160,6 @@
     this.instanceNameProvider = instanceNameProvider;
 
     this.addInstanceNameInSubject = cfg.getBoolean("sendemail", "addInstanceNameInSubject", false);
+    this.currentUserProvider = currentUserProvider;
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index f5fb6b0..ddcc0cf 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
 import com.google.gerrit.mail.MailHeader;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -127,18 +128,40 @@
         Optional<AccountState> fromUser = args.accountCache.get(fromId);
         if (fromUser.isPresent()) {
           GeneralPreferencesInfo senderPrefs = fromUser.get().generalPreferences();
+          CurrentUser user = args.currentUserProvider.get();
+          boolean isImpersonating = user.isIdentifiedUser() && user.isImpersonating();
+          if (isImpersonating && user.getAccountId() != fromId) {
+            // This should not be possible, if this is the case it means the RequestContext is not
+            // set up correctly.
+            throw new EmailException(
+                String.format(
+                    "User %s is sending email from %s, while acting on behalf of %s",
+                    user.asIdentifiedUser().getRealUser().getAccountId(),
+                    fromId,
+                    user.getAccountId()));
+          }
           if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
-            // If we are impersonating a user, make sure they receive a CC of
-            // this message so they can always review and audit what we sent
-            // on their behalf to others.
+            // Include the sender in email if they enabled email notifications on their own
+            // comments.
             //
             logger.atFine().log(
                 "CC email sender %s because the email strategy of this user is %s",
                 fromUser.get().account().id(), CC_ON_OWN_COMMENTS);
             add(RecipientType.CC, fromId);
+          } else if (isImpersonating) {
+            // If we are impersonating a user, make sure they receive a CC of
+            // this message regardless of email strategy, unless email notifications are explicitly
+            // disabled for this user. This way they can always review and audit what we sent
+            // on their behalf to others.
+            logger.atFine().log(
+                "CC email sender %s because the email is sent on behalf of and email notifications"
+                    + " are enabled for this user.",
+                fromUser.get().account().id());
+            add(RecipientType.CC, fromId);
+
           } else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) {
             // If they don't want a copy, but we queued one up anyway,
-            // drop them from the recipient lists.
+            // drop them from the recipient lists, but only if the user is not being impersonated.
             //
             logger.atFine().log(
                 "Not CCing email sender %s because the email strategy of this user is not %s but %s",
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index 109c9c3..3d38f6c 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -16,7 +16,6 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
@@ -42,7 +41,7 @@
     EmailMerge create(
         Project.NameKey project,
         Change change,
-        Account.Id submitter,
+        IdentifiedUser submitter,
         NotifyResolver.Result notify,
         RepoView repoView,
         String stickyApprovalDiff);
@@ -51,12 +50,11 @@
   private final ExecutorService sendEmailsExecutor;
   private final MergedSender.Factory mergedSenderFactory;
   private final ThreadLocalRequestContext requestContext;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final MessageIdGenerator messageIdGenerator;
 
   private final Project.NameKey project;
   private final Change change;
-  private final Account.Id submitter;
+  private final IdentifiedUser submitter;
   private final NotifyResolver.Result notify;
   private final RepoView repoView;
   private final String stickyApprovalDiff;
@@ -66,18 +64,16 @@
       @SendEmailExecutor ExecutorService executor,
       MergedSender.Factory mergedSenderFactory,
       ThreadLocalRequestContext requestContext,
-      IdentifiedUser.GenericFactory identifiedUserFactory,
       MessageIdGenerator messageIdGenerator,
       @Assisted Project.NameKey project,
       @Assisted Change change,
-      @Assisted @Nullable Account.Id submitter,
+      @Assisted @Nullable IdentifiedUser submitter,
       @Assisted NotifyResolver.Result notify,
       @Assisted RepoView repoView,
       @Assisted String stickyApprovalDiff) {
     this.sendEmailsExecutor = executor;
     this.mergedSenderFactory = mergedSenderFactory;
     this.requestContext = requestContext;
-    this.identifiedUserFactory = identifiedUserFactory;
     this.messageIdGenerator = messageIdGenerator;
     this.project = project;
     this.change = change;
@@ -99,7 +95,7 @@
       MergedSender emailSender =
           mergedSenderFactory.create(project, change.getId(), Optional.of(stickyApprovalDiff));
       if (submitter != null) {
-        emailSender.setFrom(submitter);
+        emailSender.setFrom(submitter.getAccountId());
       }
       emailSender.setNotify(notify);
       emailSender.setMessageId(
@@ -120,7 +116,7 @@
   @Override
   public CurrentUser getUser() {
     if (submitter != null) {
-      return identifiedUserFactory.create(submitter).getRealUser();
+      return submitter;
     }
     throw new OutOfScopeException("No user on email thread");
   }
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 09470d4..f181c36 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -503,7 +503,7 @@
           .create(
               ctx.getProject(),
               toMerge.change(),
-              submitter.accountId(),
+              args.caller,
               ctx.getNotify(getId()),
               ctx.getRepoView(),
               stickyApprovalDiff)
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index cd5032a..7ca763a1 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -250,7 +250,7 @@
     install(new RestApiModule());
     install(new OAuthRestModule());
     install(new DefaultProjectNameLockManager.Module());
-    install(new FileInfoJsonModule());
+    install(new FileInfoJsonModule(cfg));
 
     bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 4a8c376..0f50797 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -89,6 +89,7 @@
         .forUpdate()
         .add(allow(Permission.FORGE_COMMITTER).ref("refs/*").group(REGISTERED_USERS))
         .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT_AS).ref("refs/*").group(REGISTERED_USERS))
         .add(allow(Permission.ABANDON).ref("refs/*").group(REGISTERED_USERS))
         .add(allowLabel(LabelId.CODE_REVIEW).ref("refs/*").group(REGISTERED_USERS).range(-2, +2))
         .update();
@@ -1623,6 +1624,75 @@
     assertThat(sender).didNotSend();
   }
 
+  @Test
+  public void mergeOnBehalfOfEmailEnabled_impersonatedOwnerNotified() throws Exception {
+    StagedChange sc = stageChangeReadyForMerge();
+    // If notification is enabled, onBehalfOfUser is always notified.
+    setEmailStrategy(sc.owner, ENABLED);
+    merge(sc.changeId, other, sc.owner, ALL);
+    assertThat(sender)
+        .sent("merged", sc)
+        .to(sc.owner)
+        .cc(sc.reviewer, sc.ccer)
+        .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
+        .bcc(sc.starrer)
+        .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
+        .noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
+  public void mergeOnBehalfOfEmailEnabled_impersonatedReviewerNotified() throws Exception {
+    StagedChange sc = stageChangeReadyForMerge();
+    // If notification is enabled, onBehalfOfUser is always notified.
+    setEmailStrategy(sc.reviewer, ENABLED);
+    merge(sc.changeId, other, sc.reviewer, ALL);
+    assertThat(sender)
+        .sent("merged", sc)
+        .to(sc.owner)
+        .cc(sc.reviewer, sc.ccer)
+        .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
+        .bcc(sc.starrer)
+        .bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
+        .noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
+  public void mergeOnBehalfOfReviewerNotifyOwner_impersonatedReviewerInCC() throws Exception {
+    StagedChange sc = stageChangeReadyForMerge();
+    setEmailStrategy(sc.reviewer, ENABLED);
+    // Even though Submit strategy is OWNER, impersonated reviewer is added to CC.
+    merge(sc.changeId, other, sc.reviewer, OWNER);
+    assertThat(sender).sent("merged", sc).to(sc.owner).cc(sc.reviewer).noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
+  public void mergeOnBehalfOfOtherNotifyOwner_impersonatedOtherInCC() throws Exception {
+    StagedChange sc = stageChangeReadyForMerge();
+    // Unrelated impersonated user is added to CC.
+    merge(sc.changeId, sc.reviewer, other, OWNER);
+    assertThat(sender).sent("merged", sc).to(sc.owner).cc(other).noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
+  public void mergeOnBehalfOfEmailDisabled_doesNotNotify() throws Exception {
+    StagedChange sc = stageChangeReadyForMerge();
+    setEmailStrategy(other, EmailStrategy.DISABLED);
+    merge(sc.changeId, sc.reviewer, other, OWNER);
+    assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
+  public void mergeOnBehalfOfNotifyNone() throws Exception {
+    StagedChange sc = stageChangeReadyForMerge();
+    merge(sc.changeId, other, sc.owner, NONE);
+    assertThat(sender).didNotSend();
+  }
+
   private void merge(String changeId, TestAccount by) throws Exception {
     merge(changeId, by, ENABLED);
   }
@@ -1648,6 +1718,20 @@
     gApi.changes().id(changeId).current().submit(in);
   }
 
+  private void merge(String changeId, TestAccount by, TestAccount onBehalfOf) throws Exception {
+    merge(changeId, by, onBehalfOf, /*notify=*/ null);
+  }
+
+  private void merge(
+      String changeId, TestAccount by, TestAccount onBehalfOf, @Nullable NotifyHandling notify)
+      throws Exception {
+    requestScopeOperations.setApiUser(by.id());
+    SubmitInput in = new SubmitInput();
+    in.notify = notify;
+    in.onBehalfOf = onBehalfOf.id().toString();
+    gApi.changes().id(changeId).current().submit(in);
+  }
+
   private StagedChange stageChangeReadyForMerge() throws Exception {
     StagedChange sc = stageReviewableChange();
     requestScopeOperations.setApiUser(sc.reviewer.id());