blob: cd3e76d3ac09a740f1dc70eb7cc59fcfcaec41b7 [file] [log] [blame]
// Copyright (C) 2019 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.api.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
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.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
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.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.PureRevertInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.permissions.PermissionDeniedException;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
public class RevertIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
@Inject private AccountOperations accountOperations;
@Test
public void pureRevertReturnsTrueForPureRevert() throws Exception {
PushOneCommit.Result r = createChange();
merge(r);
String revertId = gApi.changes().id(r.getChangeId()).revert().get().id;
// Without query parameter
assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
// With query parameter
assertThat(
gApi.changes()
.id(revertId)
.pureRevert(
projectOperations.project(project).getHead("master").toObjectId().name())
.isPureRevert)
.isTrue();
}
@Test
public void pureRevertReturnsFalseOnContentChange() throws Exception {
PushOneCommit.Result r1 = createChange();
merge(r1);
// Create a revert and expect pureRevert to be true
String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
// Create a new PS and expect pureRevert to be false
PushOneCommit.Result result = amendChange(revertId);
result.assertOkStatus();
assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isFalse();
}
@Test
public void pureRevertParameterTakesPrecedence() throws Exception {
PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
merge(r1);
String oldHead = projectOperations.project(project).getHead("master").toObjectId().name();
PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content2");
merge(r2);
String revertId = gApi.changes().id(r2.getChangeId()).revert().get().changeId;
assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
assertThat(gApi.changes().id(revertId).pureRevert(oldHead).isPureRevert).isFalse();
}
@Test
public void pureRevertReturnsFalseOnInvalidInput() throws Exception {
PushOneCommit.Result r1 = createChange();
merge(r1);
BadRequestException thrown =
assertThrows(
BadRequestException.class,
() -> gApi.changes().id(createChange().getChangeId()).pureRevert("invalid id"));
assertThat(thrown).hasMessageThat().contains("invalid object ID");
}
@Test
public void pureRevertReturnsTrueWithCleanRebase() throws Exception {
PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
merge(r1);
PushOneCommit.Result r2 = createChange("commit message", "b.txt", "content2");
merge(r2);
String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
// Rebase revert onto HEAD
gApi.changes().id(revertId).rebase();
// Check that pureRevert is true which implies that the commit can be rebased onto the original
// commit.
assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
}
@Test
public void pureRevertReturnsFalseWithRebaseConflict() throws Exception {
// Create an initial commit to serve as claimed original
PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
merge(r1);
String claimedOriginal =
projectOperations.project(project).getHead("master").toObjectId().name();
// Change contents of the file to provoke a conflict
merge(createChange("commit message", "a.txt", "content2"));
// Create a commit that we can revert
PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content3");
merge(r2);
// Create a revert of r2
String revertR3Id = gApi.changes().id(r2.getChangeId()).revert().id();
// Assert that the change is a pure revert of it's 'revertOf'
assertThat(gApi.changes().id(revertR3Id).pureRevert().isPureRevert).isTrue();
// Assert that the change is not a pure revert of claimedOriginal because pureRevert is trying
// to rebase this on claimed original, which fails.
PureRevertInfo pureRevert = gApi.changes().id(revertR3Id).pureRevert(claimedOriginal);
assertThat(pureRevert.isPureRevert).isFalse();
}
@Test
public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
BadRequestException thrown =
assertThrows(
BadRequestException.class,
() -> gApi.changes().id(createChange().getChangeId()).pureRevert());
assertThat(thrown).hasMessageThat().contains("revertOf not set");
}
@Test
public void revert() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
// expected messages on source change:
// 1. Uploaded patch set 1.
// 2. Patch Set 1: Code-Review+2
// 3. Change has been successfully merged by Administrator
// 4. Patch Set 1: Reverted
List<ChangeMessageInfo> sourceMessages =
new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
assertThat(sourceMessages).hasSize(4);
String expectedMessage =
String.format("Created a revert of this change as %s", revertChange.changeId);
assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
assertThat(revertChange.messages).hasSize(1);
assertThat(revertChange.messages.iterator().next().message).isEqualTo("Uploaded patch set 1.");
assertThat(revertChange.revertOf).isEqualTo(gApi.changes().id(r.getChangeId()).get()._number);
}
@Test
public void revertChangeWithWip() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
RevertInput in = createWipRevertInput();
ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(in).get();
assertThat(revertChange.workInProgress).isTrue();
// expected messages on source change:
// 1. Uploaded patch set 1.
// 2. Patch Set 1: Code-Review+2
// 3. Change has been successfully merged by Administrator
// No "reverted" message is expected.
List<ChangeMessageInfo> sourceMessages =
new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
assertThat(sourceMessages).hasSize(3);
// Publishing creates a revert message
gApi.changes().id(revertChange.changeId).setReadyForReview();
sourceMessages = new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
assertThat(sourceMessages).hasSize(4);
assertThat(sourceMessages.get(3).message)
.isEqualTo("Created a revert of this change as " + revertChange.changeId);
}
@Test
public void revertWithDefaultTopic() throws Exception {
PushOneCommit.Result result = createChange();
gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(result.getChangeId()).topic("topic");
gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
RevertInput revertInput = new RevertInput();
assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).topic())
.isEqualTo("topic");
}
@Test
public void revertWithSetTopic() throws Exception {
PushOneCommit.Result result = createChange();
gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(result.getChangeId()).topic("topic");
gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
RevertInput revertInput = new RevertInput();
revertInput.topic = "reverted-not-default";
assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).topic())
.isEqualTo(revertInput.topic);
}
@Test
public void revertWithSetMessage() throws Exception {
PushOneCommit.Result result = createChange();
gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
RevertInput revertInput = new RevertInput();
revertInput.message = "Message from input";
ChangeInfo revertChange = gApi.changes().id(result.getChangeId()).revert(revertInput).get();
assertThat(revertChange.subject).isEqualTo(revertInput.message);
assertThat(gApi.changes().id(revertChange.id).current().commit(false).message)
.isEqualTo(String.format("Message from input\n\nChange-Id: %s\n", revertChange.changeId));
}
@Test
public void revertWithSetMessageChangeIdIgnored() throws Exception {
PushOneCommit.Result result = createChange();
gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
RevertInput revertInput = new RevertInput();
String fakeChangeId = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
String commitSubject = "Message from input";
revertInput.message = String.format("%s\n\nChange-Id: %s\n", commitSubject, fakeChangeId);
ChangeInfo revertChange = gApi.changes().id(result.getChangeId()).revert(revertInput).get();
// ChangeId provided in revert input is ignored.
assertThat(revertChange.changeId).isNotEqualTo(fakeChangeId);
assertThat(revertChange.subject).isEqualTo(commitSubject);
// ChangeId footer was replaced in revert commit message.
assertThat(gApi.changes().id(revertChange.id).current().commit(false).message)
.isEqualTo(String.format("Message from input\n\nChange-Id: %s\n", revertChange.changeId));
}
@Test
public void revertChangeWithLongSubject() throws Exception {
String changeTitle =
"This change has a very long title and therefore it will be cut to 50 characters when the"
+ " revert change will revert this change";
String result = createChange(changeTitle, "a.txt", "message").getChangeId();
gApi.changes().id(result).current().review(ReviewInput.approve());
gApi.changes().id(result).current().submit();
RevertInput revertInput = new RevertInput();
ChangeInfo revertChange = gApi.changes().id(result).revert(revertInput).get();
assertThat(revertChange.subject)
.isEqualTo(String.format("Revert \"%s...\"", changeTitle.substring(0, 59)));
assertThat(gApi.changes().id(revertChange.id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"%s...\"\n\nThis reverts commit %s.\n\nChange-Id: %s\n",
changeTitle.substring(0, 59),
gApi.changes().id(result).get().currentRevision,
revertChange.changeId));
}
@Test
public void revertNotifications() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).addReviewer(user.email());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
sender.clear();
ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(2);
assertThat(sender.getMessages(revertChange.changeId, "newchange")).hasSize(1);
assertThat(sender.getMessages(r.getChangeId(), "revert")).hasSize(1);
}
@Test
public void revertNotificationsSuppressedOnWip() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).addReviewer(user.email());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
sender.clear();
// If notify input not specified, the endpoint overrides it to NONE
RevertInput revertInput = createWipRevertInput();
revertInput.notify = null;
gApi.changes().id(r.getChangeId()).revert(revertInput);
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void suppressRevertNotifications() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).addReviewer(user.email());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
RevertInput revertInput = new RevertInput();
revertInput.notify = NotifyHandling.NONE;
sender.clear();
gApi.changes().id(r.getChangeId()).revert(revertInput);
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void revertPreservesReviewersAndCcs() throws Exception {
PushOneCommit.Result r = createChange();
ReviewInput in = ReviewInput.approve();
in.reviewer(user.email());
in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true);
// Add user as reviewer that will create the revert
in.reviewer(accountCreator.admin2().email());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
// expect both the original reviewers and CCs to be preserved
// original owner should be added as reviewer, user requesting the revert (new owner) removed
requestScopeOperations.setApiUser(accountCreator.admin2().id());
Map<ReviewerState, Collection<AccountInfo>> result =
gApi.changes().id(r.getChangeId()).revert().get().reviewers;
assertThat(result).containsKey(ReviewerState.REVIEWER);
List<Integer> reviewers =
result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
assertThat(result).containsKey(ReviewerState.CC);
List<Integer> ccs =
result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
assertThat(ccs).containsExactly(accountCreator.user2().id().get());
assertThat(reviewers).containsExactly(user.id().get(), admin.id().get());
}
@Test
public void revertAllowedIfUserAccountIsInactive() throws Exception {
PushOneCommit.Result r = createChange();
ReviewInput in = ReviewInput.approve();
in.reviewer(user.email());
in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true);
// Add user as reviewer that will create the revert
in.reviewer(accountCreator.admin2().email());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
accountOperations.account(user.id()).forUpdate().inactive().update();
accountOperations.account(accountCreator.user2().id()).forUpdate().inactive().update();
requestScopeOperations.setApiUser(accountCreator.admin2().id());
Map<ReviewerState, Collection<AccountInfo>> result =
gApi.changes().id(r.getChangeId()).revert().get().reviewers;
assertThat(result).containsKey(ReviewerState.REVIEWER);
// The active user should be preserved as reviewer. For inactive user this test doesn't
// fix specific behavior - they can be either preserved or removed depending on the
// implementation.
List<Integer> reviewers =
result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
assertThat(reviewers).contains(admin.id().get());
}
@Test
@GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
public void revertWithNonVisibleUsers() throws Exception {
// Define readable names for the users we use in this test.
TestAccount reverter = user;
TestAccount changeOwner = admin; // must be admin, since admin cloned testRepo
TestAccount reviewer = accountCreator.user2();
TestAccount cc =
accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null);
// Check that the reverter can neither see the changeOwner, the reviewer nor the cc.
requestScopeOperations.setApiUser(reverter.id());
assertThatAccountIsNotVisible(changeOwner, reviewer, cc);
// Create the change.
requestScopeOperations.setApiUser(changeOwner.id());
PushOneCommit.Result r = createChange();
// Add reviewer and cc.
ReviewInput reviewerInput = ReviewInput.approve();
reviewerInput.reviewer(reviewer.email());
reviewerInput.cc(cc.email());
gApi.changes().id(r.getChangeId()).current().review(reviewerInput);
// Approve and submit the change.
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).current().submit();
// Revert the change.
requestScopeOperations.setApiUser(reverter.id());
String revertChangeId = gApi.changes().id(r.getChangeId()).revert().get().id;
// Revert doesn't check the reviewer/CC visibility. Since the reverter can see the reverted
// change, they can also see its reviewers/CCs. This means preserving them on the revert change
// doesn't expose their account existence and it's OK to keep them even if their accounts are
// not visible to the reverter.
assertReviewers(revertChangeId, changeOwner, reviewer);
assertCcs(revertChangeId, cc);
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void revertInitialCommit() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(r.getChangeId()).revert());
assertThat(thrown).hasMessageThat().contains("Cannot revert initial commit");
}
@Test
public void cantRevertNonMergedCommit() throws Exception {
PushOneCommit.Result result = createChange();
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(result.getChangeId()).revert());
assertThat(thrown)
.hasMessageThat()
.contains("change is " + ChangeUtil.status(result.getChange().change()));
}
@Test
public void cantCreateRevertWithoutProjectWritePermission() throws Exception {
PushOneCommit.Result result = createChange();
gApi.changes()
.id(result.getChangeId())
.revision(result.getCommit().name())
.review(ReviewInput.approve());
gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().updateProject(p -> p.setState(ProjectState.READ_ONLY));
u.save();
}
String expected = "project state " + ProjectState.READ_ONLY + " does not permit write";
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(result.getChangeId()).revert());
assertThat(thrown).hasMessageThat().contains(expected);
}
@Test
public void cantCreateRevertWithoutCreateChangePermission() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
.update();
PermissionDeniedException thrown =
assertThrows(
PermissionDeniedException.class, () -> gApi.changes().id(r.getChangeId()).revert());
assertThat(thrown)
.hasMessageThat()
.contains("not permitted: create change on refs/heads/master");
}
@Test
public void cantCreateRevertWithoutReadPermission() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
ResourceNotFoundException thrown =
assertThrows(
ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).revert());
assertThat(thrown).hasMessageThat().contains("Not found: " + r.getChangeId());
}
@Test
public void revertNotAllowedForOwnerWithoutRevertPermission() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.REVERT).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
PushOneCommit.Result result = createChange();
approve(result.getChangeId());
gApi.changes().id(result.getChangeId()).current().submit();
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(result.getChangeId()).revert());
assertThat(thrown).hasMessageThat().contains("revert not permitted");
}
@Test
public void revertWithValidationOptions() throws Exception {
PushOneCommit.Result result = createChange();
approve(result.getChangeId());
gApi.changes().id(result.getChangeId()).current().submit();
RevertInput revertInput = new RevertInput();
revertInput.validationOptions = ImmutableMap.of("key", "value");
TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
try (Registration registration =
extensionRegistry.newRegistration().add(testCommitValidationListener)) {
gApi.changes().id(result.getChangeId()).revert(revertInput);
assertThat(testCommitValidationListener.receiveEvent.pushOptions)
.containsExactly("key", "value");
}
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void cantCreateRevertSubmissionWithoutProjectWritePermission() throws Exception {
String secondProject = "secondProject";
projectOperations.newProject().name(secondProject).create();
TestRepository<InMemoryRepository> secondRepo =
cloneProject(Project.nameKey("secondProject"), admin);
String topic = "topic";
String change1 =
createChange(testRepo, "master", "first change", "a.txt", "message", topic).getChangeId();
String change2 =
createChange(secondRepo, "master", "second change", "b.txt", "message", topic)
.getChangeId();
gApi.changes().id(change1).current().review(ReviewInput.approve());
gApi.changes().id(change2).current().review(ReviewInput.approve());
gApi.changes().id(change1).current().submit();
// revoke write permissions for the first repository.
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().updateProject(p -> p.setState(ProjectState.READ_ONLY));
u.save();
}
String expected = "project state " + ProjectState.READ_ONLY + " does not permit write";
// assert that if first repository has no write permissions, it will fail.
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(change1).revertSubmission());
assertThat(thrown).hasMessageThat().contains(expected);
// assert that if the first repository has no write permissions and a change from another
// repository is trying to revert the submission, it will fail.
thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(change2).revertSubmission());
assertThat(thrown).hasMessageThat().contains(expected);
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void cantCreateRevertSubmissionWithoutCreateChangePermission() throws Exception {
String secondProject = "secondProject";
projectOperations.newProject().name(secondProject).create();
TestRepository<InMemoryRepository> secondRepo =
cloneProject(Project.nameKey("secondProject"), admin);
String topic = "topic";
String change1 =
createChange(testRepo, "master", "first change", "a.txt", "message", topic).getChangeId();
String change2 =
createChange(secondRepo, "master", "second change", "b.txt", "message", topic)
.getChangeId();
gApi.changes().id(change1).current().review(ReviewInput.approve());
gApi.changes().id(change2).current().review(ReviewInput.approve());
gApi.changes().id(change1).current().submit();
// revoke create change permissions for the first repository.
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
.update();
// assert that if first repository has no write create change, it will fail.
PermissionDeniedException thrown =
assertThrows(
PermissionDeniedException.class, () -> gApi.changes().id(change1).revertSubmission());
assertThat(thrown)
.hasMessageThat()
.contains("not permitted: create change on refs/heads/master");
// assert that if the first repository has no create change permissions and a change from
// another repository is trying to revert the submission, it will fail.
thrown =
assertThrows(
PermissionDeniedException.class, () -> gApi.changes().id(change2).revertSubmission());
assertThat(thrown)
.hasMessageThat()
.contains("not permitted: create change on refs/heads/master");
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void cantCreateRevertSubmissionWithoutReadPermission() throws Exception {
String secondProject = "secondProject";
projectOperations.newProject().name(secondProject).create();
TestRepository<InMemoryRepository> secondRepo =
cloneProject(Project.nameKey("secondProject"), admin);
String topic = "topic";
String change1 =
createChange(testRepo, "master", "first change", "a.txt", "message", topic).getChangeId();
String change2 =
createChange(secondRepo, "master", "second change", "b.txt", "message", topic)
.getChangeId();
gApi.changes().id(change1).current().review(ReviewInput.approve());
gApi.changes().id(change2).current().review(ReviewInput.approve());
gApi.changes().id(change1).current().submit();
// revoke read permissions for the first repository.
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
// assert that if first repository has no read permissions, it will fail.
ResourceNotFoundException resourceNotFoundException =
assertThrows(
ResourceNotFoundException.class, () -> gApi.changes().id(change1).revertSubmission());
assertThat(resourceNotFoundException).hasMessageThat().isEqualTo("Not found: " + change1);
// assert that if the first repository has no READ permissions and a change from another
// repository is trying to revert the submission, it will fail.
AuthException authException =
assertThrows(AuthException.class, () -> gApi.changes().id(change2).revertSubmission());
assertThat(authException).hasMessageThat().isEqualTo("read not permitted");
}
@Test
public void revertSubmissionNotAllowedForOwnerWithoutRevertPermission() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.REVERT).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
PushOneCommit.Result result = createChange();
approve(result.getChangeId());
gApi.changes().id(result.getChangeId()).current().submit();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class, () -> gApi.changes().id(result.getChangeId()).revertSubmission());
assertThat(thrown).hasMessageThat().contains("revert not permitted");
}
@Test
public void revertSubmissionPreservesReviewersAndCcs() throws Exception {
String change = createChange("first change", "a.txt", "message").getChangeId();
ReviewInput in = ReviewInput.approve();
in.reviewer(user.email());
in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true);
// Add user as reviewer that will create the revert
in.reviewer(accountCreator.admin2().email());
gApi.changes().id(change).current().review(in);
gApi.changes().id(change).current().submit();
// expect both the original reviewers and CCs to be preserved
// original owner should be added as reviewer, user requesting the revert (new owner) removed
requestScopeOperations.setApiUser(accountCreator.admin2().id());
Map<ReviewerState, Collection<AccountInfo>> result =
getChangeApis(gApi.changes().id(change).revertSubmission()).get(0).get().reviewers;
assertThat(result).containsKey(ReviewerState.REVIEWER);
List<Integer> reviewers =
result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
assertThat(result).containsKey(ReviewerState.CC);
List<Integer> ccs =
result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
assertThat(ccs).containsExactly(accountCreator.user2().id().get());
assertThat(reviewers).containsExactly(user.id().get(), admin.id().get());
}
@Test
public void revertSubmissionNotifications() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
approve(firstResult);
gApi.changes().id(firstResult).addReviewer(user.email());
String secondResult = createChange("second change", "b.txt", "other").getChangeId();
approve(secondResult);
gApi.changes().id(secondResult).addReviewer(user.email());
gApi.changes().id(secondResult).current().submit();
sender.clear();
RevertInput revertInput = new RevertInput();
revertInput.notify = NotifyHandling.ALL;
RevertSubmissionInfo revertChanges =
gApi.changes().id(secondResult).revertSubmission(revertInput);
ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(4);
assertThat(sender.getMessages(revertChanges.revertChanges.get(0).changeId, "newchange"))
.hasSize(1);
assertThat(sender.getMessages(firstResult, "revert")).hasSize(1);
assertThat(sender.getMessages(revertChanges.revertChanges.get(1).changeId, "newchange"))
.hasSize(1);
assertThat(sender.getMessages(secondResult, "revert")).hasSize(1);
}
@Test
public void revertSubmissionSuppressNotifications() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
approve(firstResult);
gApi.changes().id(firstResult).addReviewer(user.email());
String secondResult = createChange("second change", "b.txt", "other").getChangeId();
approve(secondResult);
gApi.changes().id(secondResult).addReviewer(user.email());
gApi.changes().id(secondResult).current().submit();
sender.clear();
RevertInput revertInput = new RevertInput();
revertInput.notify = NotifyHandling.NONE;
gApi.changes().id(secondResult).revertSubmission(revertInput);
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void revertSubmissionSuppressNotificationsWithWip() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
approve(firstResult);
gApi.changes().id(firstResult).addReviewer(user.email());
String secondResult = createChange("second change", "b.txt", "other").getChangeId();
approve(secondResult);
gApi.changes().id(secondResult).addReviewer(user.email());
gApi.changes().id(secondResult).current().submit();
sender.clear();
RevertInput revertInput = createWipRevertInput();
revertInput.notify = NotifyHandling.NONE;
gApi.changes().id(secondResult).revertSubmission(revertInput);
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void revertSubmissionWipNotificationsWithNotifyHandlingAll() throws Exception {
String changeId1 = createChange("first change", "a.txt", "message").getChangeId();
approve(changeId1);
gApi.changes().id(changeId1).addReviewer(user.email());
String changeId2 = createChange("second change", "b.txt", "other").getChangeId();
approve(changeId2);
gApi.changes().id(changeId2).addReviewer(user.email());
gApi.changes().id(changeId2).current().submit();
sender.clear();
// If notify handling is specified, it will be used by the API
RevertInput revertInput = createWipRevertInput();
revertInput.notify = NotifyHandling.ALL;
RevertSubmissionInfo revertChanges = gApi.changes().id(changeId2).revertSubmission(revertInput);
assertThat(revertChanges.revertChanges).hasSize(2);
assertThat(sender.getMessages()).hasSize(2);
assertThat(sender.getMessages(revertChanges.revertChanges.get(0).changeId, "newchange"))
.hasSize(1);
assertThat(sender.getMessages(revertChanges.revertChanges.get(1).changeId, "newchange"))
.hasSize(1);
}
@Test
public void revertSubmissionWipMarksAllChangesAsWip() throws Exception {
String changeId1 = createChange("first change", "a.txt", "message").getChangeId();
approve(changeId1);
gApi.changes().id(changeId1).addReviewer(user.email());
String changeId2 = createChange("second change", "b.txt", "other").getChangeId();
approve(changeId2);
gApi.changes().id(changeId2).addReviewer(user.email());
gApi.changes().id(changeId2).current().submit();
sender.clear();
RevertInput revertInput = createWipRevertInput();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(changeId2).revertSubmission(revertInput);
assertThat(revertSubmissionInfo.revertChanges.stream().allMatch(r -> r.workInProgress))
.isTrue();
// expected messages on source change:
// 1. Uploaded patch set 1.
// 2. Patch Set 1: Code-Review+2
// 3. Change has been successfully merged by Administrator
// No "reverted" message is expected.
assertThat(gApi.changes().id(changeId1).get().messages).hasSize(3);
assertThat(gApi.changes().id(changeId2).get().messages).hasSize(3);
}
@Test
public void revertSubmissionIdenticalTreeIsAllowed() throws Exception {
String unrelatedChange = createChange("change1", "a.txt", "message").getChangeId();
approve(unrelatedChange);
gApi.changes().id(unrelatedChange).current().submit();
String emptyChange = createChange("change1", "a.txt", "message").getChangeId();
approve(emptyChange);
String changeToBeReverted = createChange("change2", "b.txt", "message").getChangeId();
approve(changeToBeReverted);
gApi.changes().id(changeToBeReverted).current().submit();
sender.clear();
RevertInput revertInput = new RevertInput();
revertInput.notify = NotifyHandling.ALL;
List<ChangeApi> revertChanges =
getChangeApis(gApi.changes().id(changeToBeReverted).revertSubmission(revertInput));
assertThat(revertChanges.size()).isEqualTo(2);
}
@Test
public void suppressRevertSubmissionNotifications() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
approve(firstResult);
gApi.changes().id(firstResult).addReviewer(user.email());
String secondResult = createChange("second change", "b.txt", "other").getChangeId();
approve(secondResult);
gApi.changes().id(secondResult).addReviewer(user.email());
gApi.changes().id(secondResult).current().submit();
RevertInput revertInput = new RevertInput();
revertInput.notify = NotifyHandling.NONE;
sender.clear();
gApi.changes().id(secondResult).revertSubmission(revertInput);
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void revertSubmissionOfSingleChange() throws Exception {
PushOneCommit.Result result = createChange("Change", "a.txt", "message");
String resultId = result.getChangeId();
approve(resultId);
gApi.changes().id(resultId).current().submit();
List<ChangeApi> revertChanges = getChangeApis(gApi.changes().id(resultId).revertSubmission());
String sha1Commit = result.getCommit().getName();
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1Commit);
assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(0).get().revertOf)
.isEqualTo(result.getChange().change().getChangeId());
assertThat(revertChanges.get(0).get().topic)
.startsWith("revert-" + result.getChange().change().getSubmissionId() + "-");
}
@Test
public void revertSubmissionWithSetTopic() throws Exception {
String result = createChange().getChangeId();
gApi.changes().id(result).current().review(ReviewInput.approve());
gApi.changes().id(result).topic("topic");
gApi.changes().id(result).current().submit();
RevertInput revertInput = new RevertInput();
revertInput.topic = "reverted-not-default";
assertThat(gApi.changes().id(result).revertSubmission(revertInput).revertChanges.get(0).topic)
.isEqualTo(revertInput.topic);
}
@Test
public void revertSubmissionWithSetMessage() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
String secondResult = createChange("second change", "b.txt", "message").getChangeId();
approve(firstResult);
approve(secondResult);
gApi.changes().id(secondResult).current().submit();
RevertInput revertInput = new RevertInput();
String commitMessage = "Message from input";
revertInput.message = commitMessage;
List<ChangeInfo> revertChanges =
gApi.changes().id(firstResult).revertSubmission(revertInput).revertChanges;
assertThat(revertChanges.get(0).subject).isEqualTo("Revert \"first change\"");
assertThat(gApi.changes().id(revertChanges.get(0).id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"first change\"\n\n%s\n\nChange-Id: %s\n",
commitMessage, revertChanges.get(0).changeId));
assertThat(revertChanges.get(1).subject).isEqualTo("Revert \"second change\"");
assertThat(gApi.changes().id(revertChanges.get(1).id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"second change\"\n\n%s\n\nChange-Id: %s\n",
commitMessage, revertChanges.get(1).changeId));
}
@Test
public void revertSubmissionWithSetMessageChangeIdIgnored() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
String secondResult = createChange("second change", "b.txt", "message").getChangeId();
approve(firstResult);
approve(secondResult);
gApi.changes().id(secondResult).current().submit();
RevertInput revertInput = new RevertInput();
String fakeChangeId = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
String commitSubject = "Message from input";
String revertMessage = String.format("%s\n\nChange-Id: %s\n", commitSubject, fakeChangeId);
revertInput.message = revertMessage;
List<ChangeInfo> revertChanges =
gApi.changes().id(firstResult).revertSubmission(revertInput).revertChanges;
assertThat(revertChanges.get(0).subject).isEqualTo("Revert \"first change\"");
// ChangeId provided in revert input is ignored.
assertThat(revertChanges.get(0).changeId).isNotEqualTo(fakeChangeId);
assertThat(revertChanges.get(1).changeId).isNotEqualTo(fakeChangeId);
// ChangeId footer was replaced in revert commit message.
assertThat(gApi.changes().id(revertChanges.get(0).id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"first change\"\n\n%s\n\nChange-Id: %s\n",
commitSubject, revertChanges.get(0).changeId));
assertThat(revertChanges.get(1).subject).isEqualTo("Revert \"second change\"");
assertThat(gApi.changes().id(revertChanges.get(1).id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"second change\"\n\n%s\n\nChange-Id: %s\n",
commitSubject, revertChanges.get(1).changeId));
}
@Test
public void revertSubmissionWithoutMessage() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
String secondResult = createChange("second change", "b.txt", "message").getChangeId();
approve(firstResult);
approve(secondResult);
gApi.changes().id(secondResult).current().submit();
RevertInput revertInput = new RevertInput();
List<ChangeInfo> revertChanges =
gApi.changes().id(firstResult).revertSubmission(revertInput).revertChanges;
assertThat(revertChanges.get(0).subject).isEqualTo("Revert \"first change\"");
assertThat(gApi.changes().id(revertChanges.get(0).id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"first change\"\n\nThis reverts commit %s.\n\nChange-Id: %s\n",
gApi.changes().id(firstResult).get().currentRevision,
revertChanges.get(0).changeId));
assertThat(revertChanges.get(1).subject).isEqualTo("Revert \"second change\"");
assertThat(gApi.changes().id(revertChanges.get(1).id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"second change\"\n\nThis reverts commit %s.\n\nChange-Id: %s\n",
gApi.changes().id(secondResult).get().currentRevision,
revertChanges.get(1).changeId));
}
@Test
public void revertSubmissionRevertsChangeWithLongSubject() throws Exception {
String changeTitle =
"This change has a very long title and therefore it will be cut to 56 characters when the"
+ " revert change will revert this change";
String result = createChange(changeTitle, "a.txt", "message").getChangeId();
gApi.changes().id(result).current().review(ReviewInput.approve());
gApi.changes().id(result).current().submit();
RevertInput revertInput = new RevertInput();
ChangeInfo revertChange =
gApi.changes().id(result).revertSubmission(revertInput).revertChanges.get(0);
assertThat(revertChange.subject)
.isEqualTo(String.format("Revert \"%s...\"", changeTitle.substring(0, 56)));
assertThat(gApi.changes().id(revertChange.id).current().commit(false).message)
.isEqualTo(
String.format(
"Revert \"%s...\"\n\nThis reverts commit %s.\n\nChange-Id: %s\n",
changeTitle.substring(0, 56),
gApi.changes().id(result).get().currentRevision,
revertChange.changeId));
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void revertSubmissionDifferentRepositoriesWithDependantChange() throws Exception {
projectOperations.newProject().name("secondProject").create();
TestRepository<InMemoryRepository> secondRepo =
cloneProject(Project.nameKey("secondProject"), admin);
List<PushOneCommit.Result> resultCommits = new ArrayList<>();
String topic = "topic";
resultCommits.add(
createChange(secondRepo, "master", "first change", "a.txt", "message", topic));
resultCommits.add(
createChange(secondRepo, "master", "second change", "b.txt", "Other message", topic));
resultCommits.add(
createChange(testRepo, "master", "main repo change", "a.txt", "message", topic));
for (PushOneCommit.Result result : resultCommits) {
approve(result.getChangeId());
}
// submit all changes
gApi.changes().id(resultCommits.get(1).getChangeId()).current().submit();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(resultCommits.get(1).getChangeId()).revertSubmission();
assertThat(
revertSubmissionInfo.revertChanges.stream()
.map(change -> change.created)
.distinct()
.count())
.isEqualTo(1);
List<ChangeApi> revertChanges = getChangeApis(revertSubmissionInfo);
assertThat(revertChanges).hasSize(3);
String sha1RevertOfTheSecondChange = revertChanges.get(1).current().commit(false).commit;
String sha1SecondChange = resultCommits.get(1).getCommit().getName();
String sha1ThirdChange = resultCommits.get(2).getCommit().getName();
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1RevertOfTheSecondChange);
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1SecondChange);
assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1ThirdChange);
assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1);
// has size 3 because of the same topic, and submitWholeTopic is true.
assertThat(gApi.changes().id(revertChanges.get(0).get()._number).submittedTogether())
.hasSize(3);
// expected messages on source change:
// 1. Uploaded patch set 1.
// 2. Patch Set 1: Code-Review+2
// 3. Change has been successfully merged by Administrator
// 4. Created a revert of this change as %s
for (int i = 0; i < resultCommits.size(); i++) {
assertThat(revertChanges.get(i).get().revertOf)
.isEqualTo(resultCommits.get(i).getChange().change().getChangeId());
List<ChangeMessageInfo> sourceMessages =
new ArrayList<>(gApi.changes().id(resultCommits.get(i).getChangeId()).get().messages);
assertThat(sourceMessages).hasSize(4);
String expectedMessage =
String.format(
"Created a revert of this change as %s", revertChanges.get(i).get().changeId);
assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
// Expected message on the created change: "Uploaded patch set 1."
List<ChangeMessageInfo> messages =
revertChanges.get(i).get().messages.stream().collect(toList());
assertThat(messages).hasSize(1);
assertThat(messages.get(0).message).isEqualTo("Uploaded patch set 1.");
assertThat(revertChanges.get(i).get().revertOf)
.isEqualTo(gApi.changes().id(resultCommits.get(i).getChangeId()).get()._number);
assertThat(revertChanges.get(i).get().topic)
.startsWith("revert-" + resultCommits.get(0).getChange().change().getSubmissionId());
}
assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(2);
}
@Test
public void cantRevertSubmissionWithAnOpenChange() throws Exception {
String result = createChange("change", "a.txt", "message").getChangeId();
approve(result);
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(result).revertSubmission());
assertThat(thrown).hasMessageThat().isEqualTo("change is new.");
}
@Test
public void revertSubmissionWithDependantChange() throws Exception {
PushOneCommit.Result firstResult = createChange("first change", "a.txt", "message");
PushOneCommit.Result secondResult = createChange("second change", "b.txt", "other");
approve(secondResult.getChangeId());
approve(firstResult.getChangeId());
gApi.changes().id(secondResult.getChangeId()).current().submit();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(firstResult.getChangeId()).revertSubmission();
assertThat(
revertSubmissionInfo.revertChanges.stream()
.map(change -> change.created)
.distinct()
.count())
.isEqualTo(1);
List<ChangeApi> revertChanges = getChangeApis(revertSubmissionInfo);
Collections.reverse(revertChanges);
String sha1SecondChange = secondResult.getCommit().getName();
String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit;
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1SecondChange);
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1FirstRevert);
assertThat(revertChanges.get(0).get().revertOf)
.isEqualTo(secondResult.getChange().change().getChangeId());
assertThat(revertChanges.get(0).get().cherryPickOfChange).isNull();
assertThat(revertChanges.get(1).get().revertOf)
.isEqualTo(firstResult.getChange().change().getChangeId());
assertThat(revertChanges.get(0).get().cherryPickOfChange).isNull();
assertThat(revertChanges.get(0).current().files().get("b.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges).hasSize(2);
assertThat(gApi.changes().id(revertChanges.get(0).id()).current().related().changes).hasSize(2);
}
@Test
public void revertSubmissionWithDependantChangeWithoutRevertingLastOne() throws Exception {
PushOneCommit.Result firstResult = createChange("first change", "a.txt", "message");
PushOneCommit.Result secondResult = createChange("second change", "b.txt", "other");
approve(secondResult.getChangeId());
approve(firstResult.getChangeId());
gApi.changes().id(secondResult.getChangeId()).current().submit();
String unrelated = createChange("other change", "c.txt", "message other").getChangeId();
approve(unrelated);
gApi.changes().id(unrelated).current().submit();
List<ChangeApi> revertChanges =
getChangeApis(gApi.changes().id(firstResult.getChangeId()).revertSubmission());
Collections.reverse(revertChanges);
String sha1SecondChange = secondResult.getCommit().getName();
String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit;
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1SecondChange);
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1FirstRevert);
assertThat(revertChanges.get(0).get().revertOf)
.isEqualTo(secondResult.getChange().change().getChangeId());
assertThat(revertChanges.get(1).get().revertOf)
.isEqualTo(firstResult.getChange().change().getChangeId());
assertThat(revertChanges.get(0).current().files().get("b.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges).hasSize(2);
assertThat(gApi.changes().id(revertChanges.get(0).id()).current().related().changes).hasSize(2);
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void revertSubmissionDifferentRepositories() throws Exception {
projectOperations.newProject().name("secondProject").create();
TestRepository<InMemoryRepository> secondRepo =
cloneProject(Project.nameKey("secondProject"), admin);
String topic = "topic";
PushOneCommit.Result firstResult =
createChange(testRepo, "master", "first change", "a.txt", "message", topic);
PushOneCommit.Result secondResult =
createChange(secondRepo, "master", "second change", "b.txt", "other", topic);
approve(secondResult.getChangeId());
approve(firstResult.getChangeId());
// submit both changes
gApi.changes().id(secondResult.getChangeId()).current().submit();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(secondResult.getChangeId()).revertSubmission();
assertThat(
revertSubmissionInfo.revertChanges.stream()
.map(change -> change.created)
.distinct()
.count())
.isEqualTo(1);
List<ChangeApi> revertChanges = getChangeApis(revertSubmissionInfo);
// has size 2 because of the same topic, and submitWholeTopic is true.
assertThat(gApi.changes().id(revertChanges.get(0).get()._number).submittedTogether())
.hasSize(2);
String sha1SecondChange = secondResult.getCommit().getName();
String sha1FirstChange = firstResult.getCommit().getName();
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1FirstChange);
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1SecondChange);
assertThat(revertChanges.get(0).get().revertOf)
.isEqualTo(firstResult.getChange().change().getChangeId());
assertThat(revertChanges.get(1).get().revertOf)
.isEqualTo(secondResult.getChange().change().getChangeId());
assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges).hasSize(2);
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void revertSubmissionMultipleBranches() throws Exception {
List<PushOneCommit.Result> resultCommits = new ArrayList<>();
String topic = "topic";
resultCommits.add(createChange(testRepo, "master", "first change", "c.txt", "message", topic));
testRepo.reset("HEAD~1");
createBranch(BranchNameKey.create(project, "other"));
resultCommits.add(createChange(testRepo, "other", "second change", "a.txt", "message", topic));
resultCommits.add(
createChange(testRepo, "other", "third change", "b.txt", "Other message", topic));
for (PushOneCommit.Result result : resultCommits) {
approve(result.getChangeId());
}
// submit all changes
gApi.changes().id(resultCommits.get(1).getChangeId()).current().submit();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(resultCommits.get(1).getChangeId()).revertSubmission();
assertThat(
revertSubmissionInfo.revertChanges.stream()
.map(change -> change.created)
.distinct()
.count())
.isEqualTo(1);
// Size
List<ChangeApi> revertChanges = getChangeApis(revertSubmissionInfo);
assertThat(revertChanges).hasSize(3);
assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(2);
// Contents
assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(2).current().files().get("b.txt").linesDeleted).isEqualTo(1);
// Commit message
assertThat(revertChanges.get(0).current().commit(false).message)
.matches(
Pattern.compile(
"Revert \"first change\"\n\n"
+ "This reverts commit [a-f0-9]+\\.\n\n"
+ "Change-Id: I[a-f0-9]+\n"));
assertThat(revertChanges.get(1).current().commit(false).message)
.matches(
Pattern.compile(
"Revert \"second change\"\n\n"
+ "This reverts commit [a-f0-9]+\\.\n\n"
+ "Change-Id: I[a-f0-9]+\n"));
assertThat(revertChanges.get(2).current().commit(false).message)
.matches(
Pattern.compile(
"Revert \"third change\"\n\n"
+ "This reverts commit [a-f0-9]+\\.\n\n"
+ "Change-Id: I[a-f0-9]+\n"));
// Relationships
String sha1FirstChange = resultCommits.get(0).getCommit().getName();
String sha1ThirdChange = resultCommits.get(2).getCommit().getName();
String sha1SecondRevert = revertChanges.get(2).current().commit(false).commit;
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1FirstChange);
assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1ThirdChange);
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1SecondRevert);
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void revertSubmissionDependantAndUnrelatedWithMerge() throws Exception {
String topic = "topic";
PushOneCommit.Result firstResult =
createChange(testRepo, "master", "first change", "a.txt", "message", topic);
approve(firstResult.getChangeId());
PushOneCommit.Result secondResult =
createChange(testRepo, "master", "second change", "b.txt", "message", topic);
approve(secondResult.getChangeId());
testRepo.reset("HEAD~1");
PushOneCommit.Result thirdResult =
createChange(testRepo, "master", "third change", "c.txt", "message", topic);
approve(thirdResult.getChangeId());
gApi.changes().id(firstResult.getChangeId()).current().submit();
// put the head on the merge commit created by submitting the second and third change.
testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/master:merge")).call();
testRepo.reset("merge");
// Create another change that should be ignored. The reverts should be rebased on top of the
// merge commit.
PushOneCommit.Result fourthResult =
createChange(testRepo, "master", "fourth change", "d.txt", "message", topic);
approve(fourthResult.getChangeId());
gApi.changes().id(fourthResult.getChangeId()).current().submit();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(secondResult.getChangeId()).revertSubmission();
assertThat(
revertSubmissionInfo.revertChanges.stream()
.map(change -> change.created)
.distinct()
.count())
.isEqualTo(1);
List<ChangeApi> revertChanges = getChangeApis(revertSubmissionInfo);
Collections.reverse(revertChanges);
assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1);
String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit;
String sha1SecondRevert = revertChanges.get(1).current().commit(false).commit;
// parent of the first revert is the merged change of previous changes.
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).subject)
.contains("Merge");
// Next reverts would stack on top of the previous ones.
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1FirstRevert);
assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1SecondRevert);
assertThat(revertChanges).hasSize(3);
assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(3);
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void revertSubmissionUnrelatedWithTwoMergeCommits() throws Exception {
String topic = "topic";
PushOneCommit.Result firstResult =
createChange(testRepo, "master", "first change", "a.txt", "message", topic);
approve(firstResult.getChangeId());
testRepo.reset("HEAD~1");
PushOneCommit.Result secondResult =
createChange(testRepo, "master", "second change", "b.txt", "message", topic);
approve(secondResult.getChangeId());
testRepo.reset("HEAD~1");
PushOneCommit.Result thirdResult =
createChange(testRepo, "master", "third change", "c.txt", "message", topic);
approve(thirdResult.getChangeId());
gApi.changes().id(firstResult.getChangeId()).current().submit();
// put the head on the most recent merge commit.
testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/master:merge")).call();
testRepo.reset("merge");
// Create another change that should be ignored. The reverts should be rebased on top of the
// merge commit.
PushOneCommit.Result fourthResult =
createChange(testRepo, "master", "fourth change", "d.txt", "message", topic);
approve(fourthResult.getChangeId());
gApi.changes().id(fourthResult.getChangeId()).current().submit();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(secondResult.getChangeId()).revertSubmission();
assertThat(
revertSubmissionInfo.revertChanges.stream()
.map(change -> change.created)
.distinct()
.count())
.isEqualTo(1);
List<ChangeApi> revertChanges = getChangeApis(revertSubmissionInfo);
Collections.reverse(revertChanges);
assertThat(revertChanges.get(0).current().files().get("c.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("b.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(2).current().files().get("a.txt").linesDeleted).isEqualTo(1);
String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit;
String sha1SecondRevert = revertChanges.get(1).current().commit(false).commit;
// parent of the first revert is the merged change of previous changes.
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).subject)
.contains("Merge \"third change\"");
// Next reverts would stack on top of the previous ones.
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1FirstRevert);
assertThat(revertChanges.get(2).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1SecondRevert);
assertThat(revertChanges).hasSize(3);
assertThat(gApi.changes().id(revertChanges.get(1).id()).current().related().changes).hasSize(3);
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void revertSubmissionUnrelatedWithAnotherDependantChangeWithDifferentTopic()
throws Exception {
String topic = "topic";
PushOneCommit.Result firstResult =
createChange(testRepo, "master", "first change", "a.txt", "message", topic);
approve(firstResult.getChangeId());
testRepo.reset("HEAD~1");
PushOneCommit.Result secondResult =
createChange(testRepo, "master", "second change", "b.txt", "message", topic);
approve(secondResult.getChangeId());
// A non-merged change without the same topic that is related to the second change.
createChange();
gApi.changes().id(firstResult.getChangeId()).current().submit();
RevertSubmissionInfo revertSubmissionInfo =
gApi.changes().id(secondResult.getChangeId()).revertSubmission();
List<ChangeApi> revertChanges = getChangeApis(revertSubmissionInfo);
Collections.reverse(revertChanges);
assertThat(revertChanges.get(0).current().files().get("b.txt").linesDeleted).isEqualTo(1);
assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1);
// The parent of the first revert is the merge change of the submission.
assertThat(revertChanges.get(0).current().commit(false).parents.get(0).subject)
.contains("Merge \"second change\"");
// Next revert would base itself on the previous revert.
String sha1FirstRevert = revertChanges.get(0).current().commit(false).commit;
assertThat(revertChanges.get(1).current().commit(false).parents.get(0).commit)
.isEqualTo(sha1FirstRevert);
assertThat(revertChanges).hasSize(2);
}
@Test
public void revertSubmissionSubjects() throws Exception {
String firstResult = createChange("first change", "a.txt", "message").getChangeId();
String secondResult = createChange("second change", "b.txt", "other").getChangeId();
approve(firstResult);
approve(secondResult);
gApi.changes().id(secondResult).current().submit();
List<ChangeApi> firstRevertChanges =
getChangeApis(gApi.changes().id(firstResult).revertSubmission());
assertThat(firstRevertChanges.get(0).get().subject).isEqualTo("Revert \"first change\"");
assertThat(firstRevertChanges.get(1).get().subject).isEqualTo("Revert \"second change\"");
approve(firstRevertChanges.get(0).id());
approve(firstRevertChanges.get(1).id());
gApi.changes().id(firstRevertChanges.get(0).id()).current().submit();
List<ChangeApi> secondRevertChanges =
getChangeApis(gApi.changes().id(firstRevertChanges.get(0).id()).revertSubmission());
assertThat(secondRevertChanges.get(0).get().subject).isEqualTo("Revert^2 \"second change\"");
assertThat(secondRevertChanges.get(1).get().subject).isEqualTo("Revert^2 \"first change\"");
approve(secondRevertChanges.get(0).id());
approve(secondRevertChanges.get(1).id());
gApi.changes().id(secondRevertChanges.get(0).id()).current().submit();
List<ChangeApi> thirdRevertChanges =
getChangeApis(gApi.changes().id(secondRevertChanges.get(0).id()).revertSubmission());
assertThat(thirdRevertChanges.get(0).get().subject).isEqualTo("Revert^3 \"first change\"");
assertThat(thirdRevertChanges.get(1).get().subject).isEqualTo("Revert^3 \"second change\"");
}
@Test
public void revertSubmissionWithUserChangedSubjects() throws Exception {
String firstResult = createChange("Revert^aa", "a.txt", "message").getChangeId();
String secondResult = createChange("Revert", "b.txt", "other").getChangeId();
String thirdResult = createChange("Revert^934 \"change x\"", "c.txt", "another").getChangeId();
String fourthResult = createChange("Revert^934", "d.txt", "last").getChangeId();
approve(firstResult);
approve(secondResult);
approve(thirdResult);
approve(fourthResult);
gApi.changes().id(fourthResult).current().submit();
List<ChangeApi> firstRevertChanges =
getChangeApis(gApi.changes().id(firstResult).revertSubmission());
assertThat(firstRevertChanges.get(0).get().subject).isEqualTo("Revert \"Revert^aa\"");
assertThat(firstRevertChanges.get(1).get().subject).isEqualTo("Revert \"Revert\"");
assertThat(firstRevertChanges.get(2).get().subject).isEqualTo("Revert^935 \"change x\"");
assertThat(firstRevertChanges.get(3).get().subject).isEqualTo("Revert \"Revert^934\"");
}
@Override
protected PushOneCommit.Result createChange() throws Exception {
return createChange("refs/for/master");
}
@Override
protected PushOneCommit.Result createChange(String ref) throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result result = push.to(ref);
result.assertOkStatus();
return result;
}
private List<ChangeApi> getChangeApis(RevertSubmissionInfo revertSubmissionInfo)
throws Exception {
List<ChangeApi> results = new ArrayList<>();
for (ChangeInfo changeInfo : revertSubmissionInfo.revertChanges) {
results.add(gApi.changes().id(changeInfo._number));
}
return results;
}
private RevertInput createWipRevertInput() {
RevertInput input = new RevertInput();
input.workInProgress = true;
return input;
}
private static class TestCommitValidationListener implements CommitValidationListener {
public CommitReceivedEvent receiveEvent;
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
this.receiveEvent = receiveEvent;
return ImmutableList.of();
}
}
}