#
#  Copyright (C) 2018 Codethink Limited
#  Copyright (C) 2018 Bloomberg Finance LP
#
#  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.
#
#  Authors: <NAME> <<EMAIL>>
#           <NAME> <<EMAIL>>
#           <NAME> <<EMAIL>>
#

# Pylint doesn't play well with fixtures and dependency injection from pytest
# pylint: disable=redefined-outer-name

import os
import subprocess
import shutil

import pytest

from buildstream import Node
from buildstream.exceptions import ErrorDomain
from buildstream.plugin import CoreWarnings
from buildstream._testing import cli  # pylint: disable=unused-import
from buildstream._testing import generate_project, generate_element, load_yaml
from buildstream._testing import create_repo

from tests.testutils.site import HAVE_GIT, HAVE_OLD_GIT

DATA_DIR = os.path.join(
    os.path.dirname(os.path.realpath(__file__)),
    "git",
)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_fetch_bad_ref(cli, tmpdir, datafiles):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Write out our test target with a bad ref
    element = {"kind": "import", "sources": [repo.source_config(ref="5")]}
    generate_element(project, "target.bst", element)

    # Assert that fetch raises an error here
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_main_error(ErrorDomain.STREAM, None)
    result.assert_task_error(ErrorDomain.SOURCE, None)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.skipif(HAVE_OLD_GIT, reason="old git cannot clone a shallow repo to stage the source")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_fetch_shallow(cli, tmpdir, datafiles):
    project = str(datafiles)
    workspacedir = os.path.join(str(tmpdir), "workspace")

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))
    first_commit = repo.latest_commit()
    repo.add_commit()
    repo.add_tag("tag")

    ref = "tag-0-g" + repo.latest_commit()

    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["workspace", "open", "--directory", workspacedir, "target.bst"])
    result.assert_success()

    assert subprocess.call(["git", "show", "tag"], cwd=workspacedir) == 0
    assert subprocess.call(["git", "show", first_commit], cwd=workspacedir) != 0


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_checkout(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_recursive_submodule_fetch_checkout(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create a submodule from the 'othersubrepofiles' subdir
    subsubrepo = create_repo("git", str(tmpdir), "subsubrepo")
    subsubrepo.create(os.path.join(project, "othersubrepofiles"))

    # Create another submodule from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Configure submodules
    subrepo.add_submodule("subdir", "file://" + subsubrepo.repo)
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out all files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "subdir", "unicornfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_source_enable_explicit(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=True)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_source_disable(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=False)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_submodule_does_override(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo, checkout=True)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=False)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_submodule_individual_checkout(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create another submodule from the 'othersubrepofiles' subdir
    other_subrepo = create_repo("git", str(tmpdir), "othersubrepo")
    other_subrepo.create(os.path.join(project, "othersubrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    repo.add_submodule("subdir", "file://" + subrepo.repo, checkout=False)
    ref = repo.add_submodule("othersubdir", "file://" + other_subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=True)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "othersubdir", "unicornfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_submodule_individual_checkout_explicit(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create another submodule from the 'othersubrepofiles' subdir
    other_subrepo = create_repo("git", str(tmpdir), "othersubrepo")
    other_subrepo.create(os.path.join(project, "othersubrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    repo.add_submodule("subdir", "file://" + subrepo.repo, checkout=False)
    ref = repo.add_submodule("othersubdir", "file://" + other_subrepo.repo, checkout=True)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=True)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "othersubdir", "unicornfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project-override"))
def test_submodule_fetch_project_override(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_track_ignore_inconsistent(cli, tmpdir, datafiles):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Now add a .gitmodules file with an inconsistent submodule,
    # we are calling this inconsistent because the file was created
    # but `git submodule add` was never called, so there is no reference
    # associated to the submodule.
    #
    repo.add_file(os.path.join(project, "inconsistent-submodule", ".gitmodules"))

    # Fetch should work, we're not yet at the offending ref
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()

    # Track to update to the offending commit
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    # Fetch after track will encounter an inconsistent submodule without any ref
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()

    # Assert that we are just fine without it, and emit a warning to the user.
    assert "Ignoring inconsistent submodule" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_track_no_ref_or_track(cli, tmpdir, datafiles):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Write out our test target
    gitsource = repo.source_config(ref=None)
    gitsource.pop("track")
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # Track will encounter an inconsistent submodule without any ref
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_main_error(ErrorDomain.SOURCE, "missing-track-and-ref")
    result.assert_task_error(None, None)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_ref_not_in_track(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": [CoreWarnings.REF_NOT_IN_TRACK]})

    # Create the repo from 'repofiles', create a branch without latest commit
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    gitsource = repo.source_config(ref=ref)

    # Overwrite the track value to the added branch
    gitsource["track"] = "foo"

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["build", "target.bst"])

    # Assert a warning or an error depending on what we're checking
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, CoreWarnings.REF_NOT_IN_TRACK)
    else:
        result.assert_success()
        assert "ref-not-in-track" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_unlisted_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:unlisted-submodule"]})

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Create the source, and delete the explicit configuration
    # of the submodules.
    #
    # We expect this to cause an unlisted submodule warning
    # after the source has been fetched.
    #
    gitsource = repo.source_config(ref=ref)
    del gitsource["submodules"]

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # The warning or error is reported during fetch. There should be no
    # error with `bst show`.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr

    # We will notice this directly in fetch, as it will try to fetch
    # the submodules it discovers as a result of fetching the primary repo.
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])

    # Assert a warning or an error depending on what we're checking
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:unlisted-submodule")
    else:
        result.assert_success()
        assert "git:unlisted-submodule" in result.stderr

    # Verify that `bst show` will still not error out after fetching.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_track_unlisted_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:unlisted-submodule"]})

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created, but use
    # the original ref, let the submodules appear after tracking
    repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Create the source, and delete the explicit configuration
    # of the submodules.
    gitsource = repo.source_config(ref=ref)
    del gitsource["submodules"]

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # Fetch the repo, we will not see the warning because we
    # are still pointing to a ref which predates the submodules
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr

    # We won't get a warning/error when tracking either, the source
    # has not become cached so the opportunity to check
    # for the warning has not yet arisen.
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr

    # Fetching the repo at the new ref will finally reveal the warning
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:unlisted-submodule")
    else:
        result.assert_success()
        assert "git:unlisted-submodule" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_invalid_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:invalid-submodule"]})

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    # Create the source without any submodules, and add
    # an invalid submodule configuration to it.
    #
    # We expect this to cause an invalid submodule warning
    # after the source has been fetched and we know what
    # the real submodules actually are.
    #
    gitsource = repo.source_config(ref=ref)
    gitsource["submodules"] = {"subdir": {"url": "https://pony.org/repo.git"}}

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # The warning or error is reported during fetch. There should be no
    # error with `bst show`.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:invalid-submodule" not in result.stderr

    # We will notice this directly in fetch, as it will try to fetch
    # the submodules it discovers as a result of fetching the primary repo.
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])

    # Assert a warning or an error depending on what we're checking
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:invalid-submodule")
    else:
        result.assert_success()
        assert "git:invalid-submodule" in result.stderr

    # Verify that `bst show` will still not error out after fetching.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:invalid-submodule" not in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.skipif(HAVE_OLD_GIT, reason="old git rm does not update .gitmodules")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_track_invalid_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:invalid-submodule"]})

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Add a commit beyond the ref which *removes* the submodule we've added
    repo.remove_path("subdir")

    # Create the source, this will keep the submodules so initially
    # the configuration is valid for the ref we're using
    gitsource = repo.source_config(ref=ref)

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # Fetch the repo, we will not see the warning because we
    # are still pointing to a ref which predates the submodules
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    assert "git:invalid-submodule" not in result.stderr

    # After tracking we're pointing to a ref, which would trigger an invalid
    # submodule warning. However, cache validation is only performed as part
    # of fetch.
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    # Fetch to trigger cache validation
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:invalid-submodule")
    else:
        result.assert_success()
        assert "git:invalid-submodule" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("ref_format", ["sha1", "git-describe"])
