// 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.git; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.acceptance.GitUtil.assertPushOk; import static com.google.gerrit.acceptance.GitUtil.assertPushRejected; import static com.google.gerrit.acceptance.GitUtil.pushHead; import static com.google.gerrit.acceptance.GitUtil.pushOne; import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.common.FooterConstants.CHANGE_ID; import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS; import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS; 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.common.testing.EditInfoSubject.assertThat; import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION; import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static com.google.gerrit.server.project.testing.Util.category; import static com.google.gerrit.server.project.testing.Util.value; import static java.util.Comparator.comparing; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; 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.Streams; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.DraftInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.groups.GroupInput; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.client.ProjectWatchInfo; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.common.RevisionInfo; import com.google.gerrit.extensions.common.testing.EditInfoSubject; import com.google.gerrit.mail.Address; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.BooleanProjectConfig; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.git.receive.NoteDbPushOption; import com.google.gerrit.server.git.receive.ReceiveConstants; import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.project.testing.Util; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.testing.FakeEmailSender.Message; import com.google.gerrit.testing.TestTimeUtil; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Stream; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; 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.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public abstract class AbstractPushForReview extends AbstractDaemonTest { protected enum Protocol { // TODO(dborowitz): TEST. SSH, HTTP } private LabelType patchSetLock; @BeforeClass public static void setTimeForTesting() { TestTimeUtil.resetWithClockStep(1, SECONDS); } @AfterClass public static void restoreTime() { TestTimeUtil.useSystemTime(); } @Before public void setUpPatchSetLock() throws Exception { try (ProjectConfigUpdate u = updateProject(project)) { patchSetLock = Util.patchSetLock(); u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock); AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); Util.allow( u.getConfig(), Permission.forLabel(patchSetLock.getName()), 0, 1, anonymousUsers, "refs/heads/*"); u.save(); } grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock"); } @After public void resetPublishCommentOnPushOption() throws Exception { setApiUser(admin); GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences(); prefs.publishCommentsOnPush = false; gApi.accounts().id(admin.id.get()).setPreferences(prefs); } protected void selectProtocol(Protocol p) throws Exception { String url; switch (p) { case SSH: url = adminSshSession.getUrl(); break; case HTTP: url = admin.getHttpUrl(server); break; default: throw new IllegalArgumentException("unexpected protocol: " + p); } testRepo = GitUtil.cloneProject(project, url + "/" + project.get()); } @Test public void pushForMaster() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); } @Test @TestProjectInput(createEmptyCommit = false) public void pushInitialCommitForMasterBranch() throws Exception { RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create(); String id = GitUtil.getChangeId(testRepo, c).get(); testRepo.reset(c); String r = "refs/for/master"; PushResult pr = pushHead(testRepo, r, false); assertPushOk(pr, r); ChangeInfo change = gApi.changes().id(id).info(); assertThat(change.branch).isEqualTo("master"); assertThat(change.status).isEqualTo(ChangeStatus.NEW); try (Repository repo = repoManager.openRepository(project)) { assertThat(repo.resolve("master")).isNull(); } gApi.changes().id(change.id).current().review(ReviewInput.approve()); gApi.changes().id(change.id).current().submit(); try (Repository repo = repoManager.openRepository(project)) { assertThat(repo.resolve("master")).isEqualTo(c); } } @Test @TestProjectInput(createEmptyCommit = false) public void validateConnected() throws Exception { RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create(); testRepo.reset(c); String r = "refs/heads/master"; PushResult pr = pushHead(testRepo, r, false); assertPushOk(pr, r); RevCommit amended = testRepo.amend(c).message("different initial commit").insertChangeId().create(); testRepo.reset(amended); r = "refs/for/master"; pr = pushHead(testRepo, r, false); assertPushRejected(pr, r, "no common ancestry"); } @Test @GerritConfig(name = "receive.enableSignedPush", value = "true") @TestProjectInput( enableSignedPush = InheritableBoolean.TRUE, requireSignedPush = InheritableBoolean.TRUE) public void nonSignedPushRejectedWhenSignPushRequired() throws Exception { pushTo("refs/for/master").assertErrorStatus("push cert error"); } @Test public void pushInitialCommitForRefsMetaConfigBranch() throws Exception { // delete refs/meta/config try (Repository repo = repoManager.openRepository(project); RevWalk rw = new RevWalk(repo)) { RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG); u.setForceUpdate(true); u.setExpectedOldObjectId(repo.resolve(RefNames.REFS_CONFIG)); assertThat(u.delete(rw)).isEqualTo(Result.FORCED); } RevCommit c = testRepo .commit() .message("Initial commit") .author(admin.getIdent()) .committer(admin.getIdent()) .insertChangeId() .create(); String id = GitUtil.getChangeId(testRepo, c).get(); testRepo.reset(c); String r = "refs/for/" + RefNames.REFS_CONFIG; PushResult pr = pushHead(testRepo, r, false); assertPushOk(pr, r); ChangeInfo change = gApi.changes().id(id).info(); assertThat(change.branch).isEqualTo(RefNames.REFS_CONFIG); assertThat(change.status).isEqualTo(ChangeStatus.NEW); try (Repository repo = repoManager.openRepository(project)) { assertThat(repo.resolve(RefNames.REFS_CONFIG)).isNull(); } gApi.changes().id(change.id).current().review(ReviewInput.approve()); gApi.changes().id(change.id).current().submit(); try (Repository repo = repoManager.openRepository(project)) { assertThat(repo.resolve(RefNames.REFS_CONFIG)).isEqualTo(c); } } @Test public void pushInitialCommitForNormalNonExistingBranchFails() throws Exception { RevCommit c = testRepo .commit() .message("Initial commit") .author(admin.getIdent()) .committer(admin.getIdent()) .insertChangeId() .create(); testRepo.reset(c); String r = "refs/for/foo"; PushResult pr = pushHead(testRepo, r, false); assertPushRejected(pr, r, "branch foo not found"); try (Repository repo = repoManager.openRepository(project)) { assertThat(repo.resolve("foo")).isNull(); } } @Test public void output() throws Exception { String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/"; ObjectId initialHead = testRepo.getRepository().resolve("HEAD"); PushOneCommit.Result r1 = pushTo("refs/for/master"); Change.Id id1 = r1.getChange().getId(); r1.assertOkStatus(); r1.assertChange(Change.Status.NEW, null); r1.assertMessage( "New changes:\n " + url + id1 + " " + r1.getCommit().getShortMessage() + "\n"); testRepo.reset(initialHead); String newMsg = r1.getCommit().getShortMessage() + " v2"; testRepo .branch("HEAD") .commit() .message(newMsg) .insertChangeId(r1.getChangeId().substring(1)) .create(); PushOneCommit.Result r2 = pushFactory .create(db, admin.getIdent(), testRepo, "another commit", "b.txt", "bbb") .to("refs/for/master"); Change.Id id2 = r2.getChange().getId(); r2.assertOkStatus(); r2.assertChange(Change.Status.NEW, null); r2.assertMessage( "success\n" + "\n" + "New changes:\n" + " " + url + id2 + " another commit\n" + "\n" + "Updated changes:\n" + " " + url + id1 + " " + newMsg + "\n"); } @Test public void autocloseByCommit() throws Exception { // Create a change PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); // Force push it, closing it String master = "refs/heads/master"; assertPushOk(pushHead(testRepo, master, false), master); // Attempt to push amended commit to same change String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/" + r.getChange().getId(); r = amendChange(r.getChangeId(), "refs/for/master"); r.assertErrorStatus("change " + url + " closed"); // Check change message that was added on auto-close ChangeInfo change = gApi.changes().id(r.getChange().getId().get()).get(); assertThat(Iterables.getLast(change.messages).message) .isEqualTo("Change has been successfully pushed."); } @Test public void pushWithoutChangeIdDeprecated() throws Exception { setRequireChangeId(InheritableBoolean.FALSE); testRepo .branch("HEAD") .commit() .message("A change") .author(admin.getIdent()) .committer(new PersonIdent(admin.getIdent(), testRepo.getDate())) .create(); PushResult result = pushHead(testRepo, "refs/for/master"); assertThat(result.getMessages()).contains("warning: pushing without Change-Id is deprecated"); } @Test public void autocloseByChangeId() throws Exception { // Create a change PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); // Amend the commit locally RevCommit c = testRepo.amend(r.getCommit()).create(); assertThat(c).isNotEqualTo(r.getCommit()); testRepo.reset(c); // Force push it, closing it String master = "refs/heads/master"; assertPushOk(pushHead(testRepo, master, false), master); // Attempt to push amended commit to same change String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/" + r.getChange().getId(); r = amendChange(r.getChangeId(), "refs/for/master"); r.assertErrorStatus("change " + url + " closed"); // Check that new commit was added as patch set ChangeInfo change = gApi.changes().id(r.getChange().getId().get()).get(); assertThat(change.revisions).hasSize(2); assertThat(change.currentRevision).isEqualTo(c.name()); } @Test public void pushForMasterWithTopic() throws Exception { // specify topic in ref String topic = "my/topic"; PushOneCommit.Result r = pushTo("refs/for/master/" + topic); r.assertOkStatus(); r.assertChange(Change.Status.NEW, topic); r.assertMessage("deprecated topic syntax"); // specify topic as option r = pushTo("refs/for/master%topic=" + topic); r.assertOkStatus(); r.assertChange(Change.Status.NEW, topic); } @Test public void pushForMasterWithTopicOption() throws Exception { String topicOption = "topic=myTopic"; List pushOptions = new ArrayList<>(); pushOptions.add(topicOption); PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); push.setPushOptions(pushOptions); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, "myTopic"); r.assertPushOptions(pushOptions); } @Test public void pushForMasterWithTopicInRefExceedLimitFails() throws Exception { String topic = Stream.generate(() -> "t").limit(2049).collect(joining()); PushOneCommit.Result r = pushTo("refs/for/master/" + topic); r.assertErrorStatus("topic length exceeds the limit (2048)"); } @Test public void pushForMasterWithTopicAsOptionExceedLimitFails() throws Exception { String topic = Stream.generate(() -> "t").limit(2049).collect(joining()); PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic); r.assertErrorStatus("topic length exceeds the limit (2048)"); } @Test public void pushForMasterWithNotify() throws Exception { // create a user that watches the project TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3"); List projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = project.get(); pwi.filter = "*"; pwi.notifyNewChanges = true; projectsToWatch.add(pwi); setApiUser(user3); gApi.accounts().self().setWatchedProjects(projectsToWatch); TestAccount user2 = accountCreator.user2(); String pushSpec = "refs/for/master%reviewer=" + user.email + ",cc=" + user2.email; sender.clear(); PushOneCommit.Result r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE); r.assertOkStatus(); assertThat(sender.getMessages()).isEmpty(); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER); r.assertOkStatus(); // no email notification about own changes assertThat(sender.getMessages()).isEmpty(); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER_REVIEWERS); r.assertOkStatus(); assertThat(sender.getMessages()).hasSize(1); Message m = sender.getMessages().get(0); if (notesMigration.readChanges()) { assertThat(m.rcpt()).containsExactly(user.emailAddress); } else { // CCs are considered reviewers in the storage layer and so get notified. assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress); } sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.ALL); r.assertOkStatus(); assertThat(sender.getMessages()).hasSize(1); m = sender.getMessages().get(0); assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress, user3.emailAddress); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + user3.email); r.assertOkStatus(); assertNotifyTo(user3); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + user3.email); r.assertOkStatus(); assertNotifyCc(user3); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + user3.email); r.assertOkStatus(); assertNotifyBcc(user3); // request that sender gets notified as TO, CC and BCC, email should be sent // even if the sender is the only recipient sender.clear(); pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + admin.email); assertNotifyTo(admin); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + admin.email); r.assertOkStatus(); assertNotifyCc(admin); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + admin.email); r.assertOkStatus(); assertNotifyBcc(admin); } @Test public void pushForMasterWithCc() throws Exception { // cc one user String topic = "my/topic"; PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email); r.assertOkStatus(); r.assertChange(Change.Status.NEW, topic, ImmutableList.of(), ImmutableList.of(user)); // cc several users r = pushTo( "refs/for/master/" + topic + "%cc=" + admin.email + ",cc=" + user.email + ",cc=" + accountCreator.user2().email); r.assertOkStatus(); // Check that admin isn't CC'd as they own the change r.assertChange( Change.Status.NEW, topic, ImmutableList.of(), ImmutableList.of(user, accountCreator.user2())); // cc non-existing user String nonExistingEmail = "non.existing@example.com"; r = pushTo( "refs/for/master/" + topic + "%cc=" + admin.email + ",cc=" + nonExistingEmail + ",cc=" + user.email); r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group"); } @Test public void pushForMasterWithCcByEmail() throws Exception { ConfigInput conf = new ConfigInput(); conf.enableReviewerByEmail = InheritableBoolean.TRUE; gApi.projects().name(project.get()).config(conf); PushOneCommit.Result r = pushTo("refs/for/master%cc=non.existing.1@example.com,cc=non.existing.2@example.com"); if (notesMigration.readChanges()) { r.assertOkStatus(); ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS); ImmutableList ccs = firstNonNull(ci.reviewers.get(ReviewerState.CC), ImmutableList.of()).stream() .sorted(comparing((AccountInfo a) -> a.email)) .collect(toImmutableList()); assertThat(ccs).hasSize(2); assertThat(ccs.get(0).email).isEqualTo("non.existing.1@example.com"); assertThat(ccs.get(0)._accountId).isNull(); assertThat(ccs.get(1).email).isEqualTo("non.existing.2@example.com"); assertThat(ccs.get(1)._accountId).isNull(); } else { r.assertErrorStatus("non.existing.1@example.com does not identify a registered user"); } } @Test public void pushForMasterWithCcGroup() throws Exception { TestAccount user2 = accountCreator.user2(); String group = name("group"); GroupInput gin = new GroupInput(); gin.name = group; gin.members = ImmutableList.of(user.username, user2.username); gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder. gApi.groups().create(gin); PushOneCommit.Result r = pushTo("refs/for/master%cc=" + group); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null, ImmutableList.of(), ImmutableList.of(user, user2)); } @Test public void pushForMasterWithReviewer() throws Exception { // add one reviewer String topic = "my/topic"; PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email); r.assertOkStatus(); r.assertChange(Change.Status.NEW, topic, user); // add several reviewers TestAccount user2 = accountCreator.create("another-user", "another.user@example.com", "Another User"); r = pushTo( "refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email + ",r=" + user2.email); r.assertOkStatus(); // admin is the owner of the change and should not appear as reviewer r.assertChange(Change.Status.NEW, topic, user, user2); // add non-existing user as reviewer String nonExistingEmail = "non.existing@example.com"; r = pushTo( "refs/for/master/" + topic + "%r=" + admin.email + ",r=" + nonExistingEmail + ",r=" + user.email); r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group"); } @Test public void pushForMasterWithReviewerByEmail() throws Exception { ConfigInput conf = new ConfigInput(); conf.enableReviewerByEmail = InheritableBoolean.TRUE; gApi.projects().name(project.get()).config(conf); PushOneCommit.Result r = pushTo("refs/for/master%r=non.existing.1@example.com,r=non.existing.2@example.com"); if (notesMigration.readChanges()) { r.assertOkStatus(); ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS); ImmutableList reviewers = firstNonNull(ci.reviewers.get(ReviewerState.REVIEWER), ImmutableList.of()) .stream() .sorted(comparing((AccountInfo a) -> a.email)) .collect(toImmutableList()); assertThat(reviewers).hasSize(2); assertThat(reviewers.get(0).email).isEqualTo("non.existing.1@example.com"); assertThat(reviewers.get(0)._accountId).isNull(); assertThat(reviewers.get(1).email).isEqualTo("non.existing.2@example.com"); assertThat(reviewers.get(1)._accountId).isNull(); } else { r.assertErrorStatus("non.existing.1@example.com does not identify a registered user"); } } @Test public void pushForMasterWithReviewerGroup() throws Exception { TestAccount user2 = accountCreator.user2(); String group = name("group"); GroupInput gin = new GroupInput(); gin.name = group; gin.members = ImmutableList.of(user.username, user2.username); gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder. gApi.groups().create(gin); PushOneCommit.Result r = pushTo("refs/for/master%r=" + group); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null, ImmutableList.of(user, user2), ImmutableList.of()); } @Test public void pushPrivateChange() throws Exception { // Push a private change. PushOneCommit.Result r = pushTo("refs/for/master%private"); r.assertOkStatus(); r.assertMessage(" [PRIVATE]"); assertThat(r.getChange().change().isPrivate()).isTrue(); // Pushing a new patch set without --private doesn't remove the privacy flag from the change. r = amendChange(r.getChangeId(), "refs/for/master"); r.assertOkStatus(); r.assertMessage(" [PRIVATE]"); assertThat(r.getChange().change().isPrivate()).isTrue(); // Remove the privacy flag from the change. r = amendChange(r.getChangeId(), "refs/for/master%remove-private"); r.assertOkStatus(); r.assertNotMessage(" [PRIVATE]"); assertThat(r.getChange().change().isPrivate()).isFalse(); // Normal push: privacy flag is not added back. r = amendChange(r.getChangeId(), "refs/for/master"); r.assertOkStatus(); r.assertNotMessage(" [PRIVATE]"); assertThat(r.getChange().change().isPrivate()).isFalse(); // Make the change private again. r = pushTo("refs/for/master%private"); r.assertOkStatus(); r.assertMessage(" [PRIVATE]"); assertThat(r.getChange().change().isPrivate()).isTrue(); // Can't use --private and --remove-private together. r = pushTo("refs/for/master%private,remove-private"); r.assertErrorStatus(); } @Test public void pushWorkInProgressChange() throws Exception { // Push a work-in-progress change. PushOneCommit.Result r = pushTo("refs/for/master%wip"); r.assertOkStatus(); r.assertMessage(" [WIP]"); assertThat(r.getChange().change().isWorkInProgress()).isTrue(); assertUploadTag(r.getChange(), ChangeMessagesUtil.TAG_UPLOADED_WIP_PATCH_SET); // Pushing a new patch set without --wip doesn't remove the wip flag from the change. r = amendChange(r.getChangeId(), "refs/for/master"); r.assertOkStatus(); r.assertMessage(" [WIP]"); assertThat(r.getChange().change().isWorkInProgress()).isTrue(); assertUploadTag(r.getChange(), ChangeMessagesUtil.TAG_UPLOADED_WIP_PATCH_SET); // Remove the wip flag from the change. r = amendChange(r.getChangeId(), "refs/for/master%ready"); r.assertOkStatus(); r.assertNotMessage(" [WIP]"); assertThat(r.getChange().change().isWorkInProgress()).isFalse(); assertUploadTag(r.getChange(), ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); // Normal push: wip flag is not added back. r = amendChange(r.getChangeId(), "refs/for/master"); r.assertOkStatus(); r.assertNotMessage(" [WIP]"); assertThat(r.getChange().change().isWorkInProgress()).isFalse(); assertUploadTag(r.getChange(), ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); // Make the change work-in-progress again. r = amendChange(r.getChangeId(), "refs/for/master%wip"); r.assertOkStatus(); r.assertMessage(" [WIP]"); assertThat(r.getChange().change().isWorkInProgress()).isTrue(); assertUploadTag(r.getChange(), ChangeMessagesUtil.TAG_UPLOADED_WIP_PATCH_SET); // Can't use --wip and --ready together. r = amendChange(r.getChangeId(), "refs/for/master%wip,ready"); r.assertErrorStatus(); } private void assertUploadTag(ChangeData cd, String expectedTag) throws Exception { List msgs = cd.messages(); assertThat(msgs).isNotEmpty(); assertThat(Iterables.getLast(msgs).getTag()).isEqualTo(expectedTag); } @Test public void pushWorkInProgressChangeWhenNotOwner() throws Exception { TestRepository userRepo = cloneProject(project, user); PushOneCommit.Result r = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master%wip"); r.assertOkStatus(); assertThat(r.getChange().change().getOwner()).isEqualTo(user.id); assertThat(r.getChange().change().isWorkInProgress()).isTrue(); // Admin user trying to move from WIP to ready should succeed. GitUtil.fetch(testRepo, r.getPatchSet().getRefName() + ":ps"); testRepo.reset("ps"); r = amendChange(r.getChangeId(), "refs/for/master%ready", user, testRepo); r.assertOkStatus(); // Other user trying to move from WIP to WIP should succeed. r = amendChange(r.getChangeId(), "refs/for/master%wip", admin, testRepo); r.assertOkStatus(); assertThat(r.getChange().change().isWorkInProgress()).isTrue(); // Push as change owner to move change from WIP to ready. r = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master%ready"); r.assertOkStatus(); assertThat(r.getChange().change().isWorkInProgress()).isFalse(); // Admin user trying to move from ready to WIP should succeed. GitUtil.fetch(testRepo, r.getPatchSet().getRefName() + ":ps"); testRepo.reset("ps"); r = amendChange(r.getChangeId(), "refs/for/master%wip", admin, testRepo); r.assertOkStatus(); // Other user trying to move from wip to wip should succeed. r = amendChange(r.getChangeId(), "refs/for/master%wip", admin, testRepo); r.assertOkStatus(); // Non owner, non admin and non project owner cannot flip wip bit: TestAccount user2 = accountCreator.user2(); grant( project, "refs/*", Permission.FORGE_COMMITTER, false, SystemGroupBackend.REGISTERED_USERS); TestRepository user2Repo = cloneProject(project, user2); GitUtil.fetch(user2Repo, r.getPatchSet().getRefName() + ":ps"); user2Repo.reset("ps"); r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo); r.assertErrorStatus(ReceiveConstants.ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP); // Project owner trying to move from WIP to ready should succeed. allow("refs/*", Permission.OWNER, SystemGroupBackend.REGISTERED_USERS); r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo); r.assertOkStatus(); } @Test public void pushForMasterAsEdit() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); Optional edit = getEdit(r.getChangeId()); assertThat(edit).isAbsent(); assertThat(query("has:edit")).isEmpty(); // specify edit as option r = amendChange(r.getChangeId(), "refs/for/master%edit"); r.assertOkStatus(); edit = getEdit(r.getChangeId()); assertThat(edit).isPresent(); EditInfo editInfo = edit.get(); r.assertMessage( "Updated Changes:\n " + canonicalWebUrl.get() + "c/" + project.get() + "/+/" + r.getChange().getId() + " " + editInfo.commit.subject + " [EDIT]\n"); // verify that the re-indexing was triggered for the change assertThat(query("has:edit")).hasSize(1); } @Test public void pushForMasterWithMessage() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master/%m=my_test_message"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); ChangeInfo ci = get(r.getChangeId(), MESSAGES, ALL_REVISIONS); Collection changeMessages = ci.messages; assertThat(changeMessages).hasSize(1); for (ChangeMessageInfo cm : changeMessages) { assertThat(cm.message).isEqualTo("Uploaded patch set 1.\nmy test message"); } Collection revisions = ci.revisions.values(); assertThat(revisions).hasSize(1); for (RevisionInfo ri : revisions) { assertThat(ri.description).isEqualTo("my test message"); } } @Test public void pushForMasterWithMessageTwiceWithDifferentMessages() throws Exception { enableCreateNewChangeForAllNotInTarget(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content"); // %2C is comma; the value below tests that percent decoding happens after splitting. // All three ways of representing space ("%20", "+", and "_" are also exercised. PushOneCommit.Result r = push.to("refs/for/master/%m=my_test%20+_message%2Cm="); r.assertOkStatus(); push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); r = push.to("refs/for/master/%m=new_test_message"); r.assertOkStatus(); ChangeInfo ci = get(r.getChangeId(), ALL_REVISIONS); Collection revisions = ci.revisions.values(); assertThat(revisions).hasSize(2); for (RevisionInfo ri : revisions) { if (ri.isCurrent) { assertThat(ri.description).isEqualTo("new test message"); } else { assertThat(ri.description).isEqualTo("my test message,m="); } } } @Test public void pushForMasterWithPercentEncodedMessage() throws Exception { // Exercise percent-encoding of UTF-8, underscores, and patterns reserved by git-rev-parse. PushOneCommit.Result r = pushTo( "refs/for/master/%m=" + "Punctu%2E%2e%2Eation%7E%2D%40%7Bu%7D%20%7C%20%28%E2%95%AF%C2%B0%E2%96%A1%C2%B0" + "%EF%BC%89%E2%95%AF%EF%B8%B5%20%E2%94%BB%E2%94%81%E2%94%BB%20%5E%5F%5E"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); ChangeInfo ci = get(r.getChangeId(), MESSAGES, ALL_REVISIONS); Collection changeMessages = ci.messages; assertThat(changeMessages).hasSize(1); for (ChangeMessageInfo cm : changeMessages) { assertThat(cm.message) .isEqualTo("Uploaded patch set 1.\nPunctu...ation~-@{u} | (╯°□°)╯︵ ┻━┻ ^_^"); } Collection revisions = ci.revisions.values(); assertThat(revisions).hasSize(1); for (RevisionInfo ri : revisions) { assertThat(ri.description).isEqualTo("Punctu...ation~-@{u} | (╯°□°)╯︵ ┻━┻ ^_^"); } } @Test public void pushForMasterWithInvalidPercentEncodedMessage() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master/%m=not_percent_decodable_%%oops%20"); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); ChangeInfo ci = get(r.getChangeId(), MESSAGES, ALL_REVISIONS); Collection changeMessages = ci.messages; assertThat(changeMessages).hasSize(1); for (ChangeMessageInfo cm : changeMessages) { assertThat(cm.message).isEqualTo("Uploaded patch set 1.\nnot percent decodable %%oops%20"); } Collection revisions = ci.revisions.values(); assertThat(revisions).hasSize(1); for (RevisionInfo ri : revisions) { assertThat(ri.description).isEqualTo("not percent decodable %%oops%20"); } } @Test public void pushForMasterWithApprovals() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review"); r.assertOkStatus(); ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS); LabelInfo cr = ci.labels.get("Code-Review"); assertThat(cr.all).hasSize(1); assertThat(cr.all.get(0).name).isEqualTo("Administrator"); assertThat(cr.all.get(0).value).isEqualTo(1); assertThat(Iterables.getLast(ci.messages).message) .isEqualTo("Uploaded patch set 1: Code-Review+1."); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); r = push.to("refs/for/master/%l=Code-Review+2"); ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS); cr = ci.labels.get("Code-Review"); assertThat(Iterables.getLast(ci.messages).message) .isEqualTo("Uploaded patch set 2: Code-Review+2."); // Check that the user who pushed the change was added as a reviewer since they added a vote assertThatUserIsOnlyReviewer(ci, admin); assertThat(cr.all).hasSize(1); assertThat(cr.all.get(0).name).isEqualTo("Administrator"); assertThat(cr.all.get(0).value).isEqualTo(2); push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "c.txt", "moreContent", r.getChangeId()); r = push.to("refs/for/master/%l=Code-Review+2"); ci = get(r.getChangeId(), MESSAGES); assertThat(Iterables.getLast(ci.messages).message).isEqualTo("Uploaded patch set 3."); } @Test public void pushNewPatchSetForMasterWithApprovals() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); r = push.to("refs/for/master/%l=Code-Review+2"); ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS); LabelInfo cr = ci.labels.get("Code-Review"); assertThat(Iterables.getLast(ci.messages).message) .isEqualTo("Uploaded patch set 2: Code-Review+2."); // Check that the user who pushed the new patch set was added as a reviewer since they added // a vote assertThatUserIsOnlyReviewer(ci, admin); assertThat(cr.all).hasSize(1); assertThat(cr.all.get(0).name).isEqualTo("Administrator"); assertThat(cr.all.get(0).value).isEqualTo(2); } @Test public void pushForMasterWithForgedAuthorAndCommitter() throws Exception { TestAccount user2 = accountCreator.user2(); // Create a commit with different forged author and committer. RevCommit c = commitBuilder() .author(user.getIdent()) .committer(user2.getIdent()) .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT) .message(PushOneCommit.SUBJECT) .create(); // Push commit as "Admnistrator". pushHead(testRepo, "refs/for/master"); String changeId = GitUtil.getChangeId(testRepo, c).get(); assertThat(getOwnerEmail(changeId)).isEqualTo(admin.email); assertThat(getReviewerEmails(changeId, ReviewerState.REVIEWER)) .containsExactly(user.email, user2.email); assertThat(sender.getMessages()).hasSize(1); assertThat(sender.getMessages().get(0).rcpt()) .containsExactly(user.emailAddress, user2.emailAddress); } @Test public void pushNewPatchSetForMasterWithForgedAuthorAndCommitter() throws Exception { TestAccount user2 = accountCreator.user2(); // First patch set has author and committer matching change owner. PushOneCommit.Result r = pushTo("refs/for/master"); assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email); assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER)).isEmpty(); amendBuilder() .author(user.getIdent()) .committer(user2.getIdent()) .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT + "2") .create(); pushHead(testRepo, "refs/for/master"); assertThat(getOwnerEmail(r.getChangeId())).isEqualTo(admin.email); assertThat(getReviewerEmails(r.getChangeId(), ReviewerState.REVIEWER)) .containsExactly(user.email, user2.email); assertThat(sender.getMessages()).hasSize(1); assertThat(sender.getMessages().get(0).rcpt()) .containsExactly(user.emailAddress, user2.emailAddress); } /** * There was a bug that allowed a user with Forge Committer Identity access right to upload a * commit and put *votes on behalf of another user* on it. This test checks that this is not * possible, but that the votes that are specified on push are applied only on behalf of the * uploader. * *

