Merge "Merge branch 'stable-3.9' into master"
diff --git a/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
index dfc1ffb..572001d 100644
--- a/java/com/google/gerrit/server/edit/tree/TreeCreator.java
+++ b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -18,9 +18,11 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.UsedAt;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -39,26 +41,41 @@
 
   private final ObjectId baseTreeId;
   private final ImmutableList<? extends ObjectId> baseParents;
+  private final Optional<ObjectInserter> objectInserter;
   private final List<TreeModification> treeModifications = new ArrayList<>();
 
   public static TreeCreator basedOn(RevCommit baseCommit) {
     requireNonNull(baseCommit, "baseCommit is required");
-    return new TreeCreator(baseCommit.getTree(), ImmutableList.copyOf(baseCommit.getParents()));
+    return new TreeCreator(
+        baseCommit.getTree(), ImmutableList.copyOf(baseCommit.getParents()), Optional.empty());
+  }
+
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public static TreeCreator basedOn(RevCommit baseCommit, ObjectInserter objectInserter) {
+    requireNonNull(baseCommit, "baseCommit is required");
+    return new TreeCreator(
+        baseCommit.getTree(),
+        ImmutableList.copyOf(baseCommit.getParents()),
+        Optional.of(objectInserter));
   }
 
   public static TreeCreator basedOnTree(
       ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) {
     requireNonNull(baseTreeId, "baseTreeId is required");
-    return new TreeCreator(baseTreeId, baseParents);
+    return new TreeCreator(baseTreeId, baseParents, Optional.empty());
   }
 
   public static TreeCreator basedOnEmptyTree() {
-    return new TreeCreator(ObjectId.zeroId(), ImmutableList.of());
+    return new TreeCreator(ObjectId.zeroId(), ImmutableList.of(), Optional.empty());
   }
 