@pytest.mark.parametrize("tag,extra_commit", [(False, False), (True, False), (True, True)])
def test_track_fetch(cli, tmpdir, datafiles, ref_format, tag, extra_commit):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))
    if tag:
        repo.add_tag("tag")
    if extra_commit:
        repo.add_commit()

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config()]}
    element["sources"][0]["ref-format"] = ref_format
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    # Track it
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    element = load_yaml(element_path)
    new_ref = element.get_sequence("sources").mapping_at(0).get_str("ref")

    if ref_format == "git-describe" and tag:
        # Check and strip prefix
        prefix = "tag-{}-g".format(0 if not extra_commit else 1)
        assert new_ref.startswith(prefix)
        new_ref = new_ref[len(prefix) :]

    # 40 chars for SHA-1
    assert len(new_ref) == 40

    # Fetch it
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.skipif(HAVE_OLD_GIT, reason="old git describe lacks --first-parent")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("ref_storage", [("inline"), ("project.refs")])
@pytest.mark.parametrize("tag_type", [("annotated"), ("lightweight")])
def test_git_describe(cli, tmpdir, datafiles, ref_storage, tag_type):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = ref_storage
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    def tag(name):
        if tag_type == "annotated":
            repo.add_annotated_tag(name, name)
        else:
            repo.add_tag(name)

    repo.create(repofiles)
    tag("uselesstag")

    file1 = os.path.join(str(tmpdir), "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file1)
    tag("tag1")

    file2 = os.path.join(str(tmpdir), "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.branch("branch2")
    repo.add_file(file2)
    tag("tag2")

    repo.checkout("master")
    file3 = os.path.join(str(tmpdir), "file3")
    with open(file3, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file3)

    repo.merge("branch2")

    config = repo.source_config()
    config["track"] = repo.latest_commit()
    config["track-tags"] = True

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    if ref_storage == "inline":
        result = cli.run(project=project, args=["source", "track", "target.bst"])
        result.assert_success()
    else:
        result = cli.run(project=project, args=["source", "track", "target.bst", "--deps", "all"])
        result.assert_success()

    if ref_storage == "inline":
        element = load_yaml(element_path)
        tags = element.get_sequence("sources").mapping_at(0).get_sequence("tags")
        assert len(tags) == 2
        for tag in tags:
            assert "tag" in tag
            assert "commit" in tag
            assert "annotated" in tag
            assert tag.get_bool("annotated") == (tag_type == "annotated")

        assert {(tag.get_str("tag"), tag.get_str("commit")) for tag in tags} == {
            ("tag1", repo.rev_parse("tag1^{commit}")),
            ("tag2", repo.rev_parse("tag2^{commit}")),
        }

    checkout = os.path.join(str(tmpdir), "checkout")

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkout])
    result.assert_success()

    if tag_type == "annotated":
        options = []
    else:
        options = ["--tags"]
    describe = subprocess.check_output(["git", "describe", *options], cwd=checkout, universal_newlines=True)
    assert describe.startswith("tag2-2-")

    describe_fp = subprocess.check_output(
        ["git", "describe", "--first-parent", *options], cwd=checkout, universal_newlines=True
    )
    assert describe_fp.startswith("tag1-2-")

    tags = subprocess.check_output(["git", "tag"], cwd=checkout, universal_newlines=True)
    tags = set(tags.splitlines())
    assert tags == set(["tag1", "tag2"])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.run(["git", "log", repo.rev_parse("uselesstag")], cwd=checkout, check=True)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("ref_storage", [("inline"), ("project.refs")])
