// 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.Truth.assertWithMessage;
import static com.google.gerrit.acceptance.GitUtil.getChangeId;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;

import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
import java.util.ArrayDeque;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;

@NoHttpd
public class SubmoduleSubscriptionsWholeTopicMergeIT extends AbstractSubmoduleSubscription {

  @ConfigSuite.Default
  public static Config mergeIfNecessary() {
    return submitByMergeIfNecessary();
  }

  @ConfigSuite.Config
  public static Config mergeAlways() {
    return submitByMergeAlways();
  }

  @ConfigSuite.Config
  public static Config cherryPick() {
    return submitByCherryPickConfig();
  }

  @ConfigSuite.Config
  public static Config rebaseAlways() {
    return submitByRebaseAlwaysConfig();
  }

  @ConfigSuite.Config
  public static Config rebaseIfNecessary() {
    return submitByRebaseIfNecessaryConfig();
  }

  @Inject private ProjectOperations projectOperations;

  @Test
  public void subscriptionUpdateOfManyChanges() throws Exception {
    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
    createSubmoduleSubscription(superRepo, "master", subKey, "master");

    ObjectId subHEAD =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("some change")
            .add("a.txt", "a contents ")
            .create();
    subRepo
        .git()
        .push()
        .setRemote("origin")
        .setRefSpecs(new RefSpec("HEAD:refs/heads/master"))
        .call();

    RevCommit c = subRepo.getRevWalk().parseCommit(subHEAD);

    RevCommit c1 =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("first change")
            .add("asdf", "asdf\n")
            .create();
    subRepo
        .git()
        .push()
        .setRemote("origin")
        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
        .call();

    subRepo.reset(c.getId());
    RevCommit c2 =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("qwerty")
            .add("qwerty", "qwerty")
            .create();

    RevCommit c3 =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("qwerty followup")
            .add("qwerty", "qwerty\nqwerty\n")
            .create();
    subRepo
        .git()
        .push()
        .setRemote("origin")
        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
        .call();

    String id1 = getChangeId(subRepo, c1).get();
    String id2 = getChangeId(subRepo, c2).get();
    String id3 = getChangeId(subRepo, c3).get();
    gApi.changes().id(id1).current().review(ReviewInput.approve());
    gApi.changes().id(id2).current().review(ReviewInput.approve());
    gApi.changes().id(id3).current().review(ReviewInput.approve());

    gApi.changes().id(id1).current().submit();
    ObjectId subRepoId =
        subRepo
            .git()
            .fetch()
            .setRemote("origin")
            .call()
            .getAdvertisedRef("refs/heads/master")
            .getObjectId();

    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
  }

  @Test
  public void subscriptionUpdateIncludingChangeInSuperproject() throws Exception {
    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");

    createSubmoduleSubscription(superRepo, "master", subKey, "master");

    ObjectId subHEAD =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("some change")
            .add("a.txt", "a contents ")
            .create();
    subRepo
        .git()
        .push()
        .setRemote("origin")
        .setRefSpecs(new RefSpec("HEAD:refs/heads/master"))
        .call();

    RevCommit c = subRepo.getRevWalk().parseCommit(subHEAD);

    RevCommit c1 =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("first change")
            .add("asdf", "asdf\n")
            .create();
    subRepo
        .git()
        .push()
        .setRemote("origin")
        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
        .call();

    subRepo.reset(c.getId());
    RevCommit c2 =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("qwerty")
            .add("qwerty", "qwerty")
            .create();

    RevCommit c3 =
        subRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("qwerty followup")
            .add("qwerty", "qwerty\nqwerty\n")
            .create();
    subRepo
        .git()
        .push()
        .setRemote("origin")
        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
        .call();

    RevCommit c4 =
        superRepo
            .branch("HEAD")
            .commit()
            .insertChangeId()
            .message("new change on superproject")
            .add("foo", "bar")
            .create();
    superRepo
        .git()
        .push()
        .setRemote("origin")
        .setRefSpecs(new RefSpec("HEAD:refs/for/master%topic=" + name("topic-foo")))
        .call();

    String id1 = getChangeId(subRepo, c1).get();
    String id2 = getChangeId(subRepo, c2).get();
    String id3 = getChangeId(subRepo, c3).get();
    String id4 = getChangeId(superRepo, c4).get();
    gApi.changes().id(id1).current().review(ReviewInput.approve());
    gApi.changes().id(id2).current().review(ReviewInput.approve());
    gApi.changes().id(id3).current().review(ReviewInput.approve());
    gApi.changes().id(id4).current().review(ReviewInput.approve());

    gApi.changes().id(id1).current().submit();
    ObjectId subRepoId =
        subRepo
            .git()
            .fetch()
            .setRemote("origin")
            .call()
            .getAdvertisedRef("refs/heads/master")
            .getObjectId();

    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
  }

