blob: 3f7a7e5854acacb6909a6cfb9d6faf9788be3d99 [file] [log] [blame]
// Copyright (C) 2013 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.revision;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH_FILE_ONLY;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
import static org.junit.Assert.fail;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.DraftApi;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
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.ListChangesOption;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ETagView;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.server.change.GetRevisionActions;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
public class RevisionIT extends AbstractDaemonTest {
@Inject private GetRevisionActions getRevisionActions;
@Test
public void reviewTriplet() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes()
.id(project.get() + "~master~" + r.getChangeId())
.revision(r.getCommit().name())
.review(ReviewInput.approve());
}
@Test
public void reviewCurrent() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
}
@Test
public void reviewNumber() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(1).review(ReviewInput.approve());
r = updateChange(r, "new content");
gApi.changes().id(r.getChangeId()).revision(2).review(ReviewInput.approve());
}
@Test
public void submit() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
gApi.changes().id(changeId).current().submit();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
public void postSubmitApproval() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.recommend());
String label = "Code-Review";
ApprovalInfo approval = getApproval(changeId, label);
assertThat(approval.value).isEqualTo(1);
assertThat(approval.postSubmit).isNull();
// Submit by direct push.
git().push().setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")).call();
assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
approval = getApproval(changeId, label);
assertThat(approval.value).isEqualTo(1);
assertThat(approval.postSubmit).isNull();
assertPermitted(
gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)), "Code-Review", 1, 2);
// Repeating the current label is allowed. Does not flip the postSubmit bit
// due to deduplication codepath.
gApi.changes().id(changeId).current().review(ReviewInput.recommend());
approval = getApproval(changeId, label);
assertThat(approval.value).isEqualTo(1);
assertThat(approval.postSubmit).isNull();
// Reducing vote is not allowed.
try {
gApi.changes().id(changeId).current().review(ReviewInput.dislike());
fail("expected ResourceConflictException");
} catch (ResourceConflictException e) {
assertThat(e)
.hasMessageThat()
.isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
}
approval = getApproval(changeId, label);
assertThat(approval.value).isEqualTo(1);
assertThat(approval.postSubmit).isNull();
// Increasing vote is allowed.
gApi.changes().id(changeId).current().review(ReviewInput.approve());
approval = getApproval(changeId, label);
assertThat(approval.value).isEqualTo(2);
assertThat(approval.postSubmit).isTrue();
assertPermitted(gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)), "Code-Review", 2);
// Decreasing to previous post-submit vote is still not allowed.
try {
gApi.changes().id(changeId).current().review(ReviewInput.dislike());
fail("expected ResourceConflictException");
} catch (ResourceConflictException e) {
assertThat(e)
.hasMessageThat()
.isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
}
approval = getApproval(changeId, label);
assertThat(approval.value).isEqualTo(2);
assertThat(approval.postSubmit).isTrue();
}
@Test
public void postSubmitApprovalAfterVoteRemoved() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = project.get() + "~master~" + r.getChangeId();
setApiUser(admin);
revision(r).review(ReviewInput.approve());
setApiUser(user);
revision(r).review(ReviewInput.recommend());
setApiUser(admin);
gApi.changes().id(changeId).reviewer(user.username).deleteVote("Code-Review");
Optional<ApprovalInfo> crUser =
get(changeId, DETAILED_LABELS).labels.get("Code-Review").all.stream()
.filter(a -> a._accountId == user.id.get())
.findFirst();
assertThat(crUser.isPresent()).isTrue();
assertThat(crUser.get().value).isEqualTo(0);
revision(r).submit();
setApiUser(user);
ReviewInput in = new ReviewInput();
in.label("Code-Review", 1);
in.message = "Still LGTM";
revision(r).review(in);
ApprovalInfo cr =
gApi.changes().id(changeId).get(EnumSet.of(ListChangesOption.DETAILED_LABELS)).labels
.get("Code-Review").all.stream()
.filter(a -> a._accountId == user.getId().get())
.findFirst()
.get();
assertThat(cr.postSubmit).isTrue();
}
@Test
public void postSubmitDeleteApprovalNotAllowed() throws Exception {
PushOneCommit.Result r = createChange();
revision(r).review(ReviewInput.approve());
revision(r).submit();
ReviewInput in = new ReviewInput();
in.label("Code-Review", 0);
exception.expect(ResourceConflictException.class);
exception.expectMessage("Cannot reduce vote on labels for closed change: Code-Review");
revision(r).review(in);
}
@TestProjectInput(submitType = SubmitType.CHERRY_PICK)
@Test
public void approvalCopiedDuringSubmitIsNotPostSubmit() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getChange().getId();
gApi.changes().id(id.get()).current().review(ReviewInput.approve());
gApi.changes().id(id.get()).current().submit();
ChangeData cd = r.getChange();
assertThat(cd.patchSets()).hasSize(2);
PatchSetApproval psa =
Iterators.getOnlyElement(
cd.currentApprovals().stream().filter(a -> !a.isLegacySubmit()).iterator());
assertThat(psa.getPatchSetId().get()).isEqualTo(2);
assertThat(psa.getLabel()).isEqualTo("Code-Review");
assertThat(psa.getValue()).isEqualTo(2);
assertThat(psa.isPostSubmit()).isFalse();
}
@Test
public void voteOnAbandonedChange() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).abandon();
exception.expect(ResourceConflictException.class);
exception.expectMessage("change is closed");
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject());
}
@Test
public void deleteDraft() throws Exception {
PushOneCommit.Result r = createDraft();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).delete();
}
@Test
public void cherryPick() throws Exception {
PushOneCommit.Result r = pushTo("refs/for/master%topic=someTopic");
CherryPickInput in = new CherryPickInput();
in.destination = "foo";
in.message = "it goes to stable branch";
gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
assertThat(orig.get().messages).hasSize(1);
ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
Collection<ChangeMessageInfo> messages =
gApi.changes().id(project.get() + "~master~" + r.getChangeId()).get().messages;
assertThat(messages).hasSize(2);
String cherryPickedRevision = cherry.get().currentRevision;
String expectedMessage =
String.format(
"Patch Set 1: Cherry Picked\n\n"
+ "This patchset was cherry picked to branch %s as commit %s",
in.destination, cherryPickedRevision);
Iterator<ChangeMessageInfo> origIt = messages.iterator();
origIt.next();
assertThat(origIt.next().message).isEqualTo(expectedMessage);
assertThat(cherry.get().messages).hasSize(1);
Iterator<ChangeMessageInfo> cherryIt = cherry.get().messages.iterator();
expectedMessage = "Patch Set 1: Cherry Picked from branch master.";
assertThat(cherryIt.next().message).isEqualTo(expectedMessage);
assertThat(cherry.get().subject).contains(in.message);
assertThat(cherry.get().topic).isEqualTo("someTopic-foo");
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
}
@Test
public void cherryPickwithNoTopic() throws Exception {
PushOneCommit.Result r = pushTo("refs/for/master");
CherryPickInput in = new CherryPickInput();
in.destination = "foo";
in.message = "it goes to stable branch";
gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
assertThat(cherry.get().topic).isNull();
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
}
@Test
public void cherryPickToSameBranch() throws Exception {
PushOneCommit.Result r = createChange();
CherryPickInput in = new CherryPickInput();
in.destination = "master";
in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
ChangeInfo cherryInfo =
gApi.changes()
.id(project.get() + "~master~" + r.getChangeId())
.revision(r.getCommit().name())
.cherryPick(in)
.get();
assertThat(cherryInfo.messages).hasSize(2);
Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
}
@Test
public void cherryPickToSameBranchWithRebase() throws Exception {
// Push a new change, then merge it
PushOneCommit.Result baseChange = createChange();
String triplet = project.get() + "~master~" + baseChange.getChangeId();
RevisionApi baseRevision = gApi.changes().id(triplet).current();
baseRevision.review(ReviewInput.approve());
baseRevision.submit();
// Push a new change (change 1)
PushOneCommit.Result r1 = createChange();
// Push another new change (change 2)
String subject = "Test change\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
PushOneCommit push =
pushFactory.create(
db, admin.getIdent(), testRepo, subject, "another_file.txt", "another content");
PushOneCommit.Result r2 = push.to("refs/for/master");
// Change 2's parent should be change 1
assertThat(r2.getCommit().getParents()[0].name()).isEqualTo(r1.getCommit().name());
// Cherry pick change 2 onto the same branch
triplet = project.get() + "~master~" + r2.getChangeId();
ChangeApi orig = gApi.changes().id(triplet);
CherryPickInput in = new CherryPickInput();
in.destination = "master";
in.message = subject;
ChangeApi cherry = orig.revision(r2.getCommit().name()).cherryPick(in);
ChangeInfo cherryInfo = cherry.get();
assertThat(cherryInfo.messages).hasSize(2);
Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
// Parent of change 2 should now be the change that was merged, i.e.
// change 2 is rebased onto the head of the master branch.
String newParent =
cherryInfo.revisions.get(cherryInfo.currentRevision).commit.parents.get(0).commit;
assertThat(newParent).isEqualTo(baseChange.getCommit().name());
}
@Test
public void cherryPickIdenticalTree() throws Exception {
PushOneCommit.Result r = createChange();
CherryPickInput in = new CherryPickInput();
in.destination = "foo";
in.message = "it goes to stable branch";
gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
assertThat(orig.get().messages).hasSize(1);
ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
Collection<ChangeMessageInfo> messages =
gApi.changes().id(project.get() + "~master~" + r.getChangeId()).get().messages;
assertThat(messages).hasSize(2);
assertThat(cherry.get().subject).contains(in.message);
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
exception.expect(ResourceConflictException.class);
exception.expectMessage("Cherry pick failed: identical tree");
orig.revision(r.getCommit().name()).cherryPick(in);
}
@Test
public void cherryPickConflict() throws Exception {
PushOneCommit.Result r = createChange();
CherryPickInput in = new CherryPickInput();
in.destination = "foo";
in.message = "it goes to stable branch";
gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
PushOneCommit push =
pushFactory.create(
db,
admin.getIdent(),
testRepo,
PushOneCommit.SUBJECT,
PushOneCommit.FILE_NAME,
"another content");
push.to("refs/heads/foo");
String triplet = project.get() + "~master~" + r.getChangeId();
ChangeApi orig = gApi.changes().id(triplet);
assertThat(orig.get().messages).hasSize(1);
exception.expect(ResourceConflictException.class);
exception.expectMessage("Cherry pick failed: merge conflict");
orig.revision(r.getCommit().name()).cherryPick(in);
}
@Test
public void cherryPickToExistingChange() throws Exception {
PushOneCommit.Result r1 =
pushFactory
.create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "a")
.to("refs/for/master");
String t1 = project.get() + "~master~" + r1.getChangeId();
BranchInput bin = new BranchInput();
bin.revision = r1.getCommit().getParent(0).name();
gApi.projects().name(project.get()).branch("foo").create(bin);
PushOneCommit.Result r2 =
pushFactory
.create(db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "b", r1.getChangeId())
.to("refs/for/foo");
String t2 = project.get() + "~foo~" + r2.getChangeId();
gApi.changes().id(t2).abandon();
CherryPickInput in = new CherryPickInput();
in.destination = "foo";
in.message = r1.getCommit().getFullMessage();
try {
gApi.changes().id(t1).current().cherryPick(in);
fail();
} catch (ResourceConflictException e) {
assertThat(e.getMessage())
.isEqualTo(
"Cannot create new patch set of change "
+ info(t2)._number
+ " because it is abandoned");
}
gApi.changes().id(t2).restore();
gApi.changes().id(t1).current().cherryPick(in);
assertThat(get(t2).revisions).hasSize(2);
assertThat(gApi.changes().id(t2).current().file(FILE_NAME).content().asString()).isEqualTo("a");
}
@Test
public void cherryPickMergeRelativeToDefaultParent() throws Exception {
String parent1FileName = "a.txt";
String parent2FileName = "b.txt";
PushOneCommit.Result mergeChangeResult =
createCherryPickableMerge(parent1FileName, parent2FileName);
String cherryPickBranchName = "branch_for_cherry_pick";
createBranch(new Branch.NameKey(project, cherryPickBranchName));
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = cherryPickBranchName;
cherryPickInput.message = "Cherry-pick a merge commit to another branch";
ChangeInfo cherryPickedChangeInfo =
gApi.changes()
.id(mergeChangeResult.getChangeId())
.current()
.cherryPick(cherryPickInput)
.get();
Map<String, FileInfo> cherryPickedFilesByName =
cherryPickedChangeInfo.revisions.get(cherryPickedChangeInfo.currentRevision).files;
assertThat(cherryPickedFilesByName).containsKey(parent2FileName);
assertThat(cherryPickedFilesByName).doesNotContainKey(parent1FileName);
}
@Test
public void cherryPickMergeRelativeToSpecificParent() throws Exception {
String parent1FileName = "a.txt";
String parent2FileName = "b.txt";
PushOneCommit.Result mergeChangeResult =
createCherryPickableMerge(parent1FileName, parent2FileName);
String cherryPickBranchName = "branch_for_cherry_pick";
createBranch(new Branch.NameKey(project, cherryPickBranchName));
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = cherryPickBranchName;
cherryPickInput.message = "Cherry-pick a merge commit to another branch";
cherryPickInput.parent = 2;
ChangeInfo cherryPickedChangeInfo =
gApi.changes()
.id(mergeChangeResult.getChangeId())
.current()
.cherryPick(cherryPickInput)
.get();
Map<String, FileInfo> cherryPickedFilesByName =
cherryPickedChangeInfo.revisions.get(cherryPickedChangeInfo.currentRevision).files;
assertThat(cherryPickedFilesByName).containsKey(parent1FileName);
assertThat(cherryPickedFilesByName).doesNotContainKey(parent2FileName);
}
@Test
public void cherryPickMergeUsingInvalidParent() throws Exception {
String parent1FileName = "a.txt";
String parent2FileName = "b.txt";
PushOneCommit.Result mergeChangeResult =
createCherryPickableMerge(parent1FileName, parent2FileName);
String cherryPickBranchName = "branch_for_cherry_pick";
createBranch(new Branch.NameKey(project, cherryPickBranchName));
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = cherryPickBranchName;
cherryPickInput.message = "Cherry-pick a merge commit to another branch";
cherryPickInput.parent = 0;
exception.expect(BadRequestException.class);
exception.expectMessage(
"Cherry Pick: Parent 0 does not exist. Please specify a parent in range [1, 2].");
gApi.changes().id(mergeChangeResult.getChangeId()).current().cherryPick(cherryPickInput);
}
@Test
public void cherryPickMergeUsingNonExistentParent() throws Exception {
String parent1FileName = "a.txt";
String parent2FileName = "b.txt";
PushOneCommit.Result mergeChangeResult =
createCherryPickableMerge(parent1FileName, parent2FileName);
String cherryPickBranchName = "branch_for_cherry_pick";
createBranch(new Branch.NameKey(project, cherryPickBranchName));
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = cherryPickBranchName;
cherryPickInput.message = "Cherry-pick a merge commit to another branch";
cherryPickInput.parent = 3;
exception.expect(BadRequestException.class);
exception.expectMessage(
"Cherry Pick: Parent 3 does not exist. Please specify a parent in range [1, 2].");
gApi.changes().id(mergeChangeResult.getChangeId()).current().cherryPick(cherryPickInput);
}
@Test
public void canRebase() throws Exception {
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
PushOneCommit.Result r1 = push.to("refs/for/master");
merge(r1);
push = pushFactory.create(db, admin.getIdent(), testRepo);
PushOneCommit.Result r2 = push.to("refs/for/master");
boolean canRebase =
gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).canRebase();
assertThat(canRebase).isFalse();
merge(r2);
testRepo.reset(r1.getCommit());
push = pushFactory.create(db, admin.getIdent(), testRepo);
PushOneCommit.Result r3 = push.to("refs/for/master");
canRebase = gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).canRebase();
assertThat(canRebase).isTrue();
}
@Test
public void setUnsetReviewedFlag() throws Exception {
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
gApi.changes().id(r.getChangeId()).current().setReviewed(PushOneCommit.FILE_NAME, true);
assertThat(Iterables.getOnlyElement(gApi.changes().id(r.getChangeId()).current().reviewed()))
.isEqualTo(PushOneCommit.FILE_NAME);
gApi.changes().id(r.getChangeId()).current().setReviewed(PushOneCommit.FILE_NAME, false);
assertThat(gApi.changes().id(r.getChangeId()).current().reviewed()).isEmpty();
}
@Test
public void mergeable() throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
PushOneCommit push1 =
pushFactory.create(
db,
admin.getIdent(),
testRepo,
PushOneCommit.SUBJECT,
PushOneCommit.FILE_NAME,
"push 1 content");
PushOneCommit.Result r1 = push1.to("refs/for/master");
assertMergeable(r1.getChangeId(), true);
merge(r1);
// Reset HEAD to initial so the new change is a merge conflict.
RefUpdate ru = repo().updateRef(HEAD);
ru.setNewObjectId(initial);
assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
PushOneCommit push2 =
pushFactory.create(
db,
admin.getIdent(),
testRepo,
PushOneCommit.SUBJECT,
PushOneCommit.FILE_NAME,
"push 2 content");
PushOneCommit.Result r2 = push2.to("refs/for/master");
assertMergeable(r2.getChangeId(), false);
// TODO(dborowitz): Test for other-branches.
}
@Test
public void files() throws Exception {
PushOneCommit.Result r = createChange();
Map<String, FileInfo> files =
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).files();
assertThat(files).hasSize(2);
assertThat(Iterables.all(files.keySet(), f -> f.matches(FILE_NAME + '|' + COMMIT_MSG)))
.isTrue();
}
@Test
public void filesOnMergeCommitChange() throws Exception {
PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
// list files against auto-merge
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).files().keySet())
.containsExactly(COMMIT_MSG, MERGE_LIST, "foo", "bar");
// list files against parent 1
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).files(1).keySet())
.containsExactly(COMMIT_MSG, MERGE_LIST, "bar");
// list files against parent 2
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).files(2).keySet())
.containsExactly(COMMIT_MSG, MERGE_LIST, "foo");
}
@Test
public void diff() throws Exception {
PushOneCommit.Result r = createChange();
assertDiffForNewFile(r, FILE_NAME, FILE_CONTENT);
assertDiffForNewFile(r, COMMIT_MSG, r.getCommit().getFullMessage());
}
@Test
public void diffDeletedFile() throws Exception {
pushFactory.create(db, admin.getIdent(), testRepo).to("refs/heads/master");
PushOneCommit.Result r =
pushFactory.create(db, admin.getIdent(), testRepo).rm("refs/for/master");
DiffInfo diff =
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file(FILE_NAME).diff();
assertThat(diff.metaA.lines).isEqualTo(1);
assertThat(diff.metaB).isNull();
}
@Test
public void diffOnMergeCommitChange() throws Exception {
PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
DiffInfo diff;
// automerge
diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("foo").diff();
assertThat(diff.metaA.lines).isEqualTo(5);
assertThat(diff.metaB.lines).isEqualTo(1);
diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("bar").diff();
assertThat(diff.metaA.lines).isEqualTo(5);
assertThat(diff.metaB.lines).isEqualTo(1);
// parent 1
diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("bar").diff(1);
assertThat(diff.metaA.lines).isEqualTo(1);
assertThat(diff.metaB.lines).isEqualTo(1);
// parent 2
diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("foo").diff(2);
assertThat(diff.metaA.lines).isEqualTo(1);
assertThat(diff.metaB.lines).isEqualTo(1);
}
@Test
public void description() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description())
.isEqualTo("");
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description())
.isEqualTo("test");
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("");
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description())
.isEqualTo("");
}
@Test
public void content() throws Exception {
PushOneCommit.Result r = createChange();
assertContent(r, FILE_NAME, FILE_CONTENT);
assertContent(r, COMMIT_MSG, r.getCommit().getFullMessage());
}
@Test
public void contentType() throws Exception {
PushOneCommit.Result r = createChange();
String endPoint =
"/changes/"
+ r.getChangeId()
+ "/revisions/"
+ r.getCommit().name()
+ "/files/"
+ FILE_NAME
+ "/content";
RestResponse response = adminRestSession.head(endPoint);
response.assertOK();
assertThat(response.getContentType()).startsWith("text/plain");
assertThat(response.hasContent()).isFalse();
}
private void assertMergeable(String id, boolean expected) throws Exception {
MergeableInfo m = gApi.changes().id(id).current().mergeable();
assertThat(m.mergeable).isEqualTo(expected);
assertThat(m.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
assertThat(m.mergeableInto).isNull();
ChangeInfo c = gApi.changes().id(id).info();
assertThat(c.mergeable).isEqualTo(expected);
}
@Test
public void drafts() throws Exception {
PushOneCommit.Result r = createChange();
DraftInput in = new DraftInput();
in.line = 1;
in.message = "nit: trailing whitespace";
in.path = FILE_NAME;
DraftApi draftApi =
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).createDraft(in);
assertThat(draftApi.get().message).isEqualTo(in.message);
assertThat(
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.draft(draftApi.get().id)
.get()
.message)
.isEqualTo(in.message);
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).drafts())
.hasSize(1);
in.message = "good catch!";
assertThat(
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.draft(draftApi.get().id)
.update(in)
.message)
.isEqualTo(in.message);
assertThat(
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.draft(draftApi.get().id)
.get()
.author
.email)
.isEqualTo(admin.email);
draftApi.delete();
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).drafts())
.isEmpty();
}
@Test
public void comments() throws Exception {
PushOneCommit.Result r = createChange();
CommentInput in = new CommentInput();
in.line = 1;
in.message = "nit: trailing whitespace";
in.path = FILE_NAME;
ReviewInput reviewInput = new ReviewInput();
Map<String, List<CommentInput>> comments = new HashMap<>();
comments.put(FILE_NAME, Collections.singletonList(in));
reviewInput.comments = comments;
reviewInput.message = "comment test";
gApi.changes().id(r.getChangeId()).current().review(reviewInput);
Map<String, List<CommentInfo>> out =
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).comments();
assertThat(out).hasSize(1);
CommentInfo comment = Iterables.getOnlyElement(out.get(FILE_NAME));
assertThat(comment.message).isEqualTo(in.message);
assertThat(comment.author.email).isEqualTo(admin.email);
assertThat(comment.path).isNull();
List<CommentInfo> list =
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).commentsAsList();
assertThat(list).hasSize(1);
CommentInfo comment2 = list.get(0);
assertThat(comment2.path).isEqualTo(FILE_NAME);
assertThat(comment2.line).isEqualTo(comment.line);
assertThat(comment2.message).isEqualTo(comment.message);
assertThat(comment2.author.email).isEqualTo(comment.author.email);
assertThat(
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.comment(comment.id)
.get()
.message)
.isEqualTo(in.message);
}
@Test
public void patch() throws Exception {
PushOneCommit.Result r = createChange();
ChangeApi changeApi = gApi.changes().id(r.getChangeId());
BinaryResult bin = changeApi.revision(r.getCommit().name()).patch();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String res = new String(os.toByteArray(), UTF_8);
ChangeInfo change = changeApi.get();
RevisionInfo rev = change.revisions.get(change.currentRevision);
DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
String date = df.format(rev.commit.author.date);
assertThat(res).isEqualTo(String.format(PATCH, r.getCommit().name(), date, r.getChangeId()));
}
@Test
public void patchWithPath() throws Exception {
PushOneCommit.Result r = createChange();
ChangeApi changeApi = gApi.changes().id(r.getChangeId());
BinaryResult bin = changeApi.revision(r.getCommit().name()).patch(FILE_NAME);
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String res = new String(os.toByteArray(), UTF_8);
assertThat(res).isEqualTo(PATCH_FILE_ONLY);
exception.expect(ResourceNotFoundException.class);
exception.expectMessage("File not found: nonexistent-file.");
changeApi.revision(r.getCommit().name()).patch("nonexistent-file");
}
@Test
public void actions() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(current(r).actions().keySet())
.containsExactly("cherrypick", "description", "rebase");
current(r).review(ReviewInput.approve());
assertThat(current(r).actions().keySet())
.containsExactly("submit", "cherrypick", "description", "rebase");
current(r).submit();
assertThat(current(r).actions().keySet()).containsExactly("cherrypick");
}
@Test
public void actionsETag() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
String oldETag = checkETag(getRevisionActions, r2, null);
current(r2).review(ReviewInput.approve());
oldETag = checkETag(getRevisionActions, r2, oldETag);
// Dependent change is included in ETag.
current(r1).review(ReviewInput.approve());
oldETag = checkETag(getRevisionActions, r2, oldETag);
current(r2).submit();
oldETag = checkETag(getRevisionActions, r2, oldETag);
}
@Test
public void deleteVoteOnNonCurrentPatchSet() throws Exception {
PushOneCommit.Result r = createChange(); // patch set 1
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
// patch set 2
amendChange(r.getChangeId());
// code-review
setApiUser(user);
recommend(r.getChangeId());
// check if it's blocked to delete a vote on a non-current patch set.
setApiUser(admin);
exception.expect(MethodNotAllowedException.class);
exception.expectMessage("Cannot access on non-current patch set");
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().getName())
.reviewer(user.getId().toString())
.deleteVote("Code-Review");
}
@Test
public void deleteVoteOnCurrentPatchSet() throws Exception {
PushOneCommit.Result r = createChange(); // patch set 1
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
// patch set 2
amendChange(r.getChangeId());
// code-review
setApiUser(user);
recommend(r.getChangeId());
setApiUser(admin);
gApi.changes()
.id(r.getChangeId())
.current()
.reviewer(user.getId().toString())
.deleteVote("Code-Review");
Map<String, Short> m =
gApi.changes().id(r.getChangeId()).current().reviewer(user.getId().toString()).votes();
assertThat(m).containsExactly("Code-Review", Short.valueOf((short) 0));
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
ChangeMessageInfo message = Iterables.getLast(c.messages);
assertThat(message.author._accountId).isEqualTo(admin.getId().get());
assertThat(message.message).isEqualTo("Removed Code-Review+1 by User <user@example.com>\n");
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
}
private PushOneCommit.Result updateChange(PushOneCommit.Result r, String content)
throws Exception {
PushOneCommit push =
pushFactory.create(
db, admin.getIdent(), testRepo, "test commit", "a.txt", content, r.getChangeId());
return push.to("refs/for/master");
}
private PushOneCommit.Result createDraft() throws Exception {
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
return push.to("refs/drafts/master");
}
private RevisionApi current(PushOneCommit.Result r) throws Exception {
return gApi.changes().id(r.getChangeId()).current();
}
private String checkETag(ETagView<RevisionResource> view, PushOneCommit.Result r, String oldETag)
throws Exception {
String eTag = view.getETag(parseRevisionResource(r));
assertThat(eTag).isNotEqualTo(oldETag);
return eTag;
}
private void assertContent(PushOneCommit.Result pushResult, String path, String expectedContent)
throws Exception {
BinaryResult bin =
gApi.changes()
.id(pushResult.getChangeId())
.revision(pushResult.getCommit().name())
.file(path)
.content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String res = new String(os.toByteArray(), UTF_8);
assertThat(res).isEqualTo(expectedContent);
}
private void assertDiffForNewFile(
PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
DiffInfo diff =
gApi.changes()
.id(pushResult.getChangeId())
.revision(pushResult.getCommit().name())
.file(path)
.diff();
List<String> headers = new ArrayList<>();
if (path.equals(COMMIT_MSG)) {
RevCommit c = pushResult.getCommit();
RevCommit parentCommit = c.getParents()[0];
String parentCommitId =
testRepo.getRevWalk().getObjectReader().abbreviate(parentCommit.getId(), 8).name();
headers.add("Parent: " + parentCommitId + " (" + parentCommit.getShortMessage() + ")");
SimpleDateFormat dtfmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
PersonIdent author = c.getAuthorIdent();
dtfmt.setTimeZone(author.getTimeZone());
headers.add("Author: " + author.getName() + " <" + author.getEmailAddress() + ">");
headers.add("AuthorDate: " + dtfmt.format(Long.valueOf(author.getWhen().getTime())));
PersonIdent committer = c.getCommitterIdent();
dtfmt.setTimeZone(committer.getTimeZone());
headers.add("Commit: " + committer.getName() + " <" + committer.getEmailAddress() + ">");
headers.add("CommitDate: " + dtfmt.format(Long.valueOf(committer.getWhen().getTime())));
headers.add("");
}
if (!headers.isEmpty()) {
String header = Joiner.on("\n").join(headers);
expectedContentSideB = header + "\n" + expectedContentSideB;
}
assertDiffForNewFile(diff, pushResult.getCommit(), path, expectedContentSideB);
}
private PushOneCommit.Result createCherryPickableMerge(
String parent1FileName, String parent2FileName) throws Exception {
RevCommit initialCommit = getHead(repo());
String branchAName = "branchA";
createBranch(new Branch.NameKey(project, branchAName));
String branchBName = "branchB";
createBranch(new Branch.NameKey(project, branchBName));
PushOneCommit.Result changeAResult =
pushFactory
.create(db, admin.getIdent(), testRepo, "change a", parent1FileName, "Content of a")
.to("refs/for/" + branchAName);
testRepo.reset(initialCommit);
PushOneCommit.Result changeBResult =
pushFactory
.create(db, admin.getIdent(), testRepo, "change b", parent2FileName, "Content of b")
.to("refs/for/" + branchBName);
PushOneCommit pushableMergeCommit =
pushFactory.create(
db,
admin.getIdent(),
testRepo,
"merge",
ImmutableMap.of(parent1FileName, "Content of a", parent2FileName, "Content of b"));
pushableMergeCommit.setParents(
ImmutableList.of(changeAResult.getCommit(), changeBResult.getCommit()));
PushOneCommit.Result mergeChangeResult = pushableMergeCommit.to("refs/for/" + branchAName);
mergeChangeResult.assertOkStatus();
return mergeChangeResult;
}
private ApprovalInfo getApproval(String changeId, String label) throws Exception {
ChangeInfo info = gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS));
LabelInfo li = info.labels.get(label);
assertThat(li).isNotNull();
int accountId = atrScope.get().getUser().getAccountId().get();
return li.all.stream().filter(a -> a._accountId == accountId).findFirst().get();
}
private static Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
return Iterables.transform(r, a -> new Account.Id(a._accountId));
}
}