| // 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.change; |
| |
| import static com.google.common.collect.ImmutableMap.toImmutableMap; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| import static com.google.common.truth.Truth8.assertThat; |
| import static com.google.gerrit.acceptance.GitUtil.assertPushOk; |
| import static com.google.gerrit.acceptance.GitUtil.pushHead; |
| 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.SUBJECT; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey; |
| import static com.google.gerrit.entities.RefNames.changeMetaRef; |
| import static com.google.gerrit.extensions.client.ChangeStatus.MERGED; |
| import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CHECK; |
| import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; |
| import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES; |
| import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES; |
| import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED; |
| import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS; |
| import static com.google.gerrit.extensions.client.ReviewerState.CC; |
| import static com.google.gerrit.extensions.client.ReviewerState.REMOVED; |
| import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; |
| import static com.google.gerrit.git.ObjectIds.abbreviateName; |
| import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL; |
| import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; |
| import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER; |
| import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS; |
| import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; |
| import static com.google.gerrit.server.project.testing.TestLabels.label; |
| import static com.google.gerrit.server.project.testing.TestLabels.value; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| import static com.google.gerrit.truth.CacheStatsSubject.assertThat; |
| import static com.google.gerrit.truth.CacheStatsSubject.cloneStats; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static java.util.stream.Collectors.joining; |
| import static java.util.stream.Collectors.toList; |
| import static java.util.stream.Collectors.toSet; |
| |
| import com.google.common.cache.Cache; |
| import com.google.common.cache.CacheStats; |
| 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.Lists; |
| import com.google.common.truth.ThrowableSubject; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.ChangeIndexedCounter; |
| import com.google.gerrit.acceptance.ExtensionRegistry; |
| import com.google.gerrit.acceptance.ExtensionRegistry.Registration; |
| import com.google.gerrit.acceptance.GitUtil; |
| import com.google.gerrit.acceptance.NoHttpd; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.acceptance.TestAccount; |
| import com.google.gerrit.acceptance.TestProjectInput; |
| import com.google.gerrit.acceptance.UseClockStep; |
| import com.google.gerrit.acceptance.UseTimezone; |
| import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes; |
| import com.google.gerrit.acceptance.config.GerritConfig; |
| import com.google.gerrit.acceptance.testsuite.account.AccountOperations; |
| import com.google.gerrit.acceptance.testsuite.change.IndexOperations; |
| import com.google.gerrit.acceptance.testsuite.group.GroupOperations; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| import com.google.gerrit.common.FooterConstants; |
| import com.google.gerrit.common.RawInputUtil; |
| import com.google.gerrit.common.data.GlobalCapability; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.entities.Address; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.LabelFunction; |
| import com.google.gerrit.entities.LabelId; |
| import com.google.gerrit.entities.LabelType; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.Permission; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.extensions.annotations.Exports; |
| import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput; |
| import com.google.gerrit.extensions.api.changes.AttentionSetInput; |
| import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; |
| import com.google.gerrit.extensions.api.changes.DeleteVoteInput; |
| import com.google.gerrit.extensions.api.changes.DraftApi; |
| import com.google.gerrit.extensions.api.changes.DraftInput; |
| import com.google.gerrit.extensions.api.changes.NotifyHandling; |
| import com.google.gerrit.extensions.api.changes.NotifyInfo; |
| import com.google.gerrit.extensions.api.changes.RebaseInput; |
| import com.google.gerrit.extensions.api.changes.RecipientType; |
| import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling; |
| import com.google.gerrit.extensions.api.changes.ReviewResult; |
| import com.google.gerrit.extensions.api.changes.ReviewerInfo; |
| import com.google.gerrit.extensions.api.changes.ReviewerInput; |
| import com.google.gerrit.extensions.api.changes.ReviewerResult; |
| import com.google.gerrit.extensions.api.changes.RevisionApi; |
| import com.google.gerrit.extensions.api.groups.GroupApi; |
| import com.google.gerrit.extensions.api.projects.BranchInput; |
| import com.google.gerrit.extensions.api.projects.ConfigInput; |
| import com.google.gerrit.extensions.api.projects.ProjectInput; |
| import com.google.gerrit.extensions.client.ChangeKind; |
| import com.google.gerrit.extensions.client.ChangeStatus; |
| import com.google.gerrit.extensions.client.Comment.Range; |
| import com.google.gerrit.extensions.client.InheritableBoolean; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.client.ReviewerState; |
| import com.google.gerrit.extensions.client.Side; |
| 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.ChangeInput; |
| import com.google.gerrit.extensions.common.ChangeMessageInfo; |
| import com.google.gerrit.extensions.common.CommentInfo; |
| import com.google.gerrit.extensions.common.CommitInfo; |
| import com.google.gerrit.extensions.common.GitPerson; |
| import com.google.gerrit.extensions.common.LabelInfo; |
| import com.google.gerrit.extensions.common.RevisionInfo; |
| import com.google.gerrit.extensions.common.TrackingIdInfo; |
| import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.BinaryResult; |
| import com.google.gerrit.extensions.restapi.MethodNotAllowedException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; |
| import com.google.gerrit.httpd.raw.IndexPreloadingUtil; |
| import com.google.gerrit.index.IndexConfig; |
| import com.google.gerrit.index.query.PostFilterPredicate; |
| import com.google.gerrit.server.ChangeMessagesUtil; |
| import com.google.gerrit.server.StarredChangesUtil; |
| import com.google.gerrit.server.change.ChangeMessages; |
| import com.google.gerrit.server.change.ChangeResource; |
| import com.google.gerrit.server.change.testing.TestChangeETagComputation; |
| import com.google.gerrit.server.events.CommitReceivedEvent; |
| import com.google.gerrit.server.git.ChangeMessageModifier; |
| 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.group.SystemGroupBackend; |
| import com.google.gerrit.server.index.change.ChangeIndex; |
| import com.google.gerrit.server.index.change.ChangeIndexCollection; |
| import com.google.gerrit.server.index.change.IndexedChangeQuery; |
| import com.google.gerrit.server.patch.DiffSummary; |
| import com.google.gerrit.server.patch.DiffSummaryKey; |
| import com.google.gerrit.server.patch.IntraLineDiff; |
| import com.google.gerrit.server.patch.IntraLineDiffKey; |
| import com.google.gerrit.server.project.testing.TestLabels; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory; |
| import com.google.gerrit.server.restapi.change.PostReview; |
| import com.google.gerrit.server.update.BatchUpdate; |
| import com.google.gerrit.server.update.BatchUpdateOp; |
| import com.google.gerrit.server.update.ChangeContext; |
| import com.google.gerrit.server.util.AccountTemplateUtil; |
| import com.google.gerrit.server.util.time.TimeUtil; |
| import com.google.gerrit.testing.FakeEmailSender.Message; |
| import com.google.inject.AbstractModule; |
| import com.google.inject.Inject; |
| import com.google.inject.name.Named; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.text.MessageFormat; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; |
| import org.eclipse.jgit.junit.TestRepository; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.RefUpdate.Result; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.transport.PushResult; |
| import org.junit.Test; |
| |
| @NoHttpd |
| @UseTimezone(timezone = "US/Eastern") |
| @VerifyNoPiiInChangeNotes(true) |
| public class ChangeIT extends AbstractDaemonTest { |
| |
| @Inject private AccountOperations accountOperations; |
| @Inject private ChangeIndexCollection changeIndexCollection; |
| @Inject private GroupOperations groupOperations; |
| @Inject private IndexConfig indexConfig; |
| @Inject private ProjectOperations projectOperations; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| @Inject private ExtensionRegistry extensionRegistry; |
| @Inject private IndexOperations.Change changeIndexOperations; |
| |
| @Inject |
| @Named("diff_intraline") |
| private Cache<IntraLineDiffKey, IntraLineDiff> intraCache; |
| |
| @Inject |
| @Named("diff_summary") |
| private Cache<DiffSummaryKey, DiffSummary> diffSummaryCache; |
| |
| @Test |
| public void get() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| ChangeInfo c = info(triplet); |
| assertThat(c.id).isEqualTo(triplet); |
| assertThat(c.project).isEqualTo(project.get()); |
| assertThat(c.branch).isEqualTo("master"); |
| assertThat(c.status).isEqualTo(ChangeStatus.NEW); |
| assertThat(c.subject).isEqualTo("test commit"); |
| assertThat(c.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY); |
| assertThat(c.mergeable).isNull(); |
| assertThat(c.changeId).isEqualTo(r.getChangeId()); |
| assertThat(c.created).isEqualTo(c.updated); |
| assertThat(c._number).isEqualTo(r.getChange().getId().get()); |
| |
| assertThat(c.owner._accountId).isEqualTo(admin.id().get()); |
| assertThat(c.owner.name).isNull(); |
| assertThat(c.owner.email).isNull(); |
| assertThat(c.owner.username).isNull(); |
| assertThat(c.owner.avatars).isNull(); |
| assertThat(c.submissionId).isNull(); |
| } |
| |
| @Test |
| public void diffStatShouldComputeInsertionsAndDeletions() throws Exception { |
| String fileName = "a_new_file.txt"; |
| String fileContent = "First line\nSecond line\n"; |
| PushOneCommit.Result result = createChange("Add a file", fileName, fileContent); |
| String triplet = project.get() + "~master~" + result.getChangeId(); |
| ChangeInfo change = gApi.changes().id(triplet).get(); |
| assertThat(change.insertions).isNotNull(); |
| assertThat(change.deletions).isNotNull(); |
| } |
| |
| @Test |
| public void diffStatShouldSkipInsertionsAndDeletions() throws Exception { |
| String fileName = "a_new_file.txt"; |
| String fileContent = "First line\nSecond line\n"; |
| PushOneCommit.Result result = createChange("Add a file", fileName, fileContent); |
| String triplet = project.get() + "~master~" + result.getChangeId(); |
| ChangeInfo change = |
| gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT)); |
| assertThat(change.insertions).isNull(); |
| assertThat(change.deletions).isNull(); |
| } |
| |
| @Test |
| public void skipDiffstatOptionAvoidsAllDiffComputations() throws Exception { |
| String fileName = "a_new_file.txt"; |
| String fileContent = "First line\nSecond line\n"; |
| PushOneCommit.Result result = createChange("Add a file", fileName, fileContent); |
| String triplet = project.get() + "~master~" + result.getChangeId(); |
| CacheStats startIntra = cloneStats(intraCache.stats()); |
| CacheStats startSummary = cloneStats(diffSummaryCache.stats()); |
| gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT)); |
| |
| assertThat(intraCache.stats()).since(startIntra).hasMissCount(0); |
| assertThat(intraCache.stats()).since(startIntra).hasHitCount(0); |
| assertThat(diffSummaryCache.stats()).since(startSummary).hasMissCount(0); |
| assertThat(diffSummaryCache.stats()).since(startSummary).hasHitCount(0); |
| } |
| |
| @Test |
| @GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER") |
| public void excludeMergeableInChangeInfo() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(c.mergeable).isNull(); |
| } |
| |
| @Test |
| public void getSubmissionId() throws Exception { |
| PushOneCommit.Result changeResult = createChange(); |
| String changeId = changeResult.getChangeId(); |
| |
| merge(changeResult); |
| assertThat(gApi.changes().id(changeId).get().submissionId).isNotNull(); |
| } |
| |
| @Test |
| public void setWorkInProgressNotAllowedWithoutPermission() throws Exception { |
| PushOneCommit.Result rwip = createChange(); |
| String changeId = rwip.getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setWorkInProgress()); |
| assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted"); |
| } |
| |
| @Test |
| public void setWorkInProgressAllowedAsAdmin() throws Exception { |
| requestScopeOperations.setApiUser(user.id()); |
| String changeId = |
| gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(changeId).setWorkInProgress(); |
| assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue(); |
| } |
| |
| @Test |
| public void setWorkInProgressAllowedAsProjectOwner() throws Exception { |
| requestScopeOperations.setApiUser(user.id()); |
| String changeId = |
| gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; |
| |
| com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2(); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| requestScopeOperations.setApiUser(user2.id()); |
| gApi.changes().id(changeId).setWorkInProgress(); |
| assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue(); |
| } |
| |
| @Test |
| public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception { |
| ConfigInput input = new ConfigInput(); |
| input.workInProgressByDefault = InheritableBoolean.TRUE; |
| gApi.projects().name(project.get()).config(input); |
| String changeId = |
| gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; |
| assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue(); |
| } |
| |
| @Test |
| public void setReadyForReviewNotAllowedWithoutPermission() throws Exception { |
| PushOneCommit.Result rready = createChange(); |
| String changeId = rready.getChangeId(); |
| gApi.changes().id(changeId).setWorkInProgress(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setReadyForReview()); |
| assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted"); |
| } |
| |
| @Test |
| public void setReadyForReviewAllowedAsAdmin() throws Exception { |
| requestScopeOperations.setApiUser(user.id()); |
| String changeId = |
| gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; |
| gApi.changes().id(changeId).setWorkInProgress(); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(changeId).setReadyForReview(); |
| assertThat(gApi.changes().id(changeId).get().workInProgress).isNull(); |
| } |
| |
| @Test |
| public void setReadyForReviewAllowedAsProjectOwner() throws Exception { |
| requestScopeOperations.setApiUser(user.id()); |
| String changeId = |
| gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; |
| gApi.changes().id(changeId).setWorkInProgress(); |
| |
| com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2(); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| requestScopeOperations.setApiUser(user2.id()); |
| gApi.changes().id(changeId).setReadyForReview(); |
| assertThat(gApi.changes().id(changeId).get().workInProgress).isNull(); |
| } |
| |
| @Test |
| public void hasReviewStarted() throws Exception { |
| PushOneCommit.Result r = createWorkInProgressChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo info = gApi.changes().id(changeId).get(); |
| assertThat(info.hasReviewStarted).isFalse(); |
| |
| gApi.changes().id(changeId).setReadyForReview(); |
| info = gApi.changes().id(changeId).get(); |
| assertThat(info.hasReviewStarted).isTrue(); |
| } |
| |
| @Test |
| public void pendingReviewers() throws Exception { |
| ConfigInput conf = new ConfigInput(); |
| conf.enableReviewerByEmail = InheritableBoolean.TRUE; |
| gApi.projects().name(project.get()).config(conf); |
| |
| PushOneCommit.Result r = createWorkInProgressChange(); |
| String changeId = r.getChangeId(); |
| assertThat(gApi.changes().id(changeId).get().pendingReviewers).isEmpty(); |
| |
| // Add some pending reviewers. |
| String email1 = name("user1") + "@example.com"; |
| String email2 = name("user2") + "@example.com"; |
| String email3 = name("user3") + "@example.com"; |
| String email4 = name("user4") + "@example.com"; |
| accountOperations |
| .newAccount() |
| .username(name("user1")) |
| .preferredEmail(email1) |
| .fullname("User1") |
| .create(); |
| accountOperations |
| .newAccount() |
| .username(name("user2")) |
| .preferredEmail(email2) |
| .fullname("User2") |
| .create(); |
| accountOperations |
| .newAccount() |
| .username(name("user3")) |
| .preferredEmail(email3) |
| .fullname("User3") |
| .create(); |
| accountOperations |
| .newAccount() |
| .username(name("user4")) |
| .preferredEmail(email4) |
| .fullname("User4") |
| .create(); |
| ReviewInput in = |
| ReviewInput.noScore() |
| .reviewer(email1) |
| .reviewer(email2) |
| .reviewer(email3, CC, false) |
| .reviewer(email4, CC, false) |
| .reviewer("byemail1@example.com") |
| .reviewer("byemail2@example.com") |
| .reviewer("byemail3@example.com", CC, false) |
| .reviewer("byemail4@example.com", CC, false); |
| ReviewResult result = gApi.changes().id(changeId).current().review(in); |
| assertThat(result.reviewers).isNotEmpty(); |
| ChangeInfo info = gApi.changes().id(changeId).get(); |
| Function<Collection<AccountInfo>, Collection<String>> toEmails = |
| ais -> ais.stream().map(ai -> ai.email).collect(toSet()); |
| assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) |
| .containsExactly(email1, email2, "byemail1@example.com", "byemail2@example.com"); |
| assertThat(toEmails.apply(info.pendingReviewers.get(CC))) |
| .containsExactly(email3, email4, "byemail3@example.com", "byemail4@example.com"); |
| assertThat(info.pendingReviewers.get(REMOVED)).isNull(); |
| |
| // Stage some pending reviewer removals. |
| gApi.changes().id(changeId).reviewer(email1).remove(); |
| gApi.changes().id(changeId).reviewer(email3).remove(); |
| gApi.changes().id(changeId).reviewer("byemail1@example.com").remove(); |
| gApi.changes().id(changeId).reviewer("byemail3@example.com").remove(); |
| info = gApi.changes().id(changeId).get(); |
| assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) |
| .containsExactly(email2, "byemail2@example.com"); |
| assertThat(toEmails.apply(info.pendingReviewers.get(CC))) |
| .containsExactly(email4, "byemail4@example.com"); |
| assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED))) |
| .containsExactly(email1, email3, "byemail1@example.com", "byemail3@example.com"); |
| |
| // "Undo" a removal. |
| in = ReviewInput.noScore().reviewer(email1); |
| gApi.changes().id(changeId).current().review(in); |
| info = gApi.changes().id(changeId).get(); |
| assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) |
| .containsExactly(email1, email2, "byemail2@example.com"); |
| assertThat(toEmails.apply(info.pendingReviewers.get(CC))) |
| .containsExactly(email4, "byemail4@example.com"); |
| assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED))) |
| .containsExactly(email3, "byemail1@example.com", "byemail3@example.com"); |
| |
| // "Commit" by moving out of WIP. |
| gApi.changes().id(changeId).setReadyForReview(); |
| info = gApi.changes().id(changeId).get(); |
| assertThat(info.pendingReviewers).isEmpty(); |
| assertThat(toEmails.apply(info.reviewers.get(REVIEWER))) |
| .containsExactly(email1, email2, "byemail2@example.com"); |
| assertThat(toEmails.apply(info.reviewers.get(CC))) |
| .containsExactly(email4, "byemail4@example.com"); |
| assertThat(info.reviewers.get(REMOVED)).isNull(); |
| } |
| |
| @Test |
| public void toggleWorkInProgressState() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // With message |
| gApi.changes().id(changeId).setWorkInProgress("Needs some refactoring"); |
| |
| ChangeInfo info = gApi.changes().id(changeId).get(); |
| |
| assertThat(info.workInProgress).isTrue(); |
| assertThat(Iterables.getLast(info.messages).message).contains("Needs some refactoring"); |
| assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP); |
| |
| gApi.changes().id(changeId).setReadyForReview("PTAL"); |
| |
| info = gApi.changes().id(changeId).get(); |
| assertThat(info.workInProgress).isNull(); |
| assertThat(Iterables.getLast(info.messages).message).contains("PTAL"); |
| assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY); |
| |
| // No message |
| gApi.changes().id(changeId).setWorkInProgress(); |
| |
| info = gApi.changes().id(changeId).get(); |
| |
| assertThat(info.workInProgress).isTrue(); |
| assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Work In Progress"); |
| assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP); |
| |
| gApi.changes().id(changeId).setReadyForReview(); |
| |
| info = gApi.changes().id(changeId).get(); |
| assertThat(info.workInProgress).isNull(); |
| assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Ready For Review"); |
| assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY); |
| } |
| |
| @Test |
| public void toggleWorkInProgressStateByNonOwnerWithPermission() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| String refactor = "Needs some refactoring"; |
| String ptal = "PTAL"; |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE) |
| .ref("refs/heads/master") |
| .group(REGISTERED_USERS)) |
| .update(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(changeId).setWorkInProgress(refactor); |
| |
| ChangeInfo info = gApi.changes().id(changeId).get(); |
| |
| assertThat(info.workInProgress).isTrue(); |
| assertThat(Iterables.getLast(info.messages).message).contains(refactor); |
| assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP); |
| |
| gApi.changes().id(changeId).setReadyForReview(ptal); |
| |
| info = gApi.changes().id(changeId).get(); |
| assertThat(info.workInProgress).isNull(); |
| assertThat(Iterables.getLast(info.messages).message).contains(ptal); |
| assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY); |
| } |
| |
| @Test |
| public void reviewAndStartReview() throws Exception { |
| PushOneCommit.Result r = createWorkInProgressChange(); |
| r.assertOkStatus(); |
| assertThat(r.getChange().change().isWorkInProgress()).isTrue(); |
| |
| ReviewInput in = ReviewInput.noScore().setWorkInProgress(false); |
| ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in); |
| assertThat(result.ready).isTrue(); |
| |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.workInProgress).isNull(); |
| } |
| |
| @Test |
| public void reviewAndMoveToWorkInProgress() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(r.getChange().change().isWorkInProgress()).isFalse(); |
| |
| ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); |
| ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in); |
| assertThat(result.ready).isNull(); |
| |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.workInProgress).isTrue(); |
| } |
| |
| @Test |
| public void reviewAndSetWorkInProgressAndAddReviewerAndVote() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(r.getChange().change().isWorkInProgress()).isFalse(); |
| |
| ReviewInput in = |
| ReviewInput.approve() |
| .reviewer(user.email()) |
| .label(LabelId.CODE_REVIEW, 1) |
| .setWorkInProgress(true); |
| gApi.changes().id(r.getChangeId()).current().review(in); |
| |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.workInProgress).isTrue(); |
| assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList())) |
| .containsExactly(admin.id().get(), user.id().get()); |
| assertThat(info.labels.get(LabelId.CODE_REVIEW).recommended._accountId) |
| .isEqualTo(admin.id().get()); |
| } |
| |
| @Test |
| public void reviewWithWorkInProgressAndReadyReturnsError() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput in = ReviewInput.noScore(); |
| in.ready = true; |
| in.workInProgress = true; |
| ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in); |
| assertThat(result.error).isEqualTo(PostReview.ERROR_WIP_READY_MUTUALLY_EXCLUSIVE); |
| } |
| |
| @Test |
| @TestProjectInput(cloneAs = "user1") |
| public void reviewWithWorkInProgressChangeOwner() throws Exception { |
| PushOneCommit push = pushFactory.create(user.newIdent(), testRepo); |
| PushOneCommit.Result r = push.to("refs/for/master"); |
| r.assertOkStatus(); |
| assertThat(r.getChange().change().getOwner()).isEqualTo(user.id()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); |
| gApi.changes().id(r.getChangeId()).current().review(in); |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.workInProgress).isTrue(); |
| } |
| |
| @Test |
| @TestProjectInput(cloneAs = "user1") |
| public void reviewWithWithWorkInProgressAdmin() throws Exception { |
| PushOneCommit push = pushFactory.create(user.newIdent(), testRepo); |
| PushOneCommit.Result r = push.to("refs/for/master"); |
| r.assertOkStatus(); |
| assertThat(r.getChange().change().getOwner()).isEqualTo(user.id()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); |
| gApi.changes().id(r.getChangeId()).current().review(in); |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.workInProgress).isTrue(); |
| } |
| |
| @Test |
| public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in)); |
| assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted"); |
| } |
| |
| @Test |
| public void reviewWithWorkInProgressByNonOwnerWithPermission() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE) |
| .ref("refs/heads/master") |
| .group(REGISTERED_USERS)) |
| .update(); |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).current().review(in); |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.workInProgress).isTrue(); |
| } |
| |
| @Test |
| public void reviewWithReadyByNonOwnerReturnsError() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| |
| ReviewInput in = ReviewInput.noScore().setReady(true); |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in)); |
| assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted"); |
| } |
| |
| @Test |
| public void getAmbiguous() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| String changeId = r1.getChangeId(); |
| gApi.changes().id(changeId).get(); |
| |
| BranchInput b = new BranchInput(); |
| b.revision = repo().exactRef("HEAD").getObjectId().name(); |
| gApi.projects().name(project.get()).branch("other").create(b); |
| |
| PushOneCommit push2 = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| PushOneCommit.SUBJECT, |
| PushOneCommit.FILE_NAME, |
| PushOneCommit.FILE_CONTENT, |
| changeId); |
| PushOneCommit.Result r2 = push2.to("refs/for/other"); |
| assertThat(r2.getChangeId()).isEqualTo(changeId); |
| |
| ResourceNotFoundException thrown = |
| assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(changeId).get()); |
| assertThat(thrown).hasMessageThat().contains("Multiple changes found for " + changeId); |
| } |
| |
| @FunctionalInterface |
| private interface Rebase { |
| void call(String id) throws RestApiException; |
| } |
| |
| @Test |
| public void rebaseViaRevisionApi() throws Exception { |
| testRebase(id -> gApi.changes().id(id).current().rebase()); |
| } |
| |
| @Test |
| public void rebaseViaChangeApi() throws Exception { |
| testRebase(id -> gApi.changes().id(id).rebase()); |
| } |
| |
| private void testRebase(Rebase rebase) throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| // Add an approval whose score should be copied on trivial rebase |
| gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend()); |
| |
| String changeId = r2.getChangeId(); |
| // Rebase the second change |
| rebase.call(changeId); |
| |
| // Second change should have 2 patch sets and an approval |
| ChangeInfo c2 = gApi.changes().id(changeId).get(CURRENT_REVISION, DETAILED_LABELS); |
| assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2); |
| |
| // ...and the committer and description should be correct |
| ChangeInfo info = gApi.changes().id(changeId).get(CURRENT_REVISION, CURRENT_COMMIT); |
| GitPerson committer = info.revisions.get(info.currentRevision).commit.committer; |
| assertThat(committer.name).isEqualTo(admin.fullName()); |
| assertThat(committer.email).isEqualTo(admin.email()); |
| String description = info.revisions.get(info.currentRevision).description; |
| assertThat(description).isEqualTo("Rebase"); |
| |
| // ...and the approval was copied |
| LabelInfo cr = c2.labels.get(LabelId.CODE_REVIEW); |
| assertThat(cr).isNotNull(); |
| assertThat(cr.all).hasSize(1); |
| assertThat(cr.all.get(0).value).isEqualTo(1); |
| |
| // Rebasing the second change again should fail |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, () -> gApi.changes().id(changeId).current().rebase()); |
| assertThat(thrown).hasMessageThat().contains("Change is already up to date"); |
| } |
| |
| @Test |
| public void rebaseAsUploaderInAttentionSet() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| TestAccount admin2 = accountCreator.admin2(); |
| requestScopeOperations.setApiUser(admin2.id()); |
| amendChangeWithUploader(r2, project, admin2); |
| gApi.changes() |
| .id(r2.getChangeId()) |
| .addToAttentionSet(new AttentionSetInput(admin2.id().toString(), "manual update")); |
| |
| gApi.changes().id(r2.getChangeId()).rebase(); |
| } |
| |
| @Test |
| public void rebaseOnChangeNumber() throws Exception { |
| String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name(); |
| PushOneCommit.Result r1 = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT); |
| RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision); |
| assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip); |
| |
| Change.Id id1 = r1.getChange().getId(); |
| RebaseInput in = new RebaseInput(); |
| in.base = id1.toString(); |
| gApi.changes().id(r2.getChangeId()).rebase(in); |
| |
| Change.Id id2 = r2.getChange().getId(); |
| ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT); |
| ri2 = ci2.revisions.get(ci2.currentRevision); |
| assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name()); |
| |
| List<RelatedChangeAndCommitInfo> related = |
| gApi.changes().id(id2.get()).revision(ri2._number).related().changes; |
| assertThat(related).hasSize(2); |
| assertThat(related.get(0)._changeNumber).isEqualTo(id2.get()); |
| assertThat(related.get(0)._revisionNumber).isEqualTo(2); |
| assertThat(related.get(1)._changeNumber).isEqualTo(id1.get()); |
| assertThat(related.get(1)._revisionNumber).isEqualTo(1); |
| } |
| |
| @Test |
| public void rebaseOnClosedChange() throws Exception { |
| String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name(); |
| PushOneCommit.Result r1 = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT); |
| RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision); |
| assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip); |
| |
| // Submit first change. |
| Change.Id id1 = r1.getChange().getId(); |
| gApi.changes().id(id1.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(id1.get()).current().submit(); |
| |
| // Rebase second change on first change. |
| RebaseInput in = new RebaseInput(); |
| in.base = id1.toString(); |
| gApi.changes().id(r2.getChangeId()).rebase(in); |
| |
| Change.Id id2 = r2.getChange().getId(); |
| ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT); |
| ri2 = ci2.revisions.get(ci2.currentRevision); |
| assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name()); |
| |
| assertThat(gApi.changes().id(id2.get()).revision(ri2._number).related().changes).isEmpty(); |
| } |
| |
| @Test |
| public void rebaseOnNonExistingChange() throws Exception { |
| String changeId = createChange().getChangeId(); |
| RebaseInput in = new RebaseInput(); |
| in.base = "999999"; |
| UnprocessableEntityException exception = |
| assertThrows( |
| UnprocessableEntityException.class, () -> gApi.changes().id(changeId).rebase(in)); |
| assertThat(exception).hasMessageThat().isEqualTo("Base change not found: " + in.base); |
| } |
| |
| @Test |
| public void rebaseFromRelationChainToClosedChange() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| testRepo.reset("HEAD~1"); |
| |
| createChange(); |
| PushOneCommit.Result r3 = createChange(); |
| |
| // Submit first change. |
| Change.Id id1 = r1.getChange().getId(); |
| gApi.changes().id(id1.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(id1.get()).current().submit(); |
| |
| // Rebase third change on first change. |
| RebaseInput in = new RebaseInput(); |
| in.base = id1.toString(); |
| gApi.changes().id(r3.getChangeId()).rebase(in); |
| |
| Change.Id id3 = r3.getChange().getId(); |
| ChangeInfo ci3 = get(r3.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT); |
| RevisionInfo ri3 = ci3.revisions.get(ci3.currentRevision); |
| assertThat(ri3.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name()); |
| |
| assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty(); |
| } |
| |
| @Test |
| public void rebaseNotAllowedWithoutPermission() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| // Rebase the second |
| String changeId = r2.getChangeId(); |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase()); |
| assertThat(thrown).hasMessageThat().contains("rebase not permitted"); |
| } |
| |
| @Test |
| public void rebaseAllowedWithPermission() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS)) |
| .update(); |
| |
| // Rebase the second |
| String changeId = r2.getChangeId(); |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(changeId).rebase(); |
| } |
| |
| @Test |
| public void rebaseNotAllowedWithoutPushPermission() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS)) |
| .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // Rebase the second |
| String changeId = r2.getChangeId(); |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase()); |
| assertThat(thrown).hasMessageThat().contains("rebase not permitted"); |
| } |
| |
| @Test |
| public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // Rebase the second |
| String changeId = r2.getChangeId(); |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase()); |
| assertThat(thrown).hasMessageThat().contains("rebase not permitted"); |
| } |
| |
| @Test |
| public void rebaseWithValidationOptions() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.validationOptions = ImmutableMap.of("key", "value"); |
| |
| TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(testCommitValidationListener)) { |
| // Rebase the second change |
| gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput); |
| assertThat(testCommitValidationListener.receiveEvent.pushOptions) |
| .containsExactly("key", "value"); |
| } |
| } |
| |
| @Test |
| public void deleteNewChangeAsAdmin() throws Exception { |
| deleteChangeAsUser(admin, admin); |
| } |
| |
| @Test |
| @TestProjectInput(cloneAs = "user1") |
| public void deleteNewChangeAsNormalUser() throws Exception { |
| PushOneCommit.Result changeResult = |
| pushFactory.create(user.newIdent(), testRepo).to("refs/for/master"); |
| String changeId = changeResult.getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete()); |
| assertThat(thrown).hasMessageThat().contains("delete not permitted"); |
| } |
| |
| @Test |
| public void deleteNewChangeAsUserWithDeleteChangesPermissionForGroup() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| deleteChangeAsUser(admin, user); |
| } |
| |
| @Test |
| public void deleteNewChangeAsUserWithDeleteChangesPermissionForProjectOwners() throws Exception { |
| GroupApi groupApi = gApi.groups().create(name("delete-change")); |
| groupApi.addMembers("user1"); |
| |
| Project.NameKey nameKey = Project.nameKey(name("delete-change")); |
| ProjectInput in = new ProjectInput(); |
| in.name = nameKey.get(); |
| in.owners = Lists.newArrayListWithCapacity(1); |
| in.owners.add(groupApi.name()); |
| in.createEmptyCommit = true; |
| gApi.projects().create(in); |
| |
| projectOperations |
| .project(nameKey) |
| .forUpdate() |
| .add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(PROJECT_OWNERS)) |
| .update(); |
| |
| deleteChangeAsUser(nameKey, admin, user); |
| } |
| |
| @Test |
| public void deleteChangeAsUserWithDeleteOwnChangesPermissionForGroup() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| deleteChangeAsUser(user, user); |
| } |
| |
| @Test |
| public void deleteChangeAsUserWithDeleteOwnChangesPermissionForOwners() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(CHANGE_OWNER)) |
| .update(); |
| deleteChangeAsUser(user, user); |
| } |
| |
| private void deleteChangeAsUser( |
| com.google.gerrit.acceptance.TestAccount owner, |
| com.google.gerrit.acceptance.TestAccount deleteAs) |
| throws Exception { |
| deleteChangeAsUser(project, owner, deleteAs); |
| } |
| |
| private void deleteChangeAsUser( |
| Project.NameKey projectName, |
| com.google.gerrit.acceptance.TestAccount owner, |
| com.google.gerrit.acceptance.TestAccount deleteAs) |
| throws Exception { |
| try { |
| projectOperations |
| .project(projectName) |
| .forUpdate() |
| .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(ANONYMOUS_USERS)) |
| .update(); |
| requestScopeOperations.setApiUser(owner.id()); |
| ChangeInput in = new ChangeInput(); |
| in.project = projectName.get(); |
| in.branch = "refs/heads/master"; |
| in.subject = "test"; |
| ChangeInfo changeInfo = gApi.changes().create(in).get(); |
| String changeId = changeInfo.changeId; |
| int id = changeInfo._number; |
| String commit = changeInfo.currentRevision; |
| |
| assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id().get()); |
| |
| requestScopeOperations.setApiUser(deleteAs.id()); |
| gApi.changes().id(changeId).delete(); |
| |
| assertThat(query(changeId)).isEmpty(); |
| |
| String ref = Change.id(id).toRefPrefix() + "1"; |
| eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null); |
| eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email()); |
| } finally { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*")) |
| .remove(permissionKey(Permission.DELETE_CHANGES).ref("refs/*")) |
| .update(); |
| } |
| } |
| |
| @Test |
| public void deleteNewChangeOfAnotherUserAsAdmin() throws Exception { |
| deleteChangeAsUser(user, admin); |
| } |
| |
| @Test |
| public void deleteNewChangeOfAnotherUserWithDeleteOwnChangesPermission() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| try { |
| PushOneCommit.Result changeResult = createChange(); |
| String changeId = changeResult.getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete()); |
| assertThat(thrown).hasMessageThat().contains("delete not permitted"); |
| } finally { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*")) |
| .update(); |
| } |
| } |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false) |
| public void deleteNewChangeForBranchWithoutCommits() throws Exception { |
| PushOneCommit.Result changeResult = createChange(); |
| String changeId = changeResult.getChangeId(); |
| |
| gApi.changes().id(changeId).delete(); |
| |
| assertThat(query(changeId)).isEmpty(); |
| } |
| |
| @Test |
| @TestProjectInput(cloneAs = "user1") |
| public void deleteAbandonedChangeAsNormalUser() throws Exception { |
| PushOneCommit.Result changeResult = |
| pushFactory.create(user.newIdent(), testRepo).to("refs/for/master"); |
| String changeId = changeResult.getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(changeId).abandon(); |
| |
| AuthException thrown = |
| assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete()); |
| assertThat(thrown).hasMessageThat().contains("delete not permitted"); |
| } |
| |
| @Test |
| @TestProjectInput(cloneAs = "user1") |
| public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception { |
| PushOneCommit.Result changeResult = |
| pushFactory.create(user.newIdent(), testRepo).to("refs/for/master"); |
| String changeId = changeResult.getChangeId(); |
| |
| gApi.changes().id(changeId).abandon(); |
| |
| gApi.changes().id(changeId).delete(); |
| |
| assertThat(query(changeId)).isEmpty(); |
| } |
| |
| @Test |
| public void deleteMergedChange() throws Exception { |
| PushOneCommit.Result changeResult = createChange(); |
| String changeId = changeResult.getChangeId(); |
| |
| merge(changeResult); |
| |
| MethodNotAllowedException thrown = |
| assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete()); |
| assertThat(thrown).hasMessageThat().contains("delete not permitted"); |
| } |
| |
| @Test |
| @TestProjectInput(cloneAs = "user1") |
| public void deleteMergedChangeWithDeleteOwnChangesPermission() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| try { |
| PushOneCommit.Result changeResult = |
| pushFactory.create(user.newIdent(), testRepo).to("refs/for/master"); |
| String changeId = changeResult.getChangeId(); |
| |
| merge(changeResult); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| MethodNotAllowedException thrown = |
| assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete()); |
| assertThat(thrown).hasMessageThat().contains("delete not permitted"); |
| } finally { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*")) |
| .update(); |
| } |
| } |
| |
| @Test |
| public void deleteNewChangeWithMergedPatchSet() throws Exception { |
| PushOneCommit.Result changeResult = createChange(); |
| String changeId = changeResult.getChangeId(); |
| Change.Id id = changeResult.getChange().getId(); |
| |
| merge(changeResult); |
| setChangeStatus(id, Change.Status.NEW); |
| |
| ResourceConflictException thrown = |
| assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).delete()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .contains(String.format("Cannot delete change %s: patch set 1 is already merged", id)); |
| } |
| |
| @Test |
| public void deleteChangeUpdatesIndex() throws Exception { |
| PushOneCommit.Result changeResult = createChange(); |
| String changeId = changeResult.getChangeId(); |
| Change.Id id = changeResult.getChange().getId(); |
| |
| ChangeIndex idx = changeIndexCollection.getSearchIndex(); |
| |
| Optional<ChangeData> result = |
| idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of())); |
| |
| assertThat(result).isPresent(); |
| gApi.changes().id(changeId).delete(); |
| result = idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of())); |
| assertThat(result).isEmpty(); |
| } |
| |
| @Test |
| public void deleteChangeRemovesDraftComment() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| |
| DraftInput dri = new DraftInput(); |
| dri.message = "hello"; |
| dri.path = "a.txt"; |
| dri.line = 1; |
| |
| gApi.changes().id(r.getChangeId()).current().createDraft(dri); |
| Change.Id num = r.getChange().getId(); |
| |
| try (Repository repo = repoManager.openRepository(allUsers)) { |
| assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id()))) |
| .isNotEmpty(); |
| } |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| gApi.changes().id(r.getChangeId()).delete(); |
| try (Repository repo = repoManager.openRepository(allUsers)) { |
| assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id()))) |
| .isEmpty(); |
| } |
| } |
| |
| @Test |
| public void deleteChangeRemovesItsChangeEdit() throws Exception { |
| PushOneCommit.Result result = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| String changeId = result.getChangeId(); |
| gApi.changes().id(changeId).edit().create(); |
| gApi.changes() |
| .id(changeId) |
| .edit() |
| .modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8))); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| try (Repository repo = repoManager.openRepository(project)) { |
| String expected = |
| RefNames.refsUsers(user.id()) + "/edit-" + result.getChange().getId() + "/1"; |
| assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isNotEmpty(); |
| gApi.changes().id(changeId).delete(); |
| assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isEmpty(); |
| } |
| } |
| |
| @Test |
| public void deleteChangeDoesntRemoveOtherChangeEdits() throws Exception { |
| PushOneCommit.Result result = createChange(); |
| PushOneCommit.Result irrelevantChangeResult = createChange(); |
| requestScopeOperations.setApiUser(admin.id()); |
| String changeId = result.getChangeId(); |
| String irrelevantChangeId = irrelevantChangeResult.getChangeId(); |
| |
| gApi.changes().id(irrelevantChangeId).edit().create(); |
| gApi.changes() |
| .id(irrelevantChangeId) |
| .edit() |
| .modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8))); |
| |
| gApi.changes().id(changeId).delete(); |
| |
| assertThat(gApi.changes().id(irrelevantChangeId).edit().get()).isPresent(); |
| } |
| |
| @Test |
| public void rebaseUpToDateChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase()); |
| assertThat(thrown).hasMessageThat().contains("Change is already up to date"); |
| } |
| |
| @Test |
| public void rebaseConflict() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit(); |
| |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| PushOneCommit.SUBJECT, |
| PushOneCommit.FILE_NAME, |
| "other content", |
| "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5"); |
| PushOneCommit.Result r2 = push.to("refs/for/master"); |
| r2.assertOkStatus(); |
| ResourceConflictException exception = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase()); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "The change could not be rebased due to a conflict during merge.\n\n" |
| + "merge conflict(s):\n%s", |
| PushOneCommit.FILE_NAME)); |
| } |
| |
| @Test |
| public void rebaseDoesNotAddWorkInProgress() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // create an unrelated change so that we can rebase |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result unrelated = createChange(); |
| gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(unrelated.getChangeId()).current().submit(); |
| |
| gApi.changes().id(r.getChangeId()).rebase(); |
| |
| // change is still ready for review after rebase |
| assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isNull(); |
| } |
| |
| @Test |
| public void rebaseDoesNotRemoveWorkInProgress() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| |
| // create an unrelated change so that we can rebase |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result unrelated = createChange(); |
| gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(unrelated.getChangeId()).current().submit(); |
| |
| gApi.changes().id(r.getChangeId()).rebase(); |
| |
| // change is still work in progress after rebase |
| assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isTrue(); |
| } |
| |
| @Test |
| public void rebaseConflict_conflictsAllowed() throws Exception { |
| String patchSetSubject = "patch set change"; |
| String patchSetContent = "patch set content"; |
| String baseSubject = "base change"; |
| String baseContent = "base content"; |
| |
| PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit(); |
| |
| testRepo.reset("HEAD~1"); |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), testRepo, patchSetSubject, PushOneCommit.FILE_NAME, patchSetContent); |
| PushOneCommit.Result r2 = push.to("refs/for/master"); |
| r2.assertOkStatus(); |
| |
| String changeId = r2.getChangeId(); |
| RevCommit patchSet = r2.getCommit(); |
| RevCommit base = r1.getCommit(); |
| |
| TestWorkInProgressStateChangedListener wipStateChangedListener = |
| new TestWorkInProgressStateChangedListener(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(wipStateChangedListener)) { |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.allowConflicts = true; |
| ChangeInfo changeInfo = |
| gApi.changes().id(changeId).revision(patchSet.name()).rebaseAsInfo(rebaseInput); |
| assertThat(changeInfo.containsGitConflicts).isTrue(); |
| assertThat(changeInfo.workInProgress).isTrue(); |
| } |
| assertThat(wipStateChangedListener.invoked).isTrue(); |
| assertThat(wipStateChangedListener.wip).isTrue(); |
| |
| // To get the revisions, we must retrieve the change with more change options. |
| ChangeInfo changeInfo = |
| gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); |
| assertThat(changeInfo.revisions).hasSize(2); |
| assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) |
| .isEqualTo(base.name()); |
| |
| // Verify that the file content in the created patch set is correct. |
| // We expect that it has conflict markers to indicate the conflict. |
| BinaryResult bin = |
| gApi.changes().id(changeId).current().file(PushOneCommit.FILE_NAME).content(); |
| ByteArrayOutputStream os = new ByteArrayOutputStream(); |
| bin.writeTo(os); |
| String fileContent = new String(os.toByteArray(), UTF_8); |
| String patchSetSha1 = abbreviateName(patchSet, 6); |
| String baseSha1 = abbreviateName(base, 6); |
| assertThat(fileContent) |
| .isEqualTo( |
| "<<<<<<< PATCH SET (" |
| + patchSetSha1 |
| + " " |
| + patchSetSubject |
| + ")\n" |
| + patchSetContent |
| + "\n" |
| + "=======\n" |
| + baseContent |
| + "\n" |
| + ">>>>>>> BASE (" |
| + baseSha1 |
| + " " |
| + baseSubject |
| + ")\n"); |
| |
| // Verify the message that has been posted on the change. |
| List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages(); |
| assertThat(messages).hasSize(2); |
| assertThat(Iterables.getLast(messages).message) |
| .isEqualTo( |
| "Patch Set 2: Patch Set 1 was rebased\n\n" |
| + "The following files contain Git conflicts:\n" |
| + "* " |
| + PushOneCommit.FILE_NAME |
| + "\n"); |
| } |
| |
| @Test |
| public void rebaseChangeBase() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| PushOneCommit.Result r3 = createChange(); |
| RebaseInput ri = new RebaseInput(); |
| |
| // rebase r3 directly onto master (break dep. towards r2) |
| ri.base = ""; |
| gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).rebase(ri); |
| PatchSet ps3 = r3.getPatchSet(); |
| assertThat(ps3.id().get()).isEqualTo(2); |
| |
| // rebase r2 onto r3 (referenced by ref) |
| ri.base = ps3.id().toRefName(); |
| gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri); |
| PatchSet ps2 = r2.getPatchSet(); |
| assertThat(ps2.id().get()).isEqualTo(2); |
| |
| // rebase r1 onto r2 (referenced by commit) |
| ri.base = ps2.commitId().name(); |
| gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri); |
| PatchSet ps1 = r1.getPatchSet(); |
| assertThat(ps1.id().get()).isEqualTo(2); |
| |
| // rebase r1 onto r3 (referenced by change number) |
| ri.base = String.valueOf(r3.getChange().getId().get()); |
| gApi.changes().id(r1.getChangeId()).revision(ps1.commitId().name()).rebase(ri); |
| assertThat(r1.getPatchSetId().get()).isEqualTo(3); |
| } |
| |
| @Test |
| public void rebaseChangeBaseRecursion() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| |
| RebaseInput ri = new RebaseInput(); |
| ri.base = r2.getCommit().name(); |
| String expectedMessage = |
| "base change " |
| + r2.getChangeId() |
| + " is a descendant of the current change - recursion not allowed"; |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri)); |
| assertThat(thrown).hasMessageThat().contains(expectedMessage); |
| } |
| |
| @Test |
| public void rebaseAbandonedChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); |
| gApi.changes().id(changeId).abandon(); |
| ChangeInfo info = info(changeId); |
| assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); |
| |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(changeId).revision(r.getCommit().name()).rebase()); |
| assertThat(thrown).hasMessageThat().contains("change is abandoned"); |
| } |
| |
| @Test |
| public void rebaseOntoAbandonedChange() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Abandon the first change |
| String changeId = r.getChangeId(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); |
| gApi.changes().id(changeId).abandon(); |
| ChangeInfo info = info(changeId); |
| assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); |
| |
| RebaseInput ri = new RebaseInput(); |
| ri.base = r.getCommit().name(); |
| |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri)); |
| assertThat(thrown).hasMessageThat().contains("base change is abandoned: " + changeId); |
| } |
| |
| @Test |
| public void rebaseOntoSelf() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| String commit = r.getCommit().name(); |
| RebaseInput ri = new RebaseInput(); |
| ri.base = commit; |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(changeId).revision(commit).rebase(ri)); |
| assertThat(thrown).hasMessageThat().contains("cannot rebase change onto itself"); |
| } |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false) |
| public void changeNoParentToOneParent() throws Exception { |
| // create initial commit with no parent and push it as change, so that patch |
| // set 1 has no parent |
| RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create(); |
| String id = GitUtil.getChangeId(testRepo, c).get(); |
| testRepo.reset(c); |
| |
| PushResult pr = pushHead(testRepo, "refs/for/master", false); |
| assertPushOk(pr, "refs/for/master"); |
| |
| ChangeInfo change = gApi.changes().id(id).get(); |
| assertThat(change.revisions.get(change.currentRevision).commit.parents).isEmpty(); |
| |
| // create another initial commit with no parent and push it directly into |
| // the remote repository |
| c = testRepo.amend(c.getId()).message("Initial Empty Commit").create(); |
| testRepo.reset(c); |
| pr = pushHead(testRepo, "refs/heads/master", false); |
| assertPushOk(pr, "refs/heads/master"); |
| |
| // create a successor commit and push it as second patch set to the change, |
| // so that patch set 2 has 1 parent |
| RevCommit c2 = |
| testRepo |
| .commit() |
| .message("Initial commit") |
| .parent(c) |
| .insertChangeId(id.substring(1)) |
| .create(); |
| testRepo.reset(c2); |
| |
| pr = pushHead(testRepo, "refs/for/master", false); |
| assertPushOk(pr, "refs/for/master"); |
| |
| change = gApi.changes().id(id).get(); |
| RevisionInfo rev = change.revisions.get(change.currentRevision); |
| assertThat(rev.commit.parents).hasSize(1); |
| assertThat(rev.commit.parents.get(0).commit).isEqualTo(c.name()); |
| |
| // check that change kind is correctly detected as REWORK |
| assertThat(rev.kind).isEqualTo(ChangeKind.REWORK); |
| } |
| |
| @Test |
| public void pushCommitOfOtherUser() throws Exception { |
| // admin pushes commit of user |
| PushOneCommit push = pushFactory.create(user.newIdent(), testRepo); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); |
| assertThat(change.owner._accountId).isEqualTo(admin.id().get()); |
| CommitInfo commit = change.revisions.get(change.currentRevision).commit; |
| assertThat(commit.author.email).isEqualTo(user.email()); |
| assertThat(commit.committer.email).isEqualTo(user.email()); |
| |
| // check that the author/committer was added as cc |
| Collection<AccountInfo> reviewers = change.reviewers.get(CC); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get()); |
| assertThat(change.reviewers.get(REVIEWER)).isNull(); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message m = messages.get(0); |
| assertThat(m.from().name()).isEqualTo("Administrator (Code Review)"); |
| assertThat(m.rcpt()).containsExactly(user.getNameEmail()); |
| assertThat(m.body()).contains("has uploaded this change for review"); |
| assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); |
| assertMailReplyTo(m, admin.email()); |
| } |
| |
| @Test |
| public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception { |
| // create hidden project that is only visible to administrators |
| Project.NameKey p = projectOperations.newProject().create(); |
| projectOperations |
| .project(p) |
| .forUpdate() |
| .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid())) |
| .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // admin pushes commit of user |
| TestRepository<InMemoryRepository> repo = cloneProject(p, admin); |
| PushOneCommit push = pushFactory.create(user.newIdent(), repo); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); |
| assertThat(change.owner._accountId).isEqualTo(admin.id().get()); |
| CommitInfo commit = change.revisions.get(change.currentRevision).commit; |
| assertThat(commit.author.email).isEqualTo(user.email()); |
| assertThat(commit.committer.email).isEqualTo(user.email()); |
| |
| // check the user cannot see the change |
| requestScopeOperations.setApiUser(user.id()); |
| assertThrows( |
| ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get()); |
| |
| // check that the author/committer was NOT added as reviewer (he can't see |
| // the change) |
| assertThat(change.reviewers.get(REVIEWER)).isNull(); |
| assertThat(change.reviewers.get(CC)).isNull(); |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void pushCommitWithFooterOfOtherUser() throws Exception { |
| // admin pushes commit that references 'user' in a footer |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| PushOneCommit.SUBJECT |
| + "\n\n" |
| + FooterConstants.REVIEWED_BY.getName() |
| + ": " |
| + user.newIdent().toExternalString(), |
| PushOneCommit.FILE_NAME, |
| PushOneCommit.FILE_CONTENT); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| // check that 'user' was added as reviewer |
| ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); |
| Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get()); |
| assertThat(change.reviewers.get(CC)).isNull(); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message m = messages.get(0); |
| assertThat(m.rcpt()).containsExactly(user.getNameEmail()); |
| assertThat(m.body()).contains("Hello " + user.fullName() + ",\n"); |
| assertThat(m.body()).contains("I'd like you to do a code review."); |
| assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); |
| assertMailReplyTo(m, admin.email()); |
| } |
| |
| @Test |
| public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception { |
| // create hidden project that is only visible to administrators |
| Project.NameKey p = projectOperations.newProject().create(); |
| projectOperations |
| .project(p) |
| .forUpdate() |
| .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid())) |
| .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // admin pushes commit that references 'user' in a footer |
| TestRepository<InMemoryRepository> repo = cloneProject(p, admin); |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), |
| repo, |
| PushOneCommit.SUBJECT |
| + "\n\n" |
| + FooterConstants.REVIEWED_BY.getName() |
| + ": " |
| + user.newIdent().toExternalString(), |
| PushOneCommit.FILE_NAME, |
| PushOneCommit.FILE_CONTENT); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| // check that 'user' cannot see the change |
| requestScopeOperations.setApiUser(user.id()); |
| assertThrows( |
| ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get()); |
| |
| // check that 'user' was NOT added as cc ('user' can't see the change) |
| requestScopeOperations.setApiUser(admin.id()); |
| ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); |
| assertThat(change.reviewers.get(REVIEWER)).isNull(); |
| assertThat(change.reviewers.get(CC)).isNull(); |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void addReviewerThatCannotSeeChange() throws Exception { |
| // create hidden project that is only visible to administrators |
| Project.NameKey p = projectOperations.newProject().create(); |
| projectOperations |
| .project(p) |
| .forUpdate() |
| .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid())) |
| .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // create change |
| TestRepository<InMemoryRepository> repo = cloneProject(p, admin); |
| PushOneCommit push = pushFactory.create(admin.newIdent(), repo); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| // check the user cannot see the change |
| requestScopeOperations.setApiUser(user.id()); |
| assertThrows( |
| ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get()); |
| |
| // try to add user as reviewer |
| requestScopeOperations.setApiUser(admin.id()); |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); |
| |
| assertThat(r.input).isEqualTo(user.email()); |
| assertThat(r.error).contains("does not have permission to see this change"); |
| assertThat(r.reviewers).isNull(); |
| } |
| |
| @Test |
| public void addReviewerThatIsInactiveByUsername() throws Exception { |
| PushOneCommit.Result result = createChange(); |
| |
| String username = name("new-user"); |
| Account.Id id = accountOperations.newAccount().username(username).inactive().create(); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = username; |
| ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); |
| |
| assertThat(r.input).isEqualTo(in.reviewer); |
| assertThat(r.error).isNull(); |
| assertThat(r.reviewers).hasSize(1); |
| ReviewerInfo reviewer = r.reviewers.get(0); |
| assertThat(reviewer._accountId).isEqualTo(id.get()); |
| assertThat(reviewer.username).isEqualTo(username); |
| } |
| |
| @Test |
| public void addReviewerThatIsInactiveById() throws Exception { |
| PushOneCommit.Result result = createChange(); |
| |
| String username = name("new-user"); |
| Account.Id id = accountOperations.newAccount().username(username).inactive().create(); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = Integer.toString(id.get()); |
| ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); |
| |
| assertThat(r.input).isEqualTo(in.reviewer); |
| assertThat(r.error).isNull(); |
| assertThat(r.reviewers).hasSize(1); |
| ReviewerInfo reviewer = r.reviewers.get(0); |
| assertThat(reviewer._accountId).isEqualTo(id.get()); |
| assertThat(reviewer.username).isEqualTo(username); |
| } |
| |
| @Test |
| public void addReviewerThatIsInactiveByEmail() throws Exception { |
| ConfigInput conf = new ConfigInput(); |
| conf.enableReviewerByEmail = InheritableBoolean.TRUE; |
| gApi.projects().name(project.get()).config(conf); |
| PushOneCommit.Result result = createChange(); |
| String username = "user@domain.com"; |
| Account.Id id = accountOperations.newAccount().username(username).inactive().create(); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = username; |
| in.state = ReviewerState.CC; |
| ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); |
| |
| assertThat(r.input).isEqualTo(username); |
| assertThat(r.error).isNull(); |
| assertThat(r.ccs).hasSize(1); |
| AccountInfo reviewer = r.ccs.get(0); |
| assertThat(reviewer._accountId).isEqualTo(id.get()); |
| assertThat(reviewer.username).isEqualTo(username); |
| } |
| |
| @Test |
| @UseClockStep |
| public void addReviewer() throws Exception { |
| testAddReviewerViaPostReview( |
| (changeId, reviewer) -> { |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = reviewer; |
| gApi.changes().id(changeId).addReviewer(in); |
| }); |
| } |
| |
| @Test |
| @UseClockStep |
| public void addReviewerViaPostReview() throws Exception { |
| testAddReviewerViaPostReview( |
| (changeId, reviewer) -> { |
| ReviewerInput reviewerInput = new ReviewerInput(); |
| reviewerInput.reviewer = reviewer; |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.reviewers = ImmutableList.of(reviewerInput); |
| gApi.changes().id(changeId).current().review(reviewInput); |
| }); |
| } |
| |
| private void testAddReviewerViaPostReview(AddReviewerCaller addReviewer) throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeResource rsrc = parseResource(r); |
| String oldETag = rsrc.getETag(); |
| Instant oldTs = rsrc.getChange().getLastUpdatedOn(); |
| |
| addReviewer.call(r.getChangeId(), user.email()); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message m = messages.get(0); |
| assertThat(m.rcpt()).containsExactly(user.getNameEmail()); |
| assertThat(m.body()).contains("Hello " + user.fullName() + ",\n"); |
| assertThat(m.body()).contains("I'd like you to do a code review."); |
| assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); |
| assertMailReplyTo(m, admin.email()); |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| |
| // Adding a reviewer records that user as reviewer. |
| Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get()); |
| |
| // Nobody was added as CC. |
| assertThat(c.reviewers.get(CC)).isNull(); |
| |
| // Ensure ETag and lastUpdatedOn are updated. |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); |
| |
| // Change status of reviewer and ensure ETag is updated. |
| oldETag = rsrc.getETag(); |
| accountOperations.account(user.id()).forUpdate().status("new status").update(); |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| } |
| |
| @Test |
| public void postingMessageOnOwnChangeDoesntAddCallerAsReviewer() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.message = "Foo Bar"; |
| gApi.changes().id(r.getChangeId()).current().review(reviewInput); |
| |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(c.reviewers.get(REVIEWER)).isNull(); |
| assertThat(c.reviewers.get(CC)).isNull(); |
| } |
| |
| @Test |
| public void listReviewers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| assertThat(gApi.changes().id(r.getChangeId()).reviewers()).hasSize(1); |
| |
| String username1 = name("user1"); |
| String email1 = username1 + "@example.com"; |
| accountOperations |
| .newAccount() |
| .username(username1) |
| .preferredEmail(email1) |
| .fullname("User1") |
| .create(); |
| in.reviewer = email1; |
| in.state = ReviewerState.CC; |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username)) |
| .containsExactly(user.username(), username1); |
| } |
| |
| @Test |
| public void notificationsForAddedWorkInProgressReviewers() throws Exception { |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| ReviewInput batchIn = new ReviewInput(); |
| batchIn.reviewers = ImmutableList.of(in); |
| |
| // Added reviewers not notified by default. |
| PushOneCommit.Result r = createWorkInProgressChange(); |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| assertThat(sender.getMessages()).isEmpty(); |
| |
| // Default notification handling can be overridden. |
| r = createWorkInProgressChange(); |
| in.notify = NotifyHandling.OWNER_REVIEWERS; |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| assertThat(sender.getMessages()).hasSize(1); |
| sender.clear(); |
| |
| // Reviewers added via PostReview also not notified by default. |
| // In this case, the child ReviewerInput has a notify=OWNER_REVIEWERS |
| // that should be ignored. |
| r = createWorkInProgressChange(); |
| gApi.changes().id(r.getChangeId()).current().review(batchIn); |
| assertThat(sender.getMessages()).isEmpty(); |
| |
| // Top-level notify property can force notifications when adding reviewer |
| // via PostReview. |
| r = createWorkInProgressChange(); |
| batchIn.notify = NotifyHandling.OWNER_REVIEWERS; |
| gApi.changes().id(r.getChangeId()).current().review(batchIn); |
| assertThat(sender.getMessages()).hasSize(1); |
| } |
| |
| @Test |
| @UseClockStep |
| public void addReviewerThatIsNotPerfectMatch() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeResource rsrc = parseResource(r); |
| String oldETag = rsrc.getETag(); |
| Instant oldTs = rsrc.getChange().getLastUpdatedOn(); |
| |
| // create a group named "ab" with one user: testUser |
| String email = "abcd@example.com"; |
| String fullname = "abcd"; |
| Account.Id accountIdOfTestUser = |
| accountOperations |
| .newAccount() |
| .username("abcd") |
| .preferredEmail(email) |
| .fullname(fullname) |
| .create(); |
| String testGroup = groupOperations.newGroup().name("ab").create().get(); |
| GroupApi groupApi = gApi.groups().id(testGroup); |
| groupApi.description("test group"); |
| groupApi.addMembers(user.fullName()); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = "abc"; |
| gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message m = messages.get(0); |
| assertThat(m.rcpt()).containsExactly(Address.create(fullname, email)); |
| assertThat(m.body()).contains("Hello " + fullname + ",\n"); |
| assertThat(m.body()).contains("I'd like you to do a code review."); |
| assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); |
| assertMailReplyTo(m, email); |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| |
| // Adding a reviewer records that user as reviewer. |
| Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId).isEqualTo(accountIdOfTestUser.get()); |
| |
| // Ensure ETag and lastUpdatedOn are updated. |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); |
| } |
| |
| @Test |
| @UseClockStep |
| public void addGroupAsReviewersWhenANotPerfectMatchedUserExists() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeResource rsrc = parseResource(r); |
| String oldETag = rsrc.getETag(); |
| Instant oldTs = rsrc.getChange().getLastUpdatedOn(); |
| |
| // create a group named "kobe" with one user: lee |
| String testUserFullname = "kobebryant"; |
| accountOperations |
| .newAccount() |
| .username("kobebryant") |
| .preferredEmail("kobebryant@example.com") |
| .fullname(testUserFullname) |
| .create(); |
| |
| String myGroupUserEmail = "lee@example.com"; |
| String myGroupUserFullname = "lee"; |
| Account.Id accountIdOfGroupUser = |
| accountOperations |
| .newAccount() |
| .username("lee") |
| .preferredEmail(myGroupUserEmail) |
| .fullname(myGroupUserFullname) |
| .create(); |
| |
| String testGroup = groupOperations.newGroup().name("kobe").create().get(); |
| GroupApi groupApi = gApi.groups().id(testGroup); |
| groupApi.description("test group"); |
| groupApi.addMembers(myGroupUserFullname); |
| |
| // ensure that user "user" is not in the group |
| groupApi.removeMembers(testUserFullname); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = testGroup; |
| gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message m = messages.get(0); |
| assertThat(m.rcpt()).containsExactly(Address.create(myGroupUserFullname, myGroupUserEmail)); |
| assertThat(m.body()).contains("Hello " + myGroupUserFullname + ",\n"); |
| assertThat(m.body()).contains("I'd like you to do a code review."); |
| assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); |
| assertMailReplyTo(m, myGroupUserEmail); |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| |
| // Adding a reviewer records that user as reviewer. |
| Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId).isEqualTo(accountIdOfGroupUser.get()); |
| |
| // Ensure ETag and lastUpdatedOn are updated. |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); |
| } |
| |
| @Test |
| public void deleteGroupFromReviewersFails() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // create a group named "kobe" with one user: lee |
| String myGroupUserEmail = "lee@example.com"; |
| String myGroupUserFullname = "lee"; |
| accountOperations |
| .newAccount() |
| .username("lee") |
| .preferredEmail(myGroupUserEmail) |
| .fullname(myGroupUserFullname) |
| .create(); |
| |
| String groupName = "kobe"; |
| String testGroup = groupOperations.newGroup().name(groupName).create().get(); |
| GroupApi groupApi = gApi.groups().id(testGroup); |
| groupApi.description("test group"); |
| groupApi.addMembers(myGroupUserFullname); |
| |
| // add the user as reviewer. |
| gApi.changes().id(r.getChangeId()).addReviewer(myGroupUserFullname); |
| |
| // fail to remove that user via group. |
| ReviewResult reviewResult = |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(ReviewInput.create().reviewer(testGroup, REMOVED, /* confirmed= */ true)); |
| |
| assertThat(reviewResult.error).isEqualTo("error adding reviewer"); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = testGroup; |
| in.state = REMOVED; |
| ReviewerResult reviewerResult = gApi.changes().id(r.getChangeId()).addReviewer(in); |
| assertThat(reviewerResult.error) |
| .isEqualTo(MessageFormat.format(ChangeMessages.get().groupRemovalIsNotAllowed, groupName)); |
| } |
| |
| @Test |
| @UseClockStep |
| public void addSelfAsReviewer() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeResource rsrc = parseResource(r); |
| String oldETag = rsrc.getETag(); |
| Instant oldTs = rsrc.getChange().getLastUpdatedOn(); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| |
| // There should be no email notification when adding self |
| assertThat(sender.getMessages()).isEmpty(); |
| |
| // Adding a reviewer records that user as reviewer. |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get()); |
| |
| // Ensure ETag and lastUpdatedOn are updated. |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); |
| } |
| |
| @Test |
| public void implicitlyCcOnNonVotingReviewPgStyle() throws Exception { |
| testImplicitlyCcOnNonVotingReviewPgStyle(user); |
| } |
| |
| @Test |
| public void implicitlyCcOnNonVotingReviewForUserWithoutUserNamePgStyle() throws Exception { |
| com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create(); |
| assertThat(accountWithoutUsername.username()).isNull(); |
| testImplicitlyCcOnNonVotingReviewPgStyle(accountWithoutUsername); |
| } |
| |
| private void testImplicitlyCcOnNonVotingReviewPgStyle( |
| com.google.gerrit.acceptance.TestAccount testAccount) throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(testAccount.id()); |
| assertThat(getReviewerState(r.getChangeId(), testAccount.id())).isEmpty(); |
| |
| // Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8. |
| ReviewInput in = new ReviewInput(); |
| in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS; |
| in.labels = ImmutableMap.of(); |
| in.message = "comment"; |
| in.reviewers = ImmutableList.of(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in); |
| |
| assertThat(getReviewerState(r.getChangeId(), testAccount.id())).hasValue(CC); |
| } |
| |
| @Test |
| public void implicitlyAddReviewerOnVotingReview() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.recommend().message("LGTM")); |
| |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(c.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList())) |
| .containsExactly(user.id().get()); |
| |
| // Further test: remove the vote, then comment again. The user should be |
| // implicitly re-added to the ReviewerSet, as a CC. |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).remove(); |
| c = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(c.reviewers.values()).isEmpty(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(new ReviewInput().message("hi")); |
| c = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(c.reviewers.get(CC).stream().map(ai -> ai._accountId).collect(toList())) |
| .containsExactly(user.id().get()); |
| } |
| |
| @Test |
| public void addReviewerToClosedChange() 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 c = gApi.changes().id(r.getChangeId()).get(); |
| Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId).isEqualTo(admin.id().get()); |
| assertThat(c.reviewers).doesNotContainKey(CC); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| |
| c = gApi.changes().id(r.getChangeId()).get(); |
| reviewers = c.reviewers.get(REVIEWER); |
| assertThat(reviewers).hasSize(2); |
| Iterator<AccountInfo> reviewerIt = reviewers.iterator(); |
| assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get()); |
| assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get()); |
| assertThat(c.reviewers).doesNotContainKey(CC); |
| } |
| |
| @Test |
| public void eTagChangesWhenOwnerUpdatesAccountStatus() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeResource rsrc = parseResource(r); |
| String oldETag = rsrc.getETag(); |
| |
| accountOperations.account(admin.id()).forUpdate().status("new status").update(); |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| } |
| |
| @Test |
| public void pluginCanContributeToETagComputation() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String oldETag = parseResource(r).getETag(); |
| |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag("foo"))) { |
| assertThat(parseResource(r).getETag()).isNotEqualTo(oldETag); |
| } |
| |
| assertThat(parseResource(r).getETag()).isEqualTo(oldETag); |
| } |
| |
| @Test |
| public void returningNullFromETagComputationDoesNotBreakGerrit() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String oldETag = parseResource(r).getETag(); |
| |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag(null))) { |
| assertThat(parseResource(r).getETag()).isEqualTo(oldETag); |
| } |
| } |
| |
| @Test |
| public void throwingExceptionFromETagComputationDoesNotBreakGerrit() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String oldETag = parseResource(r).getETag(); |
| |
| try (Registration registration = |
| extensionRegistry |
| .newRegistration() |
| .add( |
| TestChangeETagComputation.withException( |
| new StorageException("exception during test")))) { |
| assertThat(parseResource(r).getETag()).isEqualTo(oldETag); |
| } |
| } |
| |
| @Test |
| public void emailNotificationForFileLevelComment() throws Exception { |
| String changeId = createChange().getChangeId(); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| gApi.changes().id(changeId).addReviewer(in); |
| sender.clear(); |
| |
| ReviewInput review = new ReviewInput(); |
| ReviewInput.CommentInput comment = new ReviewInput.CommentInput(); |
| comment.path = PushOneCommit.FILE_NAME; |
| comment.side = Side.REVISION; |
| comment.message = "comment 1"; |
| review.comments = new HashMap<>(); |
| review.comments.put(comment.path, Lists.newArrayList(comment)); |
| gApi.changes().id(changeId).current().review(review); |
| |
| assertThat(sender.getMessages()).hasSize(1); |
| Message m = sender.getMessages().get(0); |
| assertThat(m.rcpt()).containsExactly(user.getNameEmail()); |
| } |
| |
| @Test |
| public void invalidRange() throws Exception { |
| String changeId = createChange().getChangeId(); |
| |
| ReviewInput review = new ReviewInput(); |
| ReviewInput.CommentInput comment = new ReviewInput.CommentInput(); |
| |
| comment.range = new Range(); |
| comment.range.startLine = 1; |
| comment.range.endLine = 1; |
| comment.range.startCharacter = -1; |
| comment.range.endCharacter = 0; |
| |
| comment.path = PushOneCommit.FILE_NAME; |
| comment.side = Side.REVISION; |
| comment.message = "comment 1"; |
| review.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment)); |
| |
| assertThrows( |
| BadRequestException.class, () -> gApi.changes().id(changeId).current().review(review)); |
| } |
| |
| @Test |
| public void listVotes() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| Map<String, Short> m = |
| gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes(); |
| |
| assertThat(m).hasSize(1); |
| assertThat(m).containsExactly(LabelId.CODE_REVIEW, Short.valueOf((short) 2)); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike()); |
| |
| m = gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes(); |
| |
| assertThat(m).hasSize(1); |
| assertThat(m).containsExactly(LabelId.CODE_REVIEW, Short.valueOf((short) -1)); |
| } |
| |
| @Test |
| @GerritConfig(name = "accounts.visibility", value = "NONE") |
| public void listVotesEvenWhenAccountsAreNotVisible() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| |
| // check finding by address works |
| Map<String, Short> m = gApi.changes().id(r.getChangeId()).reviewer(admin.email()).votes(); |
| assertThat(m).hasSize(1); |
| assertThat(m).containsEntry(LabelId.CODE_REVIEW, Short.valueOf((short) 2)); |
| |
| // check finding by id works |
| m = gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes(); |
| assertThat(m).hasSize(1); |
| assertThat(m).containsEntry(LabelId.CODE_REVIEW, Short.valueOf((short) 2)); |
| } |
| |
| @Test |
| public void removeReviewerNoVotes() throws Exception { |
| LabelType verified = |
| label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(verified); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(verified.getName()) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| gApi.changes().id(changeId).addReviewer(user.id().toString()); |
| |
| ChangeInfo c = gApi.changes().id(changeId).get(ListChangesOption.DETAILED_LABELS); |
| assertThat(getReviewers(c.reviewers.get(CC))).isEmpty(); |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id()); |
| |
| sender.clear(); |
| gApi.changes().id(changeId).reviewer(user.id().toString()).remove(); |
| assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty(); |
| |
| assertThat(sender.getMessages()).hasSize(1); |
| Message message = sender.getMessages().get(0); |
| assertThat(message.body()).contains("Removed reviewer " + user.getNameEmail() + "."); |
| assertThat(message.body()).doesNotContain("with the following votes"); |
| |
| // Make sure the change message for removing a reviewer is correct. |
| assertThat(Iterables.getLast(gApi.changes().id(changeId).messages()).message) |
| .isEqualTo("Removed reviewer " + user.getNameEmail() + "."); |
| ChangeMessageInfo changeMessageInfo = |
| Iterables.getLast(gApi.changes().id(changeId).get().messages); |
| assertThat(changeMessageInfo.message) |
| .isEqualTo("Removed reviewer " + AccountTemplateUtil.getAccountTemplate(user.id()) + "."); |
| assertThat(changeMessageInfo.accountsInMessage).containsExactly(getAccountInfo(user.id())); |
| |
| // Make sure the reviewer can still be added again. |
| gApi.changes().id(changeId).addReviewer(user.id().toString()); |
| c = gApi.changes().id(changeId).get(); |
| assertThat(getReviewers(c.reviewers.get(CC))).isEmpty(); |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id()); |
| |
| // Remove again, and then try to remove once more to verify 404 is |
| // returned. |
| gApi.changes().id(changeId).reviewer(user.id().toString()).remove(); |
| assertThrows( |
| ResourceNotFoundException.class, |
| () -> gApi.changes().id(changeId).reviewer(user.id().toString()).remove()); |
| } |
| |
| @Test |
| public void removeCC() throws Exception { |
| PushOneCommit.Result result = createChange(); |
| String changeId = result.getChangeId(); |
| // Add a cc |
| ReviewerInput reviewerInput = new ReviewerInput(); |
| reviewerInput.state = CC; |
| reviewerInput.reviewer = user.id().toString(); |
| gApi.changes().id(changeId).addReviewer(reviewerInput); |
| |
| // Remove a cc |
| sender.clear(); |
| gApi.changes().id(changeId).reviewer(user.id().toString()).remove(); |
| assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty(); |
| |
| // Make sure the email for removing a cc is correct. |
| assertThat(sender.getMessages()).hasSize(1); |
| Message message = sender.getMessages().get(0); |
| assertThat(message.body()).contains("Removed cc " + user.getNameEmail() + "."); |
| |
| // Make sure the change message for removing a reviewer is correct. |
| assertThat(Iterables.getLast(gApi.changes().id(changeId).messages()).message) |
| .isEqualTo("Removed cc " + user.getNameEmail() + "."); |
| |
| ChangeMessageInfo changeMessageInfo = |
| Iterables.getLast(gApi.changes().id(changeId).get().messages); |
| assertThat(changeMessageInfo.message) |
| .isEqualTo("Removed cc " + AccountTemplateUtil.getAccountTemplate(user.id()) + "."); |
| assertThat(changeMessageInfo.accountsInMessage).containsExactly(getAccountInfo(user.id())); |
| } |
| |
| @Test |
| public void removeReviewer() throws Exception { |
| testRemoveReviewer(true); |
| } |
| |
| @Test |
| public void removeNoNotify() throws Exception { |
| testRemoveReviewer(false); |
| } |
| |
| private void testRemoveReviewer(boolean notify) throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.recommend()); |
| |
| Collection<AccountInfo> reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER); |
| |
| assertThat(reviewers).hasSize(2); |
| Iterator<AccountInfo> reviewerIt = reviewers.iterator(); |
| assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get()); |
| assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get()); |
| |
| sender.clear(); |
| requestScopeOperations.setApiUser(admin.id()); |
| DeleteReviewerInput input = new DeleteReviewerInput(); |
| if (!notify) { |
| input.notify = NotifyHandling.NONE; |
| } |
| gApi.changes().id(changeId).reviewer(user.id().toString()).remove(input); |
| |
| if (notify) { |
| assertThat(sender.getMessages()).hasSize(1); |
| Message message = sender.getMessages().get(0); |
| assertThat(message.body()) |
| .contains("Removed reviewer " + user.getNameEmail() + " with the following votes"); |
| assertThat(message.body()).contains("* Code-Review+1 by " + user.getNameEmail()); |
| ChangeMessageInfo changeMessageInfo = |
| Iterables.getLast(gApi.changes().id(changeId).messages()); |
| assertThat(changeMessageInfo.message) |
| .contains("Removed reviewer " + user.getNameEmail() + " with the following votes"); |
| assertThat(changeMessageInfo.message).contains("* Code-Review+1 by " + user.getNameEmail()); |
| changeMessageInfo = Iterables.getLast(gApi.changes().id(changeId).get().messages); |
| assertThat(changeMessageInfo.message) |
| .contains( |
| "Removed reviewer " |
| + AccountTemplateUtil.getAccountTemplate(user.id()) |
| + " with the following votes"); |
| assertThat(changeMessageInfo.message) |
| .contains("* Code-Review+1 by " + AccountTemplateUtil.getAccountTemplate(user.id())); |
| assertThat(changeMessageInfo.accountsInMessage).containsExactly(getAccountInfo(user.id())); |
| } else { |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER); |
| assertThat(reviewers).hasSize(1); |
| reviewerIt = reviewers.iterator(); |
| assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get()); |
| |
| eventRecorder.assertReviewerDeletedEvents(changeId, user.email()); |
| } |
| |
| @Test |
| public void removeReviewerNotPermitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, |
| () -> gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove()); |
| assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted"); |
| } |
| |
| @Test |
| public void removeReviewerSelfFromMergedChangeNotPermitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(changeId); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| approve(changeId); |
| gApi.changes().id(changeId).revision(r.getCommit().name()).submit(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, |
| () -> gApi.changes().id(r.getChangeId()).reviewer("self").remove()); |
| assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted"); |
| } |
| |
| @Test |
| public void removeReviewerSelfFromAbandonedChangePermitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(changeId); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(changeId).abandon(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).reviewer("self").remove(); |
| eventRecorder.assertReviewerDeletedEvents(changeId, user.email()); |
| } |
| |
| @Test |
| public void removeOtherReviewerFromAbandonedChangeNotPermitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(changeId); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| approve(changeId); |
| gApi.changes().id(changeId).abandon(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, |
| () -> gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove()); |
| assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted"); |
| } |
| |
| @Test |
| public void deleteVote() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| sender.clear(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(user.id().toString()) |
| .deleteVote(LabelId.CODE_REVIEW); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message msg = messages.get(0); |
| assertThat(msg.rcpt()).containsExactly(user.getNameEmail()); |
| assertThat(msg.body()).contains(admin.fullName() + " has removed a vote from this change."); |
| assertThat(msg.body()) |
| .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n"); |
| |
| Map<String, Short> m = |
| gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes(); |
| |
| // Dummy 0 approval on the change to block vote copying to this patch set. |
| assertThat(m).containsExactly(LabelId.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.id().get()); |
| assertThat(message.message) |
| .isEqualTo( |
| "Removed Code-Review+1 by " + AccountTemplateUtil.getAccountTemplate(user.id()) + "\n"); |
| assertThat(message.accountsInMessage).containsExactly(getAccountInfo(user.id())); |
| assertThat(gApi.changes().id(r.getChangeId()).message(message.id).get().message) |
| .isEqualTo("Removed Code-Review+1 by User1 <user1@example.com>\n"); |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id())); |
| } |
| |
| @Test |
| public void deleteVoteNotifyNone() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| sender.clear(); |
| DeleteVoteInput in = new DeleteVoteInput(); |
| in.label = LabelId.CODE_REVIEW; |
| in.notify = NotifyHandling.NONE; |
| gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in); |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void deleteVoteNotifyAccount() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| DeleteVoteInput in = new DeleteVoteInput(); |
| in.label = LabelId.CODE_REVIEW; |
| in.notify = NotifyHandling.NONE; |
| |
| // notify unrelated account as TO |
| String email = "user2@example.com"; |
| accountOperations |
| .newAccount() |
| .username("user2") |
| .preferredEmail(email) |
| .fullname("User2") |
| .create(); |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| sender.clear(); |
| in.notifyDetails = new HashMap<>(); |
| in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email))); |
| gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in); |
| assertNotifyTo(email, "User2"); |
| |
| // notify unrelated account as CC |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| sender.clear(); |
| in.notifyDetails = new HashMap<>(); |
| in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email))); |
| gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in); |
| assertNotifyCc(email, "User2"); |
| |
| // notify unrelated account as BCC |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| sender.clear(); |
| in.notifyDetails = new HashMap<>(); |
| in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email))); |
| gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in); |
| assertNotifyBcc(email, "User2"); |
| } |
| |
| @Test |
| public void deleteVoteNotPermitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, |
| () -> |
| gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(admin.id().toString()) |
| .deleteVote(LabelId.CODE_REVIEW)); |
| assertThat(thrown).hasMessageThat().contains("delete vote not permitted"); |
| } |
| |
| @Test |
| public void nonVotingReviewerStaysAfterSubmit() throws Exception { |
| LabelType verified = |
| label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| String heads = "refs/heads/*"; |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(verified); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel(verified.getName()).ref(heads).group(CHANGE_OWNER).range(-1, 1)) |
| .add(allowLabel(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS).range(-2, +2)) |
| .update(); |
| |
| // Set Code-Review+2 and Verified+1 as admin (change owner) |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| String commit = r.getCommit().name(); |
| ReviewInput input = ReviewInput.approve(); |
| input.label(verified.getName(), 1); |
| gApi.changes().id(changeId).revision(commit).review(input); |
| |
| // Reviewers should only be "admin" |
| ChangeInfo c = gApi.changes().id(changeId).get(); |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.id())); |
| assertThat(c.reviewers.get(CC)).isNull(); |
| |
| // Add the user as reviewer |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| gApi.changes().id(changeId).addReviewer(in); |
| c = gApi.changes().id(changeId).get(); |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id())); |
| |
| // Approve the change as user, then remove the approval |
| // (only to confirm that the user does have Code-Review+2 permission) |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(changeId).revision(commit).review(ReviewInput.approve()); |
| gApi.changes().id(changeId).revision(commit).review(ReviewInput.noScore()); |
| |
| // Submit the change |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(changeId).revision(commit).submit(); |
| |
| // User should still be on the change |
| c = gApi.changes().id(changeId).get(); |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id())); |
| } |
| |
| @Test |
| public void labelPermissionsChange_doesNotAffectCurrentVotes() throws Exception { |
| String heads = "refs/heads/*"; |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS).range(-2, +2)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Approve the change as user |
| requestScopeOperations.setApiUser(user.id()); |
| approve(changeId); |
| assertThat( |
| gApi.changes().id(changeId).get(DETAILED_LABELS).labels.get("Code-Review").all.stream() |
| .collect(toImmutableMap(vote -> Account.id(vote._accountId), vote -> vote.value))) |
| .isEqualTo(ImmutableMap.of(user.id(), 2)); |
| |
| // Remove permissions for CODE_REVIEW. The user still has [-1,+1], inherited from All-Projects. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .remove(labelPermissionKey(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS)) |
| .update(); |
| |
| // No permissions to vote +2 |
| assertThrows(AuthException.class, () -> approve(changeId)); |
| |
| assertThat( |
| get(changeId, DETAILED_LABELS).labels.get(LabelId.CODE_REVIEW).all.stream() |
| .map(vote -> vote.value)) |
| .containsExactly(2); |
| |
| // The change is still submittable |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(changeId).current().submit(); |
| assertThat(info(changeId).status).isEqualTo(MERGED); |
| |
| // The +2 vote out of permissions range is still present. |
| assertThat( |
| get(changeId, DETAILED_LABELS).labels.get(LabelId.CODE_REVIEW).all.stream() |
| .collect(toImmutableMap(vote -> Account.id(vote._accountId), vote -> vote.value))) |
| .isEqualTo(ImmutableMap.of(user.id(), 2, admin.id(), 0)); |
| } |
| |
| @Test |
| public void createEmptyChange() throws Exception { |
| ChangeInput in = new ChangeInput(); |
| in.branch = Constants.MASTER; |
| in.subject = "Create a change from the API"; |
| in.project = project.get(); |
| ChangeInfo info = gApi.changes().create(in).get(); |
| assertThat(info.project).isEqualTo(in.project); |
| assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch)); |
| assertThat(info.subject).isEqualTo(in.subject); |
| assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1."); |
| } |
| |
| @Test |
| public void queryChangesNoQuery() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| List<ChangeInfo> results = gApi.changes().query().get(); |
| assertThat(results.size()).isAtLeast(1); |
| List<Integer> ids = new ArrayList<>(results.size()); |
| for (int i = 0; i < results.size(); i++) { |
| ChangeInfo info = results.get(i); |
| if (i == 0) { |
| assertThat(info._number).isEqualTo(r.getChange().getId().get()); |
| } |
| assertThat(Change.Status.forChangeStatus(info.status).isOpen()).isTrue(); |
| ids.add(info._number); |
| } |
| assertThat(ids).contains(r.getChange().getId().get()); |
| } |
| |
| @Test |
| public void queryChangesNoResults() throws Exception { |
| createChange(); |
| assertThat(query("message:test")).isNotEmpty(); |
| assertThat(query("message:{" + getClass().getName() + "fhqwhgads}")).isEmpty(); |
| } |
| |
| @Test |
| public void queryChanges() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| createChange(); |
| List<ChangeInfo> results = query("project:{" + project.get() + "} " + r1.getChangeId()); |
| assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r1.getChangeId()); |
| } |
| |
| @Test |
| public void queryChangesLimit() throws Exception { |
| createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| List<ChangeInfo> results = gApi.changes().query().withLimit(1).get(); |
| assertThat(results).hasSize(1); |
| assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r2.getChangeId()); |
| } |
| |
| @Test |
| public void queryChangesNoLimitRegisteredUser() throws Exception { |
| projectOperations |
| .allProjectsForUpdate() |
| .add( |
| allowCapability(GlobalCapability.QUERY_LIMIT) |
| .group(SystemGroupBackend.REGISTERED_USERS) |
| .range(0, 2)) |
| .update(); |
| for (int i = 0; i < 3; i++) { |
| createChange(); |
| } |
| List<ChangeInfo> resultsWithDefaultLimit = gApi.changes().query().get(); |
| List<ChangeInfo> resultsWithNoLimit = gApi.changes().query().withNoLimit().get(); |
| assertThat(resultsWithDefaultLimit).hasSize(2); |
| assertThat(resultsWithNoLimit.size()).isAtLeast(3); |
| } |
| |
| @Test |
| public void queryChangesNoLimitIgnoredForAnonymousUser() throws Exception { |
| int limit = 2; |
| projectOperations |
| .allProjectsForUpdate() |
| .add( |
| allowCapability(GlobalCapability.QUERY_LIMIT) |
| .group(SystemGroupBackend.ANONYMOUS_USERS) |
| .range(0, limit)) |
| .update(); |
| for (int i = 0; i < 3; i++) { |
| createChange(); |
| } |
| requestScopeOperations.setApiUserAnonymous(); |
| List<ChangeInfo> resultsWithDefaultLimit = gApi.changes().query().get(); |
| List<ChangeInfo> resultsWithNoLimit = gApi.changes().query().withNoLimit().get(); |
| assertThat(resultsWithDefaultLimit).hasSize(limit); |
| assertThat(resultsWithNoLimit).hasSize(limit); |
| } |
| |
| @Test |
| public void queryChangesStart() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| createChange(); |
| List<ChangeInfo> results = |
| gApi.changes().query("project:{" + project.get() + "}").withStart(1).get(); |
| assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r1.getChangeId()); |
| } |
| |
| @Test |
| public void queryChangesNoOptions() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId())); |
| assertThat(result.labels).isNull(); |
| assertThat(result.messages).isNull(); |
| assertThat(result.revisions).isNull(); |
| assertThat(result.actions).isNull(); |
| } |
| |
| @Test |
| public void queryChangesOptions() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| ChangeInfo result = Iterables.getOnlyElement(gApi.changes().query(r.getChangeId()).get()); |
| assertThat(result.labels).isNull(); |
| assertThat(result.messages).isNull(); |
| assertThat(result.actions).isNull(); |
| assertThat(result.revisions).isNull(); |
| |
| result = |
| Iterables.getOnlyElement( |
| gApi.changes() |
| .query(r.getChangeId()) |
| .withOptions( |
| ALL_REVISIONS, CHANGE_ACTIONS, CURRENT_ACTIONS, DETAILED_LABELS, MESSAGES) |
| .get()); |
| assertThat(Iterables.getOnlyElement(result.labels.keySet())).isEqualTo(LabelId.CODE_REVIEW); |
| assertThat(result.messages).hasSize(1); |
| assertThat(result.actions).isNotEmpty(); |
| |
| RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values()); |
| assertThat(rev._number).isEqualTo(r.getPatchSetId().get()); |
| assertThat(rev.created).isNotNull(); |
| assertThat(rev.uploader._accountId).isEqualTo(admin.id().get()); |
| assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName()); |
| assertThat(rev.actions).isNotEmpty(); |
| } |
| |
| @Test |
| public void queryChangesOwnerWithDifferentUsers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat( |
| Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId) |
| .isEqualTo(r.getChangeId()); |
| requestScopeOperations.setApiUser(user.id()); |
| assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty(); |
| } |
| |
| private static class OperatorModule extends AbstractModule { |
| @Override |
| public void configure() { |
| bind(ChangeOperatorFactory.class) |
| .annotatedWith(Exports.named("mytopic")) |
| .toInstance((cqb, value) -> new MyTopicPredicate(value)); |
| } |
| |
| private static class MyTopicPredicate extends PostFilterPredicate<ChangeData> { |
| MyTopicPredicate(String value) { |
| super("mytopic", value); |
| } |
| |
| @Override |
| public boolean match(ChangeData cd) { |
| return Objects.equals(cd.change().getTopic(), value); |
| } |
| |
| @Override |
| public int getCost() { |
| return 2; |
| } |
| } |
| } |
| |
| @Test |
| public void queryChangesPluginOperator() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String query = "mytopic_myplugin:foo"; |
| String expectedMessage = "Unsupported operator mytopic_myplugin:foo"; |
| assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage); |
| |
| try (AutoCloseable ignored = installPlugin("myplugin", OperatorModule.class)) { |
| assertThat(query(query)).isEmpty(); |
| gApi.changes().id(r.getChangeId()).topic("foo"); |
| assertThat(query(query).stream().map(i -> i.changeId)).containsExactly(r.getChangeId()); |
| } |
| |
| assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage); |
| } |
| |
| @Test |
| public void checkReviewedFlagBeforeAndAfterReview() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| assertThat(get(r.getChangeId(), REVIEWED).reviewed).isNull(); |
| |
| revision(r).review(ReviewInput.recommend()); |
| assertThat(get(r.getChangeId(), REVIEWED).reviewed).isTrue(); |
| } |
| |
| @Test |
| public void topic() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo(""); |
| gApi.changes().id(r.getChangeId()).topic("mytopic"); |
| assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic"); |
| gApi.changes().id(r.getChangeId()).topic(""); |
| assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo(""); |
| } |
| |
| @Test |
| public void editTopicWithoutPermissionNotAllowed() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo(""); |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, () -> gApi.changes().id(r.getChangeId()).topic("mytopic")); |
| assertThat(thrown).hasMessageThat().contains("edit topic name not permitted"); |
| } |
| |
| @Test |
| public void editTopicWithPermissionAllowed() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo(""); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.EDIT_TOPIC_NAME).ref("refs/heads/master").group(REGISTERED_USERS)) |
| .update(); |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).topic("mytopic"); |
| assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic"); |
| } |
| |
| @Test |
| public void submitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String id = r.getChangeId(); |
| |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).info(); |
| assertThat(c.submitted).isNull(); |
| assertThat(c.submitter).isNull(); |
| |
| gApi.changes().id(id).current().review(ReviewInput.approve()); |
| gApi.changes().id(id).current().submit(); |
| |
| c = gApi.changes().id(r.getChangeId()).info(); |
| assertThat(c.submitted).isNotNull(); |
| assertThat(c.submitter).isNotNull(); |
| assertThat(c.submitter._accountId).isEqualTo(atrScope.get().getUser().getAccountId().get()); |
| } |
| |
| @Test |
| public void submitStaleChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| try (AutoCloseable ignored = changeIndexOperations.disableWrites()) { |
| r = amendChange(r.getChangeId()); |
| } |
| |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); |
| |
| gApi.changes().id(r.getChangeId()).current().submit(); |
| assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED); |
| } |
| |
| @Test |
| public void submitNotAllowedWithoutPermission() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| requestScopeOperations.setApiUser(user.id()); |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, |
| () -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit()); |
| assertThat(thrown).hasMessageThat().contains("submit not permitted"); |
| } |
| |
| @Test |
| public void submitAllowedWithPermission() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS)) |
| .update(); |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); |
| assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED); |
| } |
| |
| @Test |
| public void submitToSymref() throws Exception { |
| // Create symref in the origin repository (testRepo references to a local repository) |
| try (Repository repo = repoManager.openRepository(project)) { |
| RefUpdate u = repo.updateRef("refs/heads/master_symref"); |
| assertThat(u.link("refs/heads/master")).isEqualTo(Result.NEW); |
| } |
| |
| PushOneCommit.Result r = createChange("refs/for/master_symref"); |
| String id = r.getChangeId(); |
| |
| gApi.changes().id(id).current().review(ReviewInput.approve()); |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, () -> gApi.changes().id(id).current().submit()); |
| assertThat(thrown).hasMessageThat().contains("the target branch is a symbolic ref"); |
| } |
| |
| @Test |
| public void check() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(gApi.changes().id(r.getChangeId()).get().problems).isNull(); |
| assertThat(gApi.changes().id(r.getChangeId()).get(CHECK).problems).isEmpty(); |
| } |
| |
| @Test |
| public void commitFooters() throws Exception { |
| LabelType verified = |
| label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| LabelType custom1 = |
| label("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative")); |
| LabelType custom2 = |
| label("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative")); |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(verified); |
| u.getConfig().upsertLabelType(custom1); |
| u.getConfig().upsertLabelType(custom2); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel(verified.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1)) |
| .add(allowLabel(custom1.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1)) |
| .add(allowLabel(custom2.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1)) |
| .update(); |
| |
| PushOneCommit.Result r1 = createChange(); |
| r1.assertOkStatus(); |
| PushOneCommit.Result r2 = |
| pushFactory |
| .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId()) |
| .to("refs/for/master"); |
| r2.assertOkStatus(); |
| |
| ReviewInput in = new ReviewInput(); |
| in.label(LabelId.CODE_REVIEW, 1); |
| in.label(LabelId.VERIFIED, 1); |
| in.label("Custom1", -1); |
| in.label("Custom2", 1); |
| gApi.changes().id(r2.getChangeId()).current().review(in); |
| |
| ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS); |
| assertThat(actual.revisions).hasSize(2); |
| |
| // No footers except on latest patch set. |
| assertThat(actual.revisions.get(r1.getCommit().getName()).commitWithFooters).isNull(); |
| |
| List<String> footers = |
| new ArrayList<>( |
| Arrays.asList( |
| actual.revisions.get(r2.getCommit().getName()).commitWithFooters.split("\\n"))); |
| // remove subject + blank line |
| footers.remove(0); |
| footers.remove(0); |
| |
| List<String> expectedFooters = |
| Arrays.asList( |
| "Change-Id: " + r2.getChangeId(), |
| "Reviewed-on: " |
| + canonicalWebUrl.get() |
| + "c/" |
| + project.get() |
| + "/+/" |
| + r2.getChange().getId(), |
| "Reviewed-by: Administrator <admin@example.com>", |
| "Custom2: Administrator <admin@example.com>", |
| "Tested-by: Administrator <admin@example.com>"); |
| |
| assertThat(footers).containsExactlyElementsIn(expectedFooters); |
| } |
| |
| @Test |
| public void customCommitFooters() throws Exception { |
| PushOneCommit.Result change = createChange(); |
| ChangeInfo actual; |
| ChangeMessageModifier link = |
| new ChangeMessageModifier() { |
| @Override |
| public String onSubmit( |
| String newCommitMessage, |
| RevCommit original, |
| RevCommit mergeTip, |
| BranchNameKey destination) { |
| assertThat(original.getName()).isNotEqualTo(mergeTip.getName()); |
| return newCommitMessage + "Custom: " + destination.branch(); |
| } |
| }; |
| try (Registration registration = extensionRegistry.newRegistration().add(link)) { |
| actual = gApi.changes().id(change.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS); |
| } |
| List<String> footers = |
| new ArrayList<>( |
| Arrays.asList( |
| actual.revisions.get(change.getCommit().getName()).commitWithFooters.split("\\n"))); |
| // remove subject + blank line |
| footers.remove(0); |
| footers.remove(0); |
| |
| List<String> expectedFooters = |
| Arrays.asList( |
| "Change-Id: " + change.getChangeId(), |
| "Reviewed-on: " |
| + canonicalWebUrl.get() |
| + "c/" |
| + project.get() |
| + "/+/" |
| + change.getChange().getId(), |
| "Custom: refs/heads/master"); |
| assertThat(footers).containsExactlyElementsIn(expectedFooters); |
| } |
| |
| @Test |
| public void defaultSearchDoesNotTouchDatabase() throws Exception { |
| requestScopeOperations.setApiUser(admin.id()); |
| PushOneCommit.Result r1 = createChange(); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit(); |
| |
| createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| try (AutoCloseable ignored = disableNoteDb()) { |
| assertThat( |
| gApi.changes() |
| .query() |
| .withQuery("project:{" + project.get() + "} (status:open OR status:closed)") |
| .withOptions(IndexPreloadingUtil.DASHBOARD_OPTIONS) |
| .get()) |
| .hasSize(2); |
| } |
| } |
| |
| @Test |
| public void votable() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| gApi.changes().id(triplet).addReviewer(user.username()); |
| ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS); |
| LabelInfo codeReview = c.labels.get(LabelId.CODE_REVIEW); |
| assertThat(codeReview.all).hasSize(1); |
| ApprovalInfo approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id().get()); |
| assertThat(approval.value).isEqualTo(0); |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| blockLabel(LabelId.CODE_REVIEW) |
| .ref("refs/heads/*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| c = gApi.changes().id(triplet).get(DETAILED_LABELS); |
| codeReview = c.labels.get(LabelId.CODE_REVIEW); |
| assertThat(codeReview.all).hasSize(1); |
| approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id().get()); |
| assertThat(approval.value).isNull(); |
| } |
| |
| @Test |
| @GerritConfig(name = "gerrit.editGpgKeys", value = "true") |
| @GerritConfig(name = "receive.enableSignedPush", value = "true") |
| public void pushCertificates() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = amendChange(r1.getChangeId()); |
| |
| ChangeInfo info = gApi.changes().id(r1.getChangeId()).get(ALL_REVISIONS, PUSH_CERTIFICATES); |
| |
| RevisionInfo rev1 = info.revisions.get(r1.getCommit().name()); |
| assertThat(rev1).isNotNull(); |
| assertThat(rev1.pushCertificate).isNotNull(); |
| assertThat(rev1.pushCertificate.certificate).isNull(); |
| assertThat(rev1.pushCertificate.key).isNull(); |
| |
| RevisionInfo rev2 = info.revisions.get(r2.getCommit().name()); |
| assertThat(rev2).isNotNull(); |
| assertThat(rev2.pushCertificate).isNotNull(); |
| assertThat(rev2.pushCertificate.certificate).isNull(); |
| assertThat(rev2.pushCertificate.key).isNull(); |
| } |
| |
| @Test |
| public void anonymousRestApi() throws Exception { |
| requestScopeOperations.setApiUserAnonymous(); |
| PushOneCommit.Result r = createChange(); |
| |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.changeId).isEqualTo(r.getChangeId()); |
| |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| info = gApi.changes().id(triplet).get(); |
| assertThat(info.changeId).isEqualTo(r.getChangeId()); |
| |
| info = gApi.changes().id(info._number).get(); |
| assertThat(info.changeId).isEqualTo(r.getChangeId()); |
| assertThrows( |
| AuthException.class, |
| () -> gApi.changes().id(triplet).current().review(ReviewInput.approve())); |
| } |
| |
| @Test |
| public void commitsOnPatchSetCreation() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| pushFactory |
| .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId()) |
| .to("refs/for/master") |
| .assertOkStatus(); |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| try (Repository repo = repoManager.openRepository(project); |
| RevWalk rw = new RevWalk(repo)) { |
| RevCommit commitPatchSetCreation = |
| rw.parseCommit(repo.exactRef(changeMetaRef(Change.id(c._number))).getObjectId()); |
| |
| assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2"); |
| PersonIdent expectedAuthor = |
| changeNoteUtil.newAccountIdIdent( |
| getAccount(admin.id()).id(), c.updated.toInstant(), serverIdent.get()); |
| assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor); |
| assertThat(commitPatchSetCreation.getCommitterIdent()) |
| .isEqualTo(new PersonIdent(serverIdent.get(), c.updated)); |
| assertThat(commitPatchSetCreation.getParentCount()).isEqualTo(1); |
| |
| RevCommit commitChangeCreation = rw.parseCommit(commitPatchSetCreation.getParent(0)); |
| assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change"); |
| expectedAuthor = |
| changeNoteUtil.newAccountIdIdent( |
| getAccount(admin.id()).id(), c.created.toInstant(), serverIdent.get()); |
| assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor); |
| assertThat(commitChangeCreation.getCommitterIdent()) |
| .isEqualTo(new PersonIdent(serverIdent.get(), c.created)); |
| assertThat(commitChangeCreation.getParentCount()).isEqualTo(0); |
| } |
| } |
| |
| @Test |
| public void createEmptyChangeOnNonExistingBranch() throws Exception { |
| ChangeInput in = new ChangeInput(); |
| in.branch = "foo"; |
| in.subject = "Create a change on new branch from the API"; |
| in.project = project.get(); |
| in.newBranch = true; |
| ChangeInfo info = gApi.changes().create(in).get(); |
| assertThat(info.project).isEqualTo(in.project); |
| assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch)); |
| assertThat(info.subject).isEqualTo(in.subject); |
| assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1."); |
| } |
| |
| @Test |
| public void createEmptyChangeOnExistingBranchWithNewBranch() throws Exception { |
| ChangeInput in = new ChangeInput(); |
| in.branch = Constants.MASTER; |
| in.subject = "Create a change on new branch from the API"; |
| in.project = project.get(); |
| in.newBranch = true; |
| |
| assertThrows(ResourceConflictException.class, () -> gApi.changes().create(in).get()); |
| } |
| |
| @Test |
| public void createNewPatchSetWithoutPermission() throws Exception { |
| // Create new project with clean permissions |
| Project.NameKey p = projectOperations.newProject().create(); |
| |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<InMemoryRepository> adminTestRepo = cloneProject(p, admin); |
| TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user); |
| |
| // Block default permission |
| projectOperations |
| .project(p) |
| .forUpdate() |
| .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r1.getPatchSet().refName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r2 = amendChange(r1.getChangeId(), "refs/for/master", user, userTestRepo); |
| r2.assertErrorStatus("cannot add patch set to " + r1.getChange().getId().get() + "."); |
| } |
| |
| @Test |
| public void createNewSetPatchWithPermission() throws Exception { |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<?> adminTestRepo = cloneProject(project, admin); |
| TestRepository<?> userTestRepo = cloneProject(project, user); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r1.getPatchSet().refName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r2 = amendChange(r1.getChangeId(), "refs/for/master", user, userTestRepo); |
| r2.assertOkStatus(); |
| } |
| |
| @Test |
| public void createNewPatchSetAsOwnerWithoutPermission() throws Exception { |
| // Create new project with clean permissions |
| Project.NameKey p = projectOperations.newProject().create(); |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<?> adminTestRepo = cloneProject(project, admin); |
| |
| // Block default permission |
| projectOperations |
| .project(p) |
| .forUpdate() |
| .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Fetch change |
| GitUtil.fetch(adminTestRepo, r1.getPatchSet().refName() + ":ps"); |
| adminTestRepo.reset("ps"); |
| |
| // Amend change as admin |
| PushOneCommit.Result r2 = |
| amendChange(r1.getChangeId(), "refs/for/master", admin, adminTestRepo); |
| r2.assertOkStatus(); |
| } |
| |
| private void assertLabelDescription(ChangeInfo changeInfo, String labelName, String description) { |
| assertThat(changeInfo.labels.get(labelName).description).isEqualTo(description); |
| } |
| |
| @Test |
| public void checkLabelVotesForUnsubmittedChange() throws Exception { |
| List<ListChangesOption> options = |
| EnumSet.complementOf( |
| EnumSet.of( |
| ListChangesOption.CHECK, |
| ListChangesOption.SKIP_DIFFSTAT, |
| ListChangesOption.DETAILED_LABELS)) |
| .stream() |
| .collect(Collectors.toList()); |
| PushOneCommit.Result r = createChange(); |
| ChangeInfo change = gApi.changes().id(r.getChangeId()).get(options); |
| assertThat(change.status).isEqualTo(ChangeStatus.NEW); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(change.labels.get(LabelId.CODE_REVIEW).all).isNull(); |
| |
| voteLabel(r.getChangeId(), LabelId.CODE_REVIEW, +1); |
| change = gApi.changes().id(r.getChangeId()).get(options); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| List<ApprovalInfo> codeReviewApprovals = change.labels.get(LabelId.CODE_REVIEW).all; |
| assertThat(codeReviewApprovals).hasSize(1); |
| ApprovalInfo codeReviewApproval = codeReviewApprovals.get(0); |
| // permittedVotingRange is not served if DETAILED_LABELS is not requested. |
| assertThat(codeReviewApproval.permittedVotingRange).isNull(); |
| assertThat(codeReviewApproval.value).isEqualTo(1); |
| assertThat(codeReviewApproval.username).isEqualTo(admin.username()); |
| |
| // Add another +1 vote as user |
| requestScopeOperations.setApiUser(user.id()); |
| voteLabel(r.getChangeId(), LabelId.CODE_REVIEW, +1); |
| change = gApi.changes().id(r.getChangeId()).get(options); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(change.labels.get(LabelId.CODE_REVIEW).all).hasSize(2); |
| // All available label votes and their meanings are also served if DETAILED_LABELS is not |
| // requested. |
| assertThat(change.labels.get(LabelId.CODE_REVIEW).values).isNotNull(); |
| codeReviewApprovals = change.labels.get(LabelId.CODE_REVIEW).all; |
| assertThat(codeReviewApprovals.stream().map(a -> a.permittedVotingRange).collect(toList())) |
| .containsExactly(null, null); |
| assertThat(codeReviewApprovals.stream().map(a -> a.value).collect(toList())) |
| .containsExactly(1, 1); |
| assertThat(codeReviewApprovals.stream().map(a -> a.username).collect(toList())) |
| .containsExactly(admin.username(), user.username()); |
| } |
| |
| @Test |
| public void checkLabelsForUnsubmittedChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.status).isEqualTo(ChangeStatus.NEW); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| |
| // add new label and assert that it's returned for existing changes |
| AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); |
| LabelType verified = TestLabels.verified(); |
| String heads = RefNames.REFS_HEADS + "*"; |
| |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(verified); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1)) |
| .update(); |
| |
| change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED); |
| assertThat(change.permittedLabels.keySet()) |
| .containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED); |
| assertPermitted(change, LabelId.CODE_REVIEW, -2, -1, 0, 1, 2); |
| assertPermitted(change, LabelId.VERIFIED, -1, 0, 1); |
| assertLabelDescription(change, LabelId.VERIFIED, TestLabels.VERIFIED_LABEL_DESCRIPTION); |
| |
| // add an approval on the new label |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(new ReviewInput().label(verified.getName(), verified.getMax().getValue())); |
| |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| // remove label and assert that it's no longer returned for existing |
| // changes, even if there is an approval for it |
| u.getConfig().getLabelSections().remove(verified.getName()); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .remove( |
| permissionKey(Permission.forLabel(verified.getName())) |
| .ref(heads) |
| .group(registeredUsers)) |
| .update(); |
| |
| change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| |
| // abandon the change and see that the returned labels stay the same |
| // while all permitted labels disappear. |
| gApi.changes().id(r.getChangeId()).abandon(); |
| change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.status).isEqualTo(ChangeStatus.ABANDONED); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(change.permittedLabels).isEmpty(); |
| } |
| |
| @Test |
| public void checkLabelVotesForMergedChange() throws Exception { |
| List<ListChangesOption> options = |
| EnumSet.complementOf( |
| EnumSet.of( |
| ListChangesOption.CHECK, |
| ListChangesOption.SKIP_DIFFSTAT, |
| ListChangesOption.DETAILED_LABELS)) |
| .stream() |
| .collect(Collectors.toList()); |
| PushOneCommit.Result r = createChange(); |
| voteLabel(r.getChangeId(), LabelId.CODE_REVIEW, +2); |
| |
| // Add another label for 'Verified' |
| LabelType verified = TestLabels.verified(); |
| AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); |
| String heads = RefNames.REFS_HEADS + "*"; |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(verified); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1)) |
| .update(); |
| |
| // Submit the change |
| voteLabel(r.getChangeId(), TestLabels.verified().getName(), 1); |
| gApi.changes().id(r.getChangeId()).current().submit(); |
| |
| // Make sure label votes are available if DETAILED_LABELS is not requested. |
| ChangeInfo change = gApi.changes().id(r.getChangeId()).get(options); |
| assertThat(change.status).isEqualTo(ChangeStatus.MERGED); |
| assertThat(change.labels.keySet()) |
| .containsExactly(LabelId.CODE_REVIEW, TestLabels.verified().getName()); |
| List<ApprovalInfo> codeReviewApprovals = change.labels.get(LabelId.CODE_REVIEW).all; |
| List<ApprovalInfo> verifiedApprovals = change.labels.get(TestLabels.verified().getName()).all; |
| |
| assertThat(codeReviewApprovals).hasSize(1); |
| assertThat(codeReviewApprovals.get(0).value).isEqualTo(2); |
| assertThat(codeReviewApprovals.get(0).username).isEqualTo(admin.username()); |
| assertThat(codeReviewApprovals.get(0).permittedVotingRange).isNull(); |
| |
| assertThat(verifiedApprovals).hasSize(1); |
| assertThat(verifiedApprovals.get(0).value).isEqualTo(1); |
| assertThat(verifiedApprovals.get(0).username).isEqualTo(admin.username()); |
| assertThat(codeReviewApprovals.get(0).permittedVotingRange).isNull(); |
| } |
| |
| @Test |
| public void checkLabelsForMergedChange() 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 change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.status).isEqualTo(MERGED); |
| assertThat(change.submissionId).isNotNull(); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertPermitted(change, LabelId.CODE_REVIEW, 2); |
| |
| LabelType verified = TestLabels.verified(); |
| AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); |
| String heads = RefNames.REFS_HEADS + "*"; |
| |
| // add new label and assert that it's returned for existing changes |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(verified); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1)) |
| .update(); |
| |
| change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED); |
| assertThat(change.permittedLabels.keySet()) |
| .containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED); |
| assertPermitted(change, LabelId.CODE_REVIEW, 2); |
| assertPermitted(change, LabelId.VERIFIED, 0, 1); |
| assertLabelDescription(change, LabelId.VERIFIED, TestLabels.VERIFIED_LABEL_DESCRIPTION); |
| |
| // Ignore the new label by Prolog submit rule. Permitted ranges are still going to be |
| // returned for the label. |
| GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config"); |
| testRepo.reset("config"); |
| PushOneCommit push2 = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| "Ignore Verified", |
| "rules.pl", |
| "submit_rule(submit(CR)) :-\n gerrit:max_with_block(-2, 2, 'Code-Review', CR)."); |
| push2.to(RefNames.REFS_CONFIG); |
| |
| change = gApi.changes().id(r.getChangeId()).get(); |
| assertPermitted(change, LabelId.CODE_REVIEW, 2); |
| assertPermitted(change, LabelId.VERIFIED, 0, 1); |
| |
| // add an approval on the new label. The label can still be voted +1 although it is ignored |
| // in Prolog. 0 is not permitted because votes cannot be decreased. |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(new ReviewInput().label(verified.getName(), verified.getMax().getValue())); |
| |
| change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED); |
| assertPermitted(change, LabelId.CODE_REVIEW, 2); |
| assertPermitted(change, LabelId.VERIFIED, 1); |
| |
| // remove label and assert that it's no longer returned for existing |
| // changes, even if there is an approval for it |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().getLabelSections().remove(verified.getName()); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .remove(permissionKey(verified.getName()).ref(heads).group(registeredUsers)) |
| .update(); |
| |
| change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertPermitted(change, LabelId.CODE_REVIEW, 2); |
| } |
| |
| @Test |
| public void notifyConfigForDirectoryTriggersEmail() throws Exception { |
| // Configure notifications on project level. |
| RevCommit oldHead = projectOperations.project(project).getHead("master"); |
| GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config"); |
| testRepo.reset("config"); |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| "Configure Notifications", |
| "project.config", |
| "[notify \"my=notify-config\"]\n" |
| + " email = foo@example.com\n" |
| + " filter = dir:\\\"foo/bar/baz\\\""); |
| push.to(RefNames.REFS_CONFIG); |
| testRepo.reset(oldHead); |
| |
| // Push a change that matches the filter. |
| sender.clear(); |
| push = |
| pushFactory.create( |
| admin.newIdent(), testRepo, "Test change", "foo/bar/baz/test.txt", "some content"); |
| PushOneCommit.Result r = push.to("refs/for/master"); |
| assertThat(sender.getMessages()).hasSize(1); |
| assertThat(sender.getMessages().get(0).rcpt()) |
| .containsExactly(Address.parse("foo@example.com")); |
| |
| // Comment on the change. |
| sender.clear(); |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.message = "some message"; |
| gApi.changes().id(r.getChangeId()).current().review(reviewInput); |
| assertThat(sender.getMessages()).hasSize(1); |
| assertThat(sender.getMessages().get(0).rcpt()) |
| .containsExactly(Address.parse("foo@example.com")); |
| } |
| |
| @Test |
| public void checkLabelsForMergedChangeWithNonAuthorCodeReview() throws Exception { |
| // Configure Non-Author-Code-Review |
| RevCommit oldHead = projectOperations.project(project).getHead("master"); |
| GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config"); |
| testRepo.reset("config"); |
| PushOneCommit push2 = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| "Configure Non-Author-Code-Review", |
| "rules.pl", |
| "submit_rule(S) :-\n" |
| + " gerrit:default_submit(X),\n" |
| + " X =.. [submit | Ls],\n" |
| + " add_non_author_approval(Ls, R),\n" |
| + " S =.. [submit | R].\n" |
| + "\n" |
| + "add_non_author_approval(S1, S2) :-\n" |
| + " gerrit:commit_author(A),\n" |
| + " gerrit:commit_label(label('Code-Review', 2), R),\n" |
| + " R \\= A, !,\n" |
| + " S2 = [label('Non-Author-Code-Review', ok(R)) | S1].\n" |
| + "add_non_author_approval(S1," |
| + " [label('Non-Author-Code-Review', need(_)) | S1])."); |
| push2.to(RefNames.REFS_CONFIG); |
| testRepo.reset(oldHead); |
| |
| String heads = RefNames.REFS_HEADS + "*"; |
| |
| // Allow user to approve |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(TestLabels.codeReview().getName()) |
| .ref(heads) |
| .group(REGISTERED_USERS) |
| .range(-2, 2)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); |
| |
| ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.status).isEqualTo(MERGED); |
| assertThat(change.submissionId).isNotNull(); |
| assertThat(change.labels.keySet()) |
| .containsExactly(LabelId.CODE_REVIEW, "Non-Author-Code-Review"); |
| assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2); |
| } |
| |
| @Test |
| public void checkLabelsForAutoClosedChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo); |
| PushOneCommit.Result result = push.to("refs/heads/master"); |
| result.assertOkStatus(); |
| |
| ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(change.status).isEqualTo(MERGED); |
| assertThat(change.submissionId).isNotNull(); |
| assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2); |
| } |
| |
| @Test |
| public void checkSubmissionIdForAutoClosedChange() throws Exception { |
| PushOneCommit.Result first = createChange(); |
| PushOneCommit.Result second = createChange(); |
| |
| PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo); |
| |
| PushOneCommit.Result result = push.to("refs/heads/master"); |
| result.assertOkStatus(); |
| |
| ChangeInfo firstChange = gApi.changes().id(first.getChangeId()).get(); |
| assertThat(firstChange.status).isEqualTo(MERGED); |
| assertThat(firstChange.submissionId).isNotNull(); |
| |
| ChangeInfo secondChange = gApi.changes().id(second.getChangeId()).get(); |
| assertThat(secondChange.status).isEqualTo(MERGED); |
| assertThat(secondChange.submissionId).isNotNull(); |
| |
| assertThat(secondChange.submissionId).isEqualTo(firstChange.submissionId); |
| assertThat(gApi.changes().id(second.getChangeId()).submittedTogether()).hasSize(2); |
| } |
| |
| @Test |
| public void maxPermittedValueAllowed() throws Exception { |
| final int minPermittedValue = -2; |
| final int maxPermittedValue = +2; |
| String heads = "refs/heads/*"; |
| |
| PushOneCommit.Result r = createChange(); |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| |
| gApi.changes().id(triplet).addReviewer(user.username()); |
| |
| ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS); |
| LabelInfo codeReview = c.labels.get(LabelId.CODE_REVIEW); |
| assertThat(codeReview.all).hasSize(1); |
| ApprovalInfo approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id().get()); |
| assertThat(approval.permittedVotingRange).isNotNull(); |
| // default values |
| assertThat(approval.permittedVotingRange.min).isEqualTo(-1); |
| assertThat(approval.permittedVotingRange.max).isEqualTo(1); |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(LabelId.CODE_REVIEW) |
| .ref(heads) |
| .group(REGISTERED_USERS) |
| .range(minPermittedValue, maxPermittedValue)) |
| .update(); |
| |
| c = gApi.changes().id(triplet).get(DETAILED_LABELS); |
| codeReview = c.labels.get(LabelId.CODE_REVIEW); |
| assertThat(codeReview.all).hasSize(1); |
| approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id().get()); |
| assertThat(approval.permittedVotingRange).isNotNull(); |
| assertThat(approval.permittedVotingRange.min).isEqualTo(minPermittedValue); |
| assertThat(approval.permittedVotingRange.max).isEqualTo(maxPermittedValue); |
| } |
| |
| @Test |
| public void maxPermittedValueBlocked() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| blockLabel(LabelId.CODE_REVIEW) |
| .ref("refs/heads/*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| |
| gApi.changes().id(triplet).addReviewer(user.username()); |
| |
| ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS); |
| LabelInfo codeReview = c.labels.get(LabelId.CODE_REVIEW); |
| assertThat(codeReview.all).hasSize(1); |
| ApprovalInfo approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id().get()); |
| assertThat(approval.permittedVotingRange).isNull(); |
| } |
| |
| @Test |
| public void nonStrictLabelWithInvalidLabelPerDefault() throws Exception { |
| String changeId = createChange().getChangeId(); |
| |
| // Add a review with invalid labels. |
| ReviewInput input = ReviewInput.approve().label("Code-Style", 1); |
| gApi.changes().id(changeId).current().review(input); |
| |
| Map<String, Short> votes = |
| gApi.changes().id(changeId).current().reviewer(admin.email()).votes(); |
| assertThat(votes.keySet()).containsExactly(LabelId.CODE_REVIEW); |
| assertThat(votes.values()).containsExactly((short) 2); |
| } |
| |
| @Test |
| public void nonStrictLabelWithInvalidValuePerDefault() throws Exception { |
| String changeId = createChange().getChangeId(); |
| |
| // Add a review with invalid label values. |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 3); |
| gApi.changes().id(changeId).current().review(input); |
| |
| assertThrows( |
| ResourceNotFoundException.class, |
| () -> gApi.changes().id(changeId).current().reviewer(admin.email())); |
| } |
| |
| @Test |
| @GerritConfig(name = "change.strictLabels", value = "true") |
| public void strictLabelWithInvalidLabel() throws Exception { |
| String changeId = createChange().getChangeId(); |
| ReviewInput in = new ReviewInput().label("Code-Style", 1); |
| |
| BadRequestException thrown = |
| assertThrows( |
| BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in)); |
| assertThat(thrown).hasMessageThat().contains("label \"Code-Style\" is not a configured label"); |
| } |
| |
| @Test |
| @GerritConfig(name = "change.strictLabels", value = "true") |
| public void strictLabelWithInvalidValue() throws Exception { |
| String changeId = createChange().getChangeId(); |
| ReviewInput in = new ReviewInput().label(LabelId.CODE_REVIEW, 3); |
| |
| BadRequestException thrown = |
| assertThrows( |
| BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in)); |
| assertThat(thrown).hasMessageThat().contains("label \"Code-Review\": 3 is not a valid value"); |
| } |
| |
| @Test |
| public void unresolvedCommentsBlocked() throws Exception { |
| modifySubmitRules( |
| "submit_rule(submit(R)) :- \n" |
| + "gerrit:unresolved_comments_count(0), \n" |
| + "!," |
| + "gerrit:uploader(U), \n" |
| + "R = label('All-Comments-Resolved', ok(U)).\n" |
| + "submit_rule(submit(R)) :- \n" |
| + "gerrit:unresolved_comments_count(U), \n" |
| + "U > 0," |
| + "R = label('All-Comments-Resolved', need(_)). \n\n"); |
| |
| String oldHead = projectOperations.project(project).getHead("master").name(); |
| PushOneCommit.Result result1 = |
| pushFactory.create(user.newIdent(), testRepo).to("refs/for/master"); |
| testRepo.reset(oldHead); |
| PushOneCommit.Result result2 = |
| pushFactory.create(user.newIdent(), testRepo).to("refs/for/master"); |
| |
| addComment(result1, "comment 1", true, false, null); |
| addComment(result2, "comment 2", true, true, null); |
| |
| gApi.changes().id(result1.getChangeId()).current().submit(); |
| |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(result2.getChangeId()).current().submit()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .contains("Failed to submit 1 change due to the following problems"); |
| assertThat(thrown) |
| .hasMessageThat() |
| .contains("submit requirement 'All-Comments-Resolved' is unsatisfied"); |
| } |
| |
| @Test |
| public void changeCommitMessage() throws Exception { |
| // Tests mutating the commit message as both the owner of the change and a regular user with |
| // addPatchSet permission. Asserts that both cases succeed. |
| PushOneCommit.Result r = createChange(); |
| r.assertOkStatus(); |
| assertThat(getCommitMessage(r.getChangeId())) |
| .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); |
| |
| for (com.google.gerrit.acceptance.TestAccount acc : ImmutableList.of(admin, user)) { |
| requestScopeOperations.setApiUser(acc.id()); |
| String newMessage = |
| "modified commit by " + acc.id() + "\n\nChange-Id: " + r.getChangeId() + "\n"; |
| gApi.changes().id(r.getChangeId()).setMessage(newMessage); |
| RevisionApi rApi = gApi.changes().id(r.getChangeId()).current(); |
| assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt"); |
| assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage); |
| assertThat(rApi.description()).isEqualTo("Edit commit message"); |
| } |
| |
| // Verify tags, which should differ according to whether the change was WIP |
| // at the time the commit message was edited. First, look at the last edit |
| // we created above, when the change was not WIP. |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(Iterables.getLast(info.messages).tag) |
| .isEqualTo(ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); |
| |
| // Move the change to WIP and edit the commit message again, to observe a |
| // different tag. Must switch to change owner to move into WIP. |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(r.getChangeId()).setWorkInProgress(); |
| String newMessage = "modified commit in WIP change\n\nChange-Id: " + r.getChangeId() + "\n"; |
| gApi.changes().id(r.getChangeId()).setMessage(newMessage); |
| info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(Iterables.getLast(info.messages).tag) |
| .isEqualTo(ChangeMessagesUtil.TAG_UPLOADED_WIP_PATCH_SET); |
| } |
| |
| @Test |
| public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception { |
| ConfigInput configInput = new ConfigInput(); |
| configInput.requireChangeId = InheritableBoolean.FALSE; |
| gApi.projects().name(project.get()).config(configInput); |
| |
| PushOneCommit.Result r = createChange(); |
| r.assertOkStatus(); |
| assertThat(getCommitMessage(r.getChangeId())) |
| .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); |
| |
| String newMessage = "modified commit\n"; |
| gApi.changes().id(r.getChangeId()).setMessage(newMessage); |
| RevisionApi rApi = gApi.changes().id(r.getChangeId()).current(); |
| assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt"); |
| assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage); |
| } |
| |
| @Test |
| public void changeCommitMessageNullNotAllowed() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(getCommitMessage(r.getChangeId())) |
| .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); |
| BadRequestException thrown = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| gApi.changes() |
| .id(r.getChangeId()) |
| .setMessage("test\0commit\n\nChange-Id: " + r.getChangeId() + "\n")); |
| assertThat(thrown).hasMessageThat().contains("NUL character"); |
| } |
| |
| @Test |
| public void changeCommitMessageWithWrongChangeIdFails() throws Exception { |
| PushOneCommit.Result otherChange = createChange(); |
| PushOneCommit.Result r = createChange(); |
| assertThat(getCommitMessage(r.getChangeId())) |
| .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> |
| gApi.changes() |
| .id(r.getChangeId()) |
| .setMessage( |
| "modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n")); |
| assertThat(thrown).hasMessageThat().contains("wrong Change-Id footer"); |
| } |
| |
| @Test |
| public void changeCommitMessageWithoutPermissionFails() throws Exception { |
| // Create new project with clean permissions |
| Project.NameKey p = projectOperations.newProject().create(); |
| TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user); |
| // Block default permission |
| projectOperations |
| .project(p) |
| .forUpdate() |
| .add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS)) |
| .update(); |
| // Create change as user |
| PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo); |
| PushOneCommit.Result r = push.to("refs/for/master"); |
| r.assertOkStatus(); |
| // Try to change the commit message |
| AuthException thrown = |
| assertThrows( |
| AuthException.class, () -> gApi.changes().id(r.getChangeId()).setMessage("foo")); |
| assertThat(thrown).hasMessageThat().contains("modifying commit message not permitted"); |
| } |
| |
| @Test |
| public void changeCommitMessageWithSameMessageFails() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(getCommitMessage(r.getChangeId())) |
| .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId()))); |
| assertThat(thrown).hasMessageThat().contains("new and existing commit message are the same"); |
| } |
| |
| @Test |
| public void fourByteEmoji() throws Exception { |
| // U+1F601 GRINNING FACE WITH SMILING EYES |
| String smile = new String(Character.toChars(0x1f601)); |
| assertThat(smile).isEqualTo("😁"); |
| assertThat(smile).hasLength(2); // Thanks, Java. |
| assertThat(smile.getBytes(UTF_8)).hasLength(4); |
| |
| String subject = "A happy change " + smile; |
| PushOneCommit.Result r = |
| pushFactory |
| .create(admin.newIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT) |
| .to("refs/for/master"); |
| r.assertOkStatus(); |
| String id = r.getChangeId(); |
| |
| ReviewInput ri = ReviewInput.approve(); |
| ri.message = "I like it " + smile; |
| ReviewInput.CommentInput ci = new ReviewInput.CommentInput(); |
| ci.path = FILE_NAME; |
| ci.side = Side.REVISION; |
| ci.message = "Good " + smile; |
| ri.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(ci)); |
| gApi.changes().id(id).current().review(ri); |
| |
| ChangeInfo info = gApi.changes().id(id).get(MESSAGES, CURRENT_COMMIT, CURRENT_REVISION); |
| assertThat(info.subject).isEqualTo(subject); |
| assertThat(Iterables.getLast(info.messages).message).endsWith(ri.message); |
| assertThat(Iterables.getOnlyElement(info.revisions.values()).commit.message) |
| .startsWith(subject); |
| |
| List<CommentInfo> comments = |
| Iterables.getOnlyElement(gApi.changes().id(id).commentsRequest().get().values()); |
| assertThat(Iterables.getOnlyElement(comments).message).isEqualTo(ci.message); |
| } |
| |
| @Test |
| public void putTopicExceedLimitFails() throws Exception { |
| String changeId = createChange().getChangeId(); |
| String topic = Stream.generate(() -> "t").limit(2049).collect(joining()); |
| |
| BadRequestException thrown = |
| assertThrows(BadRequestException.class, () -> gApi.changes().id(changeId).topic(topic)); |
| assertThat(thrown).hasMessageThat().contains("topic length exceeds the limit"); |
| } |
| |
| @Test |
| public void submittableAfterLosingPermissions_MaxWithBlock() throws Exception { |
| configLabel("Label", LabelFunction.MAX_WITH_BLOCK); |
| submittableAfterLosingPermissions("Label"); |
| } |
| |
| @Test |
| public void submittableAfterLosingPermissions_AnyWithBlock() throws Exception { |
| configLabel("Label", LabelFunction.ANY_WITH_BLOCK); |
| submittableAfterLosingPermissions("Label"); |
| } |
| |
| @Test |
| @GerritConfig(name = "change.submitWholeTopic", value = "true") |
| public void cantSubmitWithInvisibleChangesWithTopic() throws Exception { |
| createBranch(BranchNameKey.create(project, "secret")); |
| |
| // create two changes in the same topic. |
| String topic = "topic"; |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = createChange("refs/for/secret"); |
| approve(r1.getChangeId()); |
| approve(r2.getChangeId()); |
| gApi.changes().id(r1.getChangeId()).topic(topic); |
| gApi.changes().id(r2.getChangeId()).topic(topic); |
| |
| // make one of the changes invisible. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(block(Permission.READ).ref("refs/heads/secret").group(REGISTERED_USERS)) |
| .update(); |
| |
| // can't submit with invisible changes. |
| requestScopeOperations.setApiUser(user.id()); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| assertThrows(AuthException.class, () -> gApi.changes().id(r1.getChangeId()).current().submit()); |
| } |
| |
| @Test |
| public void cantSubmitWithInvisibleDependentChange() throws Exception { |
| // create two dependent changes. |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| approve(r1.getChangeId()); |
| approve(r2.getChangeId()); |
| |
| // make the dependent change invisible. |
| gApi.changes().id(r1.getChangeId()).setPrivate(true); |
| |
| // can't submit with invisible changes. |
| requestScopeOperations.setApiUser(user.id()); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| assertThrows(AuthException.class, () -> gApi.changes().id(r2.getChangeId()).current().submit()); |
| } |
| |
| private void submittableAfterLosingPermissions(String label) throws Exception { |
| String codeReviewLabel = LabelId.CODE_REVIEW; |
| AccountGroup.UUID registered = REGISTERED_USERS; |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel(label).ref("refs/heads/*").group(registered).range(-1, +1)) |
| .add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-2, +2)) |
| .update(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Verify user's permitted range. |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertPermitted(change, label, -1, 0, 1); |
| assertPermitted(change, codeReviewLabel, -2, -1, 0, 1, 2); |
| |
| ReviewInput input = new ReviewInput(); |
| input.label(codeReviewLabel, 2); |
| input.label(label, 1); |
| gApi.changes().id(changeId).current().review(input); |
| |
| assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().keySet()) |
| .containsExactly(codeReviewLabel, label); |
| assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values()) |
| .containsExactly((short) 2, (short) 1); |
| assertThat(gApi.changes().id(changeId).get().submittable).isTrue(); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .remove(labelPermissionKey(label).ref("refs/heads/*").group(registered)) |
| .remove(labelPermissionKey(codeReviewLabel).ref("refs/heads/*").group(registered)) |
| .add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-1, +1)) |
| .update(); |
| |
| // Verify user's new permitted range. |
| requestScopeOperations.setApiUser(user.id()); |
| change = gApi.changes().id(changeId).get(); |
| assertPermitted(change, label); |
| assertPermitted(change, codeReviewLabel, -1, 0, 1); |
| |
| assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values()) |
| .containsExactly((short) 2, (short) 1); |
| assertThat(gApi.changes().id(changeId).get().submittable).isTrue(); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes().id(changeId).current().submit(); |
| } |
| |
| @Test |
| public void draftCommentsShouldNotUpdateChangeTimestamp() throws Exception { |
| String changeId = createNewChange(); |
| Timestamp changeTs = getChangeLastUpdate(changeId); |
| DraftApi draftApi = addDraftComment(changeId); |
| assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs); |
| draftApi.delete(); |
| assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs); |
| } |
| |
| @Test |
| public void deletingAllDraftCommentsShouldNotUpdateChangeTimestamp() throws Exception { |
| String changeId = createNewChange(); |
| Timestamp changeTs = getChangeLastUpdate(changeId); |
| addDraftComment(changeId); |
| assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs); |
| gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput()); |
| assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs); |
| } |
| |
| private Timestamp getChangeLastUpdate(String changeId) throws RestApiException { |
| Timestamp changeTs = gApi.changes().id(changeId).get().updated; |
| return changeTs; |
| } |
| |
| private String createNewChange() throws Exception { |
| TestRepository<InMemoryRepository> userRepo = cloneProject(project, user); |
| PushOneCommit.Result result = |
| pushFactory.create(user.newIdent(), userRepo).to("refs/for/master"); |
| String changeId = result.getChangeId(); |
| return changeId; |
| } |
| |
| private DraftApi addDraftComment(String changeId) throws RestApiException { |
| DraftInput comment = new DraftInput(); |
| comment.message = "foo"; |
| comment.path = "/foo"; |
| return gApi.changes().id(changeId).current().createDraft(comment); |
| } |
| |
| private String getCommitMessage(String changeId) throws RestApiException, IOException { |
| return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString(); |
| } |
| |
| private void addComment( |
| PushOneCommit.Result r, |
| String message, |
| boolean omitDuplicateComments, |
| Boolean unresolved, |
| String inReplyTo) |
| throws Exception { |
| ReviewInput.CommentInput c = new ReviewInput.CommentInput(); |
| c.line = 1; |
| c.message = message; |
| c.path = FILE_NAME; |
| c.unresolved = unresolved; |
| c.inReplyTo = inReplyTo; |
| ReviewInput in = new ReviewInput(); |
| in.comments = new HashMap<>(); |
| in.comments.put(c.path, Lists.newArrayList(c)); |
| in.omitDuplicateComments = omitDuplicateComments; |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in); |
| } |
| |
| private static Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) { |
| if (r == null) { |
| return ImmutableList.of(); |
| } |
| return Iterables.transform(r, a -> Account.id(a._accountId)); |
| } |
| |
| private ChangeResource parseResource(PushOneCommit.Result r) throws Exception { |
| return parseChangeResource(r.getChangeId()); |
| } |
| |
| private Optional<ReviewerState> getReviewerState(String changeId, Account.Id accountId) |
| throws Exception { |
| ChangeInfo c = gApi.changes().id(changeId).get(DETAILED_LABELS); |
| Set<ReviewerState> states = |
| c.reviewers.entrySet().stream() |
| .filter(e -> e.getValue().stream().anyMatch(a -> a._accountId == accountId.get())) |
| .map(Map.Entry::getKey) |
| .collect(toSet()); |
| assertWithMessage(states.toString()).that(states.size()).isAtMost(1); |
| return states.stream().findFirst(); |
| } |
| |
| private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception { |
| try (BatchUpdate batchUpdate = |
| batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.now())) { |
| batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus)); |
| batchUpdate.execute(); |
| } |
| |
| ChangeStatus changeStatus = gApi.changes().id(id.get()).get().status; |
| assertThat(changeStatus).isEqualTo(newStatus.asChangeStatus()); |
| } |
| |
| private static class ChangeStatusUpdateOp implements BatchUpdateOp { |
| private final Change.Status newStatus; |
| |
| ChangeStatusUpdateOp(Change.Status newStatus) { |
| this.newStatus = newStatus; |
| } |
| |
| @Override |
| public boolean updateChange(ChangeContext ctx) throws Exception { |
| Change change = ctx.getChange(); |
| |
| // Change status. |
| PatchSet.Id currentPatchSetId = change.currentPatchSetId(); |
| ctx.getUpdate(currentPatchSetId).setStatus(newStatus); |
| |
| return true; |
| } |
| } |
| |
| 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(); |
| } |
| projectCache.evict(project); |
| } |
| |
| @Test |
| @GerritConfig(name = "trackingid.jira-bug.footer", value = "Bug:") |
| @GerritConfig(name = "trackingid.jira-bug.match", value = "JRA\\d{2,8}") |
| @GerritConfig(name = "trackingid.jira-bug.system", value = "JIRA") |
| public void trackingIds() throws Exception { |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| PushOneCommit.SUBJECT + "\n\n" + "Bug:JRA001", |
| PushOneCommit.FILE_NAME, |
| PushOneCommit.FILE_CONTENT); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| ChangeInfo change = gApi.changes().id(result.getChangeId()).get(TRACKING_IDS); |
| Collection<TrackingIdInfo> trackingIds = change.trackingIds; |
| assertThat(trackingIds).isNotNull(); |
| assertThat(trackingIds).hasSize(1); |
| assertThat(trackingIds.iterator().next().system).isEqualTo("JIRA"); |
| assertThat(trackingIds.iterator().next().id).isEqualTo("JRA001"); |
| } |
| |
| @Test |
| @GerritConfig(name = "trackingid.jira-bug.footer", value = "Bug:") |
| @GerritConfig(name = "trackingid.jira-bug.match", value = "\\d+") |
| @GerritConfig(name = "trackingid.jira-bug.system", value = "JIRA") |
| public void multipleTrackingIdsInSingleFooter() throws Exception { |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| PushOneCommit.SUBJECT + "\n\n" + "Bug: 123, 456", |
| PushOneCommit.FILE_NAME, |
| PushOneCommit.FILE_CONTENT); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| ChangeInfo change = gApi.changes().id(result.getChangeId()).get(TRACKING_IDS); |
| Collection<TrackingIdInfo> trackingIds = change.trackingIds; |
| assertThat(trackingIds).isNotNull(); |
| assertThat(trackingIds).hasSize(2); |
| assertThat(trackingIds.stream().map(t -> t.id)).containsExactly("123", "456"); |
| } |
| |
| @Test |
| public void starUnstar() throws Exception { |
| ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(changeIndexedCounter)) { |
| PushOneCommit.Result r = createChange(); |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| changeIndexedCounter.clear(); |
| |
| gApi.accounts().self().starChange(triplet); |
| ChangeInfo change = info(triplet); |
| assertThat(change.starred).isTrue(); |
| assertThat(change.stars).contains(DEFAULT_LABEL); |
| changeIndexedCounter.assertReindexOf(change); |
| |
| gApi.accounts().self().unstarChange(triplet); |
| change = info(triplet); |
| assertThat(change.starred).isNull(); |
| assertThat(change.stars).isNull(); |
| changeIndexedCounter.assertReindexOf(change); |
| } |
| } |
| |
| @Test |
| public void ignore() throws Exception { |
| String email = "user2@example.com"; |
| String fullname = "User2"; |
| accountOperations |
| .newAccount() |
| .username("user2") |
| .preferredEmail(email) |
| .fullname(fullname) |
| .create(); |
| |
| PushOneCommit.Result r = createChange(); |
| |
| ReviewerInput in = new ReviewerInput(); |
| in.reviewer = user.email(); |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| |
| in = new ReviewerInput(); |
| in.reviewer = email; |
| gApi.changes().id(r.getChangeId()).addReviewer(in); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).ignore(true); |
| assertThat(gApi.changes().id(r.getChangeId()).ignored()).isTrue(); |
| |
| // New patch set notification is not sent to users ignoring the change |
| sender.clear(); |
| requestScopeOperations.setApiUser(admin.id()); |
| amendChange(r.getChangeId()); |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Address address = Address.create(fullname, email); |
| assertThat(messages.get(0).rcpt()).containsExactly(address); |
| |
| // Review notification is not sent to users ignoring the change |
| sender.clear(); |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); |
| messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| assertThat(messages.get(0).rcpt()).containsExactly(address); |
| |
| // Abandoned notification is not sent to users ignoring the change |
| sender.clear(); |
| gApi.changes().id(r.getChangeId()).abandon(); |
| messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| assertThat(messages.get(0).rcpt()).containsExactly(address); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).ignore(false); |
| assertThat(gApi.changes().id(r.getChangeId()).ignored()).isFalse(); |
| } |
| |
| @Test |
| public void cannotIgnoreOwnChange() throws Exception { |
| String changeId = createChange().getChangeId(); |
| |
| BadRequestException thrown = |
| assertThrows(BadRequestException.class, () -> gApi.changes().id(changeId).ignore(true)); |
| assertThat(thrown).hasMessageThat().contains("cannot ignore own change"); |
| } |
| |
| @Test |
| public void cannotIgnoreStarredChange() throws Exception { |
| String changeId = createChange().getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.accounts().self().starChange(changeId); |
| assertThat(gApi.changes().id(changeId).get().starred).isTrue(); |
| |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, () -> gApi.changes().id(changeId).ignore(true)); |
| assertThat(thrown) |
| .hasMessageThat() |
| .contains( |
| "The labels " |
| + StarredChangesUtil.DEFAULT_LABEL |
| + " and " |
| + StarredChangesUtil.IGNORE_LABEL |
| + " are mutually exclusive. Only one of them can be set."); |
| } |
| |
| @Test |
| public void cannotStarIgnoredChange() throws Exception { |
| String changeId = createChange().getChangeId(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(changeId).ignore(true); |
| assertThat(gApi.changes().id(changeId).ignored()).isTrue(); |
| |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, () -> gApi.accounts().self().starChange(changeId)); |
| assertThat(thrown) |
| .hasMessageThat() |
| .contains( |
| "The labels " |
| + StarredChangesUtil.DEFAULT_LABEL |
| + " and " |
| + StarredChangesUtil.IGNORE_LABEL |
| + " are mutually exclusive. Only one of them can be set."); |
| } |
| |
| @Test |
| public void changeDetailsDoesNotRequireIndex() throws Exception { |
| // This set of options must be kept in sync with gr-rest-api-interface.js |
| Set<ListChangesOption> options = |
| ImmutableSet.of( |
| ListChangesOption.ALL_COMMITS, |
| ListChangesOption.ALL_REVISIONS, |
| ListChangesOption.CHANGE_ACTIONS, |
| ListChangesOption.DETAILED_LABELS, |
| ListChangesOption.DOWNLOAD_COMMANDS, |
| ListChangesOption.MESSAGES, |
| ListChangesOption.SUBMITTABLE, |
| ListChangesOption.WEB_LINKS, |
| ListChangesOption.SKIP_DIFFSTAT); |
| |
| PushOneCommit.Result change = createChange(); |
| int number = gApi.changes().id(change.getChangeId()).get()._number; |
| |
| try (AutoCloseable ignored = changeIndexOperations.disableReadsAndWrites()) { |
| assertThat(gApi.changes().id(project.get(), number).get(options).changeId) |
| .isEqualTo(change.getChangeId()); |
| } |
| } |
| |
| @Test |
| @GerritConfig( |
| name = "change.mergeabilityComputationBehavior", |
| value = "API_REF_UPDATED_AND_CHANGE_REINDEX") |
| public void changeQueryReturnsMergeableWhenGerritIndexMergeable() throws Exception { |
| String changeId = createChange().getChangeId(); |
| assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isTrue(); |
| } |
| |
| @Test |
| @GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER") |
| public void changeQueryDoesNotReturnMergeableWhenGerritDoesNotIndexMergeable() throws Exception { |
| String changeId = createChange().getChangeId(); |
| assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isNull(); |
| } |
| |
| private PushOneCommit.Result createWorkInProgressChange() throws Exception { |
| return pushTo("refs/for/master%wip"); |
| } |
| |
| private ThrowableSubject assertThatQueryException(String query) throws Exception { |
| try { |
| query(query); |
| } catch (BadRequestException e) { |
| return assertThat(e); |
| } |
| throw new AssertionError("expected BadRequestException"); |
| } |
| |
| @FunctionalInterface |
| private interface AddReviewerCaller { |
| void call(String changeId, String reviewer) throws RestApiException; |
| } |
| |
| private static class TestWorkInProgressStateChangedListener |
| implements WorkInProgressStateChangedListener { |
| boolean invoked; |
| Boolean wip; |
| |
| @Override |
| public void onWorkInProgressStateChanged(Event event) { |
| this.invoked = true; |
| this.wip = |
| event.getChange().workInProgress != null ? event.getChange().workInProgress : false; |
| } |
| } |
| |
| private void voteLabel(String changeId, String labelName, int score) throws RestApiException { |
| gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score)); |
| } |
| |
| private static class TestCommitValidationListener implements CommitValidationListener { |
| public CommitReceivedEvent receiveEvent; |
| |
| @Override |
| public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) |
| throws CommitValidationException { |
| this.receiveEvent = receiveEvent; |
| return ImmutableList.of(); |
| } |
| } |
| } |