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);