blob: 09957b3ee79961f3968a605e9a9ef99c192384ac [file] [log] [blame]
// Copyright (C) 2015 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.common.truth.TruthJUnit.assume;
import static com.google.gerrit.server.project.ProjectConfig.RULES_PL_FILE;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
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.Test;
@NoHttpd
public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
@ConfigSuite.Config
public static Config submitWholeTopicEnabled() {
return submitWholeTopicEnabledConfig();
}
@Inject private ProjectOperations projectOperations;
@Inject private IndexOperations.Change changeIndexOperations;
@Inject private IndexOperations.Account accountIndexOperations;
@Inject private SubmitRuleEvaluator.Factory evaluatorFactory;
@Test
@GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
public void testSubscriptionWithoutGlobalServerSetting() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
}
@Test
public void subscriptionWithoutSpecificSubscription() throws Exception {
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
}
@Test
public void subscriptionToEmptyRepo() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
}
@Test
public void subscriptionToExistingRepo() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
}
@Test
public void subscriptionWildcardACLForSingleBranch() throws Exception {
// master is allowed to be subscribed to master branch only:
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, null);
// create 'branch':
pushChangeTo(superRepo, "branch");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
createSubmoduleSubscription(superRepo, "branch", subKey, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
assertThat(hasSubmodule(superRepo, "branch", subKey)).isFalse();
}
@Test
public void subscriptionWildcardACLForMissingProject() throws Exception {
allowMatchingSubmoduleSubscription(
subKey, "refs/heads/*", Project.nameKey("not-existing-super-project"), "refs/heads/*");
pushChangeTo(subRepo, "master");
}
@Test
public void subscriptionWildcardACLForMissingBranch() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
pushChangeTo(subRepo, "foo");
}
@Test
public void subscriptionWildcardACLForMissingGitmodules() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
pushChangeTo(superRepo, "master");
pushChangeTo(subRepo, "master");
}
@Test
public void subscriptionWildcardACLOneOnOneMapping() throws Exception {
// any branch is allowed to be subscribed to the same superprojects branch:
allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
// create 'branch' in both repos:
pushChangeTo(superRepo, "branch");
pushChangeTo(subRepo, "branch");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
createSubmoduleSubscription(superRepo, "branch", subKey, "branch");
ObjectId subHEAD1 = pushChangeTo(subRepo, "master");
ObjectId subHEAD2 = pushChangeTo(subRepo, "branch");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD1);
expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD2);
// Now test that cross subscriptions do not work:
createSubmoduleSubscription(superRepo, "master", subKey, "branch");
ObjectId subHEAD3 = pushChangeTo(subRepo, "branch");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD1);
expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD3);
}
@Test
public void subscriptionWildcardACLForManyBranches() throws Exception {
// Any branch is allowed to be subscribed to any superproject branch:
allowSubmoduleSubscription(subKey, "refs/heads/*", superKey, null, false);
pushChangeTo(superRepo, "branch");
pushChangeTo(subRepo, "another-branch");
createSubmoduleSubscription(superRepo, "branch", subKey, "another-branch");
ObjectId subHEAD = pushChangeTo(subRepo, "another-branch");
expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
}
@Test
public void subscriptionWildcardACLOneToManyBranches() throws Exception {
// Any branch is allowed to be subscribed to any superproject branch:
allowSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/*", false);
pushChangeTo(superRepo, "branch");
createSubmoduleSubscription(superRepo, "branch", subKey, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
createSubmoduleSubscription(superRepo, "branch", subKey, "branch");
pushChangeTo(subRepo, "branch");
// no change expected, as only master is subscribed:
expectToHaveSubmoduleState(superRepo, "branch", subKey, subHEAD);
}
@Test
@GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "false")
public void testSubmoduleShortCommitMessage() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
// The first update doesn't include any commit messages
ObjectId subRepoId = pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
expectToHaveCommitMessage(superRepo, "master", "Update git submodules\n\n");
// Any following update also has a short message
subRepoId = pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
expectToHaveCommitMessage(superRepo, "master", "Update git submodules\n\n");
}
@Test
@GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
public void testSubmoduleSubjectCommitMessage() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
// The first update doesn't include the rev log
RevWalk rw = subRepo.getRevWalk();
expectToHaveCommitMessage(
superRepo,
"master",
"Update git submodules\n\n"
+ "* Update "
+ subKey.get()
+ " from branch 'master'\n to "
+ subHEAD.getName());
// The next commit should generate only its commit message,
// omitting previous commit logs
subHEAD = pushChangeTo(subRepo, "master");
RevCommit subCommitMsg = rw.parseCommit(subHEAD);
expectToHaveCommitMessage(
superRepo,
"master",
"Update git submodules\n\n"
+ "* Update "
+ subKey.get()
+ " from branch 'master'\n to "
+ subHEAD.getName()
+ "\n - "
+ subCommitMsg.getShortMessage());
}
@Test
public void submoduleCommitMessage() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
// The first update doesn't include the rev log
RevWalk rw = subRepo.getRevWalk();
expectToHaveCommitMessage(
superRepo,
"master",
"Update git submodules\n\n"
+ "* Update "
+ subKey.get()
+ " from branch 'master'\n to "
+ subHEAD.getName());
// The next commit should generate only its commit message,
// omitting previous commit logs
subHEAD = pushChangeTo(subRepo, "master");
RevCommit subCommitMsg = rw.parseCommit(subHEAD);
expectToHaveCommitMessage(
superRepo,
"master",
"Update git submodules\n\n"
+ "* Update "
+ subKey.get()
+ " from branch 'master'\n to "
+ subHEAD.getName()
+ "\n - "
+ subCommitMsg.getFullMessage().replace("\n", "\n "));
}
@Test
public void subscriptionUnsubscribe() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
deleteAllSubscriptions(superRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
pushChangeTo(superRepo, "refs/heads/master", "commit after unsubscribe", "");
pushChangeTo(subRepo, "refs/heads/master", "commit after unsubscribe", "");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
}
@Test
public void subscriptionUnsubscribeByDeletingGitModules() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
deleteGitModulesFile(superRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
pushChangeTo(superRepo, "refs/heads/master", "commit after unsubscribe", "");
pushChangeTo(subRepo, "refs/heads/master", "commit after unsubscribe", "");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEADbeforeUnsubscribing);
}
@Test
public void subscriptionToDifferentBranches() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/foo", superKey, "refs/heads/master");
createSubmoduleSubscription(superRepo, "master", subKey, "foo");
ObjectId subFoo = pushChangeTo(subRepo, "foo");
pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subFoo);
}
@Test
public void branchCircularSubscription() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
allowMatchingSubmoduleSubscription(superKey, "refs/heads/master", subKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
pushChangeTo(superRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
createSubmoduleSubscription(subRepo, "master", superKey, "master");
pushChangeTo(subRepo, "master");
pushChangeTo(superRepo, "master");
assertThat(hasSubmodule(subRepo, "master", superKey)).isFalse();
assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
}
@Test
public void projectCircularSubscription() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");
pushChangeTo(subRepo, "master");
pushChangeTo(superRepo, "master");
pushChangeTo(subRepo, "dev");
pushChangeTo(superRepo, "dev");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
createSubmoduleSubscription(subRepo, "dev", superKey, "dev");
ObjectId subMasterHead = pushChangeTo(subRepo, "master");
ObjectId superDevHead = pushChangeTo(superRepo, "dev");
assertThat(hasSubmodule(superRepo, "master", subKey)).isTrue();
assertThat(hasSubmodule(subRepo, "dev", superKey)).isTrue();
expectToHaveSubmoduleState(superRepo, "master", subKey, subMasterHead);
expectToHaveSubmoduleState(subRepo, "dev", superKey, superDevHead);
}
@Test
public void subscriptionFailOnMissingACL() throws Exception {
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
}
@Test
public void subscriptionFailOnWrongProjectACL() throws Exception {
allowMatchingSubmoduleSubscription(
subKey, "refs/heads/master", Project.nameKey("wrong-super-project"), "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
}
@Test
public void subscriptionFailOnWrongBranchACL() throws Exception {
allowMatchingSubmoduleSubscription(
subKey, "refs/heads/master", superKey, "refs/heads/wrong-branch");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
pushChangeTo(subRepo, "master");
assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
}
@Test
public void subscriptionInheritACL() throws Exception {
Project.NameKey configKey = projectOperations.newProject().submitType(getSubmitType()).create();
grantPush(configKey);
Project.NameKey config2Key =
projectOperations.newProject().parent(configKey).submitType(getSubmitType()).create();
grantPush(config2Key);
cloneProject(config2Key);
subKey = projectOperations.newProject().parent(config2Key).submitType(getSubmitType()).create();
grantPush(subKey);
subRepo = cloneProject(subKey);
allowMatchingSubmoduleSubscription(configKey, "refs/heads/*", superKey, "refs/heads/*");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subHEAD);
}
@Test
public void allowedButNotSubscribed() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
subRepo
.branch("HEAD")
.commit()
.insertChangeId()
.message("some change")
.add("b.txt", "b contents for testing")
.create();
String refspec = "HEAD:refs/heads/master";
PushResult r =
Iterables.getOnlyElement(
subRepo.git().push().setRemote("origin").setRefSpecs(new RefSpec(refspec)).call());
assertThat(r.getMessages()).doesNotContain("error");
assertThat(r.getRemoteUpdate("refs/heads/master").getStatus())
.isEqualTo(RemoteRefUpdate.Status.OK);
assertThat(hasSubmodule(superRepo, "master", subKey)).isFalse();
}
@Test
public void subscriptionDeepRelative() throws Exception {
Project.NameKey nest = createProjectForPush(getSubmitType());
TestRepository<?> subRepo = cloneProject(nest);
// master is allowed to be subscribed to any superprojects branch:
allowMatchingSubmoduleSubscription(nest, "refs/heads/master", superKey, null);
pushChangeTo(subRepo, "master");
createRelativeSubmoduleSubscription(superRepo, "master", "../", nest, "master");
ObjectId subHEAD = pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", nest, subHEAD);
}
@Test
@GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
@GerritConfig(name = "submodule.maxCommitMessages", value = "1")
public void submoduleSubjectCommitMessageCountLimit() throws Exception {
testSubmoduleSubjectCommitMessageAndExpectTruncation();
}
@Test
@GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "SUBJECT_ONLY")
// The value 110 must tuned to the test environment, and is sensitive to the
// length of the uniquified repository name.
@GerritConfig(name = "submodule.maxCombinedCommitMessageSize", value = "110")
public void submoduleSubjectCommitMessageSizeLimit() throws Exception {
assume().that(isSubmitWholeTopicEnabled()).isFalse();
testSubmoduleSubjectCommitMessageAndExpectTruncation();
}
@Test
@UseClockStep
public void superRepoCommitHasSameAuthorAsSubmoduleCommit() throws Exception {
// Make sure that the commit is created at an earlier timestamp than the submit timestamp.
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
PushOneCommit.Result pushResult =
createChange(subRepo, "refs/heads/master", "Change", "a.txt", "some content", null);
approve(pushResult.getChangeId());
gApi.changes().id(pushResult.getChangeId()).current().submit();
// Expect that the author name/email is preserved for the superRepo commit, but a new author
// timestamp is used.
PersonIdent authorIdent = getAuthor(superRepo, "master");
assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
assertThat(authorIdent.getWhen())
.isGreaterThan(pushResult.getCommit().getAuthorIdent().getWhen());
}
@Test
@UseClockStep
public void superRepoCommitHasSameAuthorAsSubmoduleCommits() throws Exception {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
Project.NameKey proj2 = createProjectForPush(getSubmitType());
TestRepository<?> subRepo2 = cloneProject(proj2);
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
Config config = new Config();
prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
pushSubmoduleConfig(superRepo, "master", config);
String topic = "foo";
PushOneCommit.Result pushResult1 =
createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
approve(pushResult1.getChangeId());
PushOneCommit.Result pushResult2 =
createChange(subRepo2, "refs/heads/master", "Change 2", "b.txt", "other content", topic);
approve(pushResult2.getChangeId());
// Submit the topic, 2 changes with the same author.
gApi.changes().id(pushResult1.getChangeId()).current().submit();
// Expect that the author name/email is preserved for the superRepo commit, but a new author
// timestamp is used.
PersonIdent authorIdent = getAuthor(superRepo, "master");
assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
assertThat(authorIdent.getWhen())
.isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
assertThat(authorIdent.getWhen())
.isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
}
@Test
@UseClockStep
public void superRepoCommitHasGerritAsAuthorIfAuthorsOfSubmoduleCommitsDiffer() throws Exception {
assume().that(isSubmitWholeTopicEnabled()).isTrue();
Project.NameKey proj2 = createProjectForPush(getSubmitType());
TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
Config config = new Config();
prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
pushSubmoduleConfig(superRepo, "master", config);
String topic = "foo";
// Create change as admin.
PushOneCommit.Result pushResult1 =
createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
approve(pushResult1.getChangeId());
// Create change as user.
PushOneCommit push =
pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
PushOneCommit.Result pushResult2 = push.to("refs/for/master%topic=" + name(topic));
approve(pushResult2.getChangeId());
// Submit the topic, 2 changes with the different author.
gApi.changes().id(pushResult1.getChangeId()).current().submit();
// Expect that the Gerrit server identity is chosen as author for the superRepo commit and a
// new author timestamp is used.
PersonIdent authorIdent = getAuthor(superRepo, "master");
assertThat(authorIdent.getName()).isEqualTo(serverIdent.get().getName());
assertThat(authorIdent.getEmailAddress()).isEqualTo(serverIdent.get().getEmailAddress());
assertThat(authorIdent.getWhen())
.isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
assertThat(authorIdent.getWhen())
.isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
}
@Test
public void updateOnlyRelevantSubmodules() throws Exception {
Project.NameKey subkey1 = createProjectForPush(getSubmitType());
Project.NameKey subkey2 = createProjectForPush(getSubmitType());
TestRepository<?> subRepo1 = cloneProject(subkey1);
TestRepository<?> subRepo2 = cloneProject(subkey2);
allowMatchingSubmoduleSubscription(subkey1, "refs/heads/master", superKey, "refs/heads/master");
allowMatchingSubmoduleSubscription(subkey2, "refs/heads/master", superKey, "refs/heads/master");
Config config = new Config();
prepareSubmoduleConfigEntry(config, subkey1, "master");
prepareSubmoduleConfigEntry(config, subkey2, "master");
pushSubmoduleConfig(superRepo, "master", config);
// Push once to initialize submodules.
ObjectId subTip2 = pushChangeTo(subRepo2, "master");
ObjectId subTip1 = pushChangeTo(subRepo1, "master");
expectToHaveSubmoduleState(superRepo, "master", subkey1, subTip1);
expectToHaveSubmoduleState(superRepo, "master", subkey2, subTip2);
directUpdateRef(subkey2, "refs/heads/master");
subTip1 = pushChangeTo(subRepo1, "master");
expectToHaveSubmoduleState(superRepo, "master", subkey1, subTip1);
expectToHaveSubmoduleState(superRepo, "master", subkey2, subTip2);
}
@Test
public void skipUpdatingBrokenGitlinkPointer() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
// Push once to initialize submodule.
ObjectId subTip = pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, subTip);
// Write an invalid SHA-1 directly to the gitlink.
ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
directUpdateSubmodule(superKey, "refs/heads/master", subKey, badId);
expectToHaveSubmoduleState(superRepo, "master", subKey, badId);
// Push succeeds, but gitlink update is skipped.
pushChangeTo(subRepo, "master");
expectToHaveSubmoduleState(superRepo, "master", subKey, badId);
}
@Test
public void blockSubmissionForChangesModifyingSpecifiedSubmodule() throws Exception {
ObjectId commitId = getCommitWithSubmoduleUpdate();
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = "branch";
cherryPickInput.allowConflicts = true;
// The rule will fail if the next change has a submodule file modification with subKey.
modifySubmitRulesToBlockSubmoduleChanges(String.format("file('%s','M','SUBMODULE')", subKey));
// Cherry-pick the newly created commit which contains a submodule update, to branch "branch".
ChangeApi changeApi =
gApi.projects().name(superKey.get()).commit(commitId.getName()).cherryPick(cherryPickInput);
// Add another file to this change for good measure.
PushOneCommit.Result result =
amendChange(changeApi.get().changeId, "subject", "newFile", "content");
assertThat(getStatus(result.getChange())).isEqualTo("NOT_READY");
assertThat(gApi.changes().id(result.getChangeId()).get().submittable).isFalse();
}
@Test
public void blockSubmissionWithSubmodules() throws Exception {
ObjectId commitId = getCommitWithSubmoduleUpdate();
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = "branch";
cherryPickInput.allowConflicts = true;
// The rule will fail if the next change has any submodule file.
modifySubmitRulesToBlockSubmoduleChanges("file(_,_,'SUBMODULE')");
// Cherry-pick the newly created commit which contains a submodule update, to branch "branch".
ChangeApi changeApi =
gApi.projects().name(superKey.get()).commit(commitId.getName()).cherryPick(cherryPickInput);
// Add another file to this change for good measure.
PushOneCommit.Result result =
amendChange(changeApi.get().changeId, "subject", "newFile", "content");
assertThat(getStatus(result.getChange())).isEqualTo("NOT_READY");
assertThat(gApi.changes().id(result.getChangeId()).get().submittable).isFalse();
}
@Test
public void doNotBlockSubmissionWithoutSubmodules() throws Exception {
modifySubmitRulesToBlockSubmoduleChanges("file(_,_,'SUBMODULE')");
PushOneCommit.Result result =
createChange(superRepo, "refs/heads/master", "subject", "newFile", "content", null);
assertThat(getStatus(result.getChange())).isEqualTo("OK");
assertThat(gApi.changes().id(result.getChangeId()).get().submittable).isTrue();
}
private ObjectId getCommitWithSubmoduleUpdate() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/*", superKey, "refs/heads/*");
// Create branch "branch" for the parent and the submodule
pushChangeTo(superRepo, "branch");
pushChangeTo(subRepo, "branch");
// Make the superRepo a parent repo of the subRepo, for both branches.
createSubmoduleSubscription(superRepo, "master", subKey, "master");
createSubmoduleSubscription(superRepo, "branch", subKey, "branch");
pushChangeTo(subRepo, "master");
pushChangeTo(subRepo, "branch");
// This push creates a new commit in subRepo, master branch, which makes superRepo update their
// submodule.
pushChangeTo(subRepo, "master");
// Fetch the commit from superRepo that Gerrit created automatically to fulfill the submodule
// subscription.
return superRepo
.git()
.fetch()
.setRemote("origin")
.call()
.getAdvertisedRef("refs/heads/" + "master")
.getObjectId();
}
private void modifySubmitRulesToBlockSubmoduleChanges(String filePrologQuery) throws Exception {
String newContent =
String.format(
"submit_rule(submit(R)) :-\n"
+ " gerrit:includes_file(%s),\n"
+ " !,\n"
+ " R = label('All-Submodules-Resolved', need(_)).\n"
+ "submit_rule(submit(label('All-Submodules-Resolved', ok(A)))) :-\n"
+ " gerrit:commit_author(A).",
filePrologQuery);
try (Repository repo = repoManager.openRepository(superKey);
TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
testRepo
.branch(RefNames.REFS_CONFIG)
.commit()
.author(admin.newIdent())
.committer(admin.newIdent())
.add(RULES_PL_FILE, newContent)
.message("Modify rules.pl")
.create();
}
projectCache.evict(superKey);
}
private String getStatus(ChangeData cd) throws Exception {
try (AutoCloseable ignored = changeIndexOperations.disableReadsAndWrites();
AutoCloseable accountIndex = accountIndexOperations.disableReadsAndWrites()) {
SubmitRuleEvaluator ruleEvaluator = evaluatorFactory.create(SubmitRuleOptions.defaults());
return ruleEvaluator.evaluate(cd).iterator().next().status.toString();
}
}
private ObjectId directUpdateRef(Project.NameKey project, String ref) throws Exception {
try (Repository serverRepo = repoManager.openRepository(project);
TestRepository<Repository> tr = new TestRepository<>(serverRepo)) {
return tr.branch(ref).commit().create().copy();
}
}
private void testSubmoduleSubjectCommitMessageAndExpectTruncation() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
pushChangeTo(subRepo, "master");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
// The first update doesn't include the rev log, so we ignore it
pushChangeTo(subRepo, "master");
// Next, we push two commits at once. Since maxCommitMessages=1, we expect to see only the first
// message plus ellipsis to mark truncation.
ObjectId subHEAD = pushChangesTo(subRepo, "master", 2);
RevCommit subCommitMsg = subRepo.getRevWalk().parseCommit(subHEAD);
expectToHaveCommitMessage(
superRepo,
"master",
String.format(
"Update git submodules\n\n* Update %s from branch 'master'\n to %s\n - %s\n\n[...]",
subKey.get(), subHEAD.getName(), subCommitMsg.getShortMessage()));
}
}