blob: ecc6ce5da6380cdb79f8bcfa7b95756b3dee3512 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.googlesource.chromium.plugins.gitnumberer;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.inject.Inject;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.junit.Test;
public class GitNumberFooterVerfierIT extends LightweightWithConfigIT {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject private ProjectOperations projectOperations;
// Merge (land change) tests =================================================
@Test
public void mergeInvalidParentMissingFooter() throws Exception {
initWithEmptyCommit();
assertEnabledPlugin("refs/heads/main");
PushOneCommit.Result change =
createChange("Valid Child\n\nCr-Commit-Position: refs/heads/main@{#2}", "main");
change.assertOkStatus();
submit(
change.getChangeId(),
ResourceConflictException.class,
"Cr-Commit-Position footer is required in parent commit \\p{Alnum}{40}");
}
@Test
public void mergeInvalidChangeMissingFooter() throws Exception {
initWithGoodParent();
assertEnabledPlugin("refs/heads/main");
PushOneCommit.Result change = createChange("Missing footer", "main");
change.assertOkStatus();
submit(
change.getChangeId(),
ResourceConflictException.class,
"Cr-Commit-Position footer is required");
}
@Test
public void mergeValidSimple() throws Exception {
initWithGoodParent();
assertEnabledPlugin("refs/heads/main");
PushOneCommit.Result change =
createChange("A\n\nCr-Commit-Position: refs/heads/main@{#2}", "main");
change.assertOkStatus();
submit(change.getChangeId(), null, null);
}
@Test
public void mergeInvalidNewBranchWrongLineage() throws Exception {
initWithGoodParent();
assertEnabledPlugin("refs/heads/branch");
PushOneCommit.Result change =
createChange(
"A\n\n"
+ "Cr-Commit-Position: refs/heads/branch@{#1}\n"
+ "Cr-Branched-From: "
+ this.projectOperations.project(project).getHead("main").getName()
+ "-refs/heads/wrong@{#1}",
"branch");
change.assertOkStatus();
submit(
change.getChangeId(),
ResourceConflictException.class,
"invalid lineage footers Cr-Branched-From");
}
@Test
public void mergeInvalidNewBranchWrongPosition() throws Exception {
RevCommit parent = initWithGoodParent();
assertEnabledPlugin("refs/heads/branch");
PushOneCommit.Result change =
createChange(
"2\n\n"
+ "Cr-Commit-Position: refs/heads/main@{#2}\n"
+ "Cr-Branched-From: "
+ parent.getName()
+ "-refs/heads/main@{#1}",
"branch");
change.assertOkStatus();
submit(
change.getChangeId(),
ResourceConflictException.class,
"Cr-Commit-Position footer must be for refs/heads/branch");
}
@Test
public void mergeValidNewBranch() throws Exception {
RevCommit parent = initWithGoodParent();
assertEnabledPlugin("refs/heads/branch");
PushOneCommit.Result change =
createChange(
"A\n\n"
+ "Cr-Commit-Position: refs/heads/branch@{#1}\n"
+ "Cr-Branched-From: "
+ parent.getName()
+ "-refs/heads/main@{#1}",
"branch");
change.assertOkStatus();
submit(change.getChangeId(), null, null);
}
@Test
public void mergeValidNotConfigured() throws Exception {
initWithGoodParent();
assertDisabledPlugin("refs/heads/infra/config");
PushOneCommit.Result change = createChange("Whatever", "infra/config");
change.assertOkStatus();
submit(change.getChangeId(), null, null);
}
// Push tests ==========================================================
@Test
public void pushSingleBranch() throws Exception {
// Tests single branch with 5 commits.
// First_{#1} -> Second_{#2} -> Third_n/a -> Fourth_{#14} -> Fifth_{#15}.
// Plugin enabled: YES -> NO -> NO -> YES.
project = projectOperations.newProject().noEmptyCommit().create();
testRepo = cloneProject(project);
configurePlugin("refs/heads/main");
PushOneCommit.Result first =
prepareOneCommit("Start of main branch\n\nCr-Commit-Position: refs/heads/main@{#1}")
.to("refs/heads/main");
first.assertOkStatus();
PushOneCommit.Result second =
prepareOneCommit("2nd on main branch\n\nCr-Commit-Position: refs/heads/main@{#2}")
.to("refs/heads/main");
second.assertOkStatus();
PushOneCommit third = prepareOneCommit("3nd on main branch");
PushOneCommit.Result attempt1 = third.to("refs/heads/main");
attempt1.assertErrorStatus();
assertThat(attempt1.getMessage()).contains("Cr-Commit-Position footer is required");
disablePlugin();
PushOneCommit.Result attempt2 = third.to("refs/heads/main");
attempt2.assertOkStatus();
configurePlugin("refs/heads/main");
PushOneCommit fourth =
prepareOneCommit(
"4th after bad parent.\n\n" + "Cr-Commit-Position: refs/heads/main@{#14}");
attempt1 = fourth.to("refs/heads/main");
attempt1.assertErrorStatus();
assertThat(attempt1.getMessage())
.contains(
"Cr-Commit-Position footer is required in parent commit "
+ attempt2.getCommit().getName());
disablePlugin();
attempt2 = fourth.to("refs/heads/main");
attempt2.assertOkStatus();
configurePlugin("refs/heads/not-main");
PushOneCommit.Result fifth =
prepareOneCommit("Only parent matters.\n\nCr-Commit-Position: refs/heads/main@{#15}")
.to("refs/heads/main");
fifth.assertOkStatus();
}
@Test
public void pushBranchingAllowed() throws Exception {
// Tests that verification is only ran on new commits
project = projectOperations.newProject().noEmptyCommit().create();
testRepo = cloneProject(project);
configurePlugin("refs/heads/*");
assertEnabledPlugin("refs/heads/main");
PushOneCommit.Result first =
prepareOneCommit("1st\n\nCr-Commit-Position: refs/heads/main@{#1}")
.to("refs/heads/main");
first.assertOkStatus();
PushOneCommit.Result second =
prepareOneCommit("2nd\n\nCr-Commit-Position: refs/heads/main@{#2}")
.to("refs/heads/main");
second.assertOkStatus();
// Push these commits to another branch, not enabled in plugin.
assertDisabledPlugin("refs/heads/disabled");
PushResult result = GitUtil.pushHead(testRepo, "refs/heads/disabled");
assertWithMessage(result.getMessages())
.that(result.getRemoteUpdate("refs/heads/disabled").getStatus())
.isEqualTo(RemoteRefUpdate.Status.OK);
// Push the same commits to a branch on which plugin is enabled.
assertEnabledPlugin("refs/heads/branched");
result = GitUtil.pushHead(testRepo, "refs/heads/branched");
assertWithMessage(result.getMessages())
.that(result.getRemoteUpdate("refs/heads/branched").getStatus())
.isEqualTo(RemoteRefUpdate.Status.OK);
// Push the same commits + 1 new one to a branch on which plugin is enabled.
assertEnabledPlugin("refs/heads/release");
PushOneCommit.Result release =
prepareOneCommit(
"Release off main {#2}\n\n"
+ "Cr-Commit-Position: refs/heads/release@{#1}\n"
+ "Cr-Branched-From: "
+ second.getCommit().getName()
+ "-refs/heads/main@{#2}\n")
.to("refs/heads/release");
release.assertOkStatus();
// But a new commit for release branch can't be pushed onto new branch
// before release branch itself.
assertEnabledPlugin("refs/heads/else");
PushOneCommit.Result hotfix =
prepareOneCommit(
"Else on release\n\n"
+ "Cr-Commit-Position: refs/heads/release@{#2}\n"
+ "Cr-Branched-From: "
+ second.getCommit().getName()
+ "-refs/heads/main@{#2}\n")
.to("refs/heads/else");
assertThat(hotfix.getMessage())
.contains(
hotfix.getCommit().getName()
+ " claims to be from refs/heads/release "
+ "which doesn't contain the commit");
// Do it right: first to release branch, then to else.
assertEnabledPlugin("refs/heads/release");
result = GitUtil.pushHead(testRepo, "refs/heads/release");
assertWithMessage(result.getMessages())
.that(result.getRemoteUpdate("refs/heads/release").getStatus())
.isEqualTo(RemoteRefUpdate.Status.OK);
// Push to refs/heads/else should work now.
assertEnabledPlugin("refs/heads/else");
result = GitUtil.pushHead(testRepo, "refs/heads/else");
assertWithMessage(result.getMessages())
.that(result.getRemoteUpdate("refs/heads/else").getStatus())
.isEqualTo(RemoteRefUpdate.Status.OK);
}
@Test
public void pushBackToMainAfterBranching() throws Exception {
/* Starting with
* main A{main@1}
* \
* other B{other@1}
* pushing B to main must be forbidden.
*/
project = projectOperations.newProject().noEmptyCommit().create();
testRepo = cloneProject(project);
configurePlugin("refs/heads/main");
PushOneCommit.Result first =
prepareOneCommit("1st\n\nCr-Commit-Position: refs/heads/main@{#1}")
.to("refs/heads/main");
first.assertOkStatus();
PushOneCommit.Result second =
prepareOneCommit(
"1st on other\n\nCr-Commit-Position: refs/heads/other@{#1}\n"
+ "Cr-Branched-From: "
+ first.getCommit().getName()
+ "-refs/heads/main@{#1}")
.to("refs/heads/other");
second.assertOkStatus();
// Push
PushResult result = GitUtil.pushHead(testRepo, "refs/heads/main");
assertWithMessage(result.getMessages())
.that(result.getRemoteUpdate("refs/heads/main").getStatus())
.isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
assertThat(result.getRemoteUpdate("refs/heads/main").getMessage())
.contains("Cr-Commit-Position footer must be for refs/heads/main");
}
// Helpers =============================================================
/**
* On return, commit doesn't yet exist in testRepo.
*
* @return PushOneCommit that has be pushed for commit to be created.
*/
PushOneCommit prepareOneCommit(String message) {
logger.atInfo().log("creatingCommit: \n%s", message);
return pushFactory.create(admin.newIdent(), testRepo, message, "file", message);
}
PushOneCommit.Result createChange(String message, String branch) throws Exception {
if (!branch.equals("main")) {
// Gerrit doesn't allow git push HEAD:refs/for/not-yet-existing-branch.
// So, create the branch first.
PushResult r = GitUtil.pushHead(testRepo, "refs/heads/" + branch);
GitUtil.assertPushOk(r, "refs/heads/" + branch);
}
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, message, "file", "content");
return push.to("refs/for/" + branch);
}
RevCommit initWithEmptyCommit() throws Exception {
project = projectOperations.newProject().noEmptyCommit().create();
assertThat(projectOperations.project(project).hasHead("main")).isFalse();
testRepo = cloneProject(project);
PushOneCommit.Result first =
prepareOneCommit("Initial commit")
.to("refs/heads/main");
first.assertOkStatus();
logger.atInfo().log(
"Created initial commit %s with no footers (remote: %s)",
first.getCommit(), this.projectOperations.project(project).getHead("refs/heads/main"));
// testRepo = cloneProject(project);
assertThat(this.projectOperations.project(project).getHead("main"))
.isEqualTo(first.getCommit());
configurePlugin();
return first.getCommit();
}
RevCommit initWithGoodParent() throws Exception {
project = projectOperations.newProject().noEmptyCommit().create();
assertThat(projectOperations.project(project).hasHead("main")).isFalse();
testRepo = cloneProject(project);
PushOneCommit.Result first =
prepareOneCommit("Start of main branch\n\nCr-Commit-Position: refs/heads/main@{#1}")
.to("refs/heads/main");
first.assertOkStatus();
logger.atInfo().log(
"Created initial commit %s with valid footers (remote: %s)",
first.getCommit(), this.projectOperations.project(project).getHead("refs/heads/main"));
// testRepo = cloneProject(project);
assertThat(this.projectOperations.project(project).getHead("main"))
.isEqualTo(first.getCommit());
configurePlugin();
return first.getCommit();
}
void submit(
String changeId,
Class<? extends RestApiException> expectedExceptionType,
String expectedExceptionMessagePattern)
throws Exception {
approve(changeId);
assertWithMessage("submit bit on ChangeInfo")
.that(get(changeId, SUBMITTABLE).submittable)
.isEqualTo(true);
if (expectedExceptionType != null) {
RestApiException thrown =
assertThrows(
expectedExceptionType,
() -> gApi.changes().id(changeId).current().submit(new SubmitInput()));
assertThat(thrown).hasMessageThat().containsMatch(expectedExceptionMessagePattern);
} else {
gApi.changes().id(changeId).current().submit(new SubmitInput());
ChangeInfo change = gApi.changes().id(changeId).info();
assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
}
}
}