Merge "Fix expanded result rows to not react on click to expand/collapse"
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index d3bea00..cde6864 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -186,6 +186,7 @@
 * `git/upload-pack/phase_compressing`: Time spent in the 'Compressing...' phase.
 * `git/upload-pack/phase_writing`: Time spent transferring bytes to client.
 * `git/upload-pack/pack_bytes`: Distribution of sizes of packs sent to clients.
+* `git/auto-merge/num_operations`: Number of attempted auto merge operations and context.
 
 === BatchUpdate
 
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 2b49a06..fb027bd 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -64,6 +64,7 @@
 import com.google.gerrit.server.mail.send.CreateChangeSender;
 import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -114,6 +115,7 @@
   private final ReviewerAdder reviewerAdder;
   private final MessageIdGenerator messageIdGenerator;
   private final DynamicItem<UrlFormatter> urlFormatter;
+  private final AutoMerger autoMerger;
 
   private final Change.Id changeId;
   private final PatchSet.Id psId;
@@ -164,6 +166,7 @@
       ReviewerAdder reviewerAdder,
       MessageIdGenerator messageIdGenerator,
       DynamicItem<UrlFormatter> urlFormatter,
+      AutoMerger autoMerger,
       @Assisted Change.Id changeId,
       @Assisted ObjectId commitId,
       @Assisted String refName) {
@@ -181,6 +184,7 @@
     this.reviewerAdder = reviewerAdder;
     this.messageIdGenerator = messageIdGenerator;
     this.urlFormatter = urlFormatter;
+    this.autoMerger = autoMerger;
 
     this.changeId = changeId;
     this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
@@ -378,6 +382,15 @@
       return;
     }
     ctx.addRefUpdate(cmd);
+    Optional<ReceiveCommand> autoMerge =
+        autoMerger.createAutoMergeCommitIfNecessary(
+            ctx.getRepoView(),
+            ctx.getRevWalk(),
+            ctx.getInserter(),
+            ctx.getRevWalk().parseCommit(commitId));
+    if (autoMerge.isPresent()) {
+      ctx.addRefUpdate(autoMerge.get());
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 4c1e9fb..d2bf3fe 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -60,6 +61,7 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
@@ -82,6 +84,7 @@
   private final PatchSetUtil psUtil;
   private final WorkInProgressStateChanged wipStateChanged;
   private final MessageIdGenerator messageIdGenerator;
+  private final AutoMerger autoMerger;
 
   // Assisted-injected fields.
   private final PatchSet.Id psId;
@@ -124,6 +127,7 @@
       ProjectCache projectCache,
       WorkInProgressStateChanged wipStateChanged,
       MessageIdGenerator messageIdGenerator,
+      AutoMerger autoMerger,
       @Assisted ChangeNotes notes,
       @Assisted PatchSet.Id psId,
       @Assisted ObjectId commitId) {
@@ -138,6 +142,7 @@
     this.projectCache = projectCache;
     this.wipStateChanged = wipStateChanged;
     this.messageIdGenerator = messageIdGenerator;
+    this.autoMerger = autoMerger;
 
     this.origNotes = notes;
     this.psId = psId;
@@ -214,6 +219,15 @@
       throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
     validate(ctx);
     ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName());
