blob: ac3622f5d86cfd6e4ca55b3ea52e5b7178c91325 [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.rest.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
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.entities.Permission.READ;
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.testing.GerritJUnit.assertThrows;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.inject.Inject;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Override
protected SubmitType getSubmitType() {
return SubmitType.MERGE_IF_NECESSARY;
}
@Test
public void submitWithFastForward() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change = createChange();
submit(change.getChangeId());
RevCommit updatedHead = projectOperations.project(project).getHead("master");
assertThat(updatedHead.getId()).isEqualTo(change.getCommit());
assertThat(updatedHead.getParent(0)).isEqualTo(initialHead);
assertSubmitter(change.getChangeId(), 1);
assertPersonEquals(admin.newIdent(), updatedHead.getAuthorIdent());
assertPersonEquals(admin.newIdent(), updatedHead.getCommitterIdent());
assertRefUpdatedEvents(initialHead, updatedHead);
assertChangeMergedEvents(change.getChangeId(), updatedHead.name());
}
@Test
public void submitMultipleChanges() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
testRepo.reset(initialHead);
PushOneCommit.Result change = createChange("Change 1", "b", "b");
testRepo.reset(initialHead);
PushOneCommit.Result change2 = createChange("Change 2", "c", "c");
testRepo.reset(initialHead);
PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
PushOneCommit.Result change5 = createChange("Change 5", "f", "f");
// Change 2 is a fast-forward, no need to merge.
submit(change2.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteLog().get(0);
assertThat(headAfterFirstSubmit.getShortMessage())
.isEqualTo(change2.getCommit().getShortMessage());
assertThat(headAfterFirstSubmit.getParent(0).getId()).isEqualTo(initialHead.getId());
assertPersonEquals(admin.newIdent(), headAfterFirstSubmit.getAuthorIdent());
assertPersonEquals(admin.newIdent(), headAfterFirstSubmit.getCommitterIdent());
// We need to merge changes 3, 4 and 5.
approve(change3.getChangeId());
approve(change4.getChangeId());
submit(change5.getChangeId());
RevCommit headAfterSecondSubmit = getRemoteLog().get(0);
assertThat(headAfterSecondSubmit.getParent(1).getShortMessage())
.isEqualTo(change5.getCommit().getShortMessage());
assertThat(headAfterSecondSubmit.getParent(0).getShortMessage())
.isEqualTo(change2.getCommit().getShortMessage());
assertPersonEquals(admin.newIdent(), headAfterSecondSubmit.getAuthorIdent());
assertPersonEquals(serverIdent.get(), headAfterSecondSubmit.getCommitterIdent());
// First change stays untouched.
assertNew(change.getChangeId());
// The two submit operations should have resulted in two ref-update events
// and three change-merged events.
assertRefUpdatedEvents(
initialHead, headAfterFirstSubmit, headAfterFirstSubmit, headAfterSecondSubmit);
assertChangeMergedEvents(
change2.getChangeId(),
headAfterFirstSubmit.name(),
change3.getChangeId(),
headAfterSecondSubmit.name(),
change4.getChangeId(),
headAfterSecondSubmit.name(),
change5.getChangeId(),
headAfterSecondSubmit.name());
}
@Test
public void submitChangesAcrossRepos() throws Throwable {
Project.NameKey p1 = projectOperations.newProject().create();
Project.NameKey p2 = projectOperations.newProject().create();
Project.NameKey p3 = projectOperations.newProject().create();
RevCommit initialHead2 = projectOperations.project(p2).getHead("master");
RevCommit initialHead3 = projectOperations.project(p3).getHead("master");
TestRepository<?> repo1 = cloneProject(p1);
TestRepository<?> repo2 = cloneProject(p2);
TestRepository<?> repo3 = cloneProject(p3);
PushOneCommit.Result change1a =
createChange(
repo1,
"master",
"An ancestor of the change we want to submit",
"a.txt",
"1",
"dependent-topic");
PushOneCommit.Result change1b =
createChange(
repo1,
"master",
"We're interested in submitting this change",
"a.txt",
"2",
"topic-to-submit");
PushOneCommit.Result change2a =
createChange(repo2, "master", "indirection level 1", "a.txt", "1", "topic-indirect");
PushOneCommit.Result change2b =
createChange(
repo2, "master", "should go in with first change", "a.txt", "2", "dependent-topic");
PushOneCommit.Result change3 =
createChange(repo3, "master", "indirection level 2", "a.txt", "1", "topic-indirect");
approve(change1a.getChangeId());
approve(change2a.getChangeId());
approve(change2b.getChangeId());
approve(change3.getChangeId());
submit(change1b.getChangeId());
RevCommit tip1 = getRemoteLog(p1, "master").get(0);
RevCommit tip2 = getRemoteLog(p2, "master").get(0);
RevCommit tip3 = getRemoteLog(p3, "master").get(0);
assertThat(tip1.getShortMessage()).isEqualTo(change1b.getCommit().getShortMessage());
if (isSubmitWholeTopicEnabled()) {
assertThat(tip2.getShortMessage()).isEqualTo(change2b.getCommit().getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(change3.getCommit().getShortMessage());
} else {
assertThat(tip2.getShortMessage()).isEqualTo(initialHead2.getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(initialHead3.getShortMessage());
}
}
@Test
public void submitChangesAcrossReposBlocked() throws Throwable {
Project.NameKey p1 = projectOperations.newProject().create();
Project.NameKey p2 = projectOperations.newProject().create();
Project.NameKey p3 = projectOperations.newProject().create();
TestRepository<?> repo1 = cloneProject(p1);
TestRepository<?> repo2 = cloneProject(p2);
TestRepository<?> repo3 = cloneProject(p3);
RevCommit initialHead1 = projectOperations.project(p1).getHead("master");
RevCommit initialHead2 = projectOperations.project(p2).getHead("master");
RevCommit initialHead3 = projectOperations.project(p3).getHead("master");
PushOneCommit.Result change1a =
createChange(
repo1,
"master",
"An ancestor of the change we want to submit",
"a.txt",
"1",
"dependent-topic");
PushOneCommit.Result change1b =
createChange(
repo1,
"master",
"we're interested to submit this change",
"a.txt",
"2",
"topic-to-submit");
PushOneCommit.Result change2a =
createChange(repo2, "master", "indirection level 2a", "a.txt", "1", "topic-indirect");
PushOneCommit.Result change2b =
createChange(
repo2, "master", "should go in with first change", "a.txt", "2", "dependent-topic");
PushOneCommit.Result change3 =
createChange(repo3, "master", "indirection level 2b", "a.txt", "1", "topic-indirect");
// Create a merge conflict for change3 which is only indirectly related
// via topics.
repo3.reset(initialHead3);
PushOneCommit.Result change3Conflict =
createChange(repo3, "master", "conflicting change", "a.txt", "2\n2", "conflicting-topic");
submit(change3Conflict.getChangeId());
RevCommit tipConflict = getRemoteLog(p3, "master").get(0);
assertThat(tipConflict.getShortMessage())
.isEqualTo(change3Conflict.getCommit().getShortMessage());
approve(change1a.getChangeId());
approve(change2a.getChangeId());
approve(change2b.getChangeId());
approve(change3.getChangeId());
if (isSubmitWholeTopicEnabled()) {
String msg =
"Failed to submit 5 changes due to the following problems:\n"
+ "Change "
+ change3.getChange().getId()
+ ": Change could not be "
+ "merged due to a path conflict. Please rebase the change locally "
+ "and upload the rebased commit for review.";
submitWithConflict(change1b.getChangeId(), msg);
} else {
submit(change1b.getChangeId());
}
RevCommit tip1 = getRemoteLog(p1, "master").get(0);
RevCommit tip2 = getRemoteLog(p2, "master").get(0);
RevCommit tip3 = getRemoteLog(p3, "master").get(0);
if (isSubmitWholeTopicEnabled()) {
assertThat(tip1.getShortMessage()).isEqualTo(initialHead1.getShortMessage());
assertThat(tip2.getShortMessage()).isEqualTo(initialHead2.getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(change3Conflict.getCommit().getShortMessage());
assertNoSubmitter(change1a.getChangeId(), 1);
assertNoSubmitter(change2a.getChangeId(), 1);
assertNoSubmitter(change2b.getChangeId(), 1);
assertNoSubmitter(change3.getChangeId(), 1);
} else {
assertThat(tip1.getShortMessage()).isEqualTo(change1b.getCommit().getShortMessage());
assertThat(tip2.getShortMessage()).isEqualTo(initialHead2.getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(change3Conflict.getCommit().getShortMessage());
assertNoSubmitter(change2a.getChangeId(), 1);
assertNoSubmitter(change2b.getChangeId(), 1);
assertNoSubmitter(change3.getChangeId(), 1);
}
}
@Test
public void submitWithMergedAncestorsOnOtherBranch() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change1 =
createChange(testRepo, "master", "base commit", "a.txt", "1", "");
submit(change1.getChangeId());
RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
PushOneCommit.Result change2 =
createChange(
testRepo, "master", "We want to commit this to master first", "a.txt", "2", "");
submit(change2.getChangeId());
RevCommit headAfterSecondSubmit = getRemoteLog(project, "master").get(0);
assertThat(headAfterSecondSubmit.getShortMessage())
.isEqualTo(change2.getCommit().getShortMessage());
RevCommit tip2 = getRemoteLog(project, "branch").get(0);
assertThat(tip2.getShortMessage()).isEqualTo(change1.getCommit().getShortMessage());
PushOneCommit.Result change3 =
createChange(
testRepo,
"branch",
"This commit is based on master, which includes change2, "
+ "but is targeted at branch, which doesn't include it.",
"a.txt",
"3",
"");
submit(change3.getChangeId());
List<RevCommit> log3 = getRemoteLog(project, "branch");
assertThat(log3.get(0).getShortMessage()).isEqualTo(change3.getCommit().getShortMessage());
assertThat(log3.get(1).getShortMessage()).isEqualTo(change2.getCommit().getShortMessage());
assertRefUpdatedEvents(
initialHead, headAfterFirstSubmit, headAfterFirstSubmit, headAfterSecondSubmit);
assertChangeMergedEvents(
change1.getChangeId(),
headAfterFirstSubmit.name(),
change2.getChangeId(),
headAfterSecondSubmit.name());
}
@Test
public void submitWithOpenAncestorsOnOtherBranch() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change1 =
createChange(testRepo, "master", "base commit", "a.txt", "1", "");
submit(change1.getChangeId());
RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
PushOneCommit.Result change2 =
createChange(
testRepo, "master", "We want to commit this to master first", "a.txt", "2", "");
approve(change2.getChangeId());
RevCommit tip1 = getRemoteLog(project, "master").get(0);
assertThat(tip1.getShortMessage()).isEqualTo(change1.getCommit().getShortMessage());
RevCommit tip2 = getRemoteLog(project, "branch").get(0);
assertThat(tip2.getShortMessage()).isEqualTo(change1.getCommit().getShortMessage());
PushOneCommit.Result change3a =
createChange(
testRepo,
"branch",
"This commit is based on change2 pending for master, "
+ "but is targeted itself at branch, which doesn't include it.",
"a.txt",
"3",
"a-topic-here");
Project.NameKey p3 = projectOperations.newProject().create();
TestRepository<?> repo3 = cloneProject(p3);
RevCommit repo3Head = projectOperations.project(p3).getHead("master");
PushOneCommit.Result change3b =
createChange(
repo3,
"master",
"some accompanying changes for change3a in another repo tied together via topic",
"a.txt",
"1",
"a-topic-here");
approve(change3b.getChangeId());
String cnt = isSubmitWholeTopicEnabled() ? "2 changes" : "1 change";
submitWithConflict(
change3a.getChangeId(),
"Failed to submit "
+ cnt
+ " due to the following problems:\n"
+ "Change "
+ change3a.getChange().getId()
+ ": Depends on commit that cannot be merged."
+ " Commit "
+ change3a.getCommit().name()
+ " depends on commit "
+ change2.getCommit().name()
+ " of change "
+ change2.getChange().getId()
+ " which cannot be merged.");
RevCommit tipbranch = getRemoteLog(project, "branch").get(0);
assertThat(tipbranch.getShortMessage()).isEqualTo(change1.getCommit().getShortMessage());
RevCommit tipmaster = getRemoteLog(p3, "master").get(0);
assertThat(tipmaster.getShortMessage()).isEqualTo(repo3Head.getShortMessage());
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name());
}
@Test
public void gerritWorkflow() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
// We'll setup a master and a stable branch.
// Then we create a change to be applied to master, which is
// then cherry picked back to stable. The stable branch will
// be merged up into master again.
gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
// Push a change to master
PushOneCommit push = pushFactory.create(user.newIdent(), testRepo, "small fix", "a.txt", "2");
PushOneCommit.Result change = push.to("refs/for/master");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteLog(project, "master").get(0);
assertThat(headAfterFirstSubmit.getShortMessage())
.isEqualTo(change.getCommit().getShortMessage());
// Now cherry pick to stable
CherryPickInput in = new CherryPickInput();
in.destination = "stable";
in.message = "This goes to stable as well\n" + headAfterFirstSubmit.getFullMessage();
ChangeApi orig = gApi.changes().id(change.getChangeId());
String cherryId = orig.current().cherryPick(in).id();
gApi.changes().id(cherryId).current().review(ReviewInput.approve());
gApi.changes().id(cherryId).current().submit();
// Create the merge locally
RevCommit stable = projectOperations.project(project).getHead("stable");
RevCommit master = projectOperations.project(project).getHead("master");
testRepo.git().fetch().call();
testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call();
testRepo.git().branchCreate().setName("master").setStartPoint(master).call();
RevCommit merge =
testRepo
.commit()
.parent(master)
.parent(stable)
.message("Merge stable into master")
.insertChangeId()
.create();
testRepo.branch("refs/heads/master").update(merge);
testRepo.git().push().setRefSpecs(new RefSpec("refs/heads/master:refs/for/master")).call();
String changeId = GitUtil.getChangeId(testRepo, merge).get();
approve(changeId);
submit(changeId);
RevCommit headAfterSecondSubmit = getRemoteLog(project, "master").get(0);
assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo(merge.getShortMessage());
assertRefUpdatedEvents(
initialHead, headAfterFirstSubmit, headAfterFirstSubmit, headAfterSecondSubmit);
assertChangeMergedEvents(
change.getChangeId(), headAfterFirstSubmit.name(), changeId, headAfterSecondSubmit.name());
}
@Test
public void openChangeForTargetBranchPreventsMerge() throws Throwable {
gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
// Propose a change for master, but leave it open for master!
PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "small fix", "a.txt", "2");
PushOneCommit.Result change2result = change.to("refs/for/master");
// Now cherry pick to stable
CherryPickInput in = new CherryPickInput();
in.destination = "stable";
in.message = "it goes to stable branch";
ChangeApi orig = gApi.changes().id(change2result.getChangeId());
ChangeApi cherry = orig.current().cherryPick(in);
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
// Create a commit locally
testRepo.git().fetch().setRefSpecs(new RefSpec("refs/heads/stable")).call();
PushOneCommit.Result change3 = createChange(testRepo, "stable", "test", "a.txt", "3", "");
submitWithConflict(
change3.getChangeId(),
"Failed to submit 1 change due to the following problems:\n"
+ "Change "
+ change3.getPatchSetId().changeId().get()
+ ": Depends on commit that cannot be merged."
+ " Commit "
+ change3.getCommit().name()
+ " depends on commit "
+ change2result.getCommit().name()
+ " of change "
+ change2result.getChange().getId()
+ " which cannot be merged.");
assertRefUpdatedEvents();
assertChangeMergedEvents();
}
@Test
public void dependencyOnDeletedChangePreventsMerge() throws Throwable {
// Create a change
PushOneCommit change = pushFactory.create(user.newIdent(), testRepo, "fix", "a.txt", "foo");
PushOneCommit.Result changeResult = change.to("refs/for/master");
// Create a successor change.
PushOneCommit change2 =
pushFactory.create(user.newIdent(), testRepo, "feature", "b.txt", "bar");
PushOneCommit.Result change2Result = change2.to("refs/for/master");
// Delete first change.
gApi.changes().id(changeResult.getChangeId()).delete();
// Submit is expected to fail.
submitWithConflict(
change2Result.getChangeId(),
"Failed to submit 1 change due to the following problems:\n"
+ "Change "
+ change2Result.getChange().getId()
+ ": Depends on commit that cannot be merged."
+ " Commit "
+ change2Result.getCommit().name()
+ " depends on commit "
+ changeResult.getCommit().name()
+ " which cannot be merged."
+ " Is the change of this commit not visible to '"
+ admin.username()
+ "' or was it deleted?");
assertRefUpdatedEvents();
assertChangeMergedEvents();
}
@Test
public void dependencyOnChangeForNonVisibleBranchPreventsMerge() throws Throwable {
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(LabelId.CODE_REVIEW)
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-2, 2))
.add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.update();
// Create a change
PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
PushOneCommit.Result changeResult = change.to("refs/for/master");
approve(changeResult.getChangeId());
// Create a successor change.
PushOneCommit change2 =
pushFactory.create(admin.newIdent(), testRepo, "feature", "b.txt", "bar");
PushOneCommit.Result change2Result = change2.to("refs/for/master");
// Move the first change to a destination branch that is non-visible to user so that user cannot
// this change anymore.
BranchNameKey secretBranch = BranchNameKey.create(project, "secretBranch");
gApi.projects()
.name(secretBranch.project().get())
.branch(secretBranch.branch())
.create(new BranchInput());
gApi.changes().id(changeResult.getChangeId()).move(secretBranch.branch());
projectOperations
.project(project)
.forUpdate()
.add(block(READ).ref(secretBranch.branch()).group(ANONYMOUS_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
// Verify that user cannot see the first change.
ResourceNotFoundException thrown =
assertThrows(
ResourceNotFoundException.class,
() -> gApi.changes().id(changeResult.getChangeId()).get());
assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + changeResult.getChangeId());
// Submit is expected to fail.
submitWithConflict(
change2Result.getChangeId(),
"Failed to submit 1 change due to the following problems:\n"
+ "Change "
+ change2Result.getChange().getId()
+ ": Depends on commit that cannot be merged."
+ " Commit "
+ change2Result.getCommit().name()
+ " depends on commit "
+ changeResult.getCommit().name()
+ " which cannot be merged."
+ " Is the change of this commit not visible to '"
+ user.username()
+ "' or was it deleted?");
assertRefUpdatedEvents();
assertChangeMergedEvents();
}
@Test
public void dependencyOnHiddenChangePreventsMerge() throws Throwable {
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(LabelId.CODE_REVIEW)
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-2, 2))
.add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.update();
// Create a change
PushOneCommit change = pushFactory.create(admin.newIdent(), testRepo, "fix", "a.txt", "foo");
PushOneCommit.Result changeResult = change.to("refs/for/master");
approve(changeResult.getChangeId());
// Create a successor change.
PushOneCommit change2 =
pushFactory.create(admin.newIdent(), testRepo, "feature", "b.txt", "bar");
PushOneCommit.Result change2Result = change2.to("refs/for/master");
approve(change2Result.getChangeId());
// Mark the first change private so that it's not visible to user.
gApi.changes().id(changeResult.getChangeId()).setPrivate(true, "nobody should see this");
requestScopeOperations.setApiUser(user.id());
// Verify that user cannot see the first change.
ResourceNotFoundException thrown =
assertThrows(
ResourceNotFoundException.class,
() -> gApi.changes().id(changeResult.getChangeId()).get());
assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + changeResult.getChangeId());
// Submit is expected to fail.
AuthException thrown2 =
assertThrows(
AuthException.class,
() -> gApi.changes().id(change2Result.getChangeId()).current().submit());
assertThat(thrown2)
.hasMessageThat()
.isEqualTo(
"A change to be submitted with "
+ change2Result.getChange().getId().get()
+ " is not visible");
assertRefUpdatedEvents();
assertChangeMergedEvents();
}
@Test
public void dependencyOnHiddenChangeUsingTopicPreventsMerge() throws Throwable {
// Construct a topic where a change included by topic depends on a private change that is not
// visible to the submitting user
// (c1) --- topic --- (c2b)
// |
// (c2a) <= private
assume().that(isSubmitWholeTopicEnabled()).isTrue();
Project.NameKey p1 = projectOperations.newProject().create();
Project.NameKey p2 = projectOperations.newProject().create();
projectOperations
.project(p1)
.forUpdate()
.add(
allowLabel(LabelId.CODE_REVIEW)
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-2, 2))
.add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.update();
projectOperations
.project(p2)
.forUpdate()
.add(
allowLabel(LabelId.CODE_REVIEW)
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-2, 2))
.add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.update();
TestRepository<?> repo1 = cloneProject(p1);
TestRepository<?> repo2 = cloneProject(p2);
PushOneCommit.Result change1 =
createChange(repo1, "master", "A fresh change in repo1", "a.txt", "1", "topic-to-submit");
approve(change1.getChangeId());
PushOneCommit push =
pushFactory.create(admin.newIdent(), repo2, "An ancestor change in repo2", "a.txt", "2");
PushOneCommit.Result change2a = push.to("refs/for/master");
approve(change2a.getChangeId());
PushOneCommit.Result change2b =
createChange(
repo2, "master", "A topic-linked change in repo2", "a.txt", "2", "topic-to-submit");
approve(change2b.getChangeId());
// Mark change2a private so that it's not visible to user.
gApi.changes().id(change2a.getChangeId()).setPrivate(true, "nobody should see this");
requestScopeOperations.setApiUser(user.id());
// Verify that user cannot see change2a
ResourceNotFoundException thrown =
assertThrows(
ResourceNotFoundException.class, () -> gApi.changes().id(change2a.getChangeId()).get());
assertThat(thrown).hasMessageThat().isEqualTo("Not found: " + change2a.getChangeId());
// Submit is expected to fail.
AuthException thrown2 =
assertThrows(
AuthException.class, () -> gApi.changes().id(change1.getChangeId()).current().submit());
assertThat(thrown2)
.hasMessageThat()
.isEqualTo(
"A change to be submitted with "
+ change1.getChange().getId().get()
+ " is not visible");
assertRefUpdatedEvents();
assertChangeMergedEvents();
}
}