InlineEdit: Add method to publish edits

Change-Id: I7f73c716d71a0cf335d99d90bf96452ed3798ad6
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 80213f6..cfd6cd2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -520,7 +520,7 @@
     return (IdentifiedUser) userProvider.get();
   }
 
-  private static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
+  public static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
     return new PatchSet.Id(id.getParentKey(), id.get() + 1);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index c1be069..666dc5f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -15,35 +15,63 @@
 package com.google.gerrit.server.edit;
 
 import com.google.common.base.Optional;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.TimeUtil;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.util.List;
 
 /**
  * Utility functions to manipulate change edits.
  * <p>
- * This class contains method to retrieve edits.
+ * This class contains methods to retrieve and publish edits.
  */
 @Singleton
 public class ChangeEditUtil {
   private final GitRepositoryManager gitManager;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final Provider<ReviewDb> db;
   private final Provider<IdentifiedUser> user;
 
   @Inject
   ChangeEditUtil(GitRepositoryManager gitManager,
+      PatchSetInserter.Factory patchSetInserterFactory,
+      ChangeControl.GenericFactory changeControlFactory,
+      Provider<ReviewDb> db,
       Provider<IdentifiedUser> user) {
     this.gitManager = gitManager;
+    this.patchSetInserterFactory = patchSetInserterFactory;
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
     this.user = user;
   }
 
@@ -76,6 +104,121 @@
   }
 
   /**
+   * Promote change edit to patch set, by squashing the edit into
+   * its parent.
+   *
+   * @param edit change edit to publish
+   * @throws AuthException
+   * @throws NoSuchChangeException
+   * @throws IOException
+   * @throws InvalidChangeOperationException
+   * @throws OrmException
+   * @throws ResourceConflictException
+   */
+  public void publish(ChangeEdit edit) throws AuthException,
+      NoSuchChangeException, IOException, InvalidChangeOperationException,
+      OrmException, ResourceConflictException {
+    Change change = edit.getChange();
+    Repository repo = gitManager.openRepository(change.getProject());
+    try {
+      RevWalk rw = new RevWalk(repo);
+      ObjectInserter inserter = repo.newObjectInserter();
+      try {
+        RevCommit editCommit = rw.parseCommit(edit.getRef().getObjectId());
+        if (editCommit == null) {
+          throw new NoSuchChangeException(change.getId());
+        }
+
+        PatchSet basePatchSet = getBasePatchSet(edit, editCommit);
+        if (!basePatchSet.getId().equals(change.currentPatchSetId())) {
+          throw new ResourceConflictException(
+              "only edit for current patch set can be published");
+        }
+
+        insertPatchSet(edit, change, repo, rw, basePatchSet,
+            squashEdit(repo, rw, inserter, editCommit, basePatchSet));
+      } finally {
+        inserter.release();
+        rw.release();
+      }
+
+      // TODO(davido): This should happen in the same BatchRefUpdate.
+      deleteRef(repo, edit);
+    } finally {
+      repo.close();
+    }
+  }
+
+  /**
+   * Retrieve base patch set the edit was created on.
+   *
+   * @param edit change edit to retrieve base patch set for
+   * @return parent patch set of the edit
+   * @throws IOException
+   * @throws InvalidChangeOperationException
+   */
+  public PatchSet getBasePatchSet(ChangeEdit edit) throws IOException,
+      InvalidChangeOperationException {
+    Change change = edit.getChange();
+    Repository repo = gitManager.openRepository(change.getProject());
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        return getBasePatchSet(edit, rw.parseCommit(
+            edit.getRef().getObjectId()));
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  /**
+   * Retrieve base patch set the edit was created on.
+   *
+   * @param edit change edit to retrieve base patch set for
+   * @param commit change edit commit
+   * @return parent patch set of the edit
+   * @throws IOException
+   * @throws InvalidChangeOperationException
+   */
+  public PatchSet getBasePatchSet(ChangeEdit edit, RevCommit commit)
+      throws IOException, InvalidChangeOperationException {
+    if (commit.getParentCount() != 1) {
+      throw new InvalidChangeOperationException(
+          "change edit commit has multiple parents");
+    }
+    RevCommit parentCommit = commit.getParent(0);
+    ObjectId rev = parentCommit.getId();
+    RevId parentRev = new RevId(ObjectId.toString(rev));
+    try {
+      List<PatchSet> r = db.get().patchSets().byRevision(parentRev).toList();
+      if (r.isEmpty()) {
+        throw new InvalidChangeOperationException(String.format(
+            "patch set %s change edit is based on doesn't exist",
+            rev.abbreviate(8)));
+      }
+      if (r.size() > 1) {
+        throw new InvalidChangeOperationException(String.format(
+            "multiple patch sets for change edit parent %s",
+            rev.abbreviate(8)));
+      }
+      PatchSet parentPatchSet = Iterables.getOnlyElement(r);
+      if (!edit.getChange().getId().equals(
+              parentPatchSet.getId().getParentKey())) {
+        throw new InvalidChangeOperationException(String.format(
+            "different change edit ID %d and its parent patch set %d",
+            edit.getChange().getId().get(),
+            parentPatchSet.getId().getParentKey().get()));
+      }
+      return parentPatchSet;
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+  }
+
+  /**
    * Returns reference for this change edit with sharded user and change number:
    * refs/users/UU/UUUU/edit-CCCC.
    *
@@ -88,4 +231,75 @@
         RefNames.refsUsers(accountId),
         changeId.get());
   }
+
+  private RevCommit squashEdit(Repository repo, RevWalk rw,
+      ObjectInserter inserter, RevCommit edit, PatchSet basePatchSet)
+      throws IOException, ResourceConflictException {
+    RevCommit parent = rw.parseCommit(ObjectId.fromString(
+        basePatchSet.getRevision().get()));
+    if (parent.getTree().equals(edit.getTree())) {
+      throw new ResourceConflictException("identical tree");
+    }
+    return writeSquashedCommit(rw, inserter, parent, edit);
+  }
+
+  private void insertPatchSet(ChangeEdit edit, Change change,
+      Repository repo, RevWalk rw, PatchSet basePatchSet, RevCommit squashed)
+      throws NoSuchChangeException, InvalidChangeOperationException,
+      OrmException, IOException {
+    PatchSet ps = new PatchSet(
+        ChangeUtil.nextPatchSetId(change.currentPatchSetId()));
+    ps.setRevision(new RevId(ObjectId.toString(squashed)));
+    ps.setUploader(edit.getUser().getAccountId());
+    ps.setCreatedOn(TimeUtil.nowTs());
+
+    PatchSetInserter insr =
+        patchSetInserterFactory.create(repo, rw,
+            changeControlFactory.controlFor(change, edit.getUser()),
+            squashed);
+    insr.setPatchSet(ps)
+        .setMessage(
+            String.format("Patch Set %d: Published edit on patch set %d",
+                ps.getPatchSetId(),
+                basePatchSet.getPatchSetId()))
+        .insert();
+  }
+
+  private static void deleteRef(Repository repo, ChangeEdit edit)
+      throws IOException {
+    String refName = edit.getRefName();
+    RefUpdate ru = repo.updateRef(refName, true);
+    ru.setExpectedOldObjectId(edit.getRef().getObjectId());
+    ru.setForceUpdate(true);
+    RefUpdate.Result result = ru.delete();
+    switch (result) {
+      case FORCED:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      default:
+        throw new IOException(String.format("Failed to delete ref %s: %s",
+            refName, result));
+    }
+  }
+
+  private static RevCommit writeSquashedCommit(RevWalk rw,
+      ObjectInserter inserter, RevCommit parent, RevCommit edit)
+      throws IOException {
+    CommitBuilder mergeCommit = new CommitBuilder();
+    mergeCommit.setParentIds(parent.getParent(0));
+    mergeCommit.setAuthor(parent.getAuthorIdent());
+    mergeCommit.setMessage(parent.getFullMessage());
+    mergeCommit.setCommitter(edit.getCommitterIdent());
+    mergeCommit.setTreeId(edit.getTree());
+
+    return rw.parseCommit(commit(inserter, mergeCommit));
+  }
+
+  private static ObjectId commit(ObjectInserter inserter,
+      CommitBuilder mergeCommit) throws IOException {
+    ObjectId id = inserter.insert(mergeCommit);
+    inserter.flush();
+    return id;
+  }
 }