  @Test
  public void updateManySubmodules() throws Exception {
    final int NUM = 3;
    Project.NameKey subKey[] = new Project.NameKey[NUM];
    TestRepository<?> sub[] = new TestRepository[NUM];
    String prefix = RandomStringUtils.randomAlphabetic(8);
    for (int i = 0; i < subKey.length; i++) {
      subKey[i] =
          projectOperations
              .newProject()
              .name(prefix + "sub" + i)
              .submitType(getSubmitType())
              .create();
      projectOperations
          .project(subKey[i])
          .forUpdate()
          .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
          .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
          .update();
      sub[i] = cloneProject(subKey[i]);
    }

    for (int i = 0; i < subKey.length; i++) {
      allowMatchingSubmoduleSubscription(
          subKey[i], "refs/heads/master", superKey, "refs/heads/master");
    }

    Config config = new Config();
    for (int i = 0; i < subKey.length; i++) {
      prepareSubmoduleConfigEntry(config, subKey[i], "master");
    }
    pushSubmoduleConfig(superRepo, "master", config);

    ObjectId superPreviousId = pushChangeTo(superRepo, "master");

    ObjectId subId[] = new ObjectId[NUM];

    for (int i = 0; i < sub.length; i++) {
      subId[i] = pushChangeTo(sub[i], "refs/for/master", "some message", "same-topic");
      approve(getChangeId(sub[i], subId[i]).get());
    }

    gApi.changes().id(getChangeId(sub[0], subId[0]).get()).current().submit();

    for (int i = 0; i < sub.length; i++) {
      expectToHaveSubmoduleState(superRepo, "master", subKey[i], sub[i], "master");
    }

    String heads[] = new String[NUM];
    for (int i = 0; i < heads.length; i++) {
      heads[i] =
          sub[i]
              .git()
              .fetch()
              .setRemote("origin")
              .call()
              .getAdvertisedRef("refs/heads/master")
              .getObjectId()
              .name();
    }

    if (getSubmitType() == SubmitType.MERGE_IF_NECESSARY) {
      expectToHaveCommitMessage(
          superRepo,
          "master",
          "Update git submodules\n\n"
              + "* Update "
              + subKey[0].get()
              + " from branch 'master'\n  to "
              + heads[0]
              + "\n\n* Update "
              + subKey[1].get()
              + " from branch 'master'\n  to "
              + heads[1]
              + "\n\n* Update "
              + subKey[2].get()
              + " from branch 'master'\n  to "
              + heads[2]);
    }

    superRepo
        .git()
        .fetch()
        .setRemote("origin")
        .call()
        .getAdvertisedRef("refs/heads/master")
        .getObjectId();

    assertWithMessage("submodule subscription update should have made one commit")
        .that(superRepo.getRepository().resolve("origin/master^"))
        .isEqualTo(superPreviousId);
  }

