| // Copyright (C) 2021 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.testsuite.change; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.common.truth.Truth.assertWithMessage; | |
| import static org.eclipse.jgit.lib.Constants.HEAD; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.gerrit.acceptance.GitUtil; | |
| import com.google.gerrit.acceptance.PushOneCommit; | |
| import com.google.gerrit.acceptance.TestAccount; | |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; | |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; | |
| import com.google.gerrit.entities.LabelId; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.extensions.api.GerritApi; | |
| import com.google.gerrit.extensions.api.changes.CherryPickInput; | |
| import com.google.gerrit.extensions.api.changes.ReviewInput; | |
| import com.google.gerrit.extensions.api.changes.RevisionApi; | |
| import com.google.gerrit.extensions.client.ChangeKind; | |
| import com.google.gerrit.extensions.client.ListChangesOption; | |
| import com.google.gerrit.extensions.common.ChangeInfo; | |
| import com.google.gerrit.extensions.common.CommitInfo; | |
| import com.google.inject.Inject; | |
| import java.util.List; | |
| import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; | |
| import org.eclipse.jgit.junit.TestRepository; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.PersonIdent; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| /** Helper to create changes of a certain {@link ChangeKind}. */ | |
| public class ChangeKindCreator { | |
| private GerritApi gApi; | |
| private PushOneCommit.Factory pushFactory; | |
| private RequestScopeOperations requestScopeOperations; | |
| private ProjectOperations projectOperations; | |
| @Inject | |
| private ChangeKindCreator( | |
| GerritApi gApi, | |
| PushOneCommit.Factory pushFactory, | |
| RequestScopeOperations requestScopeOperations, | |
| ProjectOperations projectOperations) { | |
| this.gApi = gApi; | |
| this.pushFactory = pushFactory; | |
| this.requestScopeOperations = requestScopeOperations; | |
| this.projectOperations = projectOperations; | |
| } | |
| /** Creates a change with the given {@link ChangeKind} and returns the change id. */ | |
| public String createChange( | |
| ChangeKind kind, TestRepository<InMemoryRepository> testRepo, TestAccount user) | |
| throws Exception { | |
| switch (kind) { | |
| case NO_CODE_CHANGE: | |
| case REWORK: | |
| case TRIVIAL_REBASE: | |
| case NO_CHANGE: | |
| return createChange(testRepo, user).getChangeId(); | |
| case MERGE_FIRST_PARENT_UPDATE: | |
| return createChangeForMergeCommit(testRepo, user); | |
| default: | |
| throw new IllegalStateException("unexpected change kind: " + kind); | |
| } | |
| } | |
| /** Updates a change with the given {@link ChangeKind}. */ | |
| public void updateChange( | |
| String changeId, | |
| ChangeKind changeKind, | |
| TestRepository<InMemoryRepository> testRepo, | |
| TestAccount user, | |
| Project.NameKey project) | |
| throws Exception { | |
| switch (changeKind) { | |
| case NO_CODE_CHANGE: | |
| noCodeChange(changeId, testRepo, user); | |
| return; | |
| case REWORK: | |
| rework(changeId, testRepo, user); | |
| return; | |
| case TRIVIAL_REBASE: | |
| trivialRebase(changeId, testRepo, user, project); | |
| return; | |
| case MERGE_FIRST_PARENT_UPDATE: | |
| updateFirstParent(changeId, testRepo, user); | |
| return; | |
| case NO_CHANGE: | |
| noChange(changeId, testRepo, user); | |
| return; | |
| default: | |
| assertWithMessage("unexpected change kind: " + changeKind).fail(); | |
| } | |
| } | |
| /** | |
| * Creates a cherry pick of the provided change with the given {@link ChangeKind} and returns the | |
| * change id. | |
| */ | |
| public String cherryPick( | |
| String changeId, | |
| ChangeKind changeKind, | |
| TestRepository<InMemoryRepository> testRepo, | |
| TestAccount user, | |
| Project.NameKey project) | |
| throws Exception { | |
| switch (changeKind) { | |
| case REWORK: | |
| case TRIVIAL_REBASE: | |
| break; | |
| case NO_CODE_CHANGE: | |
| case NO_CHANGE: | |
| case MERGE_FIRST_PARENT_UPDATE: | |
| default: | |
| assertWithMessage("unexpected change kind: " + changeKind).fail(); | |
| } | |
| testRepo.reset(projectOperations.project(project).getHead("master")); | |
| PushOneCommit.Result r = | |
| pushFactory | |
| .create( | |
| user.newIdent(), | |
| testRepo, | |
| PushOneCommit.SUBJECT, | |
| "other.txt", | |
| "new content " + System.nanoTime()) | |
| .to("refs/for/master"); | |
| r.assertOkStatus(); | |
| vote(user, r.getChangeId(), 2, 1); | |
| merge(r); | |
| String subject = | |
| ChangeKind.TRIVIAL_REBASE.equals(changeKind) | |
| ? PushOneCommit.SUBJECT | |
| : "Reworked change " + System.nanoTime(); | |
| CherryPickInput in = new CherryPickInput(); | |
| in.destination = "master"; | |
| in.message = String.format("%s\n\nChange-Id: %s", subject, changeId); | |
| ChangeInfo c = gApi.changes().id(changeId).current().cherryPick(in).get(); | |
| return c.changeId; | |
| } | |
| /** Creates a change that is a merge {@link ChangeKind} and returns the change id. */ | |
| public String createChangeForMergeCommit( | |
| TestRepository<InMemoryRepository> testRepo, TestAccount user) throws Exception { | |
| ObjectId initial = testRepo.getRepository().exactRef(HEAD).getLeaf().getObjectId(); | |
| PushOneCommit.Result parent1 = createChange("parent 1", "p1.txt", "content 1", testRepo, user); | |
| testRepo.reset(initial); | |
| PushOneCommit.Result parent2 = createChange("parent 2", "p2.txt", "content 2", testRepo, user); | |
| testRepo.reset(parent1.getCommit()); | |
| PushOneCommit merge = pushFactory.create(user.newIdent(), testRepo); | |
| merge.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit())); | |
| PushOneCommit.Result result = merge.to("refs/for/master"); | |
| result.assertOkStatus(); | |
| return result.getChangeId(); | |
| } | |
| /** Update the first parent of a merge. */ | |
| public void updateFirstParent( | |
| String changeId, TestRepository<InMemoryRepository> testRepo, TestAccount user) | |
| throws Exception { | |
| ChangeInfo c = detailedChange(changeId); | |
| List<CommitInfo> parents = c.revisions.get(c.currentRevision).commit.parents; | |
| String parent1 = parents.get(0).commit; | |
| String parent2 = parents.get(1).commit; | |
| RevCommit commitParent2 = testRepo.getRevWalk().parseCommit(ObjectId.fromString(parent2)); | |
| testRepo.reset(parent1); | |
| PushOneCommit.Result newParent1 = | |
| createChange("new parent 1", "p1-1.txt", "content 1-1", testRepo, user); | |
| PushOneCommit merge = pushFactory.create(user.newIdent(), testRepo, changeId); | |
| merge.setParents(ImmutableList.of(newParent1.getCommit(), commitParent2)); | |
| PushOneCommit.Result result = merge.to("refs/for/master"); | |
| result.assertOkStatus(); | |
| assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.MERGE_FIRST_PARENT_UPDATE); | |
| } | |
| /** Update the second parent of a merge. */ | |
| public void updateSecondParent( | |
| String changeId, TestRepository<InMemoryRepository> testRepo, TestAccount user) | |
| throws Exception { | |
| ChangeInfo c = detailedChange(changeId); | |
| List<CommitInfo> parents = c.revisions.get(c.currentRevision).commit.parents; | |
| String parent1 = parents.get(0).commit; | |
| String parent2 = parents.get(1).commit; | |
| RevCommit commitParent1 = testRepo.getRevWalk().parseCommit(ObjectId.fromString(parent1)); | |
| testRepo.reset(parent2); | |
| PushOneCommit.Result newParent2 = | |
| createChange("new parent 2", "p2-2.txt", "content 2-2", testRepo, user); | |
| PushOneCommit merge = pushFactory.create(user.newIdent(), testRepo, changeId); | |
| merge.setParents(ImmutableList.of(commitParent1, newParent2.getCommit())); | |
| PushOneCommit.Result result = merge.to("refs/for/master"); | |
| result.assertOkStatus(); | |
| assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.REWORK); | |
| } | |
| private void noCodeChange( | |
| String changeId, TestRepository<InMemoryRepository> testRepo, TestAccount user) | |
| throws Exception { | |
| TestRepository<?>.CommitBuilder commitBuilder = | |
| testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1)); | |
| commitBuilder | |
| .message("New subject " + System.nanoTime()) | |
| .author(user.newIdent()) | |
| .committer(new PersonIdent(user.newIdent(), testRepo.getDate())); | |
| commitBuilder.create(); | |
| GitUtil.pushHead(testRepo, "refs/for/master", false); | |
| assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.NO_CODE_CHANGE); | |
| } | |
| private void noChange( | |
| String changeId, TestRepository<InMemoryRepository> testRepo, TestAccount user) | |
| throws Exception { | |
| ChangeInfo change = gApi.changes().id(changeId).get(); | |
| String commitMessage = change.revisions.get(change.currentRevision).commit.message; | |
| TestRepository<?>.CommitBuilder commitBuilder = | |
| testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1)); | |
| commitBuilder | |
| .message(commitMessage) | |
| .author(user.newIdent()) | |
| .committer(new PersonIdent(user.newIdent(), testRepo.getDate())); | |
| commitBuilder.create(); | |
| GitUtil.pushHead(testRepo, "refs/for/master", false); | |
| assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.NO_CHANGE); | |
| } | |
| private void rework( | |
| String changeId, TestRepository<InMemoryRepository> testRepo, TestAccount user) | |
| throws Exception { | |
| PushOneCommit push = | |
| pushFactory.create( | |
| user.newIdent(), | |
| testRepo, | |
| PushOneCommit.SUBJECT, | |
| PushOneCommit.FILE_NAME, | |
| "new content " + System.nanoTime(), | |
| changeId); | |
| push.to("refs/for/master").assertOkStatus(); | |
| assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.REWORK); | |
| } | |
| private void trivialRebase( | |
| String changeId, | |
| TestRepository<InMemoryRepository> testRepo, | |
| TestAccount user, | |
| Project.NameKey project) | |
| throws Exception { | |
| requestScopeOperations.setApiUser(user.id()); | |
| testRepo.reset(projectOperations.project(project).getHead("master")); | |
| PushOneCommit push = | |
| pushFactory.create( | |
| user.newIdent(), | |
| testRepo, | |
| "Other Change", | |
| "a" + System.nanoTime() + ".txt", | |
| PushOneCommit.FILE_CONTENT); | |
| PushOneCommit.Result r = push.to("refs/for/master"); | |
| r.assertOkStatus(); | |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); | |
| ReviewInput in = new ReviewInput().label(LabelId.CODE_REVIEW, 2).label(LabelId.VERIFIED, 1); | |
| revision.review(in); | |
| revision.submit(); | |
| gApi.changes().id(changeId).current().rebase(); | |
| assertThat(getChangeKind(changeId)).isEqualTo(ChangeKind.TRIVIAL_REBASE); | |
| } | |
| private ChangeKind getChangeKind(String changeId) throws Exception { | |
| ChangeInfo c = gApi.changes().id(changeId).get(ListChangesOption.CURRENT_REVISION); | |
| return c.revisions.get(c.currentRevision).kind; | |
| } | |
| private PushOneCommit.Result createChange( | |
| TestRepository<InMemoryRepository> testRepo, TestAccount user) throws Exception { | |
| PushOneCommit push = pushFactory.create(user.newIdent(), testRepo); | |
| PushOneCommit.Result result = push.to("refs/for/master"); | |
| result.assertOkStatus(); | |
| return result; | |
| } | |
| private ChangeInfo detailedChange(String changeId) throws Exception { | |
| return gApi.changes() | |
| .id(changeId) | |
| .get( | |
| ListChangesOption.DETAILED_LABELS, | |
| ListChangesOption.CURRENT_REVISION, | |
| ListChangesOption.CURRENT_COMMIT); | |
| } | |
| private PushOneCommit.Result createChange( | |
| String subject, | |
| String fileName, | |
| String content, | |
| TestRepository<InMemoryRepository> testRepo, | |
| TestAccount user) | |
| throws Exception { | |
| PushOneCommit push = pushFactory.create(user.newIdent(), testRepo, subject, fileName, content); | |
| return push.to("refs/for/master"); | |
| } | |
| private void vote(TestAccount user, String changeId, int codeReviewVote, int verifiedVote) | |
| throws Exception { | |
| requestScopeOperations.setApiUser(user.id()); | |
| ReviewInput in = | |
| new ReviewInput() | |
| .label(LabelId.CODE_REVIEW, codeReviewVote) | |
| .label(LabelId.VERIFIED, verifiedVote); | |
| gApi.changes().id(changeId).current().review(in); | |
| } | |
| private void merge(PushOneCommit.Result r) throws Exception { | |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); | |
| gApi.changes().id(r.getChangeId()).current().submit(); | |
| } | |
| } |