blob: 0607a3ce3632546681c193a4a27f74a3887ad035 [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.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.entities.RefNames;
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.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.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.List;
import java.util.Map;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
public class RevertIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Test
public void pureRevertFactBlocksSubmissionOfNonReverts() throws Exception {
addPureRevertSubmitRule();
// Create a change that is not a revert of another change
PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
approve(r1.getChangeId());
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r1.getChangeId()).current().submit());
assertThat(thrown)
.hasMessageThat()
.contains("Failed to submit 1 change due to the following problems");
assertThat(thrown).hasMessageThat().contains("needs Is-Pure-Revert");
}
@Test
public void pureRevertFactBlocksSubmissionOfNonPureReverts() throws Exception {
PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
merge(r1);
addPureRevertSubmitRule();
// Create a revert and push a content change
String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
amendChange(revertId);
approve(revertId);
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(revertId).current().submit());
assertThat(thrown)
.hasMessageThat()
.contains("Failed to submit 1 change due to the following problems");
assertThat(thrown).hasMessageThat().contains("needs Is-Pure-Revert");
}
@Test
public void pureRevertFactAllowsSubmissionOfPureReverts() throws Exception {
// Create a change that we can later revert
PushOneCommit.Result r1 = pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
merge(r1);
addPureRevertSubmitRule();
// Create a revert and submit it
String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
approve(revertId);
gApi.changes().id(revertId).current().submit();
}
@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 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";
assertThat(gApi.changes().id(result.getChangeId()).revert(revertInput).get().subject)
.isEqualTo(revertInput.message);
}
@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();
List<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 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).get();
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
@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 r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
projectCache.checkedGet(project).getProject().setState(ProjectState.READ_ONLY);
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(r.getChangeId()).revert());
assertThat(thrown)
.hasMessageThat()
.contains("project state " + ProjectState.READ_ONLY + " does not permit write");
}
@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());
}
@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 void addPureRevertSubmitRule() throws Exception {
modifySubmitRules(
"submit_rule(submit(R)) :- \n"
+ "gerrit:pure_revert(1), \n"
+ "!,"
+ "gerrit:uploader(U), \n"
+ "R = label('Is-Pure-Revert', ok(U)).\n"
+ "submit_rule(submit(R)) :- \n"
+ "gerrit:pure_revert(U), \n"
+ "U \\= 1,"
+ "R = label('Is-Pure-Revert', need(_)). \n\n");
}
private void modifySubmitRules(String newContent) throws Exception {
try (Repository repo = repoManager.openRepository(project);
TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
testRepo
.branch(RefNames.REFS_CONFIG)
.commit()
.author(admin.newIdent())
.committer(admin.newIdent())
.add("rules.pl", newContent)
.message("Modify rules.pl")
.create();
}
}
}