-  private TreeCreator(ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) {
+  private TreeCreator(
+      ObjectId baseTreeId,
+      ImmutableList<? extends ObjectId> baseParents,
+      Optional<ObjectInserter> objectInserter) {
     this.baseTreeId = requireNonNull(baseTreeId, "baseTree is required");
     this.baseParents = baseParents;
+    this.objectInserter = objectInserter;
   }
 
   /**
@@ -141,17 +158,22 @@
     return pathEdits;
   }
 
+  private ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException {
+    ObjectInserter oi = objectInserter.orElse(repository.newObjectInserter());
+    try {
+      ObjectId treeId = tree.writeTree(oi);
+      oi.flush();
+      return treeId;
+    } finally {
+      if (objectInserter.isEmpty()) {
+        oi.close();
+      }
+    }
+  }
+
   private static void applyPathEdits(DirCache tree, List<DirCacheEditor.PathEdit> pathEdits) {
     DirCacheEditor dirCacheEditor = tree.editor();
     pathEdits.forEach(dirCacheEditor::add);
     dirCacheEditor.finish();
   }
-
-  private static ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException {
-    try (ObjectInserter objectInserter = repository.newObjectInserter()) {
-      ObjectId treeId = tree.writeTree(objectInserter);
-      objectInserter.flush();
-      return treeId;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 341a9d9..e254bfc 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -26,15 +26,18 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.converter.ChangeInputProtoConverter;
 import com.google.gerrit.exceptions.InvalidMergeStrategyException;
 import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -52,6 +55,7 @@
 import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.proto.Entities;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -96,6 +100,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -118,6 +123,8 @@
 public class CreateChange
     implements RestCollectionModifyView<TopLevelResource, ChangeResource, ChangeInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final ChangeInputProtoConverter CHANGE_INPUT_PROTO_CONVERTER =
+      ChangeInputProtoConverter.INSTANCE;
 
   private final BatchUpdate.Factory updateFactory;
   private final String anonymousCowardName;
@@ -191,11 +198,51 @@
     return execute(updateFactory, input, projectsCollection.parse(input.project));
   }
 
-  /** Creates the changes in the given project. This is public for reuse in the project API. */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  @FunctionalInterface
+  public interface CommitTreeSupplier {
+    @NonNull
+    ObjectId get(Repository repo, ObjectInserter oi, ChangeInput input, RevCommit mergeTip)
+        throws IOException, RestApiException;
+  }
+
+  /**
+   * Creates the changes in the given project, using the proto representation of ChangeInput -
+   * {@link com.google.gerrit.proto.Entities.ChangeInput}.
+   */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public Response<ChangeInfo> execute(
+      BatchUpdate.Factory updateFactory,
+      Entities.ChangeInput input,
+      CommitTreeSupplier commitTreeSupplier)
+      throws IOException, RestApiException, UpdateException, PermissionBackendException,
+          ConfigInvalidException {
+    return execute(
+        updateFactory,
+        CHANGE_INPUT_PROTO_CONVERTER.fromProto(input),
+        projectsCollection.parse(input.getProject()),
+        Optional.of(commitTreeSupplier));
+  }
+
+  /**
+   * Creates the changes in the given project, using the java-class representation of ChangeInput -
+   * {@link com.google.gerrit.extensions.common.ChangeInput}. This is public for reuse in the
+   * project API.
+   */
   public Response<ChangeInfo> execute(
       BatchUpdate.Factory updateFactory, ChangeInput input, ProjectResource projectResource)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
           ConfigInvalidException {
+    return execute(updateFactory, input, projectResource, Optional.empty());
+  }
+
+  private Response<ChangeInfo> execute(
+      BatchUpdate.Factory updateFactory,
+      ChangeInput input,
+      ProjectResource projectResource,
+      Optional<CommitTreeSupplier> commitTreeSupplier)
+      throws IOException, RestApiException, UpdateException, PermissionBackendException,
+          ConfigInvalidException {
     if (!user.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -204,14 +251,15 @@
     projectState.checkStatePermitsWrite();
 
     IdentifiedUser me = user.get().asIdentifiedUser();
-    checkAndSanitizeChangeInput(input, me);
+    checkAndSanitizeChangeInput(input, me, commitTreeSupplier);
 
     Project.NameKey project = projectResource.getNameKey();
     contributorAgreements.check(project, user.get());
 
     checkRequiredPermissions(project, input.branch, input.author);
 
-    ChangeInfo newChange = createNewChange(input, me, projectState, updateFactory);
+    ChangeInfo newChange =
+        createNewChange(input, me, projectState, updateFactory, commitTreeSupplier);
     return Response.created(newChange);
   }
 
@@ -225,7 +273,8 @@
    * @param me the user who sent the current request to create a change.
    * @throws BadRequestException if the input is not legal.
    */
-  private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
+  private void checkAndSanitizeChangeInput(
+      ChangeInput input, IdentifiedUser me, Optional<CommitTreeSupplier> commitTreeSupplier)
       throws RestApiException, PermissionBackendException, IOException {
     if (Strings.isNullOrEmpty(input.branch)) {
       throw new BadRequestException("branch must be non-empty");
@@ -303,6 +352,11 @@
       throw new BadRequestException("Only one of `merge` and `patch` arguments can be set.");
     }
 
+    if ((input.merge != null || input.patch != null) && commitTreeSupplier.isPresent()) {
+      throw new BadRequestException(
+          "`CommitTreeSupplier` cannot be provided along with `merge` or `patch` arguments");
+    }
+
     if (input.author != null
         && (Strings.isNullOrEmpty(input.author.email)
             || Strings.isNullOrEmpty(input.author.name))) {
@@ -332,7 +386,8 @@
       ChangeInput input,
       IdentifiedUser me,
       ProjectState projectState,
-      BatchUpdate.Factory updateFactory)
+      BatchUpdate.Factory updateFactory,
+      Optional<CommitTreeSupplier> commitTreeSupplier)
       throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
           UpdateException {
     try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
@@ -404,31 +459,22 @@
           }
         } else if (input.patch != null) {
           // create a commit with the given patch.
-          if (mergeTip == null) {
-            throw new BadRequestException("Cannot apply patch on top of an empty tree.");
-          }
-          PatchApplier.Result applyResult =
-              ApplyPatchUtil.applyPatch(git, oi, input.patch, mergeTip);
-          ObjectId treeId = applyResult.getTreeId();
-          logger.atFine().log("tree ID after applying patch: %s", treeId.name());
-          String appliedPatchCommitMessage =
-              getCommitMessage(
-                  ApplyPatchUtil.buildCommitMessage(
-                      input.subject,
-                      ImmutableList.of(),
-                      input.patch.patch,
-                      ApplyPatchUtil.getResultPatch(git, reader, mergeTip, rw.lookupTree(treeId)),
-                      applyResult.getErrors()),
-                  me);
           c =
-              rw.parseCommit(
-                  CommitUtil.createCommitWithTree(
-                      oi,
-                      author,
-                      committer,
-                      ImmutableList.of(mergeTip),
-                      appliedPatchCommitMessage,
-                      treeId));
+              createCommitWithPatch(
+                  git, reader, oi, rw, mergeTip, input.patch, input.subject, author, committer, me);
+        } else if (commitTreeSupplier.isPresent()) {
+          c =
+              createCommitWithSuppliedTree(
+                  git,
+                  oi,
+                  rw,
+                  mergeTip,
+                  input,
+                  commitTreeSupplier.get(),
+                  author,
+                  committer,
+                  commitMessage);
+
         } else {
           // create an empty commit.
           c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage);
@@ -615,6 +661,63 @@
             oi, authorIdent, committerIdent, parents, commitMessage, treeId));
   }
 