+    Optional<ReceiveCommand> autoMerge =
+        autoMerger.createAutoMergeCommitIfNecessary(
+            ctx.getRepoView(),
+            ctx.getRevWalk(),
+            ctx.getInserter(),
+            ctx.getRevWalk().parseCommit(commitId));
+    if (autoMerge.isPresent()) {
+      ctx.addRefUpdate(autoMerge.get());
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 4600f6d..f35b3dd 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -148,6 +148,7 @@
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -356,6 +357,7 @@
   private final SetPrivateOp.Factory setPrivateOpFactory;
   private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
   private final DynamicItem<UrlFormatter> urlFormatter;
+  private final AutoMerger autoMerger;
 
   // Assisted injected fields.
   private final ProjectState projectState;
@@ -440,6 +442,7 @@
       SetPrivateOp.Factory setPrivateOpFactory,
       ReplyAttentionSetUpdates replyAttentionSetUpdates,
       DynamicItem<UrlFormatter> urlFormatter,
+      AutoMerger autoMerger,
       @Assisted ProjectState projectState,
       @Assisted IdentifiedUser user,
       @Assisted ReceivePack rp,
@@ -491,6 +494,7 @@
     this.setPrivateOpFactory = setPrivateOpFactory;
     this.replyAttentionSetUpdates = replyAttentionSetUpdates;
     this.urlFormatter = urlFormatter;
+    this.autoMerger = autoMerger;
 
     // Assisted injected fields.
     this.projectState = projectState;
@@ -3063,6 +3067,25 @@
         if (progress != null) {
           bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
         }
+        bu.addRepoOnlyOp(
+            new RepoOnlyOp() {
+              @Override
+              public void updateRepo(RepoContext ctx) throws Exception {
+                // Create auto merge ref if the new patch set is a merge commit. This is only
+                // required for new patch sets on existing changes as these do not go through
+                // PatchSetInserter. New changes pushed via git go through ChangeInserter and have
+                // their auto merge commits created there.
+                Optional<ReceiveCommand> autoMerge =
+                    autoMerger.createAutoMergeCommitIfNecessary(
+                        ctx.getRepoView(),
+                        ctx.getRevWalk(),
+                        ctx.getInserter(),
+                        ctx.getRevWalk().parseCommit(newCommitId));
+                if (autoMerge.isPresent()) {
+                  ctx.addRefUpdate(autoMerge.get());
+                }
+              }
+            });
       }
     }
 
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index ac37411..de5b1da 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -16,19 +16,22 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.base.Throwables;
-import com.google.gerrit.common.Nullable;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryableAction.ActionType;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.update.RepoView;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Optional;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
@@ -37,13 +40,13 @@
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.ResolveMerger;
 import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 
 /**
  * Utility class for creating an auto-merge commit of a merge commit.
@@ -67,6 +70,8 @@
  * is that these refs should never be deleted.
  */
 public class AutoMerger {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static final String AUTO_MERGE_MSG_PREFIX = "Auto-merge of ";
 
   @UsedAt(UsedAt.Project.GOOGLE)
@@ -74,92 +79,108 @@
     return cfg.getBoolean("change", null, "cacheAutomerge", true);
   }
 
-  private final RetryHelper retryHelper;
+  private enum OperationType {
+    CACHE_LOAD,
+    IN_MEMORY_WRITE,
+    ON_DISK_WRITE
+  }
+
+  private final Counter1<OperationType> counter;
   private final PersonIdent gerritIdent;
   private final boolean save;
+  private final ThreeWayMergeStrategy configuredMergeStrategy;
 
   @Inject
   AutoMerger(
-      RetryHelper retryHelper,
+      MetricMaker metricMaker,
       @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent gerritIdent) {
-    this.retryHelper = retryHelper;
-    save = cacheAutomerge(cfg);
+    this.counter =
+        metricMaker.newCounter(
+            "git/auto-merge/num_operations",
+            new Description("AutoMerge computations").setRate().setUnit("auto merge computations"),
+            Field.ofEnum(OperationType.class, "type", Metadata.Builder::operationName).build());
+    this.save = cacheAutomerge(cfg);
     this.gerritIdent = gerritIdent;
+    this.configuredMergeStrategy = MergeUtil.getMergeStrategy(cfg);
   }
 
   /**
-   * Creates an auto-merge commit of the parents of the given merge commit.
+   * Reads or creates an auto-merge commit of the parents of the given merge commit.
    *
-   * <p>In case of an exception the creation of the auto-merge commit is retried a few times. E.g.
-   * this allows the operation to succeed if a Git update fails due to a temporary issue.
+   * <p>The result is read from Git or computed in-memory and not written back to Git. This method
+   * exists for backwards compatibility only. All new changes have their auto-merge commits written
+   * transactionally when the change or patch set is created.
    *
    * @return auto-merge commit. Headers of the returned RevCommit are parsed.
    */
