Defer object flushing when merging

Mergers in the most recent JGit know how to read previously-inserted
objects back, so a series of merges does not require aggressively
flushing the objects. Instead, flush objects in MergeOp just before
updating the relevant refs.

Change-Id: Ia85c5114e86739b4a480bea65f3413fe47cf572d
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 374cde3..262e7bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -113,87 +113,89 @@
 
     Project.NameKey project = change.getProject();
     IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
-    final Repository git;
+    Repository git = null;
+    ObjectInserter oi = null;
+    RevWalk revWalk = null;
+
     try {
       git = gitManager.openRepository(project);
+      oi = git.newObjectInserter();
+      revWalk = new RevWalk(oi.newReader());
+
+      Ref destRef = git.getRef(destinationBranch);
+      if (destRef == null) {
+        throw new InvalidChangeOperationException("Branch "
+            + destinationBranch + " does not exist.");
+      }
+
+      final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
+
+      RevCommit commitToCherryPick =
+          revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+
+      PersonIdent committerIdent =
+          identifiedUser.newCommitterIdent(TimeUtil.nowTs(),
+              serverTimeZone);
+
+      final ObjectId computedChangeId =
+          ChangeIdUtil
+              .computeChangeId(commitToCherryPick.getTree(), mergeTip,
+                  commitToCherryPick.getAuthorIdent(), committerIdent, message);
+      String commitMessage =
+          ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
+
+      RevCommit cherryPickCommit;
+      ProjectState projectState = refControl.getProjectControl().getProjectState();
+      cherryPickCommit =
+          mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
+              commitToCherryPick, committerIdent, commitMessage, revWalk);
+      oi.flush();
+
+      Change.Key changeKey;
+      final List<String> idList = cherryPickCommit.getFooterLines(
+          FooterConstants.CHANGE_ID);
+      if (!idList.isEmpty()) {
+        final String idStr = idList.get(idList.size() - 1).trim();
+        changeKey = new Change.Key(idStr);
+      } else {
+        changeKey = new Change.Key("I" + computedChangeId.name());
+      }
+
+      Branch.NameKey newDest =
+          new Branch.NameKey(change.getProject(), destRef.getName());
+      List<ChangeData> destChanges = queryProvider.get()
+          .setLimit(2)
+          .byBranchKey(newDest, changeKey);
+      if (destChanges.size() > 1) {
+        throw new InvalidChangeOperationException("Several changes with key "
+            + changeKey + " reside on the same branch. "
+            + "Cannot create a new patch set.");
+      } else if (destChanges.size() == 1) {
+        // The change key exists on the destination branch. The cherry pick
+        // will be added as a new patch set.
+        return insertPatchSet(git, revWalk, destChanges.get(0).change(),
+            cherryPickCommit, refControl, identifiedUser);
+      } else {
+        // Change key not found on destination branch. We can create a new
+        // change.
+        return createNewChange(git, revWalk, changeKey, project,
+            patch.getId(), destRef, cherryPickCommit, refControl,
+            identifiedUser, change.getTopic());
+      }
+    } catch (MergeIdenticalTreeException | MergeConflictException e) {
+      throw new MergeException("Cherry pick failed: " + e.getMessage());
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(change.getId(), e);
-    }
-
-    try {
-      RevWalk revWalk = new RevWalk(git);
-      try {
-        Ref destRef = git.getRef(destinationBranch);
-        if (destRef == null) {
-          throw new InvalidChangeOperationException("Branch "
-              + destinationBranch + " does not exist.");
-        }
-
-        final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
-
-        RevCommit commitToCherryPick =
-            revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
-
-        PersonIdent committerIdent =
-            identifiedUser.newCommitterIdent(TimeUtil.nowTs(),
-                serverTimeZone);
-
-        final ObjectId computedChangeId =
-            ChangeIdUtil
-                .computeChangeId(commitToCherryPick.getTree(), mergeTip,
-                    commitToCherryPick.getAuthorIdent(), committerIdent, message);
-        String commitMessage =
-            ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
-
-        RevCommit cherryPickCommit;
-        ObjectInserter oi = git.newObjectInserter();
-        try {
-          ProjectState projectState = refControl.getProjectControl().getProjectState();
-          cherryPickCommit =
-              mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
-                  commitToCherryPick, committerIdent, commitMessage, revWalk);
-        } catch (MergeIdenticalTreeException | MergeConflictException e) {
-          throw new MergeException("Cherry pick failed: " + e.getMessage());
-        } finally {
-          oi.release();
-        }
-
-        Change.Key changeKey;
-        final List<String> idList = cherryPickCommit.getFooterLines(
-            FooterConstants.CHANGE_ID);
-        if (!idList.isEmpty()) {
-          final String idStr = idList.get(idList.size() - 1).trim();
-          changeKey = new Change.Key(idStr);
-        } else {
-          changeKey = new Change.Key("I" + computedChangeId.name());
-        }
-
-        Branch.NameKey newDest =
-            new Branch.NameKey(change.getProject(), destRef.getName());
-        List<ChangeData> destChanges = queryProvider.get()
-            .setLimit(2)
-            .byBranchKey(newDest, changeKey);
-        if (destChanges.size() > 1) {
-          throw new InvalidChangeOperationException("Several changes with key "
-              + changeKey + " reside on the same branch. "
-              + "Cannot create a new patch set.");
-        } else if (destChanges.size() == 1) {
-          // The change key exists on the destination branch. The cherry pick
-          // will be added as a new patch set.
-          return insertPatchSet(git, revWalk, destChanges.get(0).change(),
-              cherryPickCommit, refControl, identifiedUser);
-        } else {
-          // Change key not found on destination branch. We can create a new
-          // change.
-          return createNewChange(git, revWalk, changeKey, project,
-              patch.getId(), destRef, cherryPickCommit, refControl,
-              identifiedUser, change.getTopic());
-        }
-      } finally {
+    } finally {
+      if (revWalk != null) {
         revWalk.release();
       }
-    } finally {
-      git.close();
+      if (oi != null) {
+        oi.release();
+      }
+      if (git != null) {
+        git.close();
+      }
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index fac0f7f..1ff37b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -23,6 +23,7 @@
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -50,12 +51,11 @@
       }).nullsFirst();
 
   public static RevWalk newRevWalk(Repository repo) {
-    return new RevWalk(repo) {
-      @Override
-      protected RevCommit createCommit(AnyObjectId id) {
-        return new CodeReviewCommit(id);
-      }
-    };
+    return new CodeReviewRevWalk(repo);
+  }
+
+  public static RevWalk newRevWalk(ObjectReader reader) {
+    return new CodeReviewRevWalk(reader);
   }
 
   static CodeReviewCommit revisionGone(ChangeControl ctl) {
@@ -85,6 +85,21 @@
     return r;
   }
 