@pytest.mark.parametrize("tag_type", [("annotated"), ("lightweight")])
def test_git_describe_head_is_tagged(cli, tmpdir, datafiles, ref_storage, tag_type):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = ref_storage
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    def tag(name):
        if tag_type == "annotated":
            repo.add_annotated_tag(name, name)
        else:
            repo.add_tag(name)

    repo.create(repofiles)
    tag("uselesstag")

    file1 = os.path.join(str(tmpdir), "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file1)

    file2 = os.path.join(str(tmpdir), "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.branch("branch2")
    repo.add_file(file2)

    repo.checkout("master")
    file3 = os.path.join(str(tmpdir), "file3")
    with open(file3, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file3)

    tagged_ref = repo.merge("branch2")
    tag("tag")

    config = repo.source_config()
    config["track"] = repo.latest_commit()
    config["track-tags"] = True

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    if ref_storage == "inline":
        result = cli.run(project=project, args=["source", "track", "target.bst"])
        result.assert_success()
    else:
        result = cli.run(project=project, args=["source", "track", "target.bst", "--deps", "all"])
        result.assert_success()

    if ref_storage == "inline":
        element = load_yaml(element_path)
        source = element.get_sequence("sources").mapping_at(0)
        tags = source.get_sequence("tags")
        assert len(tags) == 1

        tag = source.get_sequence("tags").mapping_at(0)
        assert "tag" in tag
        assert "commit" in tag
        assert "annotated" in tag
        assert tag.get_bool("annotated") == (tag_type == "annotated")

        tag_name = tag.get_str("tag")
        commit = tag.get_str("commit")
        assert (tag_name, commit) == ("tag", repo.rev_parse("tag^{commit}"))

    checkout = os.path.join(str(tmpdir), "checkout")

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkout])
    result.assert_success()

    if tag_type == "annotated":
        options = []
    else:
        options = ["--tags"]
    describe = subprocess.check_output(["git", "describe", *options], cwd=checkout, universal_newlines=True)
    assert describe.startswith("tag")

    tags = subprocess.check_output(["git", "tag"], cwd=checkout, universal_newlines=True)
    tags = set(tags.splitlines())
    assert tags == set(["tag"])

    rev_list = subprocess.check_output(["git", "rev-list", "--all"], cwd=checkout, universal_newlines=True)

    assert set(rev_list.splitlines()) == set([tagged_ref])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.run(["git", "log", repo.rev_parse("uselesstag")], cwd=checkout, check=True)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_git_describe_relevant_history(cli, tmpdir, datafiles):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = "project.refs"
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))
    repo.create(repofiles)

    file1 = os.path.join(str(tmpdir), "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file1)
    repo.branch("branch")
    repo.checkout("master")

    file2 = os.path.join(str(tmpdir), "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file2)

    file3 = os.path.join(str(tmpdir), "file3")
    with open(file3, "w", encoding="utf-8") as f:
        f.write("test\n")
    branch_boundary = repo.add_file(file3)

    repo.checkout("branch")
    file4 = os.path.join(str(tmpdir), "file4")
    with open(file4, "w", encoding="utf-8") as f:
        f.write("test\n")
    tagged_ref = repo.add_file(file4)
    repo.add_annotated_tag("tag1", "tag1")

    head = repo.merge("master")

    config = repo.source_config()
    config["track"] = head
    config["track-tags"] = True

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["source", "track", "target.bst", "--deps", "all"])
    result.assert_success()

    checkout = os.path.join(str(tmpdir), "checkout")

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkout])
    result.assert_success()

    describe = subprocess.check_output(["git", "describe"], cwd=checkout, universal_newlines=True)
    assert describe.startswith("tag1-2-")

    rev_list = subprocess.check_output(["git", "rev-list", "--all"], cwd=checkout, universal_newlines=True)

    assert set(rev_list.splitlines()) == set([head, tagged_ref, branch_boundary])


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_default_do_not_track_tags(cli, tmpdir, datafiles):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = "inline"
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    repo.create(repofiles)
    repo.add_tag("tag")

    config = repo.source_config()
    config["track"] = repo.latest_commit()

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    element = load_yaml(element_path)
    source = element.get_sequence("sources").mapping_at(0)
    assert "tags" not in source


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_overwrite_rogue_tag_multiple_remotes(cli, tmpdir, datafiles):
    """When using multiple remotes in cache (i.e. when using aliases), we
    need to make sure we override tags. This is not allowed to fetch
    tags that were present from different origins
    """

    project = str(datafiles)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    top_commit = repo.create(repofiles)

    repodir, reponame = os.path.split(repo.repo)
    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["aliases"] = Node.from_dict({"repo": "http://example.com/"})
    project_config["mirrors"] = [{"name": "middle-earth", "aliases": {"repo": ["file://{}/".format(repodir)]}}]
    generate_project(project, config=project_config)

    repo.add_annotated_tag("tag", "tag")

    file1 = os.path.join(repofiles, "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")

    ref = repo.add_file(file1)

    config = repo.source_config(ref=ref)
    del config["track"]
    config["url"] = "repo:{}".format(reponame)

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()

    repo.checkout(top_commit)

    file2 = os.path.join(repofiles, "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")

    new_ref = repo.add_file(file2)

    repo.delete_tag("tag")
    repo.add_annotated_tag("tag", "tag")
    repo.checkout("master")

    otherpath = os.path.join(str(tmpdir), "other_path")
    shutil.copytree(repo.repo, os.path.join(otherpath, "repo"))
    create_repo("git", otherpath)

    repodir, reponame = os.path.split(repo.repo)

    generate_project(project, config=project_config)

    config = repo.source_config(ref=new_ref)
    del config["track"]
    config["url"] = "repo:{}".format(reponame)

    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()