-  public RevCommit merge(
+  public RevCommit lookupFromGitOrMergeInMemory(
       Repository repo,
       RevWalk rw,
-      ObjectInserter ins,
-      RevCommit merge,
-      ThreeWayMergeStrategy mergeStrategy)
-      throws IOException {
-    try {
-      return retryHelper
-          .action(
-              ActionType.GIT_UPDATE,
-              "createAutoMerge",
-              () -> createAutoMergeCommit(repo, rw, ins, merge, mergeStrategy))
-          .defaultTimeoutMultiplier(2)
-          .call();
-    } catch (Exception e) {
-      Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, IOException.class);
-      throw new IllegalStateException(e);
-    }
-  }
-
-  /**
-   * Creates an auto-merge commit of the parents of the given merge commit.
-   *
-   * @return auto-merge commit. Headers of the returned RevCommit are parsed.
-   */
-  private RevCommit createAutoMergeCommit(
-      Repository repo,
-      RevWalk rw,
-      ObjectInserter ins,
+      InMemoryInserter ins,
       RevCommit merge,
       ThreeWayMergeStrategy mergeStrategy)
       throws IOException {
     checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
+    Optional<RevCommit> existingCommit =
+        lookupCommit(repo, rw, RefNames.refsCacheAutomerge(merge.name()));
+    if (existingCommit.isPresent()) {
+      counter.increment(OperationType.CACHE_LOAD);
+      return existingCommit.get();
+    }
+    counter.increment(OperationType.IN_MEMORY_WRITE);
+    logger.atWarning().log("Computing in-memory AutoMerge for " + merge.name());
+    return rw.parseCommit(createAutoMergeCommit(repo.getConfig(), rw, ins, merge, mergeStrategy));
+  }
 
-    InMemoryInserter tmpIns = null;
-    if (ins instanceof InMemoryInserter) {
-      // Caller gave us an in-memory inserter, so ensure anything we write from
-      // this method is visible to them.
-      tmpIns = (InMemoryInserter) ins;
-    } else if (!save) {
-      // If we don't plan on saving results, use a fully in-memory inserter.
-      // Using just a non-flushing wrapper is not sufficient, since in
-      // particular DfsInserter might try to write to storage after exceeding an
-      // internal buffer size.
-      tmpIns = new InMemoryInserter(rw.getObjectReader());
+  /**
+   * Creates an auto merge commit for the provided commit in case it is a merge commit. To be used
+   * whenever Gerrit creates new patch sets.
+   *
+   * <p>Callers need to include the returned {@link ReceiveCommand} in their ref transaction.
+   *
+   * @return A {@link ReceiveCommand} wrapped in an {@link Optional} to be used in a {@link
+   *     org.eclipse.jgit.lib.BatchRefUpdate}. {@link Optional#empty()} in case we don't need an
+   *     auto merge commit.
+   */
+  public Optional<ReceiveCommand> createAutoMergeCommitIfNecessary(
+      RepoView repoView, RevWalk rw, ObjectInserter ins, RevCommit maybeMergeCommit)
+      throws IOException {
+    if (maybeMergeCommit.getParentCount() != 2 || !save) {
+      logger.atFine().log("AutoMerge not required");
+      return Optional.empty();
     }
 
+    ObjectId autoMerge =
+        createAutoMergeCommit(
+            repoView.getConfig(), rw, ins, maybeMergeCommit, configuredMergeStrategy);
+    counter.increment(OperationType.ON_DISK_WRITE);
+    logger.atFine().log("Added %s AutoMerge ref update for commit", autoMerge.name());
+    return Optional.of(
+        new ReceiveCommand(
+            ObjectId.zeroId(), autoMerge, RefNames.refsCacheAutomerge(maybeMergeCommit.name())));
+  }
+
+  /**
+   * Creates an auto-merge commit of the parents of the given merge commit.
+   *
+   * @return auto-merge commit. Headers of the returned RevCommit are parsed.
+   */
+  private ObjectId createAutoMergeCommit(
+      Config repoConfig,
+      RevWalk rw,
+      ObjectInserter ins,
+      RevCommit merge,
+      ThreeWayMergeStrategy mergeStrategy)
+      throws IOException {
     rw.parseHeaders(merge);
-    String refName = RefNames.refsCacheAutomerge(merge.name());
-    Ref ref = repo.getRefDatabase().exactRef(refName);
-    if (ref != null && ref.getObjectId() != null) {
-      RevObject obj = rw.parseAny(ref.getObjectId());
-      if (obj instanceof RevCommit) {
-        return (RevCommit) obj;
-      }
-      return commit(repo, rw, tmpIns, ins, refName, obj, merge);
-    }
-
-    ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
+    ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(ins, repoConfig);
     DirCache dc = DirCache.newInCore();
     m.setDirCache(dc);
-    m.setObjectInserter(tmpIns == null ? new NonFlushingWrapper(ins) : tmpIns);
+    // If we don't plan on saving results, use a fully in-memory inserter.
+    // Using just a non-flushing wrapper is not sufficient, since in particular DfsInserter might
+    // try to write to storage after exceeding an internal buffer size.
+    m.setObjectInserter(ins instanceof InMemoryInserter ? new NonFlushingWrapper(ins) : ins);
 
     boolean couldMerge = m.merge(merge.getParents());
 
@@ -179,18 +200,6 @@
               m.getMergeResults());
     }
 