+  private static class CodeReviewRevWalk extends RevWalk {
+    private CodeReviewRevWalk(Repository repo) {
+      super(repo);
+    }
+
+    private CodeReviewRevWalk(ObjectReader reader) {
+      super(reader);
+    }
+
+    @Override
+    protected RevCommit createCommit(AnyObjectId id) {
+      return new CodeReviewCommit(id);
+    }
+  }
+
   /**
    * Unique key of the PatchSet entity from the code review system.
    * <p>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 106c1e3..8ae3d2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -430,13 +430,11 @@
       String m = "Error opening repository \"" + name.get() + '"';
       throw new MergeException(m, err);
     }
-
-    rw = CodeReviewCommit.newRevWalk(repo);
+    inserter = repo.newObjectInserter();
+    rw = CodeReviewCommit.newRevWalk(inserter.newReader());
     rw.sort(RevSort.TOPO);
     rw.sort(RevSort.COMMIT_TIME_DESC, true);
     canMergeFlag = rw.newFlag("CAN_MERGE");
-
-    inserter = repo.newObjectInserter();
   }
 
   private RefUpdate openBranch()
@@ -674,6 +672,11 @@
             + destProject.getProject().getName(), e);
       }
     }
+    try {
+      inserter.flush();
+    } catch (IOException e) {
+      throw new MergeException("Cannot flush merge results", e);
+    }
 
     branchUpdate.setRefLogIdent(refLogIdent);
     branchUpdate.setForceUpdate(false);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 295ad52..c798f4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -43,7 +43,6 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -67,7 +66,6 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -79,6 +77,12 @@
 import java.util.Set;
 import java.util.TimeZone;
 
+/**
+ * Utilities for various kinds of merges and cherry-picks.
+ * <p>
+ * <b>Note:</b> Unless otherwise noted, the methods in this class do not flush
+ * the {@link ObjectInserter}s passed in after performing a merge.
+ */
 public class MergeUtil {
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
   private static final String R_HEADS_MASTER =
@@ -177,7 +181,7 @@
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
 
     m.setBase(originalCommit.getParent(0));
-    if (m.merge(mergeTip, originalCommit)) {
+    if (m.merge(false, mergeTip, originalCommit)) {
       ObjectId tree = m.getResultTreeId();
       if (tree.equals(mergeTip.getTree())) {
         throw new MergeIdenticalTreeException("identical tree");
@@ -189,7 +193,7 @@
       mergeCommit.setAuthor(originalCommit.getAuthorIdent());
       mergeCommit.setCommitter(cherryPickCommitterIdent);
       mergeCommit.setMessage(commitMsg);
-      return rw.parseCommit(commit(inserter, mergeCommit));
+      return rw.parseCommit(inserter.insert(mergeCommit));
     } else {
       throw new MergeConflictException("merge conflict");
     }
@@ -393,7 +397,7 @@
 
     ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
     try {
-      return m.merge(new AnyObjectId[] {mergeTip, toMerge});
+      return m.merge(false, mergeTip, toMerge);
     } catch (LargeObjectException e) {
       log.warn("Cannot merge due to LargeObjectException: " + toMerge.name());
       return false;
@@ -442,7 +446,7 @@
       try {
         ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
         m.setBase(toMerge.getParent(0));
-        return m.merge(mergeTip, toMerge);
+        return m.merge(false, mergeTip, toMerge);
       } catch (IOException e) {
         throw new MergeException("Cannot merge " + toMerge.name(), e);
       }
@@ -494,7 +498,7 @@
       throws MergeException {
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
     try {
-      if (m.merge(new AnyObjectId[] {mergeTip, n})) {
+      if (m.merge(false, mergeTip, n)) {
         return writeMergeCommit(myIdent, rw, inserter, canMergeFlag, destBranch,
             mergeTip, m.getResultTreeId(), n);
       } else {
@@ -582,7 +586,7 @@
     mergeCommit.setMessage(msgbuf.toString());
 
     CodeReviewCommit mergeResult =
-        (CodeReviewCommit) rw.parseCommit(commit(inserter, mergeCommit));
+        (CodeReviewCommit) rw.parseCommit(inserter.insert(mergeCommit));
     mergeResult.setControl(n.getControl());
     return mergeResult;
   }
@@ -656,31 +660,10 @@
     Merger m = strategy.newMerger(repo, true);
     checkArgument(m instanceof ThreeWayMerger,
         "merge strategy %s does not support three-way merging", strategyName);
-    m.setObjectInserter(new ObjectInserter.Filter() {
-      @Override
-      protected ObjectInserter delegate() {
-        return inserter;
-      }
-
-      @Override
-      public void flush() {
-      }
-
-      @Override
-      public void release() {
-      }
-    });
+    m.setObjectInserter(inserter);
     return (ThreeWayMerger) m;
   }
 
-  public ObjectId commit(final ObjectInserter inserter,
-      final CommitBuilder mergeCommit) throws IOException,
-      UnsupportedEncodingException {
-    ObjectId id = inserter.insert(mergeCommit);
-    inserter.flush();
-    return id;
-  }
-
   public PatchSetApproval markCleanMerges(final RevWalk rw,
       final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
       final Set<RevCommit> alreadyAccepted) throws MergeException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 0c8c5ec..50a8117 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -275,24 +275,11 @@
     try {
       DirCache dc = DirCache.newInCore();
       m.setDirCache(dc);
-      m.setObjectInserter(new ObjectInserter.Filter() {
-        @Override
-        protected ObjectInserter delegate() {
-          return ins;
-        }
-
-        @Override
-        public void flush() {
-        }
-
-        @Override
-        public void release() {
-        }
-      });
+      m.setObjectInserter(ins);
 
       boolean couldMerge;
       try {
-        couldMerge = m.merge(b.getParents());
+        couldMerge = m.merge(false, b.getParents());
       } catch (IOException e) {
         // It is not safe to continue further down in this method as throwing
         // an exception most likely means that the merge tree was not created