#
#  Copyright (C) 2018 Codethink Limited
#  Copyright (C) 2018 Bloomberg Finance LP
#
#  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.
#
#  Authors: <NAME> <<EMAIL>>
#           <NAME> <<EMAIL>>
#           <NAME> <<EMAIL>>
#

# Pylint doesn't play well with fixtures and dependency injection from pytest
# pylint: disable=redefined-outer-name

import os
import subprocess
import shutil

import pytest

from buildstream import Node
from buildstream.exceptions import ErrorDomain
from buildstream.plugin import CoreWarnings
from buildstream._testing import cli  # pylint: disable=unused-import
from buildstream._testing import generate_project, generate_element, load_yaml
from buildstream._testing import create_repo

from tests.testutils.site import HAVE_GIT, HAVE_OLD_GIT

DATA_DIR = os.path.join(
    os.path.dirname(os.path.realpath(__file__)),
    "git",
)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_fetch_bad_ref(cli, tmpdir, datafiles):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Write out our test target with a bad ref
    element = {"kind": "import", "sources": [repo.source_config(ref="5")]}
    generate_element(project, "target.bst", element)

    # Assert that fetch raises an error here
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_main_error(ErrorDomain.STREAM, None)
    result.assert_task_error(ErrorDomain.SOURCE, None)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.skipif(HAVE_OLD_GIT, reason="old git cannot clone a shallow repo to stage the source")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_fetch_shallow(cli, tmpdir, datafiles):
    project = str(datafiles)
    workspacedir = os.path.join(str(tmpdir), "workspace")

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))
    first_commit = repo.latest_commit()
    repo.add_commit()
    repo.add_tag("tag")

    ref = "tag-0-g" + repo.latest_commit()

    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["workspace", "open", "--directory", workspacedir, "target.bst"])
    result.assert_success()

    assert subprocess.call(["git", "show", "tag"], cwd=workspacedir) == 0
    assert subprocess.call(["git", "show", first_commit], cwd=workspacedir) != 0


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_checkout(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_recursive_submodule_fetch_checkout(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create a submodule from the 'othersubrepofiles' subdir
    subsubrepo = create_repo("git", str(tmpdir), "subsubrepo")
    subsubrepo.create(os.path.join(project, "othersubrepofiles"))

    # Create another submodule from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Configure submodules
    subrepo.add_submodule("subdir", "file://" + subsubrepo.repo)
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out all files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "subdir", "unicornfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_source_enable_explicit(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=True)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_source_disable(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=False)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_submodule_does_override(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo, checkout=True)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=False)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_submodule_individual_checkout(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create another submodule from the 'othersubrepofiles' subdir
    other_subrepo = create_repo("git", str(tmpdir), "othersubrepo")
    other_subrepo.create(os.path.join(project, "othersubrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    repo.add_submodule("subdir", "file://" + subrepo.repo, checkout=False)
    ref = repo.add_submodule("othersubdir", "file://" + other_subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=True)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "othersubdir", "unicornfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_fetch_submodule_individual_checkout_explicit(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create another submodule from the 'othersubrepofiles' subdir
    other_subrepo = create_repo("git", str(tmpdir), "othersubrepo")
    other_subrepo.create(os.path.join(project, "othersubrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    repo.add_submodule("subdir", "file://" + subrepo.repo, checkout=False)
    ref = repo.add_submodule("othersubdir", "file://" + other_subrepo.repo, checkout=True)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config_extra(ref=ref, checkout_submodules=True)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))
    assert os.path.exists(os.path.join(checkoutdir, "othersubdir", "unicornfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project-override"))
def test_submodule_fetch_project_override(cli, tmpdir, datafiles):
    project = str(datafiles)
    checkoutdir = os.path.join(str(tmpdir), "checkout")

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Fetch, build, checkout
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkoutdir])
    result.assert_success()

    # Assert we checked out both files at their expected location
    assert os.path.exists(os.path.join(checkoutdir, "file.txt"))
    assert not os.path.exists(os.path.join(checkoutdir, "subdir", "ponyfile.txt"))


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_track_ignore_inconsistent(cli, tmpdir, datafiles):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config(ref=ref)]}
    generate_element(project, "target.bst", element)

    # Now add a .gitmodules file with an inconsistent submodule,
    # we are calling this inconsistent because the file was created
    # but `git submodule add` was never called, so there is no reference
    # associated to the submodule.
    #
    repo.add_file(os.path.join(project, "inconsistent-submodule", ".gitmodules"))

    # Fetch should work, we're not yet at the offending ref
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()

    # Track to update to the offending commit
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    # Fetch after track will encounter an inconsistent submodule without any ref
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()

    # Assert that we are just fine without it, and emit a warning to the user.
    assert "Ignoring inconsistent submodule" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_submodule_track_no_ref_or_track(cli, tmpdir, datafiles):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Write out our test target
    gitsource = repo.source_config(ref=None)
    gitsource.pop("track")
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # Track will encounter an inconsistent submodule without any ref
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_main_error(ErrorDomain.SOURCE, "missing-track-and-ref")
    result.assert_task_error(None, None)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_ref_not_in_track(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": [CoreWarnings.REF_NOT_IN_TRACK]})

    # Create the repo from 'repofiles', create a branch without latest commit
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    gitsource = repo.source_config(ref=ref)

    # Overwrite the track value to the added branch
    gitsource["track"] = "foo"

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["build", "target.bst"])

    # Assert a warning or an error depending on what we're checking
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, CoreWarnings.REF_NOT_IN_TRACK)
    else:
        result.assert_success()
        assert "ref-not-in-track" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_unlisted_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:unlisted-submodule"]})

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Create the source, and delete the explicit configuration
    # of the submodules.
    #
    # We expect this to cause an unlisted submodule warning
    # after the source has been fetched.
    #
    gitsource = repo.source_config(ref=ref)
    del gitsource["submodules"]

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # The warning or error is reported during fetch. There should be no
    # error with `bst show`.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr

    # We will notice this directly in fetch, as it will try to fetch
    # the submodules it discovers as a result of fetching the primary repo.
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])

    # Assert a warning or an error depending on what we're checking
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:unlisted-submodule")
    else:
        result.assert_success()
        assert "git:unlisted-submodule" in result.stderr

    # Verify that `bst show` will still not error out after fetching.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_track_unlisted_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:unlisted-submodule"]})

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created, but use
    # the original ref, let the submodules appear after tracking
    repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Create the source, and delete the explicit configuration
    # of the submodules.
    gitsource = repo.source_config(ref=ref)
    del gitsource["submodules"]

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # Fetch the repo, we will not see the warning because we
    # are still pointing to a ref which predates the submodules
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr

    # We won't get a warning/error when tracking either, the source
    # has not become cached so the opportunity to check
    # for the warning has not yet arisen.
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()
    assert "git:unlisted-submodule" not in result.stderr

    # Fetching the repo at the new ref will finally reveal the warning
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:unlisted-submodule")
    else:
        result.assert_success()
        assert "git:unlisted-submodule" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_invalid_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:invalid-submodule"]})

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    ref = repo.create(os.path.join(project, "repofiles"))

    # Create the source without any submodules, and add
    # an invalid submodule configuration to it.
    #
    # We expect this to cause an invalid submodule warning
    # after the source has been fetched and we know what
    # the real submodules actually are.
    #
    gitsource = repo.source_config(ref=ref)
    gitsource["submodules"] = {"subdir": {"url": "https://pony.org/repo.git"}}

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # The warning or error is reported during fetch. There should be no
    # error with `bst show`.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:invalid-submodule" not in result.stderr

    # We will notice this directly in fetch, as it will try to fetch
    # the submodules it discovers as a result of fetching the primary repo.
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])

    # Assert a warning or an error depending on what we're checking
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:invalid-submodule")
    else:
        result.assert_success()
        assert "git:invalid-submodule" in result.stderr

    # Verify that `bst show` will still not error out after fetching.
    result = cli.run(project=project, args=["show", "target.bst"])
    result.assert_success()
    assert "git:invalid-submodule" not in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.skipif(HAVE_OLD_GIT, reason="old git rm does not update .gitmodules")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("fail", ["warn", "error"])
