blob: ba881b606958dcb390a7484a58abbe029e42ea8e [file] [log] [blame]
// Copyright (C) 2023 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.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
/**
* Verifies that gerrit correctly detects implicit merges on submit..
*
* <p>The setup creates a repository with 2 branches: master and target. Both branches have common
* parent:
*
* <pre>{@code
* master target
* | |
* ----->base commit <------
* }</pre>
*
* Tests use only MergeAlways strategy. All other submit strategies (except cherry pick and rebase
* always) use the same checks on submit. The {@link ImplicitMergeOnSubmitExperimentsIT} validates
* that the implicit merge check is applied to all strategies (except cherry pick and rebase always)
* and {@link ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT} contains tests for the cherry pick
* and rebase always strategies.
*/
public class ImplicitMergeOnSubmitIT extends AbstractImplicitMergeTest {
private RevCommit masterTip;
private RevCommit otherTip;
RevCommit baseCommit;
@Before
public void setUp() throws Exception {
setSubmitType(SubmitType.MERGE_ALWAYS);
gApi.projects().name(project.get()).branch("other").create(new BranchInput());
baseCommit =
repo()
.parseCommit(
ObjectId.fromString(
gApi.projects().name(project.get()).branch("master").get().revision));
masterTip =
pushTo("refs/heads/master", ImmutableMap.of("master-file", "master-content"), baseCommit)
.getCommit();
otherTip =
pushTo("refs/heads/other", ImmutableMap.of("target-file", "target1-content"), baseCommit)
.getCommit();
}
@Test
public void singleChangeImplicitMerge() throws Exception {
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
}
@Test
public void chainOfChangesImplicitMerge() throws Exception {
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
PushOneCommit.Result c1 = createApprovedChange("master", implicitMerge);
PushOneCommit.Result c2 = createApprovedChange("master", c1);
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
assertSubmitRejectedWithImplicitMerge(c1.getChangeId());
assertSubmitRejectedWithImplicitMerge(c2.getChangeId());
}
@Test
public void chainOfChangesOnTopOfTargetBranchTipNoImplicitMerge() throws Exception {
PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
PushOneCommit.Result c2 = createApprovedChange("master", c1);
PushOneCommit.Result c3 = createApprovedChange("master", c2);
assertThatChangeSubmittable(c1.getChangeId());
assertThatChangeSubmittable(c2.getChangeId());
assertThatChangeSubmittable(c3.getChangeId());
}
@Test
public void chainOfChangesNotOnTopOfTargetBranchTipNoImplicitMerge() throws Exception {
// Add one more commit to master branch.
pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
PushOneCommit.Result c2 = createApprovedChange("master", c1);
PushOneCommit.Result c3 = createApprovedChange("master", c2);
assertThatChangeSubmittable(c1.getChangeId());
assertThatChangeSubmittable(c2.getChangeId());
assertThatChangeSubmittable(c3.getChangeId());
}
@Test
public void chainOfChangesNotOnTopOfTargetBranchTipWithImplicitMerge() throws Exception {
// Add one more commit to master branch.
pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
PushOneCommit.Result c2 = createApprovedChange("master", implicitMerge);
PushOneCommit.Result c3 = createApprovedChange("master", c2);
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
assertSubmitRejectedWithImplicitMerge(c2.getChangeId());
assertSubmitRejectedWithImplicitMerge(c3.getChangeId());
}
@Test
public void chainOfChangesEndsWithExplicitMerge_onlyExplcitMergeCanBeSubmitted()
throws Exception {
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
PushOneCommit.Result changeInChange = createApprovedChange("master", implicitMerge);
PushOneCommit.Result explicitMerge =
createApprovedChange("master", changeInChange.getCommit(), masterTip);
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
assertSubmitRejectedWithImplicitMerge(changeInChange.getChangeId());
assertThatChangeSubmittable(explicitMerge.getChangeId());
}
@Test
public void twoChainOfChangesSameTopic_oneChainImplicitMerge_rejectedOnSubmit() throws Exception {
cfg.setBoolean("change", null, "submitWholeTopic", true);
PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
PushOneCommit.Result c2 = createApprovedChange("master", c1);
PushOneCommit.Result c3 = createApprovedChange("master", c2.getCommit());
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
PushOneCommit.Result im1 = createApprovedChange("master", implicitMerge);
PushOneCommit.Result im2 = createApprovedChange("master", im1.getCommit());
// The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
// and doesn't remove changes created in tests. As a result, if the same topic is used in
// several tests gerrit tries to submit all changes, including changes from other tests.
// The name method returns name scoped to this test method .
String topic = name("topic");
gApi.changes().id(c1.getChangeId()).topic(topic);
gApi.changes().id(c2.getChangeId()).topic(topic);
gApi.changes().id(c3.getChangeId()).topic(topic);
gApi.changes().id(implicitMerge.getChangeId()).topic(topic);
gApi.changes().id(im1.getChangeId()).topic(topic);
gApi.changes().id(im2.getChangeId()).topic(topic);
assertSubmitRejectedWithImplicitMerge(c1.getChangeId());
}
@Test
public void twoChainOfChangesSameTopic_noImplicitMerge_canSubmit() throws Exception {
cfg.setBoolean("change", null, "submitWholeTopic", true);
PushOneCommit.Result chain1change1 = createApprovedChange("master", masterTip);
PushOneCommit.Result chain1change2 = createApprovedChange("master", chain1change1);
PushOneCommit.Result chain1change3 = createApprovedChange("master", chain1change2);
PushOneCommit.Result chain2change1 = createApprovedChange("master", masterTip);
PushOneCommit.Result chain2change2 = createApprovedChange("master", chain2change1);
PushOneCommit.Result chain2change3 = createApprovedChange("master", chain2change2);
// The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
// and doesn't remove changes created in tests. As a result, if the same topic is used in
// several tests gerrit tries to submit all changes, including changes from other tests.
// The name method returns name scoped to this test method .
String topic = name("topic");
gApi.changes().id(chain1change1.getChangeId()).topic(topic);
gApi.changes().id(chain1change2.getChangeId()).topic(topic);
gApi.changes().id(chain1change3.getChangeId()).topic(topic);
gApi.changes().id(chain2change1.getChangeId()).topic(topic);
gApi.changes().id(chain2change2.getChangeId()).topic(topic);
gApi.changes().id(chain2change3.getChangeId()).topic(topic);
assertThatChangeSubmittable(chain1change1.getChangeId());
}
@Test
public void twoChainOfChangesSameTopicNotOnTopOfBranch_noImplicitMerge_canSubmit()
throws Exception {
// Add one more commit to master branch.
pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
cfg.setBoolean("change", null, "submitWholeTopic", true);
PushOneCommit.Result chain1change1 = createApprovedChange("master", masterTip);
PushOneCommit.Result chain1change2 = createApprovedChange("master", chain1change1);
PushOneCommit.Result chain1change3 = createApprovedChange("master", chain1change2);
PushOneCommit.Result chain2change1 = createApprovedChange("master", masterTip);
PushOneCommit.Result chain2change2 = createApprovedChange("master", chain2change1);
PushOneCommit.Result chain2change3 = createApprovedChange("master", chain2change2);
// The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
// and doesn't remove changes created in tests. As a result, if the same topic is used in
// several tests gerrit tries to submit all changes, including changes from other tests.
// The name method returns name scoped to this test method .
String topic = name("topic");
gApi.changes().id(chain1change1.getChangeId()).topic(topic);
gApi.changes().id(chain1change2.getChangeId()).topic(topic);
gApi.changes().id(chain1change3.getChangeId()).topic(topic);
gApi.changes().id(chain2change1.getChangeId()).topic(topic);
gApi.changes().id(chain2change2.getChangeId()).topic(topic);
gApi.changes().id(chain2change3.getChangeId()).topic(topic);
assertThatChangeSubmittable(chain1change1.getChangeId());
}
@Test
public void twoChainOfChangesEndsWithExplicitMergeSameTopicNotTipOfBranches_canBeSubmitted()
throws Exception {
cfg.setBoolean("change", null, "submitWholeTopic", true);
// Add one more commit to master branch.
pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
PushOneCommit.Result implicitMerge1 = createApprovedChange("master", otherTip);
PushOneCommit.Result changeInChain1 = createApprovedChange("master", implicitMerge1);
PushOneCommit.Result explicitMerge1 =
createApprovedChange("master", changeInChain1.getCommit(), masterTip);
PushOneCommit.Result implicitMerge2 = createApprovedChange("master", otherTip);
PushOneCommit.Result changeInChain2 = createApprovedChange("master", implicitMerge2);
PushOneCommit.Result explicitMerge2 =
createApprovedChange("master", changeInChain2.getCommit(), masterTip);
// The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
// and doesn't remove changes created in tests. As a result, if the same topic is used in
// several tests gerrit tries to submit all changes, including changes from other tests.
// The name method returns name scoped to this test method .
String topic = name("topic");
gApi.changes().id(implicitMerge1.getChangeId()).topic(topic);
gApi.changes().id(changeInChain1.getChangeId()).topic(topic);
gApi.changes().id(explicitMerge1.getChangeId()).topic(topic);
gApi.changes().id(implicitMerge2.getChangeId()).topic(topic);
gApi.changes().id(changeInChain2.getChangeId()).topic(topic);
gApi.changes().id(explicitMerge2.getChangeId()).topic(topic);
assertThatChangeSubmittable(explicitMerge2.getChangeId());
}
@Test
public void twoChainOfChangesDifferentBranchesSameTopic_oneChainImplicitMerge_rejectedOnSubmit()
throws Exception {
cfg.setBoolean("change", null, "submitWholeTopic", true);
PushOneCommit.Result implicitMerge = createApprovedChange("other", masterTip);
PushOneCommit.Result im1 = createApprovedChange("other", implicitMerge);
PushOneCommit.Result im2 = createApprovedChange("other", im1.getCommit());
PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
PushOneCommit.Result c2 = createApprovedChange("master", c1);
PushOneCommit.Result c3 = createApprovedChange("master", c2.getCommit());
// The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
// and doesn't remove changes created in tests. As a result, if the same topic is used in
// several tests gerrit tries to submit all changes, including changes from other tests.
// The name method returns name scoped to this test method .
String topic = name("topic");
gApi.changes().id(implicitMerge.getChangeId()).topic(topic);
gApi.changes().id(im1.getChangeId()).topic(topic);
gApi.changes().id(im2.getChangeId()).topic(topic);
gApi.changes().id(c1.getChangeId()).topic(topic);
gApi.changes().id(c2.getChangeId()).topic(topic);
gApi.changes().id(c3.getChangeId()).topic(topic);
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
}
@Test
public void explicitMergeOnTopOfChain_onlyTopSubmittable() throws Exception {
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
PushOneCommit.Result im1 = createApprovedChange("master", implicitMerge);
PushOneCommit.Result im2 = createApprovedChange("master", im1.getCommit());
PushOneCommit.Result explicitMerge = createApprovedChange("master", masterTip, im2.getCommit());
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
assertSubmitRejectedWithImplicitMerge(im1.getChangeId());
assertSubmitRejectedWithImplicitMerge(im2.getChangeId());
assertThatChangeSubmittable(explicitMerge.getChangeId());
}
@Test
public void explicitMergeOnTopOfChainParentIsNotBranchTip_onlyTopSubmittable() throws Exception {
// Add one more commit to master and other branches.
pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
pushTo("refs/heads/other", ImmutableMap.of(), otherTip);
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
PushOneCommit.Result im1 = createApprovedChange("master", implicitMerge);
PushOneCommit.Result im2 = createApprovedChange("master", im1.getCommit());
PushOneCommit.Result explicitMerge = createApprovedChange("master", masterTip, im2.getCommit());
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
assertSubmitRejectedWithImplicitMerge(im1.getChangeId());
assertSubmitRejectedWithImplicitMerge(im2.getChangeId());
assertThatChangeSubmittable(explicitMerge.getChangeId());
}
@Test
public void threeBranches_onlyExplicitCommitSubmittable() throws Exception {
BranchInput bi = new BranchInput();
bi.revision = baseCommit.getName();
gApi.projects().name(project.get()).branch("third").create(bi);
RevCommit thirdBranchTip =
pushTo("refs/heads/third", ImmutableMap.of("third-file", "third-content"), baseCommit)
.getCommit();
PushOneCommit.Result explicitMerge = createApprovedChange("master", masterTip, otherTip);
PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip, thirdBranchTip);
PushOneCommit.Result explicitMerge2 =
createApprovedChange("master", explicitMerge, implicitMerge);
assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
assertThatChangeSubmittable(explicitMerge.getChangeId());
assertThatChangeSubmittable(explicitMerge2.getChangeId());
}
private void assertSubmitRejectedWithImplicitMerge(String changeId) throws Exception {
ResourceConflictException e =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
assertThat(e.getMessage()).contains("implicit merge");
}
private void assertThatChangeSubmittable(String changeId) throws Exception {
ChangeInfo ci = gApi.changes().id(changeId).current().submit();
assertThat(ci.submitted).isNotNull();
}
}