This particular bug only occurred when there was more than one label defined. However to * test that the votes that are specified on push are applied on behalf of the uploader a single * label is sufficient. */ @Test public void pushForMasterWithApprovalsForgeCommitterButNoForgeVote() throws Exception { // Create a commit with "User" as author and committer RevCommit c = commitBuilder() .author(user.getIdent()) .committer(user.getIdent()) .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT) .message(PushOneCommit.SUBJECT) .create(); // Push this commit as "Administrator" (requires Forge Committer Identity) pushHead(testRepo, "refs/for/master/%l=Code-Review+1", false); // Expected Code-Review votes: // 1. 0 from User (committer): // When the committer is forged, the committer is automatically added as // reviewer, hence we expect a dummy 0 vote for the committer. // 2. +1 from Administrator (uploader): // On push Code-Review+1 was specified, hence we expect a +1 vote from // the uploader. ChangeInfo ci = get(GitUtil.getChangeId(testRepo, c).get(), DETAILED_LABELS, MESSAGES, DETAILED_ACCOUNTS); LabelInfo cr = ci.labels.get("Code-Review"); assertThat(cr.all).hasSize(2); int indexAdmin = admin.fullName.equals(cr.all.get(0).name) ? 0 : 1; int indexUser = indexAdmin == 0 ? 1 : 0; assertThat(cr.all.get(indexAdmin).name).isEqualTo(admin.fullName); assertThat(cr.all.get(indexAdmin).value.intValue()).isEqualTo(1); assertThat(cr.all.get(indexUser).name).isEqualTo(user.fullName); assertThat(cr.all.get(indexUser).value.intValue()).isEqualTo(0); assertThat(Iterables.getLast(ci.messages).message) .isEqualTo("Uploaded patch set 1: Code-Review+1."); // Check that the user who pushed the change was added as a reviewer since they added a vote assertThatUserIsOnlyReviewer(ci, admin); } @Test public void pushWithMultipleApprovals() throws Exception { LabelType Q = category("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative")); AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); String heads = "refs/heads/*"; try (ProjectConfigUpdate u = updateProject(project)) { Util.allow(u.getConfig(), Permission.forLabel("Custom-Label"), -1, 1, anon, heads); u.getConfig().getLabelSections().put(Q.getName(), Q); u.save(); } RevCommit c = commitBuilder() .author(admin.getIdent()) .committer(admin.getIdent()) .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT) .message(PushOneCommit.SUBJECT) .create(); pushHead(testRepo, "refs/for/master/%l=Code-Review+1,l=Custom-Label-1", false); ChangeInfo ci = get(GitUtil.getChangeId(testRepo, c).get(), DETAILED_LABELS, DETAILED_ACCOUNTS); LabelInfo cr = ci.labels.get("Code-Review"); assertThat(cr.all).hasSize(1); cr = ci.labels.get("Custom-Label"); assertThat(cr.all).hasSize(1); // Check that the user who pushed the change was added as a reviewer since they added a vote assertThatUserIsOnlyReviewer(ci, admin); } @Test @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true") public void pushToRefsChangesAllowed() throws Exception { PushOneCommit.Result r = pushOneCommitToRefsChanges(); r.assertOkStatus(); } @Test public void pushNewPatchsetToRefsChanges() throws Exception { PushOneCommit.Result r = pushOneCommitToRefsChanges(); r.assertErrorStatus("upload to refs/changes not allowed"); } @Test @GerritConfig(name = "receive.allowPushToRefsChanges", value = "false") public void pushToRefsChangesNotAllowed() throws Exception { PushOneCommit.Result r = pushOneCommitToRefsChanges(); r.assertErrorStatus("upload to refs/changes not allowed"); } private PushOneCommit.Result pushOneCommitToRefsChanges() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); return push.to("refs/changes/" + r.getChange().change().getId().get()); } @Test public void pushNewPatchsetToPatchSetLockedChange() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1)); r = push.to("refs/for/master"); r.assertErrorStatus("cannot add patch set to " + r.getChange().change().getChangeId() + "."); } @Test public void pushForMasterWithApprovals_MissingLabel() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify"); r.assertErrorStatus("label \"Verify\" is not a configured label"); } @Test public void pushForMasterWithApprovals_ValueOutOfRange() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3"); r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value"); } @Test public void pushForNonExistingBranch() throws Exception { String branchName = "non-existing"; PushOneCommit.Result r = pushTo("refs/for/" + branchName); r.assertErrorStatus("branch " + branchName + " not found"); } @Test public void pushForMasterWithHashtags() throws Exception { // Hashtags only work when reading from NoteDB is enabled assume().that(notesMigration.readChanges()).isTrue(); // specify a single hashtag as option String hashtag1 = "tag1"; Set expected = ImmutableSet.of(hashtag1); PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); Set hashtags = gApi.changes().id(r.getChangeId()).getHashtags(); assertThat(hashtags).containsExactlyElementsIn(expected); // specify a single hashtag as option in new patch set String hashtag2 = "tag2"; PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); r = push.to("refs/for/master/%hashtag=" + hashtag2); r.assertOkStatus(); expected = ImmutableSet.of(hashtag1, hashtag2); hashtags = gApi.changes().id(r.getChangeId()).getHashtags(); assertThat(hashtags).containsExactlyElementsIn(expected); } @Test public void pushForMasterWithMultipleHashtags() throws Exception { // Hashtags only work when reading from NoteDB is enabled assume().that(notesMigration.readChanges()).isTrue(); // specify multiple hashtags as options String hashtag1 = "tag1"; String hashtag2 = "tag2"; Set expected = ImmutableSet.of(hashtag1, hashtag2); PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1 + ",hashtag=##" + hashtag2); r.assertOkStatus(); r.assertChange(Change.Status.NEW, null); Set hashtags = gApi.changes().id(r.getChangeId()).getHashtags(); assertThat(hashtags).containsExactlyElementsIn(expected); // specify multiple hashtags as options in new patch set String hashtag3 = "tag3"; String hashtag4 = "tag4"; PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); r = push.to("refs/for/master%hashtag=" + hashtag3 + ",hashtag=" + hashtag4); r.assertOkStatus(); expected = ImmutableSet.of(hashtag1, hashtag2, hashtag3, hashtag4); hashtags = gApi.changes().id(r.getChangeId()).getHashtags(); assertThat(hashtags).containsExactlyElementsIn(expected); } @Test public void pushForMasterWithHashtagsNoteDbDisabled() throws Exception { // Push with hashtags should fail when reading from NoteDb is disabled. assume().that(notesMigration.readChanges()).isFalse(); PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1"); r.assertErrorStatus("cannot add hashtags; noteDb is disabled"); } @Test public void pushCommitUsingSignedOffBy() throws Exception { PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent"); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); setUseSignedOffBy(InheritableBoolean.TRUE); block(project, "refs/heads/master", Permission.FORGE_COMMITTER, REGISTERED_USERS); push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT + String.format("\n\nSigned-off-by: %s <%s>", admin.fullName, admin.email), "b.txt", "anotherContent"); r = push.to("refs/for/master"); r.assertOkStatus(); push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent"); r = push.to("refs/for/master"); r.assertErrorStatus("not Signed-off-by author/committer/uploader in message footer"); } @Test public void createNewChangeForAllNotInTarget() throws Exception { enableCreateNewChangeForAllNotInTarget(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content"); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent"); r = push.to("refs/for/master"); r.assertOkStatus(); gApi.projects().name(project.get()).branch("otherBranch").create(new BranchInput()); PushOneCommit.Result r2 = push.to("refs/for/otherBranch"); r2.assertOkStatus(); assertTwoChangesWithSameRevision(r); } @Test public void pushChangeBasedOnChangeOfOtherUserWithCreateNewChangeForAllNotInTarget() throws Exception { enableCreateNewChangeForAllNotInTarget(); // create a change as admin PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content"); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); RevCommit commitChange1 = r.getCommit(); // create a second change as user (depends on the change from admin) TestRepository userRepo = cloneProject(project, user); GitUtil.fetch(userRepo, r.getPatchSet().getRefName() + ":change"); userRepo.reset("change"); push = pushFactory.create( db, user.getIdent(), userRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent"); r = push.to("refs/for/master"); r.assertOkStatus(); // assert that no new change was created for the commit of the predecessor change assertThat(query(commitChange1.name())).hasSize(1); } @Test public void pushSameCommitTwiceUsingMagicBranchBaseOption() throws Exception { grant(project, "refs/heads/master", Permission.PUSH); PushOneCommit.Result rBase = pushTo("refs/heads/master"); rBase.assertOkStatus(); gApi.projects().name(project.get()).branch("foo").create(new BranchInput()); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent"); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); PushResult pr = GitUtil.pushHead(testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false); assertThat(pr.getMessages()).containsMatch("changes: .*new: 1.*done"); // BatchUpdate implementations differ in how they hook into progress monitors. We mostly just // care that there is a new change. assertThat(pr.getMessages()).containsMatch("changes: .*new: 1.*done"); assertTwoChangesWithSameRevision(r); } @Test public void pushSameCommitTwice() throws Exception { try (ProjectConfigUpdate u = updateProject(project)) { u.getConfig() .getProject() .setBooleanConfig( BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE); u.save(); } PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content"); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent"); r = push.to("refs/for/master"); r.assertOkStatus(); assertPushRejected( pushHead(testRepo, "refs/for/master", false), "refs/for/master", "commit(s) already exists (as current patchset)"); } @Test public void pushSameCommitTwiceWhenIndexFailed() throws Exception { try (ProjectConfigUpdate u = updateProject(project)) { u.getConfig() .getProject() .setBooleanConfig( BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE); u.save(); } PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content"); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent"); r = push.to("refs/for/master"); r.assertOkStatus(); indexer.delete(r.getChange().getId()); assertPushRejected( pushHead(testRepo, "refs/for/master", false), "refs/for/master", "commit(s) already exists (as current patchset)"); } private void assertTwoChangesWithSameRevision(PushOneCommit.Result result) throws Exception { List changes = query(result.getCommit().name()); assertThat(changes).hasSize(2); ChangeInfo c1 = get(changes.get(0).id, CURRENT_REVISION); ChangeInfo c2 = get(changes.get(1).id, CURRENT_REVISION); assertThat(c1.project).isEqualTo(c2.project); assertThat(c1.branch).isNotEqualTo(c2.branch); assertThat(c1.changeId).isEqualTo(c2.changeId); assertThat(c1.currentRevision).isEqualTo(c2.currentRevision); } @Test public void pushAFewChanges() throws Exception { testPushAFewChanges(); } @Test public void pushAFewChangesWithCreateNewChangeForAllNotInTarget() throws Exception { enableCreateNewChangeForAllNotInTarget(); testPushAFewChanges(); } private void testPushAFewChanges() throws Exception { int n = 10; String r = "refs/for/master"; ObjectId initialHead = testRepo.getRepository().resolve("HEAD"); List commits = createChanges(n, r); // Check that a change was created for each. for (RevCommit c : commits) { assertThat(byCommit(c).change().getSubject()) .named("change for " + c.name()) .isEqualTo(c.getShortMessage()); } List commits2 = amendChanges(initialHead, commits, r); // Check that there are correct patch sets. for (int i = 0; i < n; i++) { RevCommit c = commits.get(i); RevCommit c2 = commits2.get(i); String name = "change for " + c2.name(); ChangeData cd = byCommit(c); assertThat(cd.change().getSubject()).named(name).isEqualTo(c2.getShortMessage()); assertThat(getPatchSetRevisions(cd)) .named(name) .containsExactlyEntriesIn(ImmutableMap.of(1, c.name(), 2, c2.name())); } // Pushing again results in "no new changes". assertPushRejected(pushHead(testRepo, r, false), r, "no new changes"); } @Test public void pushWithoutChangeId() throws Exception { testPushWithoutChangeId(); } @Test public void pushWithoutChangeIdWithCreateNewChangeForAllNotInTarget() throws Exception { enableCreateNewChangeForAllNotInTarget(); testPushWithoutChangeId(); } private void testPushWithoutChangeId() throws Exception { RevCommit c = createCommit(testRepo, "Message without Change-Id"); assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty(); pushForReviewRejected(testRepo, "missing Change-Id in message footer"); setRequireChangeId(InheritableBoolean.FALSE); pushForReviewOk(testRepo); } @Test public void errorMessageFormat() throws Exception { RevCommit c = createCommit(testRepo, "Message without Change-Id"); assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty(); String ref = "refs/for/master"; PushResult r = pushHead(testRepo, ref); RemoteRefUpdate refUpdate = r.getRemoteUpdate(ref); assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON); String reason = String.format( "commit %s: missing Change-Id in message footer", c.toObjectId().abbreviate(7).name()); assertThat(refUpdate.getMessage()).isEqualTo(reason); assertThat(r.getMessages()).contains("\nERROR: " + reason); } @Test @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true") public void testPushWithChangedChangeId() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT + "\n\n" + "Change-Id: I55eab7c7a76e95005fa9cc469aa8f9fc16da9eba\n", "b.txt", "anotherContent", r.getChangeId()); r = push.to("refs/changes/" + r.getChange().change().getId().get()); r.assertErrorStatus( String.format( "commit %s: %s", r.getCommit().abbreviate(RevId.ABBREV_LEN).name(), ChangeIdValidator.CHANGE_ID_MISMATCH_MSG)); } @Test public void pushWithMultipleChangeIds() throws Exception { testPushWithMultipleChangeIds(); } @Test public void pushWithMultipleChangeIdsWithCreateNewChangeForAllNotInTarget() throws Exception { enableCreateNewChangeForAllNotInTarget(); testPushWithMultipleChangeIds(); } private void testPushWithMultipleChangeIds() throws Exception { createCommit( testRepo, "Message with multiple Change-Id\n" + "\n" + "Change-Id: I10f98c2ef76e52e23aa23be5afeb71e40b350e86\n" + "Change-Id: Ie9a132e107def33bdd513b7854b50de911edba0a\n"); pushForReviewRejected(testRepo, "multiple Change-Id lines in message footer"); setRequireChangeId(InheritableBoolean.FALSE); pushForReviewRejected(testRepo, "multiple Change-Id lines in message footer"); } @Test public void pushWithInvalidChangeId() throws Exception { testpushWithInvalidChangeId(); } @Test public void pushWithInvalidChangeIdWithCreateNewChangeForAllNotInTarget() throws Exception { enableCreateNewChangeForAllNotInTarget(); testpushWithInvalidChangeId(); } private void testpushWithInvalidChangeId() throws Exception { createCommit(testRepo, "Message with invalid Change-Id\n\nChange-Id: X\n"); pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer"); setRequireChangeId(InheritableBoolean.FALSE); pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer"); } @Test public void pushWithInvalidChangeIdFromEgit() throws Exception { testPushWithInvalidChangeIdFromEgit(); } @Test public void pushWithInvalidChangeIdFromEgitWithCreateNewChangeForAllNotInTarget() throws Exception { enableCreateNewChangeForAllNotInTarget(); testPushWithInvalidChangeIdFromEgit(); } private void testPushWithInvalidChangeIdFromEgit() throws Exception { createCommit( testRepo, "Message with invalid Change-Id\n" + "\n" + "Change-Id: I0000000000000000000000000000000000000000\n"); pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer"); setRequireChangeId(InheritableBoolean.FALSE); pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer"); } @Test public void pushWithChangeIdInSubjectLine() throws Exception { createCommit(testRepo, "Change-Id: I1234000000000000000000000000000000000000"); pushForReviewRejected(testRepo, "missing subject; Change-Id must be in message footer"); setRequireChangeId(InheritableBoolean.FALSE); pushForReviewRejected(testRepo, "missing subject; Change-Id must be in message footer"); } @Test public void pushCommitWithSameChangeIdAsPredecessorChange() throws Exception { PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content"); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); RevCommit commitChange1 = r.getCommit(); createCommit(testRepo, commitChange1.getFullMessage()); pushForReviewRejected( testRepo, "same Change-Id in multiple changes.\n" + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each" + " commit"); try (ProjectConfigUpdate u = updateProject(project)) { u.getConfig() .getProject() .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE); u.save(); } pushForReviewRejected( testRepo, "same Change-Id in multiple changes.\n" + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each" + " commit"); } @Test public void pushTwoCommitWithSameChangeId() throws Exception { RevCommit commitChange1 = createCommitWithChangeId(testRepo, "some change"); createCommit(testRepo, commitChange1.getFullMessage()); pushForReviewRejected( testRepo, "same Change-Id in multiple changes.\n" + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each" + " commit"); try (ProjectConfigUpdate u = updateProject(project)) { u.getConfig() .getProject() .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE); u.save(); } pushForReviewRejected( testRepo, "same Change-Id in multiple changes.\n" + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each" + " commit"); } private static RevCommit createCommit(TestRepository testRepo, String message) throws Exception { return testRepo.branch("HEAD").commit().message(message).add("a.txt", "content").create(); } private static RevCommit createCommitWithChangeId(TestRepository testRepo, String message) throws Exception { RevCommit c = testRepo .branch("HEAD") .commit() .message(message) .insertChangeId() .add("a.txt", "content") .create(); return testRepo.getRevWalk().parseCommit(c); } @Test public void cantAutoCloseChangeAlreadyMergedToBranch() throws Exception { PushOneCommit.Result r1 = createChange(); Change.Id id1 = r1.getChange().getId(); PushOneCommit.Result r2 = createChange(); Change.Id id2 = r2.getChange().getId(); // Merge change 1 behind Gerrit's back. try (Repository repo = repoManager.openRepository(project)) { TestRepository tr = new TestRepository<>(repo); tr.branch("refs/heads/master").update(r1.getCommit()); } assertThat(gApi.changes().id(id1.get()).info().status).isEqualTo(ChangeStatus.NEW); assertThat(gApi.changes().id(id2.get()).info().status).isEqualTo(ChangeStatus.NEW); r2 = amendChange(r2.getChangeId()); r2.assertOkStatus(); // Change 1 is still new despite being merged into the branch, because // ReceiveCommits only considers commits between the branch tip (which is // now the merged change 1) and the push tip (new patch set of change 2). assertThat(gApi.changes().id(id1.get()).info().status).isEqualTo(ChangeStatus.NEW); assertThat(gApi.changes().id(id2.get()).info().status).isEqualTo(ChangeStatus.NEW); } @Test @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true") public void accidentallyPushNewPatchSetDirectlyToBranchAndRecoverByPushingToRefsChanges() throws Exception { Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch(); ChangeData cd = byChangeId(id); String ps1Rev = Iterables.getOnlyElement(cd.patchSets()).getRevision().get(); String r = "refs/changes/" + id; assertPushOk(pushHead(testRepo, r, false), r); // Added a new patch set and auto-closed the change. cd = byChangeId(id); assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED); assertThat(getPatchSetRevisions(cd)) .containsExactlyEntriesIn( ImmutableMap.of(1, ps1Rev, 2, testRepo.getRepository().resolve("HEAD").name())); } @Test public void accidentallyPushNewPatchSetDirectlyToBranchAndCantRecoverByPushingToRefsFor() throws Exception { Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch(); ChangeData cd = byChangeId(id); String ps1Rev = Iterables.getOnlyElement(cd.patchSets()).getRevision().get(); String r = "refs/for/master"; assertPushRejected(pushHead(testRepo, r, false), r, "no new changes"); // Change not updated. cd = byChangeId(id); assertThat(cd.change().getStatus()).isEqualTo(Change.Status.NEW); assertThat(getPatchSetRevisions(cd)).containsExactlyEntriesIn(ImmutableMap.of(1, ps1Rev)); } @Test public void forcePushAbandonedChange() throws Exception { grant(project, "refs/*", Permission.PUSH, true); PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content"); PushOneCommit.Result r = push1.to("refs/for/master"); r.assertOkStatus(); // abandon the change String changeId = r.getChangeId(); assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); gApi.changes().id(changeId).abandon(); ChangeInfo info = get(changeId); assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); push1.setForce(true); PushOneCommit.Result r1 = push1.to("refs/heads/master"); r1.assertOkStatus(); ChangeInfo result = Iterables.getOnlyElement(gApi.changes().query(r.getChangeId()).get()); assertThat(result.status).isEqualTo(ChangeStatus.MERGED); } private Change.Id accidentallyPushNewPatchSetDirectlyToBranch() throws Exception { PushOneCommit.Result r = createChange(); RevCommit ps1Commit = r.getCommit(); Change c = r.getChange().change(); RevCommit ps2Commit; try (Repository repo = repoManager.openRepository(project)) { // Create a new patch set of the change directly in Gerrit's repository, // without pushing it. In reality it's more likely that the client would // create and push this behind Gerrit's back (e.g. an admin accidentally // using direct ssh access to the repo), but that's harder to do in tests. TestRepository tr = new TestRepository<>(repo); ps2Commit = tr.branch("refs/heads/master") .commit() .message(ps1Commit.getShortMessage() + " v2") .insertChangeId(r.getChangeId().substring(1)) .create(); } testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/master")).call(); testRepo.reset(ps2Commit); ChangeData cd = byCommit(ps1Commit); assertThat(cd.change().getStatus()).isEqualTo(Change.Status.NEW); assertThat(getPatchSetRevisions(cd)) .containsExactlyEntriesIn(ImmutableMap.of(1, ps1Commit.name())); return c.getId(); } @Test public void pushWithEmailInFooter() throws Exception { pushWithReviewerInFooter(user.emailAddress.toString(), user); } @Test public void pushWithNameInFooter() throws Exception { pushWithReviewerInFooter(user.fullName, user); } @Test public void pushWithEmailInFooterNotFound() throws Exception { pushWithReviewerInFooter(new Address("No Body", "notarealuser@example.com").toString(), null); } @Test public void pushWithNameInFooterNotFound() throws Exception { pushWithReviewerInFooter("Notauser", null); } @Test public void pushNewPatchsetOverridingStickyLabel() throws Exception { try (ProjectConfigUpdate u = updateProject(project)) { LabelType codeReview = Util.codeReview(); codeReview.setCopyMaxScore(true); u.getConfig().getLabelSections().put(codeReview.getName(), codeReview); u.save(); } PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review+2"); r.assertOkStatus(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId()); r = push.to("refs/for/master%l=Code-Review+1"); r.assertOkStatus(); } @Test public void createChangeForMergedCommit() throws Exception { String master = "refs/heads/master"; grant(project, master, Permission.PUSH, true); // Update master with a direct push. RevCommit c1 = testRepo.commit().message("Non-change 1").create(); RevCommit c2 = testRepo.parseBody( testRepo.commit().parent(c1).message("Non-change 2").insertChangeId().create()); String changeId = Iterables.getOnlyElement(c2.getFooterLines(CHANGE_ID)); testRepo.reset(c2); assertPushOk(pushHead(testRepo, master, false, true), master); String q = "commit:" + c1.name() + " OR commit:" + c2.name() + " OR change:" + changeId; assertThat(gApi.changes().query(q).get()).isEmpty(); // Push c2 as a merged change. String r = "refs/for/master%merged"; assertPushOk(pushHead(testRepo, r, false), r); EnumSet opts = EnumSet.of(ListChangesOption.CURRENT_REVISION); ChangeInfo info = gApi.changes().id(changeId).get(opts); assertThat(info.currentRevision).isEqualTo(c2.name()); assertThat(info.status).isEqualTo(ChangeStatus.MERGED); // Only c2 was created as a change. String q1 = "commit: " + c1.name(); assertThat(gApi.changes().query(q1).get()).isEmpty(); // Push c1 as a merged change. testRepo.reset(c1); assertPushOk(pushHead(testRepo, r, false), r); List infos = gApi.changes().query(q1).withOptions(opts).get(); assertThat(infos).hasSize(1); info = infos.get(0); assertThat(info.currentRevision).isEqualTo(c1.name()); assertThat(info.status).isEqualTo(ChangeStatus.MERGED); } @Test public void mergedOptionFailsWhenCommitIsNotMerged() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master%merged"); r.assertErrorStatus("not merged into branch"); } @Test public void mergedOptionFailsWhenCommitIsMergedOnOtherBranch() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(r.getChangeId()).current().submit(); try (Repository repo = repoManager.openRepository(project)) { TestRepository tr = new TestRepository<>(repo); tr.branch("refs/heads/branch").commit().message("Initial commit on branch").create(); } pushTo("refs/for/master%merged").assertErrorStatus("not merged into branch"); } @Test public void mergedOptionFailsWhenChangeExists() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(r.getChangeId()).current().submit(); testRepo.reset(r.getCommit()); String ref = "refs/for/master%merged"; PushResult pr = pushHead(testRepo, ref, false); RemoteRefUpdate rru = pr.getRemoteUpdate(ref); assertThat(rru.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON); assertThat(rru.getMessage()).contains("no new changes"); } @Test public void mergedOptionWithNewCommitWithSameChangeIdFails() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(r.getChangeId()).current().submit(); RevCommit c2 = testRepo .amend(r.getCommit()) .message("New subject") .insertChangeId(r.getChangeId().substring(1)) .create(); testRepo.reset(c2); String ref = "refs/for/master%merged"; PushResult pr = pushHead(testRepo, ref, false); RemoteRefUpdate rru = pr.getRemoteUpdate(ref); assertThat(rru.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON); assertThat(rru.getMessage()).contains("not merged into branch"); } @Test public void mergedOptionWithExistingChangeInsertsPatchSet() throws Exception { String master = "refs/heads/master"; grant(project, master, Permission.PUSH, true); PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); ObjectId c1 = r.getCommit().copy(); // Create a PS2 commit directly on master in the server's repo. This // simulates the client amending locally and pushing directly to the branch, // expecting the change to be auto-closed, but the change metadata update // fails. ObjectId c2; try (Repository repo = repoManager.openRepository(project)) { TestRepository tr = new TestRepository<>(repo); RevCommit commit2 = tr.amend(c1).message("New subject").insertChangeId(r.getChangeId().substring(1)).create(); c2 = commit2.copy(); tr.update(master, c2); } testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/master")).call(); testRepo.reset(c2); String ref = "refs/for/master%merged"; assertPushOk(pushHead(testRepo, ref, false), ref); ChangeInfo info = gApi.changes().id(r.getChangeId()).get(ALL_REVISIONS); assertThat(info.currentRevision).isEqualTo(c2.name()); assertThat(info.revisions.keySet()).containsExactly(c1.name(), c2.name()); // TODO(dborowitz): Fix ReceiveCommits to also auto-close the change. assertThat(info.status).isEqualTo(ChangeStatus.NEW); } @Test public void publishCommentsOnPushPublishesDraftsOnAllRevisions() throws Exception { PushOneCommit.Result r = createChange(); String rev1 = r.getCommit().name(); CommentInfo c1 = addDraft(r.getChangeId(), rev1, newDraft(FILE_NAME, 1, "comment1")); CommentInfo c2 = addDraft(r.getChangeId(), rev1, newDraft(FILE_NAME, 1, "comment2")); r = amendChange(r.getChangeId()); String rev2 = r.getCommit().name(); CommentInfo c3 = addDraft(r.getChangeId(), rev2, newDraft(FILE_NAME, 1, "comment3")); assertThat(getPublishedComments(r.getChangeId())).isEmpty(); gApi.changes().id(r.getChangeId()).addReviewer(user.email); sender.clear(); amendChange(r.getChangeId(), "refs/for/master%publish-comments"); Collection comments = getPublishedComments(r.getChangeId()); assertThat(comments.stream().map(c -> c.id)).containsExactly(c1.id, c2.id, c3.id); assertThat(comments.stream().map(c -> c.message)) .containsExactly("comment1", "comment2", "comment3"); assertThat(getLastMessage(r.getChangeId())).isEqualTo("Uploaded patch set 3.\n\n(3 comments)"); List messages = sender.getMessages().stream() .map(Message::body) .sorted(Comparator.comparingInt(m -> m.contains("reexamine") ? 0 : 1)) .collect(toList()); assertThat(messages).hasSize(2); assertThat(messages.get(0)).contains("Gerrit-MessageType: newpatchset"); assertThat(messages.get(0)).contains("I'd like you to reexamine a change"); assertThat(messages.get(0)).doesNotContain("Uploaded patch set 3"); assertThat(messages.get(1)).contains("Gerrit-MessageType: comment"); assertThat(messages.get(1)) .containsMatch( Pattern.compile( // A little weird that the comment email contains this text, but it's actually // what's in the ChangeMessage. Really we should fuse the emails into one, but until // then, this test documents the current behavior. "Uploaded patch set 3\\.\n" + "\n" + "\\(3 comments\\)\\n.*" + "PS1, Line 1:.*" + "comment1\\n.*" + "PS1, Line 1:.*" + "comment2\\n.*" + "PS2, Line 1:.*" + "comment3\\n", Pattern.DOTALL)); } @Test public void publishCommentsOnPushWithMessage() throws Exception { PushOneCommit.Result r = createChange(); String rev = r.getCommit().name(); addDraft(r.getChangeId(), rev, newDraft(FILE_NAME, 1, "comment1")); r = amendChange(r.getChangeId(), "refs/for/master%publish-comments,m=The_message"); Collection comments = getPublishedComments(r.getChangeId()); assertThat(comments.stream().map(c -> c.message)).containsExactly("comment1"); assertThat(getLastMessage(r.getChangeId())) .isEqualTo("Uploaded patch set 2.\n\n(1 comment)\n\nThe message"); } @Test public void publishCommentsOnPushPublishesDraftsOnMultipleChanges() throws Exception { ObjectId initialHead = testRepo.getRepository().resolve("HEAD"); List commits = createChanges(2, "refs/for/master"); String id1 = byCommit(commits.get(0)).change().getKey().get(); String id2 = byCommit(commits.get(1)).change().getKey().get(); CommentInfo c1 = addDraft(id1, commits.get(0).name(), newDraft(FILE_NAME, 1, "comment1")); CommentInfo c2 = addDraft(id2, commits.get(1).name(), newDraft(FILE_NAME, 1, "comment2")); assertThat(getPublishedComments(id1)).isEmpty(); assertThat(getPublishedComments(id2)).isEmpty(); amendChanges(initialHead, commits, "refs/for/master%publish-comments"); Collection cs1 = getPublishedComments(id1); assertThat(cs1.stream().map(c -> c.message)).containsExactly("comment1"); assertThat(cs1.stream().map(c -> c.id)).containsExactly(c1.id); assertThat(getLastMessage(id1)) .isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)"); Collection cs2 = getPublishedComments(id2); assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2"); assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id); assertThat(getLastMessage(id2)) .isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)"); } @Test public void publishCommentsOnPushOnlyPublishesDraftsOnUpdatedChanges() throws Exception { PushOneCommit.Result r1 = createChange(); PushOneCommit.Result r2 = createChange(); String id1 = r1.getChangeId(); String id2 = r2.getChangeId(); addDraft(id1, r1.getCommit().name(), newDraft(FILE_NAME, 1, "comment1")); CommentInfo c2 = addDraft(id2, r2.getCommit().name(), newDraft(FILE_NAME, 1, "comment2")); assertThat(getPublishedComments(id1)).isEmpty(); assertThat(getPublishedComments(id2)).isEmpty(); amendChange(id2, "refs/for/master%publish-comments"); assertThat(getPublishedComments(id1)).isEmpty(); assertThat(gApi.changes().id(id1).drafts()).hasSize(1); Collection cs2 = getPublishedComments(id2); assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2"); assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id); assertThat(getLastMessage(id1)).doesNotMatch("[Cc]omment"); assertThat(getLastMessage(id2)).isEqualTo("Uploaded patch set 2.\n\n(1 comment)"); } @Test public void publishCommentsOnPushWithPreference() throws Exception { PushOneCommit.Result r = createChange(); addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1")); r = amendChange(r.getChangeId()); assertThat(getPublishedComments(r.getChangeId())).isEmpty(); GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences(); prefs.publishCommentsOnPush = true; gApi.accounts().id(admin.id.get()).setPreferences(prefs); r = amendChange(r.getChangeId()); assertThat(getPublishedComments(r.getChangeId()).stream().map(c -> c.message)) .containsExactly("comment1"); } @Test public void publishCommentsOnPushOverridingPreference() throws Exception { PushOneCommit.Result r = createChange(); addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1")); GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences(); prefs.publishCommentsOnPush = true; gApi.accounts().id(admin.id.get()).setPreferences(prefs); r = amendChange(r.getChangeId(), "refs/for/master%no-publish-comments"); assertThat(getPublishedComments(r.getChangeId())).isEmpty(); } @Test public void pushWithDraftOptionIsDisabledPerDefault() throws Exception { for (String ref : ImmutableSet.of("refs/drafts/master", "refs/for/master%draft")) { PushOneCommit.Result r = pushTo(ref); r.assertErrorStatus(); r.assertMessage("draft workflow is disabled"); } } @GerritConfig(name = "change.allowDrafts", value = "true") @Test public void pushDraftGetsPrivateChange() throws Exception { String changeId1 = createChange("refs/drafts/master").getChangeId(); String changeId2 = createChange("refs/for/master%draft").getChangeId(); ChangeInfo info1 = gApi.changes().id(changeId1).get(); ChangeInfo info2 = gApi.changes().id(changeId2).get(); assertThat(info1.status).isEqualTo(ChangeStatus.NEW); assertThat(info2.status).isEqualTo(ChangeStatus.NEW); assertThat(info1.isPrivate).isTrue(); assertThat(info2.isPrivate).isTrue(); assertThat(info1.revisions).hasSize(1); assertThat(info2.revisions).hasSize(1); } @GerritConfig(name = "change.allowDrafts", value = "true") @Sandboxed @Test public void pushWithDraftOptionToExistingNewChangeGetsChangeEdit() throws Exception { String changeId = createChange().getChangeId(); EditInfoSubject.assertThat(getEdit(changeId)).isAbsent(); ChangeInfo changeInfo = gApi.changes().id(changeId).get(); ChangeStatus originalChangeStatus = changeInfo.status; PushOneCommit.Result result = amendChange(changeId, "refs/drafts/master"); result.assertOkStatus(); changeInfo = gApi.changes().id(changeId).get(); assertThat(changeInfo.status).isEqualTo(originalChangeStatus); assertThat(changeInfo.isPrivate).isNull(); assertThat(changeInfo.revisions).hasSize(1); EditInfoSubject.assertThat(getEdit(changeId)).isPresent(); } @GerritConfig(name = "receive.maxBatchCommits", value = "2") @Test public void maxBatchCommits() throws Exception { List commits = new ArrayList<>(); commits.addAll(initChanges(2)); String master = "refs/heads/master"; assertPushOk(pushHead(testRepo, master), master); commits.addAll(initChanges(3)); assertPushRejected( pushHead(testRepo, master), master, "more than 2 commits, and skip-validation not set"); grantSkipValidation(project, master, SystemGroupBackend.REGISTERED_USERS); PushResult r = pushHead(testRepo, master, false, false, ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION)); assertPushOk(r, master); // No open changes; branch was advanced. String q = commits.stream().map(ObjectId::name).collect(joining(" OR commit:", "commit:", "")); assertThat(gApi.changes().query(q).get()).isEmpty(); assertThat(gApi.projects().name(project.get()).branch(master).get().revision) .isEqualTo(Iterables.getLast(commits).name()); } @Test public void pushToPublishMagicBranchIsAllowed() throws Exception { // Push to "refs/publish/*" will be a synonym of "refs/for/*". createChange("refs/publish/master"); PushOneCommit.Result result = pushTo("refs/publish/master"); result.assertOkStatus(); assertThat(result.getMessage()) .endsWith("Pushing to refs/publish/* is deprecated, use refs/for/* instead.\n"); } @Test public void pushNoteDbRef() throws Exception { String ref = "refs/changes/34/1234/meta"; RevCommit c = testRepo.commit().message("Junk NoteDb commit").create(); PushResult pr = pushOne(testRepo, c.name(), ref, false, false, null); assertThat(pr.getMessages()).doesNotContain(NoteDbPushOption.OPTION_NAME); assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow"); pr = pushOne(testRepo, c.name(), ref, false, false, ImmutableList.of("notedb=foobar")); assertThat(pr.getMessages()).contains("Invalid value in -o notedb=foobar"); assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow"); List opts = ImmutableList.of("notedb=allow"); pr = pushOne(testRepo, c.name(), ref, false, false, opts); assertPushRejected(pr, ref, "NoteDb update requires access database permission"); allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); pr = pushOne(testRepo, c.name(), ref, false, false, opts); assertPushRejected(pr, ref, "prohibited by Gerrit: not permitted: create"); grant(project, "refs/changes/*", Permission.CREATE); grant(project, "refs/changes/*", Permission.PUSH); grantSkipValidation(project, "refs/changes/*", REGISTERED_USERS); pr = pushOne(testRepo, c.name(), ref, false, false, opts); assertPushOk(pr, ref); } @Test public void pushNoteDbRefWithoutOptionOnlyFailsThatCommand() throws Exception { String ref = "refs/changes/34/1234/meta"; RevCommit noteDbCommit = testRepo.commit().message("Junk NoteDb commit").create(); RevCommit changeCommit = testRepo.branch("HEAD").commit().message("A change").insertChangeId().create(); PushResult pr = Iterables.getOnlyElement( testRepo .git() .push() .setRefSpecs( new RefSpec(noteDbCommit.name() + ":" + ref), new RefSpec(changeCommit.name() + ":refs/heads/permitted")) .call()); assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow"); assertPushOk(pr, "refs/heads/permitted"); } private DraftInput newDraft(String path, int line, String message) { DraftInput d = new DraftInput(); d.path = path; d.side = Side.REVISION; d.line = line; d.message = message; d.unresolved = true; return d; } private CommentInfo addDraft(String changeId, String revId, DraftInput in) throws Exception { return gApi.changes().id(changeId).revision(revId).createDraft(in).get(); } private Collection getPublishedComments(String changeId) throws Exception { return gApi.changes().id(changeId).comments().values().stream() .flatMap(Collection::stream) .collect(toList()); } private String getLastMessage(String changeId) throws Exception { return Streams.findLast( gApi.changes().id(changeId).get(MESSAGES).messages.stream().map(m -> m.message)) .get(); } private void assertThatUserIsOnlyReviewer(ChangeInfo ci, TestAccount reviewer) { assertThat(ci.reviewers).isNotNull(); assertThat(ci.reviewers.keySet()).containsExactly(ReviewerState.REVIEWER); assertThat(ci.reviewers.get(ReviewerState.REVIEWER).iterator().next().email) .isEqualTo(reviewer.email); } private void pushWithReviewerInFooter(String nameEmail, TestAccount expectedReviewer) throws Exception { int n = 5; String r = "refs/for/master"; ObjectId initialHead = testRepo.getRepository().resolve("HEAD"); List commits = createChanges(n, r, ImmutableList.of("Acked-By: " + nameEmail)); for (int i = 0; i < n; i++) { RevCommit c = commits.get(i); ChangeData cd = byCommit(c); String name = "reviewers for " + (i + 1); if (expectedReviewer != null) { assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.getId()); // Remove reviewer from PS1 so we can test adding this same reviewer on PS2 below. gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.getId().toString()).remove(); } assertThat(byCommit(c).reviewers().all()).named(name).isEmpty(); } List commits2 = amendChanges(initialHead, commits, r); for (int i = 0; i < n; i++) { RevCommit c = commits2.get(i); ChangeData cd = byCommit(c); String name = "reviewers for " + (i + 1); if (expectedReviewer != null) { assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.getId()); } else { assertThat(byCommit(c).reviewers().all()).named(name).isEmpty(); } } } private List createChanges(int n, String refsFor) throws Exception { return createChanges(n, refsFor, ImmutableList.of()); } private List createChanges(int n, String refsFor, List footerLines) throws Exception { List commits = initChanges(n, footerLines); assertPushOk(pushHead(testRepo, refsFor, false), refsFor); return commits; } private List initChanges(int n) throws Exception { return initChanges(n, ImmutableList.of()); } private List initChanges(int n, List footerLines) throws Exception { List commits = new ArrayList<>(n); for (int i = 1; i <= n; i++) { String msg = "Change " + i; if (!footerLines.isEmpty()) { StringBuilder sb = new StringBuilder(msg).append("\n\n"); for (String line : footerLines) { sb.append(line).append('\n'); } msg = sb.toString(); } TestRepository.CommitBuilder cb = testRepo.branch("HEAD").commit().message(msg).insertChangeId(); if (!commits.isEmpty()) { cb.parent(commits.get(commits.size() - 1)); } RevCommit c = cb.create(); testRepo.getRevWalk().parseBody(c); commits.add(c); } return commits; } private List amendChanges( ObjectId initialHead, List origCommits, String refsFor) throws Exception { testRepo.reset(initialHead); List newCommits = new ArrayList<>(origCommits.size()); for (RevCommit c : origCommits) { String msg = c.getShortMessage() + "v2"; if (!c.getShortMessage().equals(c.getFullMessage())) { msg = msg + c.getFullMessage().substring(c.getShortMessage().length()); } TestRepository.CommitBuilder cb = testRepo.branch("HEAD").commit().message(msg); if (!newCommits.isEmpty()) { cb.parent(origCommits.get(newCommits.size() - 1)); } RevCommit c2 = cb.create(); testRepo.getRevWalk().parseBody(c2); newCommits.add(c2); } assertPushOk(pushHead(testRepo, refsFor, false), refsFor); return newCommits; } private static Map getPatchSetRevisions(ChangeData cd) throws Exception { Map revisions = new HashMap<>(); for (PatchSet ps : cd.patchSets()) { revisions.put(ps.getPatchSetId(), ps.getRevision().get()); } return revisions; } private ChangeData byCommit(ObjectId id) throws Exception { List cds = queryProvider.get().byCommit(id); assertThat(cds).named("change for " + id.name()).hasSize(1); return cds.get(0); } private ChangeData byChangeId(Change.Id id) throws Exception { List cds = queryProvider.get().byLegacyChangeId(id); assertThat(cds).named("change " + id).hasSize(1); return cds.get(0); } private static void pushForReviewOk(TestRepository testRepo) throws GitAPIException { pushForReview(testRepo, RemoteRefUpdate.Status.OK, null); } private static void pushForReviewRejected(TestRepository testRepo, String expectedMessage) throws GitAPIException { pushForReview(testRepo, RemoteRefUpdate.Status.REJECTED_OTHER_REASON, expectedMessage); } private static void pushForReview( TestRepository testRepo, RemoteRefUpdate.Status expectedStatus, String expectedMessage) throws GitAPIException { String ref = "refs/for/master"; PushResult r = pushHead(testRepo, ref); RemoteRefUpdate refUpdate = r.getRemoteUpdate(ref); assertThat(refUpdate.getStatus()).isEqualTo(expectedStatus); if (expectedMessage != null) { assertThat(refUpdate.getMessage()).contains(expectedMessage); } } private void grantSkipValidation(Project.NameKey project, String ref, AccountGroup.UUID groupUuid) throws Exception { // See SKIP_VALIDATION implementation in default permission backend. try (ProjectConfigUpdate u = updateProject(project)) { Util.allow(u.getConfig(), Permission.FORGE_AUTHOR, groupUuid, ref); Util.allow(u.getConfig(), Permission.FORGE_COMMITTER, groupUuid, ref); Util.allow(u.getConfig(), Permission.FORGE_SERVER, groupUuid, ref); Util.allow(u.getConfig(), Permission.PUSH_MERGE, groupUuid, "refs/for/" + ref); u.save(); } } private PushOneCommit.Result amendChange(String changeId, String ref) throws Exception { return amendChange(changeId, ref, admin, testRepo); } private String getOwnerEmail(String changeId) throws Exception { return get(changeId, DETAILED_ACCOUNTS).owner.email; } private ImmutableList getReviewerEmails(String changeId, ReviewerState state) throws Exception { Collection infos = get(changeId, DETAILED_LABELS, DETAILED_ACCOUNTS).reviewers.get(state); return infos != null ? infos.stream().map(a -> a.email).collect(toImmutableList()) : ImmutableList.of(); } }