def test_track_invalid_submodule(cli, tmpdir, datafiles, fail):
    project = str(datafiles)

    # Make the warning an error if we're testing errors
    if fail == "error":
        generate_project(project, config={"fatal-warnings": ["git:invalid-submodule"]})

    # Create the submodule first from the 'subrepofiles' subdir
    subrepo = create_repo("git", str(tmpdir), "subrepo")
    subrepo.create(os.path.join(project, "subrepofiles"))

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))

    # Add a submodule pointing to the one we created
    ref = repo.add_submodule("subdir", "file://" + subrepo.repo)

    # Add a commit beyond the ref which *removes* the submodule we've added
    repo.remove_path("subdir")

    # Create the source, this will keep the submodules so initially
    # the configuration is valid for the ref we're using
    gitsource = repo.source_config(ref=ref)

    # Write out our test target
    element = {"kind": "import", "sources": [gitsource]}
    generate_element(project, "target.bst", element)

    # Fetch the repo, we will not see the warning because we
    # are still pointing to a ref which predates the submodules
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()
    assert "git:invalid-submodule" not in result.stderr

    # After tracking we're pointing to a ref, which would trigger an invalid
    # submodule warning. However, cache validation is only performed as part
    # of fetch.
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    # Fetch to trigger cache validation
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    if fail == "error":
        result.assert_main_error(ErrorDomain.STREAM, None)
        result.assert_task_error(ErrorDomain.PLUGIN, "git:invalid-submodule")
    else:
        result.assert_success()
        assert "git:invalid-submodule" in result.stderr


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("ref_format", ["sha1", "git-describe"])
@pytest.mark.parametrize("tag,extra_commit", [(False, False), (True, False), (True, True)])
def test_track_fetch(cli, tmpdir, datafiles, ref_format, tag, extra_commit):
    project = str(datafiles)

    # Create the repo from 'repofiles' subdir
    repo = create_repo("git", str(tmpdir))
    repo.create(os.path.join(project, "repofiles"))
    if tag:
        repo.add_tag("tag")
    if extra_commit:
        repo.add_commit()

    # Write out our test target
    element = {"kind": "import", "sources": [repo.source_config()]}
    element["sources"][0]["ref-format"] = ref_format
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    # Track it
    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    element = load_yaml(element_path)
    new_ref = element.get_sequence("sources").mapping_at(0).get_str("ref")

    if ref_format == "git-describe" and tag:
        # Check and strip prefix
        prefix = "tag-{}-g".format(0 if not extra_commit else 1)
        assert new_ref.startswith(prefix)
        new_ref = new_ref[len(prefix) :]

    # 40 chars for SHA-1
    assert len(new_ref) == 40

    # Fetch it
    result = cli.run(project=project, args=["source", "fetch", "target.bst"])
    result.assert_success()


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.skipif(HAVE_OLD_GIT, reason="old git describe lacks --first-parent")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("ref_storage", [("inline"), ("project.refs")])
@pytest.mark.parametrize("tag_type", [("annotated"), ("lightweight")])
def test_git_describe(cli, tmpdir, datafiles, ref_storage, tag_type):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = ref_storage
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    def tag(name):
        if tag_type == "annotated":
            repo.add_annotated_tag(name, name)
        else:
            repo.add_tag(name)

    repo.create(repofiles)
    tag("uselesstag")

    file1 = os.path.join(str(tmpdir), "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file1)
    tag("tag1")

    file2 = os.path.join(str(tmpdir), "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.branch("branch2")
    repo.add_file(file2)
    tag("tag2")

    repo.checkout("master")
    file3 = os.path.join(str(tmpdir), "file3")
    with open(file3, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file3)

    repo.merge("branch2")

    config = repo.source_config()
    config["track"] = repo.latest_commit()
    config["track-tags"] = True

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    if ref_storage == "inline":
        result = cli.run(project=project, args=["source", "track", "target.bst"])
        result.assert_success()
    else:
        result = cli.run(project=project, args=["source", "track", "target.bst", "--deps", "all"])
        result.assert_success()

    if ref_storage == "inline":
        element = load_yaml(element_path)
        tags = element.get_sequence("sources").mapping_at(0).get_sequence("tags")
        assert len(tags) == 2
        for tag in tags:
            assert "tag" in tag
            assert "commit" in tag
            assert "annotated" in tag
            assert tag.get_bool("annotated") == (tag_type == "annotated")

        assert {(tag.get_str("tag"), tag.get_str("commit")) for tag in tags} == {
            ("tag1", repo.rev_parse("tag1^{commit}")),
            ("tag2", repo.rev_parse("tag2^{commit}")),
        }

    checkout = os.path.join(str(tmpdir), "checkout")

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkout])
    result.assert_success()

    if tag_type == "annotated":
        options = []
    else:
        options = ["--tags"]
    describe = subprocess.check_output(["git", "describe", *options], cwd=checkout, universal_newlines=True)
    assert describe.startswith("tag2-2-")

    describe_fp = subprocess.check_output(
        ["git", "describe", "--first-parent", *options], cwd=checkout, universal_newlines=True
    )
    assert describe_fp.startswith("tag1-2-")

    tags = subprocess.check_output(["git", "tag"], cwd=checkout, universal_newlines=True)
    tags = set(tags.splitlines())
    assert tags == set(["tag1", "tag2"])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.run(["git", "log", repo.rev_parse("uselesstag")], cwd=checkout, check=True)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
