blob: ea820138774811b95670d70d39a58c679a07a7b5 [file] [log] [blame]
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.acceptance.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.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.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.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.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.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static java.util.Comparator.comparing;
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.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.SkipProjectClone;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
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.entities.AccountGroup;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
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.git.ObjectIds;
import com.google.gerrit.mail.Address;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.receive.NoteDbPushOption;
import com.google.gerrit.server.git.receive.ReceiveConstants;
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.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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.concurrent.atomic.AtomicInteger;
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.AnyObjectId;
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.Before;
import org.junit.Test;
@SkipProjectClone
@UseClockStep
public abstract class AbstractPushForReview extends AbstractDaemonTest {
protected enum Protocol {
// Only test protocols which are actually served by the Gerrit server, since each separate test
// class is large and slow.
//
// This list excludes the test InProcessProtocol, which is used by large numbers of other
// acceptance tests. Small tests of InProcessProtocol are still possible, without incurring a
// new large slow test.
SSH,
HTTP
}
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
private static String NEW_CHANGE_INDICATOR = " [NEW]";
private LabelType patchSetLock;
@Before
public void setUpPatchSetLock() throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
patchSetLock = TestLabels.patchSetLock();
u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(patchSetLock.getName())
.ref("refs/heads/*")
.group(ANONYMOUS_USERS)
.range(0, 1))
.add(
allowLabel(patchSetLock.getName())
.ref("refs/heads/*")
.group(adminGroupUuid())
.range(0, 1))
.update();
}
@After
public void resetPublishCommentOnPushOption() throws Exception {
requestScopeOperations.setApiUser(admin.id());
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 pushInitialCommitSeriesForMasterBranch() throws Exception {
testPushInitialCommitSeriesForMasterBranch();
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void pushInitialCommitSeriesForMasterBranchWithCreateNewChangeForAllNotInTarget()
throws Exception {
enableCreateNewChangeForAllNotInTarget();
testPushInitialCommitSeriesForMasterBranch();
}
private void testPushInitialCommitSeriesForMasterBranch() throws Exception {
RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
String id = GitUtil.getChangeId(testRepo, c).get();
testRepo.reset(c);
RevCommit c2 = testRepo.commit().parent(c).message("Second commit").insertChangeId().create();
String id2 = GitUtil.getChangeId(testRepo, c2).get();
testRepo.reset(c2);
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);
ChangeInfo change2 = gApi.changes().id(id2).info();
assertThat(change2.branch).isEqualTo("master");
assertThat(change2.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);
}
gApi.changes().id(change2.id).current().review(ReviewInput.approve());
gApi.changes().id(change2.id).current().submit();
try (Repository repo = repoManager.openRepository(project)) {
assertThat(repo.resolve("master")).isEqualTo(c2);
}
}
@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.newIdent())
.committer(admin.newIdent())
.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.newIdent())
.committer(admin.newIdent())
.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(
url + id1 + " " + r1.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\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(admin.newIdent(), 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"
+ " "
+ url
+ id1
+ " "
+ newMsg
+ "\n"
+ " "
+ url
+ id2
+ " another commit"
+ NEW_CHANGE_INDICATOR
+ "\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.newIdent())
.committer(new PersonIdent(admin.newIdent(), 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<String> pushOptions = new ArrayList<>();
pushOptions.add(topicOption);
PushOneCommit push = pushFactory.create(admin.newIdent(), 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<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
ProjectWatchInfo pwi = new ProjectWatchInfo();
pwi.project = project.get();
pwi.filter = "*";
pwi.notifyNewChanges = true;
projectsToWatch.add(pwi);
requestScopeOperations.setApiUser(user3.id());
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);
assertThat(m.rcpt()).containsExactly(user.getEmailAddress());
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.getEmailAddress(), user2.getEmailAddress(), user3.getEmailAddress());
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");
r.assertOkStatus();
ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
ImmutableList<AccountInfo> ccs =
firstNonNull(ci.reviewers.get(ReviewerState.CC), ImmutableList.<AccountInfo>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();
}
@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");
r.assertOkStatus();
ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
ImmutableList<AccountInfo> reviewers =
firstNonNull(ci.reviewers.get(ReviewerState.REVIEWER), ImmutableList.<AccountInfo>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();
}
@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.
String changeId = r.getChangeId();
r = amendChange(changeId, "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(changeId, "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(changeId, "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(changeId, "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(changeId, "refs/for/master%wip,ready");
r.assertErrorStatus();
// Pushing directly to the branch removes the work-in-progress flag
String master = "refs/heads/master";
assertPushOk(pushHead(testRepo, master, false), master);
ChangeInfo result = Iterables.getOnlyElement(gApi.changes().query(changeId).get());
assertThat(result.status).isEqualTo(ChangeStatus.MERGED);
assertThat(result.workInProgress).isNull();
}
private void assertUploadTag(ChangeData cd, String expectedTag) throws Exception {
List<ChangeMessage> 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(user.newIdent(), 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().refName() + ":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(user.newIdent(), 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().refName() + ":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();
projectOperations
.project(project)
.forUpdate()
.add(
allow(Permission.FORGE_COMMITTER)
.ref("refs/*")
.group(SystemGroupBackend.REGISTERED_USERS))
.update();
TestRepository<?> user2Repo = cloneProject(project, user2);
GitUtil.fetch(user2Repo, r.getPatchSet().refName() + ":ps");
user2Repo.reset("ps");
r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo);
r.assertErrorStatus(ReceiveConstants.ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP);
// Non owner, non admin and non project owner with toggleWipState should succeed.
projectOperations
.project(project)
.forUpdate()
.add(
allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
.ref(RefNames.REFS_HEADS + "*")
.group(REGISTERED_USERS))
.update();
r = amendChange(r.getChangeId(), "refs/for/master%ready", user2, user2Repo);
r.assertOkStatus();
// Project owner trying to move from WIP to ready should succeed.
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.OWNER).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
.update();
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<EditInfo> 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(
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<ChangeMessageInfo> changeMessages = ci.messages;
assertThat(changeMessages).hasSize(1);
for (ChangeMessageInfo cm : changeMessages) {
assertThat(cm.message).isEqualTo("Uploaded patch set 1.\nmy test message");
}
Collection<RevisionInfo> 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(admin.newIdent(), 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(
admin.newIdent(),
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<RevisionInfo> 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<ChangeMessageInfo> changeMessages = ci.messages;
assertThat(changeMessages).hasSize(1);
for (ChangeMessageInfo cm : changeMessages) {
assertThat(cm.message)
.isEqualTo("Uploaded patch set 1.\nPunctu...ation~-@{u} | (╯°□°)╯︵ ┻━┻ ^_^");
}
Collection<RevisionInfo> 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<ChangeMessageInfo> 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<RevisionInfo> 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(
admin.newIdent(),
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(
admin.newIdent(),
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(
admin.newIdent(),
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.newIdent())
.committer(user2.newIdent())
.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.getEmailAddress(), user2.getEmailAddress());
}
@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.newIdent())
.committer(user2.newIdent())
.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.getEmailAddress(), user2.getEmailAddress());
}
/**
* 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.
*
* <p>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.newIdent())
.committer(user.newIdent())
.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 =
label("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
String heads = "refs/heads/*";
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().getLabelSections().put(Q.getName(), Q);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(allowLabel("Custom-Label").ref(heads).group(ANONYMOUS_USERS).range(-1, 1))
.update();
RevCommit c =
commitBuilder()
.author(admin.newIdent())
.committer(admin.newIdent())
.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
public void pushNewPatchsetToPatchSetLockedChange() throws Exception {
PushOneCommit.Result r = pushTo("refs/for/master");
r.assertOkStatus();
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
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 {
// specify a single hashtag as option
String hashtag1 = "tag1";
Set<String> expected = ImmutableSet.of(hashtag1);
PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, null);
Set<String> 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(
admin.newIdent(),
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 {
// specify multiple hashtags as options
String hashtag1 = "tag1";
String hashtag2 = "tag2";
Set<String> 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<String> 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(
admin.newIdent(),
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 pushCommitUsingSignedOffBy() throws Exception {
PushOneCommit push =
pushFactory.create(
admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "anotherContent");
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
setUseSignedOffBy(InheritableBoolean.TRUE);
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.FORGE_COMMITTER).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
push =
pushFactory.create(
admin.newIdent(),
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(
admin.newIdent(), 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(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
push =
pushFactory.create(
admin.newIdent(), 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(admin.newIdent(), 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().refName() + ":change");
userRepo.reset("change");
push =
pushFactory.create(
user.newIdent(), 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 {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
.update();
PushOneCommit.Result rBase = pushTo("refs/heads/master");
rBase.assertOkStatus();
gApi.projects().name(project.get()).branch("foo").create(new BranchInput());
PushOneCommit push =
pushFactory.create(
admin.newIdent(), 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);
// 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 {
enableCreateNewChangeForAllNotInTarget();
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
push =
pushFactory.create(
admin.newIdent(), 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 {
enableCreateNewChangeForAllNotInTarget();
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
push =
pushFactory.create(
admin.newIdent(), 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<ChangeInfo> 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<RevCommit> commits = createChanges(n, r);
// Check that a change was created for each.
for (RevCommit c : commits) {
assertWithMessage("change for " + c.name())
.that(byCommit(c).change().getSubject())
.isEqualTo(c.getShortMessage());
}
List<RevCommit> 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);
assertWithMessage(name).that(cd.change().getSubject()).isEqualTo(c2.getShortMessage());
assertWithMessage(name)
.that(getPatchSetRevisions(cd))
.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 pushWithChangeIdAboveFooter() throws Exception {
testPushWithChangeIdAboveFooter();
}
@Test
public void pushWithChangeIdAboveFooterWithCreateNewChangeForAllNotInTarget() throws Exception {
enableCreateNewChangeForAllNotInTarget();
testPushWithChangeIdAboveFooter();
}
private void testPushWithChangeIdAboveFooter() throws Exception {
RevCommit c =
createCommit(
testRepo,
PushOneCommit.SUBJECT
+ "\n\n"
+ "Change-Id: Ied70ea827f5bf968f1f6aaee6594e07c846d217a\n\n"
+ "More text, uh oh.\n");
assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
pushForReviewRejected(testRepo, "Change-Id must be in message footer");
setRequireChangeId(InheritableBoolean.FALSE);
pushForReviewRejected(testRepo, "Change-Id must be in message footer");
}
@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", abbreviateName(c));
assertThat(refUpdate.getMessage()).isEqualTo(reason);
assertThat(r.getMessages()).contains("\nERROR: " + reason);
}
@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(admin.newIdent(), 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
public void accidentallyPushNewPatchSetDirectlyToBranchAndCantRecoverByPushingToRefsFor()
throws Exception {
Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
ChangeData cd = byChangeId(id);
String ps1Rev = Iterables.getOnlyElement(cd.patchSets()).commitId().name();
String r = "refs/for/master";
assertPushRejected(pushHead(testRepo, r, false), r, "no new changes");
// Change not updated.
cd = byChangeId(id);
assertThat(cd.change().isNew()).isTrue();
assertThat(getPatchSetRevisions(cd)).containsExactlyEntriesIn(ImmutableMap.of(1, ps1Rev));
}
@Test
public void forcePushAbandonedChange() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.PUSH).ref("refs/*").group(adminGroupUuid()).force(true))
.update();
PushOneCommit push1 =
pushFactory.create(admin.newIdent(), 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);
TestRepository<?> tr = new TestRepository<>(repo)) {
// 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.
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().isNew()).isTrue();
assertThat(getPatchSetRevisions(cd))
.containsExactlyEntriesIn(ImmutableMap.of(1, ps1Commit.name()));
return c.getId();
}
@Test
public void pushWithEmailInFooter() throws Exception {
pushWithReviewerInFooter(user.getEmailAddress().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 = TestLabels.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(
admin.newIdent(),
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";
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()).force(true))
.update();
// 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<ListChangesOption> 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<ChangeInfo> 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<Repository> 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";
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.PUSH).ref(master).group(adminGroupUuid()).force(true))
.update();
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<Repository> 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<CommentInfo> 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<String> 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<CommentInfo> 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<RevCommit> 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<CommentInfo> 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<CommentInfo> 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<CommentInfo> 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 noEditAndUpdateAllUsersInSameChangeStack() throws Exception {
List<RevCommit> commits = createChanges(2, "refs/for/master");
String id2 = byCommit(commits.get(1)).change().getKey().get();
addDraft(id2, commits.get(1).name(), newDraft(FILE_NAME, 1, "comment2"));
// First change in stack unchanged.
RevCommit unChanged = commits.remove(0);
// Publishing draft comments on change 2 updates All-Users.
amendChanges(unChanged.toObjectId(), commits, "refs/for/master%publish-comments");
}
@GerritConfig(name = "receive.maxBatchCommits", value = "2")
@Test
public void maxBatchCommits() throws Exception {
testMaxBatchCommits();
}
@GerritConfig(name = "receive.maxBatchCommits", value = "2")
@Test
public void maxBatchCommitsWithDefaultValidator() throws Exception {
try (Registration registration = extensionRegistry.newRegistration().add(new TestValidator())) {
testMaxBatchCommits();
}
}
@GerritConfig(name = "receive.maxBatchCommits", value = "2")
@Test
public void maxBatchCommitsWithValidateAllCommitsValidator() throws Exception {
try (Registration registration = extensionRegistry.newRegistration().add(new TestValidator())) {
testMaxBatchCommits();
}
}
private void testMaxBatchCommits() throws Exception {
List<RevCommit> 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());
}
private static class TestValidator implements CommitValidationListener {
private final AtomicInteger count = new AtomicInteger();
private final boolean validateAll;
TestValidator(boolean validateAll) {
this.validateAll = validateAll;
}
TestValidator() {
this(false);
}
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) {
count.incrementAndGet();
return Collections.emptyList();
}
@Override
public boolean shouldValidateAllCommits() {
return validateAll;
}
public int count() {
return count.get();
}
}
@Test
public void skipValidation() throws Exception {
String master = "refs/heads/master";
TestValidator validator = new TestValidator();
try (Registration registration = extensionRegistry.newRegistration().add(validator)) {
// Validation listener is called on normal push
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
PushOneCommit.Result r = push.to(master);
r.assertOkStatus();
assertThat(validator.count()).isEqualTo(1);
// Push is rejected and validation listener is not called when not allowed
// to use skip option
PushOneCommit push2 =
pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
push2.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
r = push2.to(master);
r.assertErrorStatus("not permitted: skip validation");
assertThat(validator.count()).isEqualTo(1);
// Validation listener is not called when skip option is used
grantSkipValidation(project, master, SystemGroupBackend.REGISTERED_USERS);
PushOneCommit push3 =
pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
push3.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
r = push3.to(master);
r.assertOkStatus();
assertThat(validator.count()).isEqualTo(1);
// Validation listener that needs to validate all commits gets called even
// when the skip option is used.
TestValidator validator2 = new TestValidator(true);
try (Registration registration2 = extensionRegistry.newRegistration().add(validator2)) {
PushOneCommit push4 =
pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
push4.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
r = push4.to(master);
r.assertOkStatus();
// First listener was not called; its count remains the same.
assertThat(validator.count()).isEqualTo(1);
// Second listener was called.
assertThat(validator2.count()).isEqualTo(1);
}
}
}
@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<String> opts = ImmutableList.of("notedb=allow");
pr = pushOne(testRepo, c.name(), ref, false, false, opts);
assertPushRejected(pr, ref, "NoteDb update requires access database permission");
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
.update();
pr = pushOne(testRepo, c.name(), ref, false, false, opts);
assertPushRejected(pr, ref, "prohibited by Gerrit: not permitted: create");
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.CREATE).ref("refs/changes/*").group(adminGroupUuid()))
.add(allow(Permission.PUSH).ref("refs/changes/*").group(adminGroupUuid()))
.update();
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");
}
@Test
public void pushCommitsWithSameTreeNoChanges() throws Exception {
RevCommit c =
testRepo
.commit()
.message("Foo")
.parent(getHead(testRepo.getRepository(), "HEAD"))
.insertChangeId()
.create();
testRepo.reset(c);
String r = "refs/for/master";
PushResult pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
RevCommit amended = testRepo.amend(c).create();
testRepo.reset(amended);
pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
assertThat(pr.getMessages())
.contains(
"warning: no changes between prior commit "
+ abbreviateName(c)
+ " and new commit "
+ abbreviateName(amended));
}
@Test
public void pushCommitsWithSameTreeNoFilesChangedMessageUpdated() throws Exception {
RevCommit c =
testRepo
.commit()
.message("Foo")
.parent(getHead(testRepo.getRepository(), "HEAD"))
.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);
RevCommit amended =
testRepo.amend(c).message("Foo Bar").insertChangeId(id.substring(1)).create();
testRepo.reset(amended);
pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
assertThat(pr.getMessages())
.contains("warning: " + abbreviateName(amended) + ": no files changed, message updated");
}
@Test
public void pushCommitsWithSameTreeNoFilesChangedAuthorChanged() throws Exception {
RevCommit c =
testRepo
.commit()
.message("Foo")
.parent(getHead(testRepo.getRepository(), "HEAD"))
.insertChangeId()
.create();
testRepo.reset(c);
String r = "refs/for/master";
PushResult pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
RevCommit amended = testRepo.amend(c).author(user.newIdent()).create();
testRepo.reset(amended);
pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
assertThat(pr.getMessages())
.contains("warning: " + abbreviateName(amended) + ": no files changed, author changed");
}
@Test
public void pushCommitsWithSameTreeNoFilesChangedWasRebased() throws Exception {
RevCommit head = getHead(testRepo.getRepository(), "HEAD");
RevCommit c = testRepo.commit().message("Foo").parent(head).insertChangeId().create();
testRepo.reset(c);
String r = "refs/for/master";
PushResult pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
testRepo.reset(head);
RevCommit newBase = testRepo.commit().message("Base").parent(head).insertChangeId().create();
testRepo.reset(newBase);
pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
testRepo.reset(c);
RevCommit amended = testRepo.amend(c).parent(newBase).create();
testRepo.reset(amended);
pr = pushHead(testRepo, r, false);
assertPushOk(pr, r);
assertThat(pr.getMessages())
.contains("warning: " + abbreviateName(amended) + ": no files changed, was rebased");
}
@Test
public void sequentialCommitMessages() 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(
url + id1 + " " + r1.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
PushOneCommit.Result r2 = pushTo("refs/for/master");
Change.Id id2 = r2.getChange().getId();
r2.assertOkStatus();
r2.assertChange(Change.Status.NEW, null);
r2.assertMessage(
url + id2 + " " + r2.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
testRepo.reset(initialHead);
// rearrange the commit so that change no. 2 is the parent of change no. 1
String r1Message = "Position 2";
String r2Message = "Position 1";
testRepo
.branch("HEAD")
.commit()
.message(r2Message)
.insertChangeId(r2.getChangeId().substring(1))
.create();
testRepo
.branch("HEAD")
.commit()
.message(r1Message)
.insertChangeId(r1.getChangeId().substring(1))
.create();
PushOneCommit.Result r3 =
pushFactory
.create(admin.newIdent(), testRepo, "another commit", "b.txt", "bbb")
.to("refs/for/master");
Change.Id id3 = r3.getChange().getId();
r3.assertOkStatus();
r3.assertChange(Change.Status.NEW, null);
// should display commit r2, r1, r3 in that order.
r3.assertMessage(
"success\n"
+ "\n"
+ " "
+ url
+ id2
+ " "
+ r2Message
+ "\n"
+ " "
+ url
+ id1
+ " "
+ r1Message
+ "\n"
+ " "
+ url
+ id3
+ " another commit"
+ NEW_CHANGE_INDICATOR
+ "\n");
}
@Test
public void cannotPushTheSameCommitTwiceForReviewToTheSameBranch() throws Exception {
testCannotPushTheSameCommitTwiceForReviewToTheSameBranch();
}
@Test
public void cannotPushTheSameCommitTwiceForReviewToTheSameBranchCreateNewChangeForAllNotInTarget()
throws Exception {
enableCreateNewChangeForAllNotInTarget();
testCannotPushTheSameCommitTwiceForReviewToTheSameBranch();
}
private void testCannotPushTheSameCommitTwiceForReviewToTheSameBranch() throws Exception {
setRequireChangeId(InheritableBoolean.FALSE);
// create a commit without Change-Id
testRepo
.branch("HEAD")
.commit()
.author(user.newIdent())
.committer(user.newIdent())
.add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
.message(PushOneCommit.SUBJECT)
.create();
// push the commit for review to create a change
PushResult r = pushHead(testRepo, "refs/for/master");
assertPushOk(r, "refs/for/master");
// try to push the same commit for review again to create another change on the same branch,
// it's expected that this is rejected with "no new changes"
r = pushHead(testRepo, "refs/for/master");
assertPushRejected(r, "refs/for/master", "no new changes");
}
@Test
public void pushTheSameCommitTwiceForReviewToDifferentBranches() throws Exception {
setRequireChangeId(InheritableBoolean.FALSE);
// create a commit without Change-Id
testRepo
.branch("HEAD")
.commit()
.author(user.newIdent())
.committer(user.newIdent())
.add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT)
.message(PushOneCommit.SUBJECT)
.create();
// push the commit for review to create a change
PushResult r = pushHead(testRepo, "refs/for/master");
assertPushOk(r, "refs/for/master");
// create another branch
gApi.projects().name(project.get()).branch("otherBranch").create(new BranchInput());
// try to push the same commit for review again to create a change on another branch,
// it's expected that this is rejected with "no new changes" since
// CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET is false
r = pushHead(testRepo, "refs/for/otherBranch");
assertPushRejected(r, "refs/for/otherBranch", "no new changes");
enableCreateNewChangeForAllNotInTarget();
// try to push the same commit for review again to create a change on another branch,
// now it should succeed since CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET is true
r = pushHead(testRepo, "refs/for/otherBranch");
assertPushOk(r, "refs/for/otherBranch");
}
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<CommentInfo> 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<RevCommit> 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) {
assertWithMessage(name).that(cd.reviewers().all()).containsExactly(expectedReviewer.id());
// Remove reviewer from PS1 so we can test adding this same reviewer on PS2 below.
gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.id().toString()).remove();
}
assertWithMessage(name).that(byCommit(c).reviewers().all()).isEmpty();
}
List<RevCommit> 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) {
assertWithMessage(name).that(cd.reviewers().all()).containsExactly(expectedReviewer.id());
} else {
assertWithMessage(name).that(byCommit(c).reviewers().all()).isEmpty();
}
}
}
private List<RevCommit> createChanges(int n, String refsFor) throws Exception {
return createChanges(n, refsFor, ImmutableList.of());
}
private List<RevCommit> createChanges(int n, String refsFor, List<String> footerLines)
throws Exception {
List<RevCommit> commits = initChanges(n, footerLines);
assertPushOk(pushHead(testRepo, refsFor, false), refsFor);
return commits;
}
private List<RevCommit> initChanges(int n) throws Exception {
return initChanges(n, ImmutableList.of());
}
private List<RevCommit> initChanges(int n, List<String> footerLines) throws Exception {
List<RevCommit> 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<RevCommit> amendChanges(
ObjectId initialHead, List<RevCommit> origCommits, String refsFor) throws Exception {
testRepo.reset(initialHead);
List<RevCommit> 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<Integer, String> getPatchSetRevisions(ChangeData cd) throws Exception {
Map<Integer, String> revisions = new HashMap<>();
for (PatchSet ps : cd.patchSets()) {
revisions.put(ps.number(), ps.commitId().name());
}
return revisions;
}
private ChangeData byCommit(ObjectId id) throws Exception {
List<ChangeData> cds = queryProvider.get().byCommit(id);
assertWithMessage("change for " + id.name()).that(cds).hasSize(1);
return cds.get(0);
}
private ChangeData byChangeId(Change.Id id) throws Exception {
List<ChangeData> cds = queryProvider.get().byLegacyChangeId(id);
assertWithMessage("change " + id).that(cds).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.
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.FORGE_AUTHOR).ref(ref).group(groupUuid))
.add(allow(Permission.FORGE_COMMITTER).ref(ref).group(groupUuid))
.add(allow(Permission.FORGE_SERVER).ref(ref).group(groupUuid))
.add(allow(Permission.PUSH_MERGE).ref("refs/for/" + ref).group(groupUuid))
.update();
}
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<String> getReviewerEmails(String changeId, ReviewerState state)
throws Exception {
Collection<AccountInfo> infos =
get(changeId, DETAILED_LABELS, DETAILED_ACCOUNTS).reviewers.get(state);
return infos != null
? infos.stream().map(a -> a.email).collect(toImmutableList())
: ImmutableList.of();
}
private String abbreviateName(AnyObjectId id) throws Exception {
return ObjectIds.abbreviateName(id, testRepo.getRevWalk().getObjectReader());
}
}