-    return commit(repo, rw, tmpIns, ins, refName, treeId, merge);
-  }
-
-  private RevCommit commit(
-      Repository repo,
-      RevWalk rw,
-      @Nullable InMemoryInserter tmpIns,
-      ObjectInserter ins,
-      String refName,
-      ObjectId tree,
-      RevCommit merge)
-      throws IOException {
     rw.parseHeaders(merge);
     // For maximum stability, choose a single ident using the committer time of
     // the input commit, using the server name and timezone.
@@ -200,50 +209,34 @@
     CommitBuilder cb = new CommitBuilder();
     cb.setAuthor(ident);
     cb.setCommitter(ident);
-    cb.setTreeId(tree);
+    cb.setTreeId(treeId);
     cb.setMessage(AUTO_MERGE_MSG_PREFIX + merge.name() + '\n');
     for (RevCommit p : merge.getParents()) {
       cb.addParentId(p);
     }
 
-    if (!save) {
-      checkArgument(tmpIns != null);
-      try (ObjectReader tmpReader = tmpIns.newReader();
+    if (ins instanceof InMemoryInserter) {
+      // When using an InMemoryInserter we need to read back the values from that inserter because
+      // they are not available.
+      try (ObjectReader tmpReader = ins.newReader();
           RevWalk tmpRw = new RevWalk(tmpReader)) {
-        return tmpRw.parseCommit(tmpIns.insert(cb));
+        return tmpRw.parseCommit(ins.insert(cb));
       }
     }
 
-    checkArgument(tmpIns == null);
-    checkArgument(!(ins instanceof InMemoryInserter));
-    ObjectId commitId = ins.insert(cb);
-    ins.flush();
+    return rw.parseCommit(ins.insert(cb));
+  }
 
-    RefUpdate ru = repo.updateRef(refName);
-    ru.setNewObjectId(commitId);
-    ru.disableRefLog();
-    switch (ru.forceUpdate()) {
-      case FAST_FORWARD:
-      case FORCED:
-      case NEW:
-      case NO_CHANGE:
-        return rw.parseCommit(commitId);
-      case LOCK_FAILURE:
-        throw new LockFailureException(
-            String.format("Failed to create auto-merge of %s", merge.name()), ru);
-      case IO_FAILURE:
-      case NOT_ATTEMPTED:
-      case REJECTED:
-      case REJECTED_CURRENT_BRANCH:
-      case REJECTED_MISSING_OBJECT:
-      case REJECTED_OTHER_REASON:
-      case RENAMED:
-      default:
-        throw new IOException(
-            String.format(
-                "Failed to create auto-merge of %s: Cannot write %s (%s)",
-                merge.name(), refName, ru.getResult()));
+  private Optional<RevCommit> lookupCommit(Repository repo, RevWalk rw, String refName)
+      throws IOException {
+    Ref ref = repo.getRefDatabase().exactRef(refName);
+    if (ref != null && ref.getObjectId() != null) {
+      RevObject obj = rw.parseAny(ref.getObjectId());
+      if (obj instanceof RevCommit) {
+        return Optional.of((RevCommit) obj);
+      }
     }
+    return Optional.empty();
   }
 
   private static class NonFlushingWrapper extends ObjectInserter.Filter {
diff --git a/java/com/google/gerrit/server/patch/BaseCommitUtil.java b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
index de4a10e..7c06a62 100644
--- a/java/com/google/gerrit/server/patch/BaseCommitUtil.java
+++ b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
@@ -37,16 +37,11 @@
 class BaseCommitUtil {
   private final AutoMerger autoMerger;
   private final ThreeWayMergeStrategy mergeStrategy;
-
-  /** If true, auto-merge results are stored in the repository. */
-  private final boolean saveAutomerge;
-
   private final GitRepositoryManager repoManager;
 
   @Inject
   BaseCommitUtil(AutoMerger am, @GerritServerConfig Config cfg, GitRepositoryManager repoManager) {
     this.autoMerger = am;
-    this.saveAutomerge = AutoMerger.cacheAutomerge(cfg);
     this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
     this.repoManager = repoManager;
   }
@@ -54,7 +49,7 @@
   RevObject getBaseCommit(Project.NameKey project, ObjectId newCommit, @Nullable Integer parentNum)
       throws IOException {
     try (Repository repo = repoManager.openRepository(project);
-        ObjectInserter ins = newInserter(repo);
+        InMemoryInserter ins = new InMemoryInserter(repo);
         ObjectReader reader = ins.newReader();
         RevWalk rw = new RevWalk(reader)) {
       return getParentCommit(repo, ins, rw, parentNum, newCommit);
@@ -93,7 +88,7 @@
    */
   RevObject getParentCommit(
       Repository repo,
-      ObjectInserter ins,
+      InMemoryInserter ins,
       RevWalk rw,
       @Nullable Integer parentNum,
       ObjectId commitId)
@@ -112,16 +107,12 @@
         }
         // Only support auto-merge for 2 parents, not octopus merges
         if (current.getParentCount() == 2) {
-          return autoMerger.merge(repo, rw, ins, current, mergeStrategy);
+          return autoMerger.lookupFromGitOrMergeInMemory(repo, rw, ins, current, mergeStrategy);
         }
         return null;
     }
   }
 
-  private ObjectInserter newInserter(Repository repo) {
-    return saveAutomerge ? repo.newObjectInserter() : new InMemoryInserter(repo);
-  }
-
   private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
     ObjectId id = ins.insert(Constants.OBJ_TREE, new byte[] {});
     ins.flush();
diff --git a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java b/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
index d1c0b45..017e276 100644
--- a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
@@ -107,7 +107,6 @@
   private final PatchListKey key;
   private final Project.NameKey project;
   private final long timeoutMillis;
-  private final boolean save;
 
   @Inject
   PatchListLoader(
@@ -133,13 +132,12 @@
             "timeout",
             TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
             TimeUnit.MILLISECONDS);
-    save = AutoMerger.cacheAutomerge(cfg);
   }
 
   @Override
   public PatchList call() throws IOException, PatchListNotAvailableException {
     try (Repository repo = repoManager.openRepository(project);
-        ObjectInserter ins = newInserter(repo);
+        InMemoryInserter ins = new InMemoryInserter(repo);
         ObjectReader reader = ins.newReader();
         RevWalk rw = new RevWalk(reader)) {
       return readPatchList(repo, rw, ins);
@@ -163,11 +161,7 @@
     }
   }
 
-  private ObjectInserter newInserter(Repository repo) {
-    return save ? repo.newObjectInserter() : new InMemoryInserter(repo);
-  }
-
-  private PatchList readPatchList(Repository repo, RevWalk rw, ObjectInserter ins)
+  private PatchList readPatchList(Repository repo, RevWalk rw, InMemoryInserter ins)
       throws IOException, PatchListNotAvailableException {
     ObjectReader reader = rw.getObjectReader();
     checkArgument(reader.getCreatedFromInserter() == ins);
@@ -634,7 +628,7 @@
   }
 
   private RevObject aFor(
-      PatchListKey key, Repository repo, RevWalk rw, ObjectInserter ins, RevCommit b)
+      PatchListKey key, Repository repo, RevWalk rw, InMemoryInserter ins, RevCommit b)
       throws IOException {
     if (key.getOldId() != null) {
       return rw.parseAny(key.getOldId());
@@ -657,7 +651,7 @@
         }
         // Only support auto-merge for 2 parents, not octopus merges
         if (b.getParentCount() == 2) {
-          return autoMerger.merge(repo, rw, ins, b, mergeStrategy);
+          return autoMerger.lookupFromGitOrMergeInMemory(repo, rw, ins, b, mergeStrategy);
         }
         return null;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index 12b4d44..04828f2 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -39,7 +40,6 @@
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -86,7 +86,7 @@
       throws RestApiException, IOException, InvalidChangeOperationException {
     Project.NameKey project = resource.getRevision().getChange().getProject();
     try (Repository repository = repoManager.openRepository(project);
-        ObjectInserter ins = repository.newObjectInserter();
+        InMemoryInserter ins = new InMemoryInserter(repository);
         ObjectReader reader = ins.newReader();
         RevWalk revWalk = new RevWalk(reader)) {
       String refName =
@@ -115,7 +115,9 @@
         result = blame(parents[0], path, repository, revWalk);
 
       } else if (parents.length == 2) {
-        ObjectId automerge = autoMerger.merge(repository, revWalk, ins, revCommit, mergeStrategy);
+        ObjectId automerge =
+            autoMerger.lookupFromGitOrMergeInMemory(
+                repository, revWalk, ins, revCommit, mergeStrategy);
         result = blame(automerge, path, repository, revWalk);
 
       } else {
diff --git a/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java b/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
new file mode 100644
index 0000000..46688dd
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
@@ -0,0 +1,189 @@
+// Copyright (C) 2021 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.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Ensures that auto merge commits are created when a new patch set or change is uploaded. */
+public class AutoMergeIT extends AbstractDaemonTest {
+  private RevCommit parent1;
+  private RevCommit parent2;
+
+  @Before
+  public void setup() throws Exception {
+    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+    PushOneCommit.Result p1 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "parent 1",
+                ImmutableMap.of("foo", "foo-1.2", "bar", "bar-1.2"))
+            .to("refs/for/master");
+    parent1 = p1.getCommit();
+
+    // reset HEAD in order to create a sibling of the first change
+    testRepo.reset(initial);
+
+    PushOneCommit.Result p2 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "parent 2",
+                ImmutableMap.of("foo", "foo-2.2", "bar", "bar-2.2"))
+            .to("refs/for/master");
+    parent2 = p2.getCommit();
+  }
+
+  @Test
+  public void autoMergeCreatedWhenPushingNewChange() throws Exception {
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(parent1, parent2));
+    PushOneCommit.Result result = m.to("refs/for/master");
+    result.assertOkStatus();
+    assertAutoMergeCreated(result.getCommit());
+  }
+
+  @Test
+  public void autoMergeCreatedWhenPushingNewPatchSet() throws Exception {
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(parent1, parent2));
+    PushOneCommit.Result ps1 = m.to("refs/for/master");
+    RevCommit ps2 =
+        testRepo
+            .amend(ps1.getCommit())
+            .message("PS2")
+            .insertChangeId(ps1.getChangeId().substring(1))
+            .create();
+    testRepo.reset(ps2);
+    GitUtil.pushHead(testRepo, "refs/for/master");
+    // Make sure we have two patch sets
+    assertThat(ps2.getParents().length).isEqualTo(2);
+    assertThat(gApi.changes().id(ps1.getChangeId()).get().revisions.size()).isEqualTo(2);
+    assertAutoMergeCreated(ps2);
+  }
+
+  @Test
+  public void autoMergeCreatedWhenChangeCreatedOnApi() throws Exception {
+    ChangeInput ci = new ChangeInput(project.get(), "master", "Merge commit");
+    ci.merge = new MergeInput();
+    ci.merge.source = parent1.name();
+
+    String newChangePatchSetSha1 = gApi.changes().create(ci).get().currentRevision;
+    assertAutoMergeCreated(ObjectId.fromString(newChangePatchSetSha1));
+  }
+
+  @Test
+  public void autoMergeCreatedWhenNewPatchSetCreatedOnApi() throws Exception {
+    ChangeInput ci = new ChangeInput(project.get(), "master", "Merge commit");
+    ci.merge = new MergeInput();
+    ci.merge.source = parent1.name();
+
+    String changeId = gApi.changes().create(ci).get().changeId;
+    gApi.changes().id(changeId).setMessage("New Commit Message\n\nChange-Id: " + changeId);
+    assertThat(gApi.changes().id(changeId).get().revisions.size()).isEqualTo(2);
+    String newChangePatchSetSha1 = gApi.changes().id(changeId).get().currentRevision;
+    assertAutoMergeCreated(ObjectId.fromString(newChangePatchSetSha1));
+  }
+
+  @Test
+  public void autoMergeCreatedWhenChangeEditIsPublished() throws Exception {
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(parent1, parent2));
+    PushOneCommit.Result result = m.to("refs/for/master");
+    result.assertOkStatus();
+    assertAutoMergeCreated(result.getCommit());
+
+    gApi.changes()
+        .id(result.getChangeId())
+        .edit()
+        .modifyFile("new-file", RawInputUtil.create("content"));
+    gApi.changes().id(result.getChangeId()).edit().publish();
+    assertThat(gApi.changes().id(result.getChangeId()).get().revisions.size()).isEqualTo(2);
+    String newChangePatchSetSha1 = gApi.changes().id(result.getChangeId()).get().currentRevision;
+    assertAutoMergeCreated(ObjectId.fromString(newChangePatchSetSha1));
+  }
+
+  @Test
+  public void noAutoMergeCreatedWhenPushingNonMergeCommit() throws Exception {
+    PushOneCommit.Result change = createChange();
+    change.assertOkStatus();
+    assertNoAutoMergeCreated(change.getCommit());
+  }
+
+  @Test
+  public void autoMergeComputedInMemoryWhenMissing() throws Exception {
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(parent1, parent2));
+    PushOneCommit.Result result = m.to("refs/for/master");
+    result.assertOkStatus();
+    assertAutoMergeCreated(result.getCommit());
+
+    // Delete auto merge branch
+    deleteAutoMergeBranch(result.getCommit());
+    // Trigger AutoMerge computation
+    assertThat(gApi.changes().id(result.getChangeId()).revision(1).file("foo").blameRequest().get())
+        .isNotEmpty();
+    assertNoAutoMergeCreated(result.getCommit());
+  }
+
+  private void assertAutoMergeCreated(ObjectId mergeCommit) throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef(RefNames.refsCacheAutomerge(mergeCommit.name()))).isNotNull();
+    }
+  }
+
+  private void assertNoAutoMergeCreated(ObjectId mergeCommit) throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.exactRef(RefNames.refsCacheAutomerge(mergeCommit.name()))).isNull();
+    }
+  }
+
+  private void deleteAutoMergeBranch(ObjectId mergeCommit) throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      RefUpdate ru = repo.updateRef(RefNames.refsCacheAutomerge(mergeCommit.name()));
+      ru.setForceUpdate(true);
+      assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+    }
+    assertNoAutoMergeCreated(mergeCommit);
+  }
+}
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 12afddf..8fe3b24 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -377,6 +377,8 @@
     --iron-overlay-backdrop: {
       transition: none;
     };
+    --paper-tooltip-delay-in: 200ms;
+    --paper-tooltip-delay-out: 0;
     --paper-tooltip-duration-in: 0;
     --paper-tooltip-duration-out: 0;
     --paper-tooltip-background: var(--tooltip-background-color);