// 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 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.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 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", newContent)
          .message("Modify rules.pl")
          .create();
    }
    projectCache.evict(superKey);
  }

  private String getStatus(ChangeData cd) throws Exception {

    try (AutoCloseable changeIndex = disableChangeIndex()) {
      try (AutoCloseable accountIndex = disableAccountIndex()) {
        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()));
  }
}