  @Test
  public void doNotUseFastForward() throws Exception {
    // like setup, but without empty commit
    superKey =
        projectOperations
            .newProject()
            .submitType(getSubmitType())
            .createEmptyCommit(false)
            .create();
    grantPush(superKey);
    subKey =
        projectOperations
            .newProject()
            .submitType(getSubmitType())
            .createEmptyCommit(false)
            .create();
    grantPush(subKey);
    superRepo = cloneProject(superKey);
    subRepo = cloneProject(subKey);

    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");

    createSubmoduleSubscription(superRepo, "master", subKey, "master");

    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");

    ObjectId superId = pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");

    String subChangeId = getChangeId(subRepo, subId).get();
    approve(subChangeId);
    approve(getChangeId(superRepo, superId).get());

    gApi.changes().id(subChangeId).current().submit();

    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
    RevCommit superHead = projectOperations.project(superKey).getHead("master");
    assertThat(superHead.getShortMessage()).contains("some message");
    assertThat(superHead.getId()).isNotEqualTo(superId);
  }

  @Test
  public void useFastForwardWhenNoSubmodule() throws Exception {
    // like setup, but without empty commit
    superKey =
        projectOperations
            .newProject()
            .submitType(getSubmitType())
            .createEmptyCommit(false)
            .create();
    grantPush(superKey);
    subKey =
        projectOperations
            .newProject()
            .submitType(getSubmitType())
            .createEmptyCommit(false)
            .create();
    grantPush(subKey);
    superRepo = cloneProject(superKey);
    subRepo = cloneProject(subKey);

    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
    ObjectId superId = pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");

    String subChangeId = getChangeId(subRepo, subId).get();
    approve(subChangeId);
    approve(getChangeId(superRepo, superId).get());

    gApi.changes().id(subChangeId).current().submit();

    RevCommit superHead = projectOperations.project(superKey).getHead("master");
    assertThat(superHead.getShortMessage()).isEqualTo("some message");
    assertThat(superHead.getId()).isEqualTo(superId);
  }

  @Test
  public void sameProjectSameBranchDifferentPaths() throws Exception {
    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");

    Config config = new Config();
    prepareSubmoduleConfigEntry(config, subKey, "master");
    Project.NameKey copyKey = nameKey("sub-copy");
    prepareSubmoduleConfigEntry(config, subKey, copyKey, "master");
    pushSubmoduleConfig(superRepo, "master", config);

    ObjectId superPreviousId = pushChangeTo(superRepo, "master");

    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "");

    approve(getChangeId(subRepo, subId).get());

    gApi.changes().id(getChangeId(subRepo, subId).get()).current().submit();

    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");
    expectToHaveSubmoduleState(superRepo, "master", copyKey, subRepo, "master");

    superRepo
        .git()
        .fetch()
        .setRemote("origin")
        .call()
        .getAdvertisedRef("refs/heads/master")
        .getObjectId();