@pytest.mark.parametrize("ref_storage", [("inline"), ("project.refs")])
@pytest.mark.parametrize("tag_type", [("annotated"), ("lightweight")])
def test_git_describe_head_is_tagged(cli, tmpdir, datafiles, ref_storage, tag_type):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = ref_storage
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    def tag(name):
        if tag_type == "annotated":
            repo.add_annotated_tag(name, name)
        else:
            repo.add_tag(name)

    repo.create(repofiles)
    tag("uselesstag")

    file1 = os.path.join(str(tmpdir), "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file1)

    file2 = os.path.join(str(tmpdir), "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.branch("branch2")
    repo.add_file(file2)

    repo.checkout("master")
    file3 = os.path.join(str(tmpdir), "file3")
    with open(file3, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file3)

    tagged_ref = repo.merge("branch2")
    tag("tag")

    config = repo.source_config()
    config["track"] = repo.latest_commit()
    config["track-tags"] = True

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    if ref_storage == "inline":
        result = cli.run(project=project, args=["source", "track", "target.bst"])
        result.assert_success()
    else:
        result = cli.run(project=project, args=["source", "track", "target.bst", "--deps", "all"])
        result.assert_success()

    if ref_storage == "inline":
        element = load_yaml(element_path)
        source = element.get_sequence("sources").mapping_at(0)
        tags = source.get_sequence("tags")
        assert len(tags) == 1

        tag = source.get_sequence("tags").mapping_at(0)
        assert "tag" in tag
        assert "commit" in tag
        assert "annotated" in tag
        assert tag.get_bool("annotated") == (tag_type == "annotated")

        tag_name = tag.get_str("tag")
        commit = tag.get_str("commit")
        assert (tag_name, commit) == ("tag", repo.rev_parse("tag^{commit}"))

    checkout = os.path.join(str(tmpdir), "checkout")

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkout])
    result.assert_success()

    if tag_type == "annotated":
        options = []
    else:
        options = ["--tags"]
    describe = subprocess.check_output(["git", "describe", *options], cwd=checkout, universal_newlines=True)
    assert describe.startswith("tag")

    tags = subprocess.check_output(["git", "tag"], cwd=checkout, universal_newlines=True)
    tags = set(tags.splitlines())
    assert tags == set(["tag"])

    rev_list = subprocess.check_output(["git", "rev-list", "--all"], cwd=checkout, universal_newlines=True)

    assert set(rev_list.splitlines()) == set([tagged_ref])

    with pytest.raises(subprocess.CalledProcessError):
        subprocess.run(["git", "log", repo.rev_parse("uselesstag")], cwd=checkout, check=True)


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_git_describe_relevant_history(cli, tmpdir, datafiles):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = "project.refs"
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))
    repo.create(repofiles)

    file1 = os.path.join(str(tmpdir), "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file1)
    repo.branch("branch")
    repo.checkout("master")

    file2 = os.path.join(str(tmpdir), "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")
    repo.add_file(file2)

    file3 = os.path.join(str(tmpdir), "file3")
    with open(file3, "w", encoding="utf-8") as f:
        f.write("test\n")
    branch_boundary = repo.add_file(file3)

    repo.checkout("branch")
    file4 = os.path.join(str(tmpdir), "file4")
    with open(file4, "w", encoding="utf-8") as f:
        f.write("test\n")
    tagged_ref = repo.add_file(file4)
    repo.add_annotated_tag("tag1", "tag1")

    head = repo.merge("master")

    config = repo.source_config()
    config["track"] = head
    config["track-tags"] = True

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["source", "track", "target.bst", "--deps", "all"])
    result.assert_success()

    checkout = os.path.join(str(tmpdir), "checkout")

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()
    result = cli.run(project=project, args=["artifact", "checkout", "target.bst", "--directory", checkout])
    result.assert_success()

    describe = subprocess.check_output(["git", "describe"], cwd=checkout, universal_newlines=True)
    assert describe.startswith("tag1-2-")

    rev_list = subprocess.check_output(["git", "rev-list", "--all"], cwd=checkout, universal_newlines=True)

    assert set(rev_list.splitlines()) == set([head, tagged_ref, branch_boundary])


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_default_do_not_track_tags(cli, tmpdir, datafiles):
    project = str(datafiles)

    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["ref-storage"] = "inline"
    generate_project(project, config=project_config)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    repo.create(repofiles)
    repo.add_tag("tag")

    config = repo.source_config()
    config["track"] = repo.latest_commit()

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)
    element_path = os.path.join(project, "target.bst")

    result = cli.run(project=project, args=["source", "track", "target.bst"])
    result.assert_success()

    element = load_yaml(element_path)
    source = element.get_sequence("sources").mapping_at(0)
    assert "tags" not in source