+  private CodeReviewCommit createCommitWithPatch(
+      Repository repo,
+      ObjectReader reader,
+      ObjectInserter oi,
+      CodeReviewRevWalk rw,
+      RevCommit mergeTip,
+      ApplyPatchInput patch,
+      String subject,
+      PersonIdent authorIdent,
+      PersonIdent committerIdent,
+      IdentifiedUser me)
+      throws IOException, RestApiException {
+    if (mergeTip == null) {
+      throw new BadRequestException("Cannot apply patch on top of an empty tree.");
+    }
+    PatchApplier.Result applyResult = ApplyPatchUtil.applyPatch(repo, oi, patch, mergeTip);
+    ObjectId treeId = applyResult.getTreeId();
+    logger.atFine().log("tree ID after applying patch: %s", treeId.name());
+    String appliedPatchCommitMessage =
+        getCommitMessage(
+            ApplyPatchUtil.buildCommitMessage(
+                subject,
+                ImmutableList.of(),
+                patch.patch,
+                ApplyPatchUtil.getResultPatch(repo, reader, mergeTip, rw.lookupTree(treeId)),
+                applyResult.getErrors()),
+            me);
+    return rw.parseCommit(
+        CommitUtil.createCommitWithTree(
+            oi,
+            authorIdent,
+            committerIdent,
+            ImmutableList.of(mergeTip),
+            appliedPatchCommitMessage,
+            treeId));
+  }
+
+  private static CodeReviewCommit createCommitWithSuppliedTree(
+      Repository repo,
+      ObjectInserter oi,
+      CodeReviewRevWalk rw,
+      RevCommit mergeTip,
+      ChangeInput input,
+      CommitTreeSupplier commitTreeSupplier,
+      PersonIdent authorIdent,
+      PersonIdent committerIdent,
+      String commitMessage)
+      throws IOException, RestApiException {
+    if (mergeTip == null) {
+      throw new BadRequestException("`CommitTreeSupplier` cannot be used on top of an empty tree.");
+    }
+    ObjectId treeId = commitTreeSupplier.get(repo, oi, input, mergeTip);
+    return rw.parseCommit(
+        CommitUtil.createCommitWithTree(
+            oi, authorIdent, committerIdent, ImmutableList.of(mergeTip), commitMessage, treeId));
+  }
+
   private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
     return inserter.insert(new TreeFormatter());
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index dcdbce3..698eac8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.converter.ChangeInputProtoConverter;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -82,7 +83,11 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.restapi.change.ApplyPatchUtil;
+import com.google.gerrit.server.restapi.change.CreateChange;
+import com.google.gerrit.server.restapi.change.CreateChange.CommitTreeSupplier;
 import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
+import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.stream.JsonReader;
 import com.google.inject.Inject;
@@ -110,9 +115,13 @@
 
 @UseClockStep
 public class CreateChangeIT extends AbstractDaemonTest {
+  private static final ChangeInputProtoConverter CHANGE_INPUT_PROTO_CONVERTER =
+      ChangeInputProtoConverter.INSTANCE;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ExtensionRegistry extensionRegistry;
+  @Inject private CreateChange createChangeImpl;
+  @Inject private BatchUpdate.Factory updateFactory;;
 
   @Before
   public void addNonCommitHead() throws Exception {
@@ -1157,6 +1166,26 @@
   }
 
   @Test
+  public void createChangeWithCommitTreeSupplier_success() throws Exception {
+    createBranch(BranchNameKey.create(project, "other"));
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.branch = "other";
+    input.subject = "custom commit message";
+    ApplyPatchInput applyPatchInput = new ApplyPatchInput();
+    applyPatchInput.patch = PATCH_INPUT;
+    CommitTreeSupplier commitTreeSupplier =
+        (repo, oi, in, mergeTip) ->
+            ApplyPatchUtil.applyPatch(repo, oi, applyPatchInput, mergeTip).getTreeId();
+
+    ChangeInfo info = assertCreateWithCommitTreeSupplierSucceeds(input, commitTreeSupplier);
+
+    DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
+    assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+    assertThat(info.revisions.get(info.currentRevision).commit.message)
+        .isEqualTo("custom commit message\n\nChange-Id: " + info.changeId + "\n");
+  }
+
+  @Test
   @UseSystemTime
   public void sha1sOfTwoNewChangesDiffer() throws Exception {
     ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
@@ -1351,6 +1380,18 @@
     return out;
   }
 
+  private ChangeInfo assertCreateWithCommitTreeSupplierSucceeds(
+      ChangeInput input, CommitTreeSupplier commitTreeSupplier) throws Exception {
+    ChangeInfo res =
+        createChangeImpl
+            .execute(updateFactory, CHANGE_INPUT_PROTO_CONVERTER.toProto(input), commitTreeSupplier)
+            .value();
+    // The original result doesn't contain any revision data.
+    ChangeInfo out = gApi.changes().id(res.changeId).get(ALL_REVISIONS, CURRENT_COMMIT);
+    validateCreateSucceeds(input, out);
+    return out;
+  }
+
   private static <T> T readContentFromJson(RestResponse r, Class<T> clazz) throws Exception {
     try (JsonReader jsonReader = new JsonReader(r.getReader())) {
       return newGson().fromJson(jsonReader, clazz);