blob: 4cb1733d7605ffe2056ef58c59c532b11c9ebefd [file]
// Copyright (C) 2014 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.rest.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.TestExtensions.TestCommitValidationInfoListener;
import static com.google.gerrit.acceptance.TestExtensions.TestCommitValidationListener;
import static com.google.gerrit.acceptance.TestExtensions.TestValidationOptionsListener;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.entities.Permission.CREATE;
import static com.google.gerrit.entities.Permission.READ;
import static com.google.gerrit.entities.RefNames.HEAD;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.common.testing.GitPersonSubject.assertThat;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.UseSystemTime;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
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;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.common.NoMergeBaseReason;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.git.validators.CommitValidationInfo;
import com.google.gerrit.server.patch.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;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
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.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.util.Base64;
import org.junit.Before;
import org.junit.Test;
@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 {
testRefAction(
() -> {
try (Repository repo = repoManager.openRepository(project);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId answer = ins.insert(Constants.OBJ_BLOB, new byte[] {42});
ins.flush();
ins.close();
RefUpdate update = repo.getRefDatabase().newUpdate("refs/heads/answer", false);
update.setNewObjectId(answer);
assertThat(update.forceUpdate()).isEqualTo(RefUpdate.Result.NEW);
}
});
}
@Test
public void createEmptyChange_MissingBranch() throws Exception {
ChangeInput ci = new ChangeInput();
ci.project = project.get();
assertCreateFails(ci, BadRequestException.class, "branch must be non-empty");
}
@Test
public void createEmptyChange_NonExistingBranch() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.branch = "non-existing";
assertCreateFails(ci, BadRequestException.class, "Destination branch does not exist");
}
@Test
public void createEmptyChange_MissingMessage() throws Exception {
ChangeInput ci = new ChangeInput();
ci.project = project.get();
ci.branch = "master";
assertCreateFails(ci, BadRequestException.class, "commit message must be non-empty");
}
@Test
public void createEmptyChange_InvalidStatus() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.MERGED);
assertCreateFails(ci, BadRequestException.class, "unsupported change status");
}
@Test
public void createEmptyChange_InvalidChangeId() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Subject\n\nChange-Id: I0000000000000000000000000000000000000000";
assertCreateFails(
ci, ResourceConflictException.class, "invalid Change-Id line format in message footer");
}
@Test
public void createEmptyChange_InvalidSubject() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Change-Id: I1234000000000000000000000000000000000000";
assertCreateFails(
ci,
ResourceConflictException.class,
"missing subject; Change-Id must be in message footer");
}
@Test
public void createNewChange_InvalidCommentInCommitMessage() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "#12345 Test";
assertCreateFails(ci, BadRequestException.class, "commit message must be non-empty");
}
@Test
public void createNewChange_RequiresAuthentication() throws Exception {
requestScopeOperations.setApiUserAnonymous();
assertCreateFails(
newChangeInput(ChangeStatus.NEW), AuthException.class, "Authentication required");
}
@Test
@GerritConfig(name = "change.topicLimit", value = "3")
public void createNewChange_exceedsTopicLimit() throws Exception {
assertCreateSucceeds(newChangeWithTopic("limited"));
assertCreateSucceeds(newChangeWithTopic("limited"));
assertCreateSucceeds(newChangeWithTopic("limited"));
ChangeInput ci = newChangeWithTopic("limited");
assertCreateFails(ci, BadRequestException.class, "topicLimit");
}
@Test
public void createNewChange() throws Exception {
ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
assertThat(info.revisions.get(info.currentRevision).commit.message)
.contains("Change-Id: " + info.changeId);
// Verify the message that has been posted on the change.
List<ChangeMessageInfo> messages = gApi.changes().id(info._number).messages();
assertThat(messages).hasSize(1);
assertThat(Iterables.getOnlyElement(messages).message).isEqualTo("Uploaded patch set 1.");
RevisionInfo currentRevision =
gApi.changes().id(info.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.containsConflicts).isFalse();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours).isNull();
assertThat(currentRevision.conflicts.theirs).isNull();
assertThat(currentRevision.conflicts.mergeStrategy).isNull();
assertThat(currentRevision.conflicts.noBaseReason)
.isEqualTo(NoMergeBaseReason.NO_MERGE_PERFORMED);
}
@Test
public void createNewChangeWithCommentsInCommitMessage() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject += "\n# Comment line";
ChangeInfo info = gApi.changes().create(ci).get();
assertThat(info.revisions.get(info.currentRevision).commit.message)
.doesNotContain("# Comment line");
}
@Test
public void createNewChangeWithChangeId() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
String changeId = "I1234000000000000000000000000000000000000";
String changeIdLine = "Change-Id: " + changeId;
ci.subject = "Subject\n\n" + changeIdLine;
ChangeInfo info = assertCreateSucceeds(ci);
assertThat(info.changeId).isEqualTo(changeId);
assertThat(info.revisions.get(info.currentRevision).commit.message).contains(changeIdLine);
}
@Test
public void formatResponse_fieldsPresentWhenRequested() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
String changeId = "I1234000000000000000000000000000000000000";
String changeIdLine = "Change-Id: " + changeId;
ci.subject = "Subject\n\n" + changeIdLine;
ci.responseFormatOptions =
ImmutableList.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_ACTIONS);
// Must use REST directly because the Java API returns a ChangeApi upon
// creation that will do its own formatting when #get is called on it.
RestResponse resp = adminRestSession.post("/changes/", ci);
resp.assertCreated();
ChangeInfo res = readContentFromJson(resp, ChangeInfo.class);
assertThat(res.actions).isNotEmpty();
assertThat(res.revisions.values()).hasSize(1);
}
@Test
public void formatResponse_fieldsAbsentWhenNotRequested() throws Exception {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
String changeId = "I1234000000000000000000000000000000000000";
String changeIdLine = "Change-Id: " + changeId;
ci.subject = "Subject\n\n" + changeIdLine;
// Must use REST directly because the Java API returns a ChangeApi upon
// creation that will do its own formatting when #get is called on it.
RestResponse resp = adminRestSession.post("/changes/", ci);
resp.assertCreated();
ChangeInfo res = readContentFromJson(resp, ChangeInfo.class);
assertThat(res.actions).isNull();
assertThat(res.revisions).isNull();
}
@Test
public void cannotCreateChangeOnGerritInternalRefs() throws Exception {
requestScopeOperations.setApiUser(admin.id());
projectOperations
.project(project)
.forUpdate()
.add(allow(CREATE).ref("refs/*").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Subject";
ci.branch = "refs/changes/00/1000"; // disallowedRef
Throwable thrown = assertThrows(RestApiException.class, () -> gApi.changes().create(ci));
assertThat(thrown).hasMessageThat().contains("Cannot create a change on ref " + ci.branch);
}
@Test
public void cannotCreateChangeOnTagRefs() throws Exception {
requestScopeOperations.setApiUser(admin.id());
projectOperations
.project(project)
.forUpdate()
.add(allow(CREATE).ref("refs/*").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Subject";
ci.branch = "refs/tags/v1.0"; // disallowed ref
Throwable thrown = assertThrows(RestApiException.class, () -> gApi.changes().create(ci));
assertThat(thrown).hasMessageThat().contains("Cannot create a change on ref " + ci.branch);
}
@Test
public void canCreateChangeOnRefsMetaConfig() throws Exception {
requestScopeOperations.setApiUser(admin.id());
projectOperations
.project(project)
.forUpdate()
.add(allow(CREATE).ref("refs/*").group(REGISTERED_USERS))
.add(allow(READ).ref("refs/meta/config").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Subject";
ci.branch = RefNames.REFS_CONFIG;
assertThat(gApi.changes().create(ci).info().branch).isEqualTo(RefNames.REFS_CONFIG);
}
@Test
public void canCreateChangeOnRefsMetaDashboards() throws Exception {
String branchName = "refs/meta/dashboards/project_1";
requestScopeOperations.setApiUser(admin.id());
projectOperations
.project(project)
.forUpdate()
.add(allow(CREATE).ref(branchName).group(REGISTERED_USERS))
.add(allow(READ).ref(branchName).group(REGISTERED_USERS))
.update();
BranchNameKey branchNameKey = BranchNameKey.create(project, branchName);
createBranch(branchNameKey);
requestScopeOperations.setApiUser(user.id());
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Subject";
ci.branch = branchName;
assertThat(gApi.changes().create(ci).info().branch).isEqualTo(branchName);
}
@Test
public void cannotCreateChangeWithChangeIfOfExistingChangeOnSameBranch() throws Exception {
String changeId = createChange().getChangeId();
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Subject\n\nChange-Id: " + changeId;
assertCreateFails(
ci,
ResourceConflictException.class,
"A change with Change-Id " + changeId + " already exists for this branch.");
}
@Test
public void canCreateChangeWithChangeIfOfExistingChangeOnOtherBranch() throws Exception {
String changeId = createChange().getChangeId();
createBranch(BranchNameKey.create(project, "other"));
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
ci.subject = "Subject\n\nChange-Id: " + changeId;
ci.branch = "other";
ChangeInfo info = assertCreateSucceeds(ci);
assertThat(info.changeId).isEqualTo(changeId);
}
@Test
public void notificationsOnChangeCreation() throws Exception {
requestScopeOperations.setApiUser(user.id());
watch(project.get());
// check that watcher is notified
requestScopeOperations.setApiUser(admin.id());
assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
assertThat(m.body()).contains(admin.fullName() + " has uploaded this change for review.");
// check that watcher is not notified if notify=NONE
sender.clear();
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.notify = NotifyHandling.NONE;
assertCreateSucceeds(input);
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void createNewChangeSignedOffByFooter() throws Exception {
setSignedOffByFooter(true);
try {
ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
String message = info.revisions.get(info.currentRevision).commit.message;
assertThat(message)
.contains(
String.format(
"%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
} finally {
setSignedOffByFooter(false);
}
}
@Test
public void createNewChange_projectConfigRequiresSignedOffByFooter() throws Exception {
projectOperations.project(project).forUpdate().useSignedOffBy().update();
ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
String message = info.revisions.get(info.currentRevision).commit.message;
assertThat(message)
.contains(
String.format(
"%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
}
@Test
public void createNewChange_projectConfigDoesNotRequireSignedOffByFooter() throws Exception {
projectOperations
.project(project)
.forUpdate()
.useSignedOffBy(InheritableBoolean.FALSE)
.update();
ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
String message = info.revisions.get(info.currentRevision).commit.message;
assertThat(message).doesNotContain(SIGNED_OFF_BY_TAG);
}
@Test
public void createNewChangeSignedOffByFooterWithChangeId() throws Exception {
setSignedOffByFooter(true);
try {
ChangeInput ci = newChangeInput(ChangeStatus.NEW);
String changeId = "I1234000000000000000000000000000000000000";
String changeIdLine = "Change-Id: " + changeId;
ci.subject = "Subject\n\n" + changeIdLine;
ChangeInfo info = assertCreateSucceeds(ci);
assertThat(info.changeId).isEqualTo(changeId);
String message = info.revisions.get(info.currentRevision).commit.message;
assertThat(message).contains(changeIdLine);
assertThat(message)
.contains(
String.format(
"%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
} finally {
setSignedOffByFooter(false);
}
}
@Test
public void createNewPrivateChange() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.isPrivate = true;
assertCreateSucceeds(input);
}
@Test
public void createDefaultAuthor() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
ChangeInfo info = assertCreateSucceeds(input);
GitPerson person = gApi.changes().id(info.id).current().commit(false).author;
assertThat(person).email().isEqualTo(admin.email());
}
@Test
public void createAuthorOverrideBadRequest() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.author = new AccountInput();
input.author.name = "name";
assertCreateFails(input, BadRequestException.class, "email");
input.author.name = null;
input.author.email = "gerritlessjane@invalid";
assertCreateFails(input, BadRequestException.class, "email");
}
@Test
public void createAuthorOverride() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.author = new AccountInput();
input.author.email = "gerritlessjane@invalid";
// This is an email address that doesn't exist as account on the Gerrit server.
input.author.name = "Gerritless Jane";
ChangeInfo info = assertCreateSucceeds(input);
RevisionApi rApi = gApi.changes().id(info.id).current();
GitPerson author = rApi.commit(false).author;
assertThat(author).email().isEqualTo(input.author.email);
assertThat(author).name().isEqualTo(input.author.name);
GitPerson committer = rApi.commit(false).committer;
assertThat(committer).email().isEqualTo(admin.getNameEmail().email());
}
@Test
public void createAuthorPermission() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.author = new AccountInput();
input.author.name = "Jane";
input.author.email = "jane@invalid";
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
.update();
assertCreateFails(input, AuthException.class, "forge author");
}
@Test
public void createAuthorAddedAsCcAndNotified() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.author = new AccountInput();
input.author.email = user.email();
input.author.name = user.fullName();
ChangeInfo info = assertCreateSucceeds(input);
assertThat(info.reviewers.get(ReviewerState.CC)).hasSize(1);
assertThat(Iterables.getOnlyElement(info.reviewers.get(ReviewerState.CC)).email)
.isEqualTo(user.email());
assertThat(
Iterables.getOnlyElement(Iterables.getOnlyElement(sender.getMessages()).rcpt()).email())
.isEqualTo(user.email());
}
@Test
public void createAuthorAddedAsCcNotNotifiedWithNotifyNone() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.author = new AccountInput();
input.author.email = user.email();
input.author.name = user.fullName();
input.notify = NotifyHandling.NONE;
ChangeInfo info = assertCreateSucceeds(input);
assertThat(info.reviewers.get(ReviewerState.CC)).hasSize(1);
assertThat(Iterables.getOnlyElement(info.reviewers.get(ReviewerState.CC)).email)
.isEqualTo(user.email());
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void createWithMergeConflictAuthorAddedAsCcNotNotifiedWithNotifyNone() throws Exception {
String fileName = "shared.txt";
String sourceBranch = "sourceBranch";
String sourceSubject = "source change";
String sourceContent = "source content";
String targetBranch = "targetBranch";
String targetSubject = "target change";
String targetContent = "target content";
changeInTwoBranches(
sourceBranch,
sourceSubject,
fileName,
sourceContent,
targetBranch,
targetSubject,
fileName,
targetContent);
ChangeInput input =
newMergeChangeInput(targetBranch, sourceBranch, "", /* allowConflicts= */ true);
input.workInProgress = true;
input.author = new AccountInput();
input.author.email = user.email();
input.author.name = user.fullName();
input.notify = NotifyHandling.NONE;
ChangeInfo info = assertCreateSucceeds(input);
assertThat(info.reviewers.get(ReviewerState.CC)).hasSize(1);
assertThat(Iterables.getOnlyElement(info.reviewers.get(ReviewerState.CC)).email)
.isEqualTo(user.email());
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void createAuthorNotAddedAsCcWithAvoidAddingOriginalAuthorAsReviewer() throws Exception {
projectOperations
.project(project)
.forUpdate()
.skipAddingAuthorAndCommitterAsReviewers()
.update();
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.author = new AccountInput();
input.author.email = user.email();
input.author.name = user.fullName();
ChangeInfo info = assertCreateSucceeds(input);
assertThat(info.reviewers).isEmpty();
}
@Test
public void createNewWorkInProgressChange() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.workInProgress = true;
assertCreateSucceeds(input);
}
@Test
public void createChangeWithParentCommit() throws Exception {
ImmutableMap<String, PushOneCommit.Result> setup =
changeInTwoBranches("foo", "foo.txt", "bar", "bar.txt");
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.baseCommit = setup.get("master").getCommit().getId().name();
ChangeInfo result = assertCreateSucceeds(input);
assertThat(gApi.changes().id(result.id).current().commit(false).parents.get(0).commit)
.isEqualTo(input.baseCommit);
}
@Test
public void createChangeWithParentChange() throws Exception {
Result change = createChange();
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.baseChange = change.getChangeId();
ChangeInfo result = assertCreateSucceeds(input);
assertThat(gApi.changes().id(result.id).current().commit(false).parents.get(0).commit)
.isEqualTo(change.getCommit().getId().name());
}
@Test
public void createChangeWithBadParentCommitFails() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.baseCommit = "notasha1";
assertCreateFails(
input, UnprocessableEntityException.class, "Base notasha1 doesn't represent a valid SHA-1");
}
@Test
public void createChangeWithNonExistingParentCommitFails() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.baseCommit = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
assertCreateFails(
input,
UnprocessableEntityException.class,
String.format("Base %s doesn't exist", input.baseCommit));
}
@Test
public void createChangeWithParentCommitOnWrongBranchFails() throws Exception {
ImmutableMap<String, PushOneCommit.Result> setup =
changeInTwoBranches("foo", "foo.txt", "bar", "bar.txt");
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.branch = "foo";
input.baseCommit = setup.get("bar").getCommit().getId().name();
assertCreateFails(
input,
BadRequestException.class,
String.format("Commit %s doesn't exist on ref refs/heads/foo", input.baseCommit));
}
@Test
public void createChangeWithParentCommitWithNonExistingTargetBranch() throws Exception {
Result initialCommit =
pushFactory
.create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
.to("refs/heads/master");
initialCommit.assertOkStatus();
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.branch = "non-existing";
input.baseCommit = initialCommit.getCommit().getName();
assertCreateFails(input, BadRequestException.class, "Destination branch does not exist");
}
@Test
public void createChangeOnNonExistingBaseChangeFails() throws Exception {
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.baseChange = "999999";
assertCreateFails(
input, UnprocessableEntityException.class, "Base change not found: " + input.baseChange);
}
@Test
public void createChangeWithoutAccessToParentCommitFails() throws Exception {
ImmutableMap<String, PushOneCommit.Result> results =
changeInTwoBranches("invisible-branch", "a.txt", "visible-branch", "b.txt");
projectOperations
.project(project)
.forUpdate()
.add(block(READ).ref("refs/heads/invisible-branch").group(REGISTERED_USERS))
.update();
ChangeInput in = newChangeInput(ChangeStatus.NEW);
in.branch = "visible-branch";
in.baseChange = results.get("invisible-branch").getChangeId();
assertCreateFails(
in, UnprocessableEntityException.class, "Base change not found: " + in.baseChange);
}
@Test
public void noteDbCommit() throws Exception {
ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
RevCommit commit =
rw.parseCommit(repo.exactRef(changeMetaRef(Change.id(c._number))).getObjectId());
assertThat(commit.getShortMessage()).isEqualTo("Create change");
PersonIdent expectedAuthor =
changeNoteUtil.newAccountIdIdent(
getAccount(admin.id()).id(), c.created.toInstant(), serverIdent.get());
assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commit.getCommitterIdent())
.isEqualTo(new PersonIdent(serverIdent.get(), c.created));
assertThat(commit.getParentCount()).isEqualTo(0);
}
}
@Test
public void createMergeChangeNoConflictsUsingResolveStrategy() throws Exception {
testCreateMergeChangeNoConflicts("resolve");
}
@Test
public void createMergeChangeNoConflictsUsingRecursiveStrategy() throws Exception {
testCreateMergeChangeNoConflicts("recursive");
}
@Test
public void createMergeChangeNoConflictsUsingSimpleTwoWayInCoreStrategy() throws Exception {
testCreateMergeChangeNoConflicts("simple-two-way-in-core");
}
@Test
public void createMergeChangeNoConflictsUsingOursStrategy() throws Exception {
testCreateMergeChangeNoConflicts("ours");
}
@Test
public void createMergeChangeNoConflictsUsingTheirsStrategy() throws Exception {
testCreateMergeChangeNoConflicts("theirs");
}
public void testCreateMergeChangeNoConflicts(String mergeStrategy) throws Exception {
String sourceBranch = "sourceBranch";
String targetBranch = "targetBranch";
ImmutableMap<String, Result> results =
changeInTwoBranches(sourceBranch, "a.txt", targetBranch, "b.txt");
RevCommit baseCommit = results.get("master").getCommit();
ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, mergeStrategy);
ChangeInfo change = assertCreateSucceeds(in);
// Verify the message that has been posted on the change.
List<ChangeMessageInfo> messages = gApi.changes().id(project.get(), change._number).messages();
assertThat(messages).hasSize(1);
assertThat(Iterables.getOnlyElement(messages).message).isEqualTo("Uploaded patch set 1.");
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.ours)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(results.get(sourceBranch).getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(mergeStrategy);
assertThat(currentRevision.conflicts.containsConflicts).isFalse();
if ("ours".equals(mergeStrategy) || "theirs".equals(mergeStrategy)) {
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.noBaseReason)
.isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
} else {
assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
assertThat(currentRevision.conflicts.noBaseReason).isNull();
}
}
@Test
public void createMergeChangeAuthor() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in = newMergeChangeInput("branchA", "branchB", "");
in.author = new AccountInput();
in.author.name = "Gerritless Jane";
in.author.email = "gerritlessjane@invalid";
ChangeInfo change = assertCreateSucceeds(in);
RevisionApi rApi = gApi.changes().id(change.id).current();
GitPerson author = rApi.commit(false).author;
assertThat(author).email().isEqualTo(in.author.email);
GitPerson committer = rApi.commit(false).committer;
assertThat(committer).email().isEqualTo(admin.getNameEmail().email());
}
@Test
public void createMergeChangeFailsDueToConflictsUsingResolveStrategy() throws Exception {
testCreateMergeChangeFailsDueToConflicts("resolve");
}
@Test
public void createMergeChangeFailsDueToConflictsUsingRecursiveStrategy() throws Exception {
testCreateMergeChangeFailsDueToConflicts("recursive");
}
@Test
public void createMergeChangeFailsDueToConflictsUsingSimpleTwoWayInCoreStrategy()
throws Exception {
testCreateMergeChangeFailsDueToConflicts("simple-two-way-in-core");
}
private void testCreateMergeChangeFailsDueToConflicts(String mergeStrategy) throws Exception {
String fileName = "shared.txt";
changeInTwoBranches("branchA", fileName, "branchB", fileName);
ChangeInput in = newMergeChangeInput("branchA", "branchB", mergeStrategy);
assertCreateFails(
in,
RestApiException.class,
"simple-two-way-in-core".equals(mergeStrategy)
? "merge conflict(s)"
: String.format(
"""
merge conflict(s):
* %s
""",
fileName));
}
@Test
public void createMergeChangeSucceedsWithConflictsUsingOursStrategy() throws Exception {
testCreateMergeChangeSucceedsWithConflicts("ours");
}
@Test
public void createMergeChangeSucceedsWithConflictsUsingTheirsStrategy() throws Exception {
testCreateMergeChangeSucceedsWithConflicts("theirs");
}
private void testCreateMergeChangeSucceedsWithConflicts(String mergeStrategy) throws Exception {
String sourceBranch = "sourceBranch";
String targetBranch = "targetBranch";
ImmutableMap<String, Result> results =
changeInTwoBranches(sourceBranch, "shared.txt", targetBranch, "shared.txt");
ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, mergeStrategy);
ChangeInfo change = assertCreateSucceeds(in);
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(results.get(sourceBranch).getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(mergeStrategy);
assertThat(currentRevision.conflicts.noBaseReason)
.isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
assertThat(currentRevision.conflicts.containsConflicts).isFalse();
}
@Test
public void createMergeChangeWithConflictsAllowedUsingRecursiveStrategy() throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "recursive", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void createMergeChangeWithConflictsAllowedUsingRecursivetrategyAndDiff3()
throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "recursive", /* useDiff3= */ true);
}
@Test
public void createMergeChangeWithConflictsAllowedUsingResolveStrategy() throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "resolve", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void createMergeChangeWithConflictsAllowedUsingResolveStrategyAndDiff3() throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "resolve", /* useDiff3= */ true);
}
@Test
public void createMergeChangeWithConflictsAllowedUsingSimpleTwoWayInCoreStrategy()
throws Exception {
testCreateMergeChangeConflictsAllowed(
/* strategy= */ "simple-two-way-in-core", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void createMergeChangeWithConflictsAllowedUsingSimpleTwoWayInCoreStrategyAndDiff3()
throws Exception {
testCreateMergeChangeConflictsAllowed(
/* strategy= */ "simple-two-way-in-core", /* useDiff3= */ true);
}
@Test
public void createMergeChangeWithConflictsAllowedUsingOursStrategy() throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "ours", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void createMergeChangeWithConflictsAllowedUsingOursStrategyAndDiff3() throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "ours", /* useDiff3= */ true);
}
@Test
public void createMergeChangeWithConflictsAllowedUsingTheirsStrategy() throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "theirs", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void createMergeChangeWithConflictsAllowedUsingTheirsStrategyAndDiff3() throws Exception {
testCreateMergeChangeConflictsAllowed(/* strategy= */ "theirs", /* useDiff3= */ true);
}
private void testCreateMergeChangeConflictsAllowed(String strategy, boolean useDiff3)
throws Exception {
String fileName = "shared.txt";
String sourceBranch = "sourceBranch";
String sourceSubject = "source change";
String sourceContent = "source content";
String targetBranch = "targetBranch";
String targetSubject = "target change";
String targetContent = "target content";
ImmutableMap<String, Result> results =
changeInTwoBranches(
sourceBranch,
sourceSubject,
fileName,
sourceContent,
targetBranch,
targetSubject,
fileName,
targetContent);
RevCommit baseCommit = results.get("master").getCommit();
ChangeInput in =
newMergeChangeInput(targetBranch, sourceBranch, strategy, /* allowConflicts= */ true);
if ("ours".equals(strategy) || "theirs".equals(strategy)) {
ChangeInfo change = assertCreateSucceeds(in);
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(results.get(sourceBranch).getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
assertThat(currentRevision.conflicts.noBaseReason)
.isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
assertThat(currentRevision.conflicts.containsConflicts).isFalse();
// Verify that the file content in the created change is correct.
// We expect that it has conflict markers to indicate the conflict.
BinaryResult bin =
gApi.changes().id(project.get(), change._number).current().file(fileName).content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent).isEqualTo("ours".equals(strategy) ? targetContent : sourceContent);
return;
}
if ("simple-two-way-in-core".equals(strategy)) {
assertCreateFails(
in,
BadRequestException.class,
"merge with conflicts is not supported with merge strategy: simple-two-way-in-core");
return;
}
ChangeInfo change = assertCreateSucceedsWithConflicts(in);
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
assertThat(currentRevision.conflicts.ours)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(results.get(sourceBranch).getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
assertThat(currentRevision.conflicts.noBaseReason).isNull();
assertThat(currentRevision.conflicts.containsConflicts).isTrue();
// Verify that the file content in the created change is correct.
// We expect that it has conflict markers to indicate the conflict.
BinaryResult bin =
gApi.changes().id(project.get(), change._number).current().file(fileName).content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent)
.isEqualTo(
String.format(
"""
<<<<<<< TARGET BRANCH (%s %s)
%s
%s=======
%s
>>>>>>> SOURCE BRANCH (%s %s)
""",
projectOperations.project(project).getHead(targetBranch).getName(),
targetSubject,
targetContent,
(useDiff3
? String.format(
"||||||| BASE (%s %s)\n",
baseCommit.getName(), baseCommit.getShortMessage())
: ""),
sourceContent,
projectOperations.project(project).getHead(sourceBranch).getName(),
sourceSubject));
// Verify the message that has been posted on the change.
List<ChangeMessageInfo> messages = gApi.changes().id(change._number).messages();
assertThat(messages).hasSize(1);
assertThat(Iterables.getOnlyElement(messages).message)
.isEqualTo(
"Uploaded patch set 1.\n\n"
+ "The following files contain Git conflicts:\n"
+ "* "
+ fileName
+ "\n");
}
@Test
public void createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingRecursiveStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "recursive", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingRecursiveStrategyAndDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "recursive", /* useDiff3= */ true);
}
@Test
public void createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingResolveStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "resolve", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingResolveStrategyAndDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "resolve", /* useDiff3= */ true);
}
@Test
public void
createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingSimpleTwoWayInCoreStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "simple-two-way-in-core", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingSimpleTwoWayInCoreStrategyAndDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "simple-two-way-in-core", /* useDiff3= */ true);
}
@Test
public void createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingOursStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "ours", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingOursStrategyAndDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "ours", /* useDiff3= */ true);
}
@Test
public void
createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingTheirsStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "theirs", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingTheirsStrategyAndDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
/* strategy= */ "theirs", /* useDiff3= */ true);
}
private void testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
String strategy, boolean useDiff3) throws Exception {
String fileName = "shared.txt";
String sourceBranch = "sourceBranch";
String sourceSubject = "source change";
String sourceContent = "source content";
String targetBranch = "targetBranch";
String targetSubject = "target change";
String targetContent = "target content";
Project.NameKey projectWithoutInitialCommit =
projectOperations.newProject().createEmptyCommit(false).create();
// Create sourceBranch with an initial commit.
TestRepository<InMemoryRepository> testRepo =
cloneProject(projectWithoutInitialCommit, getCloneAsAccount(configRule.description()));
RevCommit initialCommitSource =
testRepo.parseBody(
testRepo
.commit()
.message(sourceSubject)
.insertChangeId()
.add(fileName, sourceContent)
.create());
testRepo.reset(initialCommitSource);
PushResult r = pushHead(testRepo, "refs/heads/" + sourceBranch);
assertThat(r.getRemoteUpdate("refs/heads/" + sourceBranch).getStatus()).isEqualTo(Status.OK);
// Create targetBranch with another initial commit.
RevCommit initialCommitTarget =
testRepo.parseBody(
testRepo
.commit()
.message(targetSubject)
.insertChangeId()
.add(fileName, targetContent)
.create());
testRepo.reset(initialCommitTarget);
r = pushHead(testRepo, "refs/heads/" + targetBranch);
assertThat(r.getRemoteUpdate("refs/heads/" + targetBranch).getStatus()).isEqualTo(Status.OK);
// Merge sourceBranch into targetBranch with conflicts allowed.
ChangeInput in =
newMergeChangeInput(
projectWithoutInitialCommit,
targetBranch,
sourceBranch,
/* strategy= */ strategy,
/* allowConflicts= */ true);
if ("ours".equals(strategy) || "theirs".equals(strategy)) {
ChangeInfo change = assertCreateSucceeds(in);
// Verify the conflicts information.
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(initialCommitTarget.name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours).isEqualTo(initialCommitTarget.name());
assertThat(currentRevision.conflicts.theirs).isEqualTo(initialCommitSource.name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
assertThat(currentRevision.conflicts.noBaseReason)
.isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
assertThat(currentRevision.conflicts.containsConflicts).isFalse();
// Verify that the file content in the created change is correct.
// We expect that it has conflict markers to indicate the conflict.
BinaryResult bin =
gApi.changes()
.id(projectWithoutInitialCommit.get(), change._number)
.current()
.file(fileName)
.content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent).isEqualTo("ours".equals(strategy) ? targetContent : sourceContent);
return;
}
if ("simple-two-way-in-core".equals(strategy)) {
assertCreateFails(
in,
BadRequestException.class,
"merge with conflicts is not supported with merge strategy: simple-two-way-in-core");
return;
}
ChangeInfo change = assertCreateSucceedsWithConflicts(in);
// Verify the conflicts information.
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(initialCommitTarget.name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours).isEqualTo(initialCommitTarget.name());
assertThat(currentRevision.conflicts.theirs).isEqualTo(initialCommitSource.name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
assertThat(currentRevision.conflicts.noBaseReason)
.isEqualTo(NoMergeBaseReason.NO_COMMON_ANCESTOR);
assertThat(currentRevision.conflicts.containsConflicts).isTrue();
// Verify that the file content in the created change is correct.
// We expect that it has conflict markers to indicate the conflict.
BinaryResult bin =
gApi.changes()
.id(projectWithoutInitialCommit.get(), change._number)
.current()
.file(fileName)
.content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent)
.isEqualTo(
String.format(
"""
<<<<<<< TARGET BRANCH (%s %s)
%s
%s=======
%s
>>>>>>> SOURCE BRANCH (%s %s)
""",
projectOperations
.project(projectWithoutInitialCommit)
.getHead(targetBranch)
.getName(),
targetSubject,
targetContent,
(useDiff3 ? "||||||| BASE (no common ancestor)\n" : ""),
sourceContent,
projectOperations
.project(projectWithoutInitialCommit)
.getHead(sourceBranch)
.getName(),
sourceSubject));
// Verify the message that has been posted on the change.
List<ChangeMessageInfo> messages =
gApi.changes().id(projectWithoutInitialCommit.get(), change._number).messages();
assertThat(messages).hasSize(1);
assertThat(Iterables.getOnlyElement(messages).message)
.isEqualTo(
String.format(
"""
Uploaded patch set 1.
The following files contain Git conflicts:
* %s
""",
fileName));
}
@Test
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingRecursiveStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
/* strategy= */ "recursive", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingRecursiveStrategyAndDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
/* strategy= */ "recursive", /* useDiff3= */ true);
}
@Test
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingResolveStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
/* strategy= */ "resolve", /* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingResolveStrategyAndDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
/* strategy= */ "resolve", /* useDiff3= */ true);
}
@Test
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingOursStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
/* strategy= */ "ours", /* useDiff3= */ false);
}
@Test
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingTheirsStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
/* strategy= */ "theirs", /* useDiff3= */ false);
}
@Test
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingSimpleTwoWayInCoreStrategy()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
/* strategy= */ "simple-two-way-in-core", /* useDiff3= */ false);
}
private void testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
String strategy, boolean useDiff3) throws Exception {
String sourceBranch = "sourceBranch";
String targetBranch = "targetBranch";
// Create source and target branch with non-conflicting commits.
// Later these commits will become the base commits for the criss-cross-merge.
ImmutableMap<String, Result> results =
changeInTwoBranches(
sourceBranch,
"base 1",
"a.txt",
"a content",
targetBranch,
"base 2",
"b.txt",
"b content");
RevCommit baseCommitInSource = results.get(sourceBranch).getCommit();
RevCommit baseCommitInTarget = results.get(targetBranch).getCommit();
// Create merge commits in both branches (1. merge the target branch into the source branch, 2.
// merge the source branch into the target branch).
PushOneCommit mergeCommitInSource = pushFactory.create(user.newIdent(), testRepo);
mergeCommitInSource.setParents(ImmutableList.of(baseCommitInSource, baseCommitInTarget));
mergeCommitInSource.to("refs/heads/" + sourceBranch).assertOkStatus();
PushOneCommit mergeCommitInTarget = pushFactory.create(user.newIdent(), testRepo);
mergeCommitInTarget.setParents(ImmutableList.of(baseCommitInTarget, baseCommitInSource));
mergeCommitInTarget.to("refs/heads/" + targetBranch).assertOkStatus();
// Create conflicting commits in both branches.
String fileName = "shared.txt";
String sourceSubject = "source change";
String sourceContent = "source content";
String targetSubject = "target change";
String targetContent = "target content";
PushOneCommit pushConflictingCommitInSource =
pushFactory.create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent);
pushConflictingCommitInSource.setParent(
projectOperations.project(project).getHead(sourceBranch));
PushOneCommit.Result pushConflictingCommitInSourceResult =
pushConflictingCommitInSource.to("refs/heads/" + sourceBranch);
pushConflictingCommitInSourceResult.assertOkStatus();
PushOneCommit pushConflictingCommitInTarget =
pushFactory.create(user.newIdent(), testRepo, targetSubject, fileName, targetContent);
pushConflictingCommitInTarget.setParent(
projectOperations.project(project).getHead(targetBranch));
PushOneCommit.Result pushConflictingCommitInTargetResult =
pushConflictingCommitInTarget.to("refs/heads/" + targetBranch);
pushConflictingCommitInTargetResult.assertOkStatus();
// Merge the source branch into the target with conflicts allowed. This is a criss-cross-merge:
//
// (criss-cross-merge)
// / \
// (conflictingCommitInSource) (conflictingCommitInTarget)
// | |
// (mergeCommitInSource) (mergeCommitInTarget)
// | X |
// (baseCommitInSource) (baseCommitInTarget)
// \ /
// (initialCommit)
ChangeInput mergeInput =
newMergeChangeInput(targetBranch, sourceBranch, strategy, /* allowConflicts= */ true);
// The resolve/simple-two-way-in-core strategy doesn't support criss-cross-merges.
if ("resolve".equals(strategy) || "simple-two-way-in-core".equals(strategy)) {
assertCreateFails(
mergeInput,
ResourceConflictException.class,
"Cannot create merge commit: No merge base could be determined."
+ " Reason=MULTIPLE_MERGE_BASES_NOT_SUPPORTED.");
return;
}
// The ours/theirs strategy never results in conflicts.
if ("ours".equals(strategy) || "theirs".equals(strategy)) {
ChangeInfo change = assertCreateSucceeds(mergeInput);
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
assertThat(currentRevision.commit.parents.get(1).commit)
.isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours)
.isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
assertThat(currentRevision.conflicts.noBaseReason)
.isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
assertThat(currentRevision.conflicts.containsConflicts).isFalse();
// Verify that the file content in the created change is correct.
// We expect that it doesn't have conflict markers and the content from "ours" version was
// used.
BinaryResult bin =
gApi.changes().id(project.get(), change._number).current().file(fileName).content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent).isEqualTo("ours".equals(strategy) ? targetContent : sourceContent);
return;
}
assume().that(strategy).isEqualTo("recursive");
ChangeInfo change = assertCreateSucceedsWithConflicts(mergeInput);
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
assertThat(currentRevision.commit.parents.get(1).commit)
.isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours)
.isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
assertThat(currentRevision.conflicts.noBaseReason).isEqualTo(NoMergeBaseReason.COMPUTED_BASE);
assertThat(currentRevision.conflicts.containsConflicts).isTrue();
// Verify that the file content in the created change is correct.
// We expect that it has conflict markers to indicate the conflict.
BinaryResult bin =
gApi.changes().id(project.get(), change._number).current().file(fileName).content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent)
.isEqualTo(
String.format(
"""
<<<<<<< TARGET BRANCH (%s %s)
%s
%s=======
%s
>>>>>>> SOURCE BRANCH (%s %s)
""",
pushConflictingCommitInTargetResult.getCommit().name(),
targetSubject,
targetContent,
(useDiff3 ? "||||||| BASE (computed base)\n" : ""),
sourceContent,
pushConflictingCommitInSourceResult.getCommit().name(),
sourceSubject));
// Verify the message that has been posted on the change.
List<ChangeMessageInfo> messages = gApi.changes().id(project.get(), change._number).messages();
assertThat(messages).hasSize(1);
assertThat(Iterables.getOnlyElement(messages).message)
.isEqualTo(
String.format(
"""
Uploaded patch set 1.
The following files contain Git conflicts:
* %s
""",
fileName));
}
@Test
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesWithConflictsAllowed()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesConflictsAllowed(
/* useDiff3= */ false);
}
@Test
@GerritConfig(name = "change.diff3ConflictView", value = "true")
public void
createMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesWithConflictsAllowedUsingDiff3()
throws Exception {
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesConflictsAllowed(
/* useDiff3= */ true);
}
private void
testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesConflictsAllowed(
boolean useDiff3) throws Exception {
String sourceBranch = "sourceBranch";
String targetBranch = "targetBranch";
// Create source and target branch with conflicting commits.
// Later these commits will become the base commits for the criss-cross-merge.
String baseFile = "base.txt";
String baseContentSource = "base source";
String baseContentTarget = "base target";
ImmutableMap<String, Result> results =
changeInTwoBranches(
sourceBranch,
"base 1",
baseFile,
baseContentSource,
targetBranch,
"base 2",
baseFile,
baseContentTarget);
RevCommit baseCommitInSource = results.get(sourceBranch).getCommit();
RevCommit baseCommitInTarget = results.get(targetBranch).getCommit();
// Create merge commits in both branches (1. merge the target branch into the source branch, 2.
// merge the source branch into the target branch).
PushOneCommit mergeCommitInSource =
pushFactory.create(
user.newIdent(), testRepo, "Merge in Source", baseFile, baseContentSource);
mergeCommitInSource.setParents(ImmutableList.of(baseCommitInSource, baseCommitInTarget));
mergeCommitInSource.to("refs/heads/" + sourceBranch).assertOkStatus();
PushOneCommit mergeCommitInTarget =
pushFactory.create(
user.newIdent(), testRepo, "Merge in Target", baseFile, baseContentTarget);
mergeCommitInTarget.setParents(ImmutableList.of(baseCommitInTarget, baseCommitInSource));
mergeCommitInTarget.to("refs/heads/" + targetBranch).assertOkStatus();
// Create conflicting commits in both branches.
String fileName = "shared.txt";
String sourceSubject = "source change";
String sourceContent = "source content";
String targetSubject = "target change";
String targetContent = "target content";
PushOneCommit pushConflictingCommitInSource =
pushFactory.create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent);
pushConflictingCommitInSource.setParent(
projectOperations.project(project).getHead(sourceBranch));
PushOneCommit.Result pushConflictingCommitInSourceResult =
pushConflictingCommitInSource.to("refs/heads/" + sourceBranch);
pushConflictingCommitInSourceResult.assertOkStatus();
PushOneCommit pushConflictingCommitInTarget =
pushFactory.create(user.newIdent(), testRepo, targetSubject, fileName, targetContent);
pushConflictingCommitInTarget.setParent(
projectOperations.project(project).getHead(targetBranch));
PushOneCommit.Result pushConflictingCommitInTargetResult =
pushConflictingCommitInTarget.to("refs/heads/" + targetBranch);
pushConflictingCommitInTargetResult.assertOkStatus();
// Merge the source branch into the target with conflicts allowed. This is a criss-cross-merge:
//
// (criss-cross-merge)
// / \
// (conflictingCommitInSource) (conflictingCommitInTarget)
// | |
// (mergeCommitInSource) (mergeCommitInTarget)
// | X |
// (baseCommitInSource) (baseCommitInTarget)
// \ /
// (initialCommit)
ChangeInput mergeInput =
newMergeChangeInput(targetBranch, sourceBranch, "recursive", /* allowConflicts= */ true);
ChangeInfo change = assertCreateSucceedsWithConflicts(mergeInput);
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
assertThat(currentRevision.commit.parents.get(1).commit)
.isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isNull();
assertThat(currentRevision.conflicts.ours)
.isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
assertThat(currentRevision.conflicts.noBaseReason).isEqualTo(NoMergeBaseReason.COMPUTED_BASE);
assertThat(currentRevision.conflicts.containsConflicts).isTrue();
// Verify that the content of the file that was conflicting in the bases is correct.
// We expect that it has conflict markers to indicate the conflict and that the base version
// that is computed by the recursive merge is a auto merge of the two bases, containing conflict
// markers as well.
String computedBaseContent =
String.format(
"""
<<<<<<< OURS
%s
=======
%s
>>>>>>> THEIRS
""",
baseContentTarget, baseContentSource);
BinaryResult bin =
gApi.changes().id(project.get(), change._number).current().file(baseFile).content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent)
.isEqualTo(
String.format(
"""
<<<<<<< TARGET BRANCH (%s %s)
%s
%s=======
%s
>>>>>>> SOURCE BRANCH (%s %s)
""",
pushConflictingCommitInTargetResult.getCommit().name(),
targetSubject,
baseContentTarget,
(useDiff3 ? "||||||| BASE (computed base)\n" + computedBaseContent : ""),
baseContentSource,
pushConflictingCommitInSourceResult.getCommit().name(),
sourceSubject));
// Verify that the file content in the created change is correct.
// We expect that it has conflict markers to indicate the conflict.
bin = gApi.changes().id(project.get(), change._number).current().file(fileName).content();
os = new ByteArrayOutputStream();
bin.writeTo(os);
fileContent = new String(os.toByteArray(), UTF_8);
assertThat(fileContent)
.isEqualTo(
String.format(
"""
<<<<<<< TARGET BRANCH (%s %s)
%s
%s=======
%s
>>>>>>> SOURCE BRANCH (%s %s)
""",
pushConflictingCommitInTargetResult.getCommit().name(),
targetSubject,
targetContent,
(useDiff3 ? "||||||| BASE (computed base)\n" : ""),
sourceContent,
pushConflictingCommitInSourceResult.getCommit().name(),
sourceSubject));
// Verify the message that has been posted on the change.
List<ChangeMessageInfo> messages = gApi.changes().id(project.get(), change._number).messages();
assertThat(messages).hasSize(1);
assertThat(Iterables.getOnlyElement(messages).message)
.isEqualTo(
String.format(
"""
Uploaded patch set 1.
The following files contain Git conflicts:
* %s
* %s
""",
baseFile, fileName));
}
@Test
public void createMergeChangeFailsWithConflictIfThereAreTooManyCommonPredecessors()
throws Exception {
// Create an initial commit in master.
Result initialCommit =
pushFactory
.create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
.to("refs/heads/master");
initialCommit.assertOkStatus();
String file = "shared.txt";
List<RevCommit> parents = new ArrayList<>();
// RecursiveMerger#MAX_BASES = 200, cannot use RecursiveMerger#MAX_BASES as it is not static.
int maxBases = 200;
// Create more than RecursiveMerger#MAX_BASES base commits.
for (int i = 1; i <= maxBases + 1; i++) {
parents.add(
testRepo
.commit()
.message("Base " + i)
.add(file, "content " + i)
.parent(initialCommit.getCommit())
.create());
}
// Create 2 branches.
String branchA = "branchA";
String branchB = "branchB";
createBranch(BranchNameKey.create(project, branchA));
createBranch(BranchNameKey.create(project, branchB));
// Push an octopus merge to both of the branches.
Result octopusA =
pushFactory
.create(user.newIdent(), testRepo)
.setParents(parents)
.to("refs/heads/" + branchA);
octopusA.assertOkStatus();
Result octopusB =
pushFactory
.create(user.newIdent(), testRepo)
.setParents(parents)
.to("refs/heads/" + branchB);
octopusB.assertOkStatus();
// Creating a merge commit for the 2 octopus commits fails, because they have more than
// RecursiveMerger#MAX_BASES common predecessors.
assertCreateFails(
newMergeChangeInput("branchA", "branchB", ""),
ResourceConflictException.class,
"Cannot create merge commit: No merge base could be determined."
+ " Reason=TOO_MANY_MERGE_BASES.");
}
@Test
public void updateCommitMessageOfMergeChangeWithConflicts() throws Exception {
// Create a change with a merge commit that contains conflicts
String sourceBranch = "source";
String targetBranch = "target";
ImmutableMap<String, Result> results =
changeInTwoBranches(
sourceBranch,
"source change",
"shared.txt",
"source content",
targetBranch,
"target change",
"shared.txt",
"target content");
RevCommit baseCommit = results.get("master").getCommit();
ChangeInput in =
newMergeChangeInput(targetBranch, sourceBranch, "", /* allowConflicts= */ true);
in.subject = "Merge " + sourceBranch + " to " + targetBranch;
ChangeInfo change = assertCreateSucceedsWithConflicts(in);
// Verify the conflicts information
RevisionInfo currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision._number).isEqualTo(1);
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
assertThat(currentRevision.conflicts.ours)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(results.get(sourceBranch).getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
assertThat(currentRevision.conflicts.noBaseReason).isNull();
assertThat(currentRevision.conflicts.containsConflicts).isTrue();
// Update the commit message
gApi.changes()
.id(change.id)
.setMessage(
"Merge "
+ sourceBranch
+ " branch into "
+ targetBranch
+ " branch\n\nChange-Id: "
+ change.changeId);
// Verify that the conflicts information has been copied
currentRevision =
gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
assertThat(currentRevision._number).isEqualTo(2);
assertThat(currentRevision.commit.parents.get(0).commit)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts).isNotNull();
assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
assertThat(currentRevision.conflicts.ours)
.isEqualTo(results.get(targetBranch).getCommit().name());
assertThat(currentRevision.conflicts.theirs)
.isEqualTo(results.get(sourceBranch).getCommit().name());
assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
assertThat(currentRevision.conflicts.noBaseReason).isNull();
assertThat(currentRevision.conflicts.containsConflicts).isTrue();
}
@Test
public void invalidSource() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in = newMergeChangeInput("branchA", "invalid", "");
assertCreateFails(in, BadRequestException.class, "Cannot resolve 'invalid' to a commit");
}
@Test
public void invalidStrategy() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in = newMergeChangeInput("branchA", "branchB", "octopus");
assertCreateFails(in, BadRequestException.class, "invalid merge strategy: octopus");
}
@Test
public void alreadyMerged() throws Exception {
ObjectId c0 =
testRepo
.branch("HEAD")
.commit()
.insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo
.git()
.push()
.setRemote("origin")
.setRefSpecs(new RefSpec("HEAD:refs/heads/master"))
.call();
testRepo
.branch("HEAD")
.commit()
.insertChangeId()
.message("second commit")
.add("b.txt", "b contents ")
.create();
testRepo
.git()
.push()
.setRemote("origin")
.setRefSpecs(new RefSpec("HEAD:refs/heads/master"))
.call();
ChangeInput in = newMergeChangeInput("master", c0.getName(), "");
assertCreateFails(
in, ChangeAlreadyMergedException.class, "'" + c0.getName() + "' has already been merged");
}
@Test
public void onlyContentMerged() throws Exception {
testRepo
.branch("HEAD")
.commit()
.insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo
.git()
.push()
.setRemote("origin")
.setRefSpecs(new RefSpec("HEAD:refs/heads/master"))
.call();
// create a change, and cherrypick into master
PushOneCommit.Result cId = createChange();
RevCommit commitId = cId.getCommit();
CherryPickInput cpi = new CherryPickInput();
cpi.destination = "master";
cpi.message = "cherry pick the commit";
ChangeApi orig = gApi.changes().id(cId.getChangeId());
ChangeApi cherry = orig.current().cherryPick(cpi);
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
ObjectId remoteId = projectOperations.project(project).getHead("master");
assertThat(remoteId).isNotEqualTo(commitId);
ChangeInput in = newMergeChangeInput("master", commitId.getName(), "");
assertCreateSucceeds(in);
}
@Test
public void createChangeOnExistingBranchNotPermitted() throws Exception {
createBranch(BranchNameKey.create(project, "foo"));
projectOperations
.project(project)
.forUpdate()
// Allow reading for refs/meta/config so that the project is visible to the user. Otherwise
// the request will fail with an UnprocessableEntityException "Project not found:".
.add(allow(READ).ref("refs/meta/config").group(REGISTERED_USERS))
.add(block(READ).ref("refs/heads/*").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.branch = "foo";
assertCreateFails(input, ResourceNotFoundException.class, "ref refs/heads/foo not found");
}
@Test
public void createChangeOnNonExistingBranch() throws Exception {
requestScopeOperations.setApiUser(user.id());
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.branch = "foo";
input.newBranch = true;
assertCreateSucceeds(input);
}
@Test
public void createChangeOnNonExistingBranchNotPermitted() throws Exception {
projectOperations
.project(project)
.forUpdate()
// Allow reading for refs/meta/config so that the project is visible to the user. Otherwise
// the request will fail with an UnprocessableEntityException "Project not found:".
.add(allow(READ).ref("refs/meta/config").group(REGISTERED_USERS))
.add(block(READ).ref("refs/heads/*").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.branch = "foo";
// sets this option to be true to make sure permission check happened before this option could
// be considered.
input.newBranch = true;
assertCreateFails(input, ResourceNotFoundException.class, "ref refs/heads/foo not found");
}
@Test
public void createMergeChangeOnNonExistingBranchNotPossible() throws Exception {
requestScopeOperations.setApiUser(user.id());
ChangeInput input = newMergeChangeInput("foo", "master", "");
input.newBranch = true;
assertCreateFails(
input, BadRequestException.class, "Cannot create merge: destination branch does not exist");
}
@Test
public void createChangeWithBothMergeAndPatch_fails() throws Exception {
ChangeInput input = newMergeChangeInput("foo", "master", "");
input.patch = new ApplyPatchInput();
assertCreateFails(
input, BadRequestException.class, "Only one of `merge` and `patch` arguments can be set");
}
private static final String PATCH_FILE_NAME = "a_file.txt";
private static final String PATCH_NEW_FILE_CONTENT = "First added line\nSecond added line\n";
private static final String PATCH_INPUT =
"diff --git a/a_file.txt b/a_file.txt\n"
+ "new file mode 100644\n"
+ "index 0000000..f0eec86\n"
+ "--- /dev/null\n"
+ "+++ b/a_file.txt\n"
+ "@@ -0,0 +1,2 @@\n"
+ "+First added line\n"
+ "+Second added line\n";
private static final String MODIFICATION_PATCH_INPUT =
"diff --git a/a_file.txt b/a_file.txt\n"
+ "new file mode 100644\n"
+ "--- a/a_file.txt\n"
+ "+++ b/a_file.txt.txt\n"
+ "@@ -1,2 +1 @@\n"
+ "-First original line\n"
+ "-Second original line\n"
+ "+Modified line\n";
@Test
public void createPatchApplyingChange_success() throws Exception {
createBranch(BranchNameKey.create(project, "other"));
ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
ChangeInfo info = assertCreateSucceeds(input);
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("apply patch to other\n\nChange-Id: " + info.changeId + "\n");
}
@Test
public void createPatchApplyingChange_fromGerritPatch_success() throws Exception {
String head = getHead(repo(), HEAD).name();
createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
PushOneCommit.Result baseCommit =
createChange("Add file", PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
baseCommit.assertOkStatus();
BinaryResult originalPatch = gApi.changes().id(baseCommit.getChangeId()).current().patch();
createBranchWithRevision(BranchNameKey.create(project, "other"), head);
ChangeInput input = newPatchApplyingChangeInput("other", originalPatch.asString());
ChangeInfo info = assertCreateSucceeds(input);
DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
}
@Test
public void createPatchApplyingChange_fromGerritPatchUsingRest_success() throws Exception {
String head = getHead(repo(), HEAD).name();
createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
PushOneCommit.Result baseCommit =
createChange("Add file", PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
baseCommit.assertOkStatus();
createBranchWithRevision(BranchNameKey.create(project, "other"), head);
RestResponse patchResp =
userRestSession.get("/changes/" + baseCommit.getChangeId() + "/revisions/current/patch");
patchResp.assertOK();
String originalPatch = new String(Base64.decode(patchResp.getEntityContent()), UTF_8);
ChangeInput input = newPatchApplyingChangeInput("other", originalPatch);
ChangeInfo info = assertCreateSucceedsUsingRest(input);
DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
}
@Test
public void createPatchApplyingChange_withParentChange_success() throws Exception {
Result change = createChange();
ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
input.baseChange = change.getChangeId();
ChangeInfo info = assertCreateSucceeds(input);
assertThat(gApi.changes().id(info.id).current().commit(false).parents.get(0).commit)
.isEqualTo(change.getCommit().getId().name());
DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
}
@Test
public void createPatchApplyingChange_withParentCommit_success() throws Exception {
Ref previousHead = testRepo.getRepository().exactRef("HEAD");
createBranch(BranchNameKey.create(project, "other"));
Result baseChange = createChange("refs/heads/other");
/* Reset HEAD before creating change against master as to not create an implicit merge. */
testRepo.reset(previousHead.getObjectId());
PushOneCommit.Result ignoredCommit = createChange();
ignoredCommit.assertOkStatus();
ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
input.baseCommit = baseChange.getCommit().getId().name();
ChangeInfo info = assertCreateSucceeds(input);
assertThat(gApi.changes().id(info.id).current().commit(false).parents.get(0).commit)
.isEqualTo(input.baseCommit);
DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
}
@Test
public void createPatchApplyingChange_withEmptyTip_fails() throws Exception {
ChangeInput input = newPatchApplyingChangeInput("foo", "patch");
input.newBranch = true;
assertCreateFails(
input, BadRequestException.class, "Cannot apply patch on top of an empty tree");
}
@Test
public void createPatchApplyingChange_fromBadPatch_fails() throws Exception {
final String invalidPatch = "@@ -2,2 +2,3 @@ a\n" + " b\n" + "+c\n" + " d";
createBranch(BranchNameKey.create(project, "other"));
ChangeInput input = newPatchApplyingChangeInput("other", invalidPatch);
assertCreateFails(input, BadRequestException.class, "Invalid patch format");
}
@Test
public void createPatchApplyingChange_withAuthorOverride_success() throws Exception {
createBranch(BranchNameKey.create(project, "other"));
ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
input.author = new AccountInput();
input.author.email = "gerritlessjane@invalid";
// This is an email address that doesn't exist as account on the Gerrit server.
input.author.name = "Gerritless Jane";
ChangeInfo info = assertCreateSucceeds(input);
RevisionApi rApi = gApi.changes().id(info.id).current();
GitPerson author = rApi.commit(false).author;
assertThat(author).email().isEqualTo(input.author.email);
assertThat(author).name().isEqualTo(input.author.name);
GitPerson committer = rApi.commit(false).committer;
assertThat(committer).email().isEqualTo(admin.getNameEmail().email());
}
@Test
public void createPatchApplyingChange_withConflicts_appendErrorsToCommitMessage()
throws Exception {
createBranch(BranchNameKey.create(project, "other"));
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
"Adding unexpected base content, which will cause errors",
PATCH_FILE_NAME,
"unexpected base content");
Result conflictingChange = push.to("refs/heads/other");
conflictingChange.assertOkStatus();
ChangeInput input = newPatchApplyingChangeInput("other", MODIFICATION_PATCH_INPUT);
ChangeInfo info = assertCreateSucceeds(input);
assertThat(info.revisions.get(info.currentRevision).commit.message).contains("errors occurred");
}
@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, or, 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
public void changePatch_multipleParents_success() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in = newMergeChangeInput("branchA", "branchB", "");
ChangeInfo change = assertCreateSucceeds(in);
RestResponse patchResp =
userRestSession.get("/changes/" + change.id + "/revisions/current/patch?parent=1");
patchResp.assertOK();
assertThat(new String(Base64.decode(patchResp.getEntityContent()), UTF_8))
.contains("+B content");
patchResp = userRestSession.get("/changes/" + change.id + "/revisions/current/patch?parent=2");
patchResp.assertOK();
assertThat(new String(Base64.decode(patchResp.getEntityContent()), UTF_8))
.contains("+A content");
}
@Test
public void changePatch_multipleParents_failure() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in = newMergeChangeInput("branchA", "branchB", "");
ChangeInfo change = assertCreateSucceeds(in);
RestResponse patchResp =
userRestSession.get("/changes/" + change.id + "/revisions/current/patch");
// Maintaining historic logic of failing with 409 Conflict in this case.
patchResp.assertConflict();
}
@Test
public void changePatch_parent_badRequest() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in = newMergeChangeInput("branchA", "branchB", "");
ChangeInfo change = assertCreateSucceeds(in);
RestResponse patchResp =
userRestSession.get("/changes/" + change.id + "/revisions/current/patch?parent=3");
// Parent 3 does not exist.
patchResp.assertBadRequest();
patchResp = userRestSession.get("/changes/" + change.id + "/revisions/current/patch?parent=0");
// Parent 0 does not exist.
patchResp.assertBadRequest();
}
@Test
@UseSystemTime
public void sha1sOfTwoNewChangesDiffer() throws Exception {
ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
ChangeInfo info1 = assertCreateSucceeds(changeInput);
ChangeInfo info2 = assertCreateSucceeds(changeInput);
assertThat(info1.currentRevision).isNotEqualTo(info2.currentRevision);
}
@Test
@UseSystemTime
public void sha1sOfTwoNewChangesDifferIfCreatedConcurrently() throws Exception {
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
for (int i = 0; i < 10; i++) {
ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
CyclicBarrier sync = new CyclicBarrier(2);
Callable<ChangeInfo> createChange =
() -> {
requestScopeOperations.setApiUser(admin.id());
sync.await();
return assertCreateSucceeds(changeInput);
};
Future<ChangeInfo> changeInfo1 = executor.submit(createChange);
Future<ChangeInfo> changeInfo2 = executor.submit(createChange);
assertThat(changeInfo1.get().currentRevision)
.isNotEqualTo(changeInfo2.get().currentRevision);
}
}
}
@Test
public void createChangeWithSubmittedMergeSource() throws Exception {
// Provide coverage for a performance optimization in CommitsCollection#canRead.
BranchInput branchInput = new BranchInput();
String mergeTarget = "refs/heads/new-branch";
RevCommit startCommit = projectOperations.project(project).getHead("master");
branchInput.revision = startCommit.name();
branchInput.ref = mergeTarget;
gApi.projects().name(project.get()).branch(mergeTarget).create(branchInput);
// To create a merge commit, create two changes from the same parent,
// and submit them one after the other.
PushOneCommit.Result result1 =
pushFactory
.create(
admin.newIdent(), testRepo, "subject1", ImmutableMap.of("file1.txt", "content 1"))
.to("refs/for/master");
result1.assertOkStatus();
testRepo.branch("HEAD").update(startCommit);
PushOneCommit.Result result2 =
pushFactory
.create(
admin.newIdent(), testRepo, "subject2", ImmutableMap.of("file2.txt", "content 2"))
.to("refs/for/master");
result2.assertOkStatus();
ReviewInput reviewInput = ReviewInput.approve().label("Code-Review", 2);
gApi.changes().id(result1.getChangeId()).revision("current").review(reviewInput);
gApi.changes().id(result1.getChangeId()).revision("current").submit();
gApi.changes().id(result2.getChangeId()).revision("current").review(reviewInput);
gApi.changes().id(result2.getChangeId()).revision("current").submit();
String mergeRev = gApi.projects().name(project.get()).branch("master").get().revision;
RevCommit mergeCommit = projectOperations.project(project).getHead("master");
assertThat(mergeCommit.getParents().length).isEqualTo(2);
testRepo.git().fetch().call();
testRepo.branch("HEAD").update(mergeCommit);
PushOneCommit.Result result3 =
pushFactory
.create(
admin.newIdent(), testRepo, "subject3", ImmutableMap.of("file1.txt", "content 3"))
.to("refs/for/master");
result2.assertOkStatus();
gApi.changes().id(result3.getChangeId()).revision("current").review(reviewInput);
gApi.changes().id(result3.getChangeId()).revision("current").submit();
// Now master doesn't point directly to mergeRev
ChangeInput in = new ChangeInput();
in.branch = mergeTarget;
in.merge = new MergeInput();
in.project = project.get();
in.merge.source = mergeRev;
in.subject = "propagate merge";
gApi.changes().create(in);
}
@Test
public void createChangeWithSourceBranch() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
// create a merge change from branchA to master in gerrit
ChangeInput in = new ChangeInput();
in.project = project.get();
in.branch = "branchA";
in.subject = "message";
in.status = ChangeStatus.NEW;
MergeInput mergeInput = new MergeInput();
String mergeRev = gApi.projects().name(project.get()).branch("branchB").get().revision;
mergeInput.source = mergeRev;
in.merge = mergeInput;
assertCreateSucceeds(in);
// Succeeds with a visible branch
in.merge.sourceBranch = "refs/heads/branchB";
gApi.changes().create(in);
// Make it invisible
projectOperations
.project(project)
.forUpdate()
.add(block(READ).ref(in.merge.sourceBranch).group(REGISTERED_USERS))
.update();
// Now it fails.
assertThrows(BadRequestException.class, () -> gApi.changes().create(in));
}
@Test
public void createChangeWithValidationOptions() throws Exception {
ChangeInput changeInput = new ChangeInput();
changeInput.project = project.get();
changeInput.branch = "master";
changeInput.subject = "A change";
changeInput.status = ChangeStatus.NEW;
changeInput.validationOptions = ImmutableMap.of("key", "value");
TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
TestValidationOptionsListener testValidationOptionsListener =
new TestValidationOptionsListener();
try (Registration registration =
extensionRegistry
.newRegistration()
.add(testCommitValidationListener)
.add(testValidationOptionsListener)) {
assertCreateSucceeds(changeInput);
assertThat(testCommitValidationListener.receiveEvent.pushOptions)
.containsExactly("key", "value");
assertThat(testValidationOptionsListener.validationOptions).containsExactly("key", "value");
}
}
@Test
public void commitValidationInfoListenerIsInvokedOnChangeCreation() throws Exception {
ChangeInput changeInput = new ChangeInput();
changeInput.project = project.get();
changeInput.branch = "master";
changeInput.subject = "A change";
changeInput.status = ChangeStatus.NEW;
changeInput.validationOptions = ImmutableMap.of("key", "value");
TestCommitValidationInfoListener testCommitValidationInfoListener =
new TestCommitValidationInfoListener();
TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
try (Registration registration =
extensionRegistry
.newRegistration()
.add(testCommitValidationInfoListener)
.add(testCommitValidationListener)) {
ChangeInfo changeInfo = assertCreateSucceeds(changeInput);
assertThat(testCommitValidationInfoListener.validationInfoByValidator)
.containsKey(TestCommitValidationListener.class.getName());
assertThat(
testCommitValidationInfoListener
.validationInfoByValidator
.get(TestCommitValidationListener.class.getName())
.status())
.isEqualTo(CommitValidationInfo.Status.PASSED);
assertThat(testCommitValidationInfoListener.receiveEvent.commit.name())
.isEqualTo(changeInfo.currentRevision);
assertThat(testCommitValidationInfoListener.receiveEvent.pushOptions)
.containsExactly("key", "value");
assertThat(testCommitValidationInfoListener.patchSetId)
.isEqualTo(PatchSet.id(Change.id(changeInfo._number), changeInfo.currentRevisionNumber));
assertThat(testCommitValidationInfoListener.hasChangeModificationRefContext).isTrue();
assertThat(testCommitValidationInfoListener.hasDirectPushRefContext).isFalse();
}
}
@Test
public void createChangeWithCustomKeyedValues() throws Exception {
ChangeInput changeInput = new ChangeInput();
changeInput.project = project.get();
changeInput.branch = "master";
changeInput.subject = "A change";
changeInput.status = ChangeStatus.NEW;
changeInput.customKeyedValues = ImmutableMap.of("key", "value");
ChangeInfo result = assertCreateSucceeds(changeInput);
assertThat(result.customKeyedValues).containsExactly("key", "value");
}
private ChangeInput newChangeInput(ChangeStatus status) {
ChangeInput in = new ChangeInput();
in.project = project.get();
in.branch = "master";
in.subject = "Empty change";
in.topic = "support-gerrit-workflow-in-browser";
in.status = status;
return in;
}
private ChangeInput newChangeWithTopic(String topic) {
ChangeInput in = newChangeInput(ChangeStatus.NEW);
in.topic = topic;
return in;
}
private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception {
ChangeInfo out = gApi.changes().create(in).get();
validateCreateSucceeds(in, out);
return out;
}
private ChangeInfo assertCreateSucceedsUsingRest(ChangeInput in) throws Exception {
RestResponse resp = adminRestSession.post("/changes/", in);
resp.assertCreated();
ChangeInfo res = readContentFromJson(resp, ChangeInfo.class);
// The original result doesn't contain any revision data.
ChangeInfo out = gApi.changes().id(res.changeId).get(ALL_REVISIONS, CURRENT_COMMIT);
validateCreateSucceeds(in, out);
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);
}
}
private void validateCreateSucceeds(ChangeInput in, ChangeInfo out) throws Exception {
assertThat(out.project).isEqualTo(in.project);
assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
assertThat(out.subject).isEqualTo(Splitter.on("\n").splitToList(in.subject).get(0));
assertThat(out.topic).isEqualTo(in.topic);
assertThat(out.status).isEqualTo(in.status);
if (Boolean.TRUE.equals(in.isPrivate)) {
assertThat(out.isPrivate).isTrue();
} else {
assertThat(out.isPrivate).isNull();
}
if (Boolean.TRUE.equals(in.workInProgress)) {
assertThat(out.workInProgress).isTrue();
} else {
assertThat(out.workInProgress).isNull();
}
assertThat(out.revisions).hasSize(1);
assertThat(out.submitted).isNull();
assertThat(out.containsGitConflicts).isNull();
assertThat(in.status).isEqualTo(ChangeStatus.NEW);
}
private ChangeInfo assertCreateSucceedsWithConflicts(ChangeInput in) throws Exception {
ChangeInfo out = gApi.changes().createAsInfo(in);
assertThat(out.project).isEqualTo(in.project);
assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
assertThat(out.subject).isEqualTo(Splitter.on("\n").splitToList(in.subject).get(0));
assertThat(out.topic).isEqualTo(in.topic);
assertThat(out.status).isEqualTo(in.status);
if (in.isPrivate) {
assertThat(out.isPrivate).isTrue();
} else {
assertThat(out.isPrivate).isNull();
}
assertThat(out.submitted).isNull();
assertThat(out.containsGitConflicts).isTrue();
assertThat(out.workInProgress).isTrue();
assertThat(in.status).isEqualTo(ChangeStatus.NEW);
return out;
}
private void assertCreateFails(
ChangeInput in, Class<? extends RestApiException> errType, String errSubstring)
throws Exception {
Throwable thrown = assertThrows(errType, () -> gApi.changes().create(in));
assertThat(thrown).hasMessageThat().contains(errSubstring);
}
// TODO(davido): Expose setting of account preferences in the API
private void setSignedOffByFooter(boolean value) throws Exception {
RestResponse r = adminRestSession.get("/accounts/" + admin.email() + "/preferences");
r.assertOK();
GeneralPreferencesInfo i = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
i.signedOffBy = value;
r = adminRestSession.put("/accounts/" + admin.email() + "/preferences", i);
r.assertOK();
GeneralPreferencesInfo o = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
if (value) {
assertThat(o.signedOffBy).isTrue();
} else {
assertThat(o.signedOffBy).isNull();
}
requestScopeOperations.resetCurrentApiUser();
}
private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef, String strategy) {
return newMergeChangeInput(
project, targetBranch, sourceRef, strategy, /* allowConflicts= */ false);
}
private ChangeInput newMergeChangeInput(
String targetBranch, String sourceRef, String strategy, boolean allowConflicts) {
return newMergeChangeInput(project, targetBranch, sourceRef, strategy, allowConflicts);
}
private ChangeInput newMergeChangeInput(
Project.NameKey projectName,
String targetBranch,
String sourceRef,
String strategy,
boolean allowConflicts) {
// create a merge change from branchA to master in gerrit
ChangeInput in = new ChangeInput();
in.project = projectName.get();
in.branch = targetBranch;
in.subject = "merge " + sourceRef + " to " + targetBranch;
in.status = ChangeStatus.NEW;
MergeInput mergeInput = new MergeInput();
mergeInput.source = sourceRef;
in.merge = mergeInput;
if (!Strings.isNullOrEmpty(strategy)) {
in.merge.strategy = strategy;
}
in.merge.allowConflicts = allowConflicts;
return in;
}
private ChangeInput newPatchApplyingChangeInput(String targetBranch, String patch) {
// create a change applying the given patch on the target branch in gerrit
ChangeInput in = new ChangeInput();
in.project = project.get();
in.branch = targetBranch;
in.subject = "apply patch to " + targetBranch;
in.status = ChangeStatus.NEW;
ApplyPatchInput patchInput = new ApplyPatchInput();
patchInput.patch = patch;
in.patch = patchInput;
return in;
}
/**
* Create an empty commit in master, two new branches with one commit each.
*
* @param branchA name of first branch to create
* @param fileA name of file to commit to branchA
* @param branchB name of second branch to create
* @param fileB name of file to commit to branchB
* @return A {@code Map} of branchName => commit result.
*/
private ImmutableMap<String, Result> changeInTwoBranches(
String branchA, String fileA, String branchB, String fileB) throws Exception {
return changeInTwoBranches(
branchA, "change A", fileA, "A content", branchB, "change B", fileB, "B content");
}
/**
* Create an empty commit in master, two new branches with one commit each.
*
* @param branchA name of first branch to create
* @param subjectA commit message subject for the change on branchA
* @param fileA name of file to commit to branchA
* @param contentA file content to commit to branchA
* @param branchB name of second branch to create
* @param subjectB commit message subject for the change on branchB
* @param fileB name of file to commit to branchB
* @param contentB file content to commit to branchB
* @return A {@code Map} of branchName => commit result.
*/
private ImmutableMap<String, Result> changeInTwoBranches(
String branchA,
String subjectA,
String fileA,
String contentA,
String branchB,
String subjectB,
String fileB,
String contentB)
throws Exception {
// create a initial commit in master
Result initialCommit =
pushFactory
.create(user.newIdent(), testRepo, "initial commit", "readme.txt", "initial commit")
.to("refs/heads/master");
initialCommit.assertOkStatus();
// create two new branches
createBranch(BranchNameKey.create(project, branchA));
createBranch(BranchNameKey.create(project, branchB));
// create a commit in branchA
Result changeA =
pushFactory
.create(user.newIdent(), testRepo, subjectA, fileA, contentA)
.to("refs/heads/" + branchA);
changeA.assertOkStatus();
// create a commit in branchB
PushOneCommit commitB =
pushFactory.create(user.newIdent(), testRepo, subjectB, fileB, contentB);
commitB.setParent(initialCommit.getCommit());
Result changeB = commitB.to("refs/heads/" + branchB);
changeB.assertOkStatus();
return ImmutableMap.of("master", initialCommit, branchA, changeA, branchB, changeB);
}
}