@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, "template"))
def test_overwrite_rogue_tag_multiple_remotes(cli, tmpdir, datafiles):
    """When using multiple remotes in cache (i.e. when using aliases), we
    need to make sure we override tags. This is not allowed to fetch
    tags that were present from different origins
    """

    project = str(datafiles)

    repofiles = os.path.join(str(tmpdir), "repofiles")
    os.makedirs(repofiles, exist_ok=True)
    file0 = os.path.join(repofiles, "file0")
    with open(file0, "w", encoding="utf-8") as f:
        f.write("test\n")

    repo = create_repo("git", str(tmpdir))

    top_commit = repo.create(repofiles)

    repodir, reponame = os.path.split(repo.repo)
    project_config = load_yaml(os.path.join(project, "project.conf"))
    project_config["aliases"] = Node.from_dict({"repo": "http://example.com/"})
    project_config["mirrors"] = [{"name": "middle-earth", "aliases": {"repo": ["file://{}/".format(repodir)]}}]
    generate_project(project, config=project_config)

    repo.add_annotated_tag("tag", "tag")

    file1 = os.path.join(repofiles, "file1")
    with open(file1, "w", encoding="utf-8") as f:
        f.write("test\n")

    ref = repo.add_file(file1)

    config = repo.source_config(ref=ref)
    del config["track"]
    config["url"] = "repo:{}".format(reponame)

    # Write out our test target
    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()

    repo.checkout(top_commit)

    file2 = os.path.join(repofiles, "file2")
    with open(file2, "w", encoding="utf-8") as f:
        f.write("test\n")

    new_ref = repo.add_file(file2)

    repo.delete_tag("tag")
    repo.add_annotated_tag("tag", "tag")
    repo.checkout("master")

    otherpath = os.path.join(str(tmpdir), "other_path")
    shutil.copytree(repo.repo, os.path.join(otherpath, "repo"))
    create_repo("git", otherpath)

    repodir, reponame = os.path.split(repo.repo)

    generate_project(project, config=project_config)

    config = repo.source_config(ref=new_ref)
    del config["track"]
    config["url"] = "repo:{}".format(reponame)

    element = {
        "kind": "import",
        "sources": [config],
    }
    generate_element(project, "target.bst", element)

    result = cli.run(project=project, args=["build", "target.bst"])
    result.assert_success()