    assertWithMessage("submodule subscription update should have made one commit")
        .that(superRepo.getRepository().resolve("origin/master^"))
        .isEqualTo(superPreviousId);
  }

  @Test
  public void sameProjectDifferentBranchDifferentPaths() throws Exception {
    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
    allowMatchingSubmoduleSubscription(subKey, "refs/heads/dev", superKey, "refs/heads/master");

    ObjectId devHead = pushChangeTo(subRepo, "dev");
    Config config = new Config();
    prepareSubmoduleConfigEntry(config, subKey, nameKey("sub-master"), "master");
    prepareSubmoduleConfigEntry(config, subKey, nameKey("sub-dev"), "dev");
    pushSubmoduleConfig(superRepo, "master", config);

    ObjectId subMasterId =
        pushChangeTo(
            subRepo, "refs/for/master", "some message", "b.txt", "content b", "same-topic");

    subRepo.reset(devHead);
    ObjectId subDevId =
        pushChangeTo(
            subRepo, "refs/for/dev", "some message in dev", "b.txt", "content b", "same-topic");

    approve(getChangeId(subRepo, subMasterId).get());
    approve(getChangeId(subRepo, subDevId).get());

    ObjectId superPreviousId = pushChangeTo(superRepo, "master");

    gApi.changes().id(getChangeId(subRepo, subMasterId).get()).current().submit();

    expectToHaveSubmoduleState(superRepo, "master", nameKey("sub-master"), subRepo, "master");
    expectToHaveSubmoduleState(superRepo, "master", nameKey("sub-dev"), subRepo, "dev");

    superRepo
        .git()
        .fetch()
        .setRemote("origin")
        .call()
        .getAdvertisedRef("refs/heads/master")
        .getObjectId();

    assertWithMessage("submodule subscription update should have made one commit")
        .that(superRepo.getRepository().resolve("origin/master^"))
        .isEqualTo(superPreviousId);
  }

  @Test
  public void nonSubmoduleInSameTopic() throws Exception {
    Project.NameKey standaloneKey = createProjectForPush(getSubmitType());
    TestRepository<?> standAlone = cloneProject(standaloneKey);

    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");

    createSubmoduleSubscription(superRepo, "master", subKey, "master");

    ObjectId superPreviousId = pushChangeTo(superRepo, "master");

    ObjectId subId = pushChangeTo(subRepo, "refs/for/master", "some message", "same-topic");
    ObjectId standAloneId =
        pushChangeTo(standAlone, "refs/for/master", "some message", "same-topic");

    String subChangeId = getChangeId(subRepo, subId).get();
    String standAloneChangeId = getChangeId(standAlone, standAloneId).get();
    approve(subChangeId);
    approve(standAloneChangeId);

    gApi.changes().id(subChangeId).current().submit();

    expectToHaveSubmoduleState(superRepo, "master", subKey, subRepo, "master");

    ChangeStatus status = gApi.changes().id(standAloneChangeId).info().status;
    assertThat(status).isEqualTo(ChangeStatus.MERGED);

    superRepo
        .git()
        .fetch()
        .setRemote("origin")
        .call()
        .getAdvertisedRef("refs/heads/master")
        .getObjectId();

    assertWithMessage("submodule subscription update should have made one commit")
        .that(superRepo.getRepository().resolve("origin/master^"))
        .isEqualTo(superPreviousId);
  }

  @Test
  public void recursiveSubmodules() throws Exception {
    Project.NameKey topKey = createProjectForPush(getSubmitType());
    Project.NameKey midKey = createProjectForPush(getSubmitType());
    Project.NameKey botKey = createProjectForPush(getSubmitType());
    TestRepository<?> topRepo = cloneProject(topKey);
    TestRepository<?> midRepo = cloneProject(midKey);
    TestRepository<?> bottomRepo = cloneProject(botKey);

    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");

    createSubmoduleSubscription(topRepo, "master", midKey, "master");
    createSubmoduleSubscription(midRepo, "master", botKey, "master");

    ObjectId bottomHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
    ObjectId topHead = pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");

    String id1 = getChangeId(bottomRepo, bottomHead).get();
    String id2 = getChangeId(topRepo, topHead).get();

    gApi.changes().id(id1).current().review(ReviewInput.approve());
    gApi.changes().id(id2).current().review(ReviewInput.approve());

    gApi.changes().id(id1).current().submit();

    expectToHaveSubmoduleState(midRepo, "master", botKey, bottomRepo, "master");
    expectToHaveSubmoduleState(topRepo, "master", midKey, midRepo, "master");
  }

  @Test
  public void triangleSubmodules() throws Exception {
    Project.NameKey topKey = createProjectForPush(getSubmitType());
    Project.NameKey midKey = createProjectForPush(getSubmitType());
    Project.NameKey botKey = createProjectForPush(getSubmitType());
    TestRepository<?> topRepo = cloneProject(topKey);
    TestRepository<?> midRepo = cloneProject(midKey);
    TestRepository<?> bottomRepo = cloneProject(botKey);

    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", topKey, "refs/heads/master");

    createSubmoduleSubscription(midRepo, "master", botKey, "master");
    Config config = new Config();
    prepareSubmoduleConfigEntry(config, botKey, "master");
    prepareSubmoduleConfigEntry(config, midKey, "master");
    pushSubmoduleConfig(topRepo, "master", config);

    ObjectId bottomHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
    ObjectId topHead = pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");

    String id1 = getChangeId(bottomRepo, bottomHead).get();
    String id2 = getChangeId(topRepo, topHead).get();

    gApi.changes().id(id1).current().review(ReviewInput.approve());
    gApi.changes().id(id2).current().review(ReviewInput.approve());

    gApi.changes().id(id1).current().submit();

    expectToHaveSubmoduleState(midRepo, "master", botKey, bottomRepo, "master");
    expectToHaveSubmoduleState(topRepo, "master", midKey, midRepo, "master");
    expectToHaveSubmoduleState(topRepo, "master", botKey, bottomRepo, "master");
  }

  private void testBranchCircularSubscription(ThrowingConsumer<String> apiCall) throws Exception {
    Project.NameKey topKey = createProjectForPush(getSubmitType());
    Project.NameKey midKey = createProjectForPush(getSubmitType());
    Project.NameKey botKey = createProjectForPush(getSubmitType());
    TestRepository<?> topRepo = cloneProject(topKey);
    TestRepository<?> midRepo = cloneProject(midKey);
    TestRepository<?> bottomRepo = cloneProject(botKey);

    createSubmoduleSubscription(midRepo, "master", botKey, "master");
    createSubmoduleSubscription(topRepo, "master", midKey, "master");
    createSubmoduleSubscription(bottomRepo, "master", topKey, "master");

    allowMatchingSubmoduleSubscription(botKey, "refs/heads/master", midKey, "refs/heads/master");
    allowMatchingSubmoduleSubscription(midKey, "refs/heads/master", topKey, "refs/heads/master");
    allowMatchingSubmoduleSubscription(topKey, "refs/heads/master", botKey, "refs/heads/master");

    ObjectId bottomMasterHead = pushChangeTo(bottomRepo, "refs/for/master", "some message", "");
    String changeId = getChangeId(bottomRepo, bottomMasterHead).get();

    approve(changeId);

    ResourceConflictException thrown =
        assertThrows(ResourceConflictException.class, () -> apiCall.accept(changeId));
    assertThat(thrown).hasMessageThat().contains("Branch level circular subscriptions detected");
    assertThat(thrown).hasMessageThat().contains(topKey.get() + ",refs/heads/master");
    assertThat(thrown).hasMessageThat().contains(midKey.get() + ",refs/heads/master");
    assertThat(thrown).hasMessageThat().contains(botKey.get() + ",refs/heads/master");
  }

  @Test
  public void branchCircularSubscription() throws Exception {
    testBranchCircularSubscription(changeId -> gApi.changes().id(changeId).current().submit());
  }

  @Test
  public void projectCircularSubscriptionWholeTopic() throws Exception {
    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
    allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");

    pushChangeTo(subRepo, "dev");
    pushChangeTo(superRepo, "dev");

    createSubmoduleSubscription(superRepo, "master", subKey, "master");
    createSubmoduleSubscription(subRepo, "dev", superKey, "dev");

    ObjectId subMasterHead =
        pushChangeTo(
            subRepo, "refs/for/master", "b.txt", "content b", "some message", "same-topic");
    ObjectId superDevHead = pushChangeTo(superRepo, "refs/for/dev", "some message", "same-topic");

    approve(getChangeId(subRepo, subMasterHead).get());
    approve(getChangeId(superRepo, superDevHead).get());

    Throwable thrown =
        assertThrows(
            Throwable.class,
            () -> gApi.changes().id(getChangeId(subRepo, subMasterHead).get()).current().submit());
    assertThat(thrown).hasMessageThat().contains("Project level circular subscriptions detected");
    assertThat(thrown).hasMessageThat().contains(subKey.get());
    assertThat(thrown).hasMessageThat().contains(superKey.get());
  }

  @Test
  public void projectNoSubscriptionWholeTopic() throws Exception {
    Project.NameKey keyA = createProjectForPush(getSubmitType());
    Project.NameKey keyB = createProjectForPush(getSubmitType());

    TestRepository<?> repoA = cloneProject(keyA);
    TestRepository<?> repoB = cloneProject(keyB);
    // bootstrap the dev branch
    ObjectId a0 = pushChangeTo(repoA, "dev");

    // bootstrap the dev branch
    ObjectId b0 = pushChangeTo(repoB, "dev");

    // create a change for master branch in repo a
    ObjectId aHead =
        pushChangeTo(
            repoA,
            "refs/for/master",
            "master.txt",
            "content master A",
            "some message in a master.txt",
            "same-topic");

    // create a change for master branch in repo b
    ObjectId bHead =
        pushChangeTo(
            repoB,
            "refs/for/master",
            "master.txt",
            "content master B",
            "some message in b master.txt",
            "same-topic");

    // create a change for dev branch in repo a
    repoA.reset(a0);
    ObjectId aDevHead =
        pushChangeTo(
            repoA,
            "refs/for/dev",
            "dev.txt",
            "content dev A",
            "some message in a dev.txt",
            "same-topic");

    // create a change for dev branch in repo b
    repoB.reset(b0);
    ObjectId bDevHead =
        pushChangeTo(
            repoB,
            "refs/for/dev",
            "dev.txt",
            "content dev B",
            "some message in b dev.txt",
            "same-topic");

    approve(getChangeId(repoA, aHead).get());
    approve(getChangeId(repoB, bHead).get());
    approve(getChangeId(repoA, aDevHead).get());
    approve(getChangeId(repoB, bDevHead).get());

    gApi.changes().id(getChangeId(repoA, aDevHead).get()).current().submit();
    assertThat(projectOperations.project(keyA).getHead("refs/heads/master").getShortMessage())
        .contains("some message in a master.txt");
    assertThat(projectOperations.project(keyA).getHead("refs/heads/dev").getShortMessage())
        .contains("some message in a dev.txt");
    assertThat(projectOperations.project(keyB).getHead("refs/heads/master").getShortMessage())
        .contains("some message in b master.txt");
    assertThat(projectOperations.project(keyB).getHead("refs/heads/dev").getShortMessage())
        .contains("some message in b dev.txt");
  }

  @Test
  public void twoProjectsMultipleBranchesWholeTopic() throws Exception {
    Project.NameKey keyA = createProjectForPush(getSubmitType());
    Project.NameKey keyB = createProjectForPush(getSubmitType());
    TestRepository<?> repoA = cloneProject(keyA);
    TestRepository<?> repoB = cloneProject(keyB);
    // bootstrap the dev branch
    pushChangeTo(repoA, "dev");

    // bootstrap the dev branch
    ObjectId b0 = pushChangeTo(repoB, "dev");

    allowMatchingSubmoduleSubscription(keyB, "refs/heads/master", keyA, "refs/heads/master");
    allowMatchingSubmoduleSubscription(keyB, "refs/heads/dev", keyA, "refs/heads/dev");

    createSubmoduleSubscription(repoA, "master", keyB, "master");
    createSubmoduleSubscription(repoA, "dev", keyB, "dev");

    // create a change for master branch in repo b
    ObjectId bHead =
        pushChangeTo(
            repoB,
            "refs/for/master",
            "master.txt",
            "content master B",
            "some message in b master.txt",
            "same-topic");

    // create a change for dev branch in repo b
    repoB.reset(b0);
    ObjectId bDevHead =
        pushChangeTo(
            repoB,
            "refs/for/dev",
            "dev.txt",
            "content dev B",
            "some message in b dev.txt",
            "same-topic");

    approve(getChangeId(repoB, bHead).get());
    approve(getChangeId(repoB, bDevHead).get());
    gApi.changes().id(getChangeId(repoB, bHead).get()).current().submit();

    expectToHaveSubmoduleState(repoA, "master", keyB, repoB, "master");
    expectToHaveSubmoduleState(repoA, "dev", keyB, repoB, "dev");
  }

  @Test
  public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
    Project.NameKey subKey1 = createProjectForPush(getSubmitType());
    TestRepository<?> sub1 = cloneProject(subKey1);
    Project.NameKey subKey2 = createProjectForPush(getSubmitType());
    TestRepository<?> sub2 = 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);

    ObjectId superPreviousId = pushChangeTo(superRepo, "master");

    String topic = "same-topic";
    ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic);
    ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", topic);

    String changeId1 = getChangeId(sub1, sub1Id).get();
    String changeId2 = getChangeId(sub2, sub2Id).get();
    approve(changeId1);
    approve(changeId2);

    TestSubmitInput input = new TestSubmitInput();
    input.generateLockFailures =
        new ArrayDeque<>(
            ImmutableList.of(
                false, // Change 1, attempt 1: success
                true, // Change 2, attempt 1: lock failure
                false, // Change 1, attempt 2: success
                false, // Change 2, attempt 2: success
                false)); // Leftover value to check total number of calls.
    gApi.changes().id(changeId1).current().submit(input);

    assertThat(info(changeId1).status).isEqualTo(ChangeStatus.MERGED);
    assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED);

    sub1.git().fetch().call();
    RevWalk rw1 = sub1.getRevWalk();
    RevCommit master1 = rw1.parseCommit(projectOperations.project(subKey1).getHead("master"));
    RevCommit change1Ps = parseCurrentRevision(rw1, changeId1);
    assertThat(rw1.isMergedInto(change1Ps, master1)).isTrue();

    sub2.git().fetch().call();
    RevWalk rw2 = sub2.getRevWalk();
    RevCommit master2 = rw2.parseCommit(projectOperations.project(subKey2).getHead("master"));
    RevCommit change2Ps = parseCurrentRevision(rw2, changeId2);
    assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue();

    assertThat(input.generateLockFailures).containsExactly(false);

    expectToHaveSubmoduleState(superRepo, "master", subKey1, sub1, "master");
    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");

    assertWithMessage("submodule subscription update should have made one commit")
        .that(superRepo.getRepository().resolve("origin/master^"))
        .isEqualTo(superPreviousId);
  }

  @Test
  public void skipUpdatingBrokenGitlinkPointer() throws Exception {
    Project.NameKey subKey1 = createProjectForPush(getSubmitType());
    TestRepository<?> sub1 = cloneProject(subKey1);
    Project.NameKey subKey2 = createProjectForPush(getSubmitType());
    TestRepository<?> sub2 = 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);

    // Write an invalid SHA-1 directly to one of the gitlinks.
    ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
    directUpdateSubmodule(superKey, "refs/heads/master", subKey1, badId);
    expectToHaveSubmoduleState(superRepo, "master", subKey1, badId);

    String topic = "same-topic";
    ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic);
    ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", topic);

    String changeId1 = getChangeId(sub1, sub1Id).get();
    String changeId2 = getChangeId(sub2, sub2Id).get();
    approve(changeId1);
    approve(changeId2);

    gApi.changes().id(changeId1).current().submit();

    assertThat(info(changeId1).status).isEqualTo(ChangeStatus.MERGED);
    assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED);

    // sub1 was skipped but sub2 succeeded.
    expectToHaveSubmoduleState(superRepo, "master", subKey1, badId);
    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");
  }

  private Project.NameKey nameKey(String s) {
    return Project.nameKey(name(s));
  }
}