# #  Copyright (C) 2018 Codethink Limited #  Copyright (C) 2018 Bloomberg Finance LP # #  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. # #  Authors: <NAME> <<EMAIL>> #           <NAME> <<EMAIL>> #           <NAME> <<EMAIL>> # # Pylint doesn't play well with fixtures and dependency injection from pytest # pylint: disable=redefined-outer-name # pylint: disable=unused-import # Create the repo from 'repofiles' subdir # Write out our test target with a bad ref # Assert that fetch raises an error here # Create the repo from 'repofiles' subdir # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Write out our test target # Fetch, build, checkout # Assert we checked out both files at their expected location # Create a submodule from the 'othersubrepofiles' subdir # Create another submodule from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Configure submodules # Write out our test target # Fetch, build, checkout # Assert we checked out all files at their expected location # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Write out our test target # Fetch, build, checkout # Assert we checked out both files at their expected location # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Write out our test target # Fetch, build, checkout # Assert we checked out both files at their expected location # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Write out our test target # Fetch, build, checkout # Assert we checked out both files at their expected location # Create the submodule first from the 'subrepofiles' subdir # Create another submodule from the 'othersubrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Write out our test target # Fetch, build, checkout # Assert we checked out files at their expected location # Create the submodule first from the 'subrepofiles' subdir # Create another submodule from the 'othersubrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Write out our test target # Fetch, build, checkout # Assert we checked out files at their expected location # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Write out our test target # Fetch, build, checkout # Assert we checked out both files at their expected location # Create the repo from 'repofiles' subdir # Write out our test target # Now add a .gitmodules file with an inconsistent submodule, # we are calling this inconsistent because the file was created # but `git submodule add` was never called, so there is no reference # associated to the submodule. # # Fetch should work, we're not yet at the offending ref # Track to update to the offending commit # Fetch after track will encounter an inconsistent submodule without any ref # Assert that we are just fine without it, and emit a warning to the user. # Create the repo from 'repofiles' subdir # Write out our test target # Track will encounter an inconsistent submodule without any ref # Make the warning an error if we're testing errors # Create the repo from 'repofiles', create a branch without latest commit # Overwrite the track value to the added branch # Write out our test target # Assert a warning or an error depending on what we're checking # Make the warning an error if we're testing errors # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Create the source, and delete the explicit configuration # of the submodules. # # We expect this to cause an unlisted submodule warning # after the source has been fetched. # # Write out our test target # The warning or error is reported during fetch. There should be no # error with `bst show`. # We will notice this directly in fetch, as it will try to fetch # the submodules it discovers as a result of fetching the primary repo. # Assert a warning or an error depending on what we're checking # Verify that `bst show` will still not error out after fetching. # Make the warning an error if we're testing errors # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created, but use # the original ref, let the submodules appear after tracking # Create the source, and delete the explicit configuration # of the submodules. # Write out our test target # Fetch the repo, we will not see the warning because we # are still pointing to a ref which predates the submodules # We won't get a warning/error when tracking either, the source # has not become cached so the opportunity to check # for the warning has not yet arisen. # Fetching the repo at the new ref will finally reveal the warning # Make the warning an error if we're testing errors # Create the repo from 'repofiles' subdir # Create the source without any submodules, and add # an invalid submodule configuration to it. # # We expect this to cause an invalid submodule warning # after the source has been fetched and we know what # the real submodules actually are. # # Write out our test target # The warning or error is reported during fetch. There should be no # error with `bst show`. # We will notice this directly in fetch, as it will try to fetch # the submodules it discovers as a result of fetching the primary repo. # Assert a warning or an error depending on what we're checking # Verify that `bst show` will still not error out after fetching. # Make the warning an error if we're testing errors # Create the submodule first from the 'subrepofiles' subdir # Create the repo from 'repofiles' subdir # Add a submodule pointing to the one we created # Add a commit beyond the ref which *removes* the submodule we've added # Create the source, this will keep the submodules so initially # the configuration is valid for the ref we're using # Write out our test target # Fetch the repo, we will not see the warning because we # are still pointing to a ref which predates the submodules # After tracking we're pointing to a ref, which would trigger an invalid # submodule warning. However, cache validation is only performed as part # of fetch. # Fetch to trigger cache validation # Create the repo from 'repofiles' subdir # Write out our test target # Track it # Check and strip prefix # 40 chars for SHA-1 # Fetch it # Write out our test target # Write out our test target # Write out our test target # Write out our test target When using multiple remotes in cache (i.e. when using aliases), we     need to make sure we override tags. This is not allowed to fetch     tags that were present from different origins # Write out our test target