Consolidate how Spack uses git (#34700)

Local `git` tests will fail with `fatal: transport 'file' not allowed` when using git 2.38.1 or higher, due to a fix for `CVE-2022-39253`.

This was fixed in CI in #33429, but that doesn't help the issue for anyone's local environment. Instead of fixing this with git config in CI, we should ensure that the tests run anywhere.

- [x] Introduce `spack.util.git`.
- [x] Use `spack.util.git.get_git()` to get a git executable, instead of `which("git")` everywhere.
- [x] Make all `git` tests use a `git` fixture that goes through `spack.util.git.get_git()`.
- [x] Add `-c protocol.file.allow=always` to all `git` invocations under `pytest`.
- [x] Revert changes from #33429, which are no longer needed.
This commit is contained in:
Todd Gamblin 2022-12-28 00:44:11 -08:00 committed by GitHub
parent 558695793f
commit 5f8c706128
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 138 additions and 162 deletions

View file

@ -4,10 +4,6 @@ git config --global user.email "spack@example.com"
git config --global user.name "Test User" git config --global user.name "Test User"
git config --global core.longpaths true git config --global core.longpaths true
# See https://github.com/git/git/security/advisories/GHSA-3wp6-j8xr-qw85 (CVE-2022-39253)
# This is needed to let some fixture in our unit-test suite run
git config --global protocol.file.allow always
if ($(git branch --show-current) -ne "develop") if ($(git branch --show-current) -ne "develop")
{ {
git branch develop origin/develop git branch develop origin/develop

View file

@ -2,10 +2,6 @@
git config --global user.email "spack@example.com" git config --global user.email "spack@example.com"
git config --global user.name "Test User" git config --global user.name "Test User"
# See https://github.com/git/git/security/advisories/GHSA-3wp6-j8xr-qw85 (CVE-2022-39253)
# This is needed to let some fixture in our unit-test suite run
git config --global protocol.file.allow always
# create a local pr base branch # create a local pr base branch
if [[ -n $GITHUB_BASE_REF ]]; then if [[ -n $GITHUB_BASE_REF ]]; then
git fetch origin "${GITHUB_BASE_REF}:${GITHUB_BASE_REF}" git fetch origin "${GITHUB_BASE_REF}:${GITHUB_BASE_REF}"

View file

@ -33,7 +33,7 @@
import spack.mirror import spack.mirror
import spack.paths import spack.paths
import spack.repo import spack.repo
import spack.util.executable as exe import spack.util.git
import spack.util.gpg as gpg_util import spack.util.gpg as gpg_util
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
import spack.util.url as url_util import spack.util.url as url_util
@ -486,7 +486,7 @@ def get_stack_changed(env_path, rev1="HEAD^", rev2="HEAD"):
whether or not the stack was changed. Returns True if the environment whether or not the stack was changed. Returns True if the environment
manifest changed between the provided revisions (or additionally if the manifest changed between the provided revisions (or additionally if the
`.gitlab-ci.yml` file itself changed). Returns False otherwise.""" `.gitlab-ci.yml` file itself changed). Returns False otherwise."""
git = exe.which("git") git = spack.util.git.git()
if git: if git:
with fs.working_dir(spack.paths.prefix): with fs.working_dir(spack.paths.prefix):
git_log = git( git_log = git(
@ -1655,7 +1655,7 @@ def get_spack_info():
entry, otherwise, return a string containing the spack version.""" entry, otherwise, return a string containing the spack version."""
git_path = os.path.join(spack.paths.prefix, ".git") git_path = os.path.join(spack.paths.prefix, ".git")
if os.path.exists(git_path): if os.path.exists(git_path):
git = exe.which("git") git = spack.util.git.git()
if git: if git:
with fs.working_dir(spack.paths.prefix): with fs.working_dir(spack.paths.prefix):
git_log = git("log", "-1", output=str, error=os.devnull, fail_on_error=False) git_log = git("log", "-1", output=str, error=os.devnull, fail_on_error=False)
@ -1695,7 +1695,7 @@ def setup_spack_repro_version(repro_dir, checkout_commit, merge_commit=None):
spack_git_path = spack.paths.prefix spack_git_path = spack.paths.prefix
git = exe.which("git") git = spack.util.git.git()
if not git: if not git:
tty.error("reproduction of pipeline job requires git") tty.error("reproduction of pipeline job requires git")
return False return False

View file

@ -14,9 +14,9 @@
import spack.paths import spack.paths
import spack.repo import spack.repo
import spack.util.git
import spack.util.spack_json as sjson import spack.util.spack_json as sjson
from spack.cmd import spack_is_git_repo from spack.cmd import spack_is_git_repo
from spack.util.executable import which
description = "show contributors to packages" description = "show contributors to packages"
section = "developer" section = "developer"
@ -116,7 +116,7 @@ def blame(parser, args):
# make sure this is a git repo # make sure this is a git repo
if not spack_is_git_repo(): if not spack_is_git_repo():
tty.die("This spack is not a git clone. Can't use 'spack blame'") tty.die("This spack is not a git clone. Can't use 'spack blame'")
git = which("git", required=True) git = spack.util.git.git(required=True)
# Get name of file to blame # Get name of file to blame
blame_file = None blame_file = None

View file

@ -9,7 +9,8 @@
from llnl.util.filesystem import mkdirp, working_dir from llnl.util.filesystem import mkdirp, working_dir
import spack.paths import spack.paths
from spack.util.executable import ProcessError, which import spack.util.git
from spack.util.executable import ProcessError
_SPACK_UPSTREAM = "https://github.com/spack/spack" _SPACK_UPSTREAM = "https://github.com/spack/spack"
@ -32,7 +33,7 @@ def setup_parser(subparser):
def get_origin_info(remote): def get_origin_info(remote):
git_dir = os.path.join(spack.paths.prefix, ".git") git_dir = os.path.join(spack.paths.prefix, ".git")
git = which("git", required=True) git = spack.util.git.git(required=True)
try: try:
branch = git("symbolic-ref", "--short", "HEAD", output=str) branch = git("symbolic-ref", "--short", "HEAD", output=str)
except ProcessError: except ProcessError:
@ -69,13 +70,13 @@ def clone(parser, args):
if files_in_the_way: if files_in_the_way:
tty.die( tty.die(
"There are already files there! " "Delete these files before boostrapping spack.", "There are already files there! " "Delete these files before boostrapping spack.",
*files_in_the_way *files_in_the_way,
) )
tty.msg("Installing:", "%s/bin/spack" % prefix, "%s/lib/spack/..." % prefix) tty.msg("Installing:", "%s/bin/spack" % prefix, "%s/lib/spack/..." % prefix)
with working_dir(prefix): with working_dir(prefix):
git = which("git", required=True) git = spack.util.git.git(required=True)
git("init", "--shared", "-q") git("init", "--shared", "-q")
git("remote", "add", "origin", origin_url) git("remote", "add", "origin", origin_url)
git("fetch", "origin", "%s:refs/remotes/origin/%s" % (branch, branch), "-n", "-q") git("fetch", "origin", "%s:refs/remotes/origin/%s" % (branch, branch), "-n", "-q")

View file

@ -17,6 +17,7 @@
import spack.config import spack.config
import spack.paths import spack.paths
import spack.platforms import spack.platforms
import spack.util.git
from spack.main import get_version from spack.main import get_version
from spack.util.executable import which from spack.util.executable import which
@ -35,7 +36,7 @@ def _debug_tarball_suffix():
now = datetime.now() now = datetime.now()
suffix = now.strftime("%Y-%m-%d-%H%M%S") suffix = now.strftime("%Y-%m-%d-%H%M%S")
git = which("git") git = spack.util.git.git()
if not git: if not git:
return "nobranch-nogit-%s" % suffix return "nobranch-nogit-%s" % suffix

View file

@ -13,15 +13,11 @@
import llnl.util.tty as tty import llnl.util.tty as tty
import spack.paths import spack.paths
from spack.util.executable import which
description = "list and check license headers on files in spack" description = "list and check license headers on files in spack"
section = "developer" section = "developer"
level = "long" level = "long"
#: need the git command to check new files
git = which("git")
#: SPDX license id must appear in the first <license_lines> lines of a file #: SPDX license id must appear in the first <license_lines> lines of a file
license_lines = 7 license_lines = 7
@ -238,9 +234,6 @@ def setup_parser(subparser):
def license(parser, args): def license(parser, args):
if not git:
tty.die("spack license requires git in your environment")
licensed_files[:] = [re.compile(regex) for regex in licensed_files] licensed_files[:] = [re.compile(regex) for regex in licensed_files]
commands = { commands = {

View file

@ -13,6 +13,7 @@
from llnl.util.filesystem import working_dir from llnl.util.filesystem import working_dir
import spack.paths import spack.paths
import spack.util.git
from spack.util.executable import which from spack.util.executable import which
description = "runs source code style checks on spack" description = "runs source code style checks on spack"
@ -81,7 +82,7 @@ def changed_files(base="develop", untracked=True, all_files=False, root=None):
if root is None: if root is None:
root = spack.paths.prefix root = spack.paths.prefix
git = which("git", required=True) git = spack.util.git.git(required=True)
# ensure base is in the repo # ensure base is in the repo
base_sha = git( base_sha = git(

View file

@ -15,8 +15,8 @@
import spack.cmd.common.arguments as arguments import spack.cmd.common.arguments as arguments
import spack.config import spack.config
import spack.paths import spack.paths
import spack.util.git
import spack.util.gpg import spack.util.gpg
from spack.util.executable import which
from spack.util.spack_yaml import syaml_dict from spack.util.spack_yaml import syaml_dict
description = "set up spack for our tutorial (WARNING: modifies config!)" description = "set up spack for our tutorial (WARNING: modifies config!)"
@ -84,7 +84,7 @@ def tutorial(parser, args):
# If you don't put this last, you'll get import errors for the code # If you don't put this last, you'll get import errors for the code
# that follows (exacerbated by the various lazy singletons we use) # that follows (exacerbated by the various lazy singletons we use)
tty.msg("Ensuring we're on the releases/v{0}.{1} branch".format(*spack.spack_version_info[:2])) tty.msg("Ensuring we're on the releases/v{0}.{1} branch".format(*spack.spack_version_info[:2]))
git = which("git", required=True) git = spack.util.git.git(required=True)
with working_dir(spack.paths.prefix): with working_dir(spack.paths.prefix):
git("checkout", tutorial_branch) git("checkout", tutorial_branch)
# NO CODE BEYOND HERE # NO CODE BEYOND HERE

View file

@ -10,7 +10,7 @@
import llnl.util.filesystem as fs import llnl.util.filesystem as fs
import llnl.util.tty as tty import llnl.util.tty as tty
import spack.util.executable as executable import spack.util.git
#: Global variable used to cache in memory the content of images.json #: Global variable used to cache in memory the content of images.json
_data = None _data = None
@ -97,7 +97,7 @@ def _verify_ref(url, ref, enforce_sha):
# Do a checkout in a temporary directory # Do a checkout in a temporary directory
msg = 'Cloning "{0}" to verify ref "{1}"'.format(url, ref) msg = 'Cloning "{0}" to verify ref "{1}"'.format(url, ref)
tty.info(msg, stream=sys.stderr) tty.info(msg, stream=sys.stderr)
git = executable.which("git", required=True) git = spack.util.git.git(required=True)
with fs.temporary_dir(): with fs.temporary_dir():
git("clone", "-q", url, ".") git("clone", "-q", url, ".")
sha = git( sha = git(

View file

@ -48,6 +48,7 @@
import spack.error import spack.error
import spack.url import spack.url
import spack.util.crypto as crypto import spack.util.crypto as crypto
import spack.util.git
import spack.util.pattern as pattern import spack.util.pattern as pattern
import spack.util.url as url_util import spack.util.url as url_util
import spack.util.web as web_util import spack.util.web as web_util
@ -765,7 +766,7 @@ def version_from_git(git_exe):
@property @property
def git(self): def git(self):
if not self._git: if not self._git:
self._git = which("git", required=True) self._git = spack.util.git.git()
# Disable advice for a quieter fetch # Disable advice for a quieter fetch
# https://github.com/git/git/blob/master/Documentation/RelNotes/1.7.2.txt # https://github.com/git/git/blob/master/Documentation/RelNotes/1.7.2.txt

View file

@ -45,7 +45,7 @@
import spack.store import spack.store
import spack.util.debug import spack.util.debug
import spack.util.environment import spack.util.environment
import spack.util.executable as exe import spack.util.git
import spack.util.path import spack.util.path
from spack.error import SpackError from spack.error import SpackError
@ -136,7 +136,7 @@ def get_version():
version = spack.spack_version version = spack.spack_version
git_path = os.path.join(spack.paths.prefix, ".git") git_path = os.path.join(spack.paths.prefix, ".git")
if os.path.exists(git_path): if os.path.exists(git_path):
git = exe.which("git") git = spack.util.git.git()
if not git: if not git:
return version return version
rev = git( rev = git(

View file

@ -41,9 +41,9 @@
import spack.spec import spack.spec
import spack.tag import spack.tag
import spack.util.file_cache import spack.util.file_cache
import spack.util.git
import spack.util.naming as nm import spack.util.naming as nm
import spack.util.path import spack.util.path
from spack.util.executable import which
#: Package modules are imported as spack.pkg.<repo-namespace>.<pkg-name> #: Package modules are imported as spack.pkg.<repo-namespace>.<pkg-name>
ROOT_PYTHON_NAMESPACE = "spack.pkg" ROOT_PYTHON_NAMESPACE = "spack.pkg"
@ -198,27 +198,16 @@ class GitExe:
# #
# Not using -C as that is not supported for git < 1.8.5. # Not using -C as that is not supported for git < 1.8.5.
def __init__(self): def __init__(self):
self._git_cmd = which("git", required=True) self._git_cmd = spack.util.git.git(required=True)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
with working_dir(packages_path()): with working_dir(packages_path()):
return self._git_cmd(*args, **kwargs) return self._git_cmd(*args, **kwargs)
_git = None
def get_git():
"""Get a git executable that runs *within* the packages path."""
global _git
if _git is None:
_git = GitExe()
return _git
def list_packages(rev): def list_packages(rev):
"""List all packages associated with the given revision""" """List all packages associated with the given revision"""
git = get_git() git = GitExe()
# git ls-tree does not support ... merge-base syntax, so do it manually # git ls-tree does not support ... merge-base syntax, so do it manually
if rev.endswith("..."): if rev.endswith("..."):
@ -270,7 +259,7 @@ def get_all_package_diffs(type, rev1="HEAD^1", rev2="HEAD"):
removed, added = diff_packages(rev1, rev2) removed, added = diff_packages(rev1, rev2)
git = get_git() git = GitExe()
out = git("diff", "--relative", "--name-only", rev1, rev2, output=str).strip() out = git("diff", "--relative", "--name-only", rev1, rev2, output=str).strip()
lines = [] if not out else re.split(r"\s+", out) lines = [] if not out else re.split(r"\s+", out)
@ -293,7 +282,7 @@ def get_all_package_diffs(type, rev1="HEAD^1", rev2="HEAD"):
def add_package_to_git_stage(packages): def add_package_to_git_stage(packages):
"""add a package to the git stage with `git add`""" """add a package to the git stage with `git add`"""
git = get_git() git = GitExe()
for pkg_name in packages: for pkg_name in packages:
filename = spack.repo.path.filename_for_package_name(pkg_name) filename = spack.repo.path.filename_for_package_name(pkg_name)

View file

@ -22,11 +22,11 @@
import spack.fetch_strategy import spack.fetch_strategy
import spack.package_base import spack.package_base
import spack.platforms import spack.platforms
import spack.util.git
from spack.error import SpackError from spack.error import SpackError
from spack.reporter import Reporter from spack.reporter import Reporter
from spack.reporters.extract import extract_test_parts from spack.reporters.extract import extract_test_parts
from spack.util.crypto import checksum from spack.util.crypto import checksum
from spack.util.executable import which
from spack.util.log_parse import parse_log_events from spack.util.log_parse import parse_log_events
__all__ = ["CDash"] __all__ = ["CDash"]
@ -108,7 +108,7 @@ def __init__(self, args):
) )
self.buildIds = collections.OrderedDict() self.buildIds = collections.OrderedDict()
self.revision = "" self.revision = ""
git = which("git") git = spack.util.git.git()
with working_dir(spack.paths.spack_root): with working_dir(spack.paths.spack_root):
self.revision = git("rev-parse", "HEAD", output=str).strip() self.revision = git("rev-parse", "HEAD", output=str).strip()
self.generator = "spack-{0}".format(spack.main.get_version()) self.generator = "spack-{0}".format(spack.main.get_version())

View file

@ -18,6 +18,7 @@
import spack.environment as ev import spack.environment as ev
import spack.error import spack.error
import spack.paths as spack_paths import spack.paths as spack_paths
import spack.util.git
import spack.util.gpg import spack.util.gpg
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
@ -180,14 +181,13 @@ def test_setup_spack_repro_version(tmpdir, capfd, last_two_git_commits, monkeypa
monkeypatch.setattr(spack.paths, "prefix", "/garbage") monkeypatch.setattr(spack.paths, "prefix", "/garbage")
ret = ci.setup_spack_repro_version(repro_dir, c2, c1) ret = ci.setup_spack_repro_version(repro_dir, c2, c1)
out, err = capfd.readouterr() _, err = capfd.readouterr()
assert not ret assert not ret
assert "Unable to find the path" in err assert "Unable to find the path" in err
monkeypatch.setattr(spack.paths, "prefix", prefix_save) monkeypatch.setattr(spack.paths, "prefix", prefix_save)
monkeypatch.setattr(spack.util.git, "git", lambda: None)
monkeypatch.setattr(spack.util.executable, "which", lambda cmd: None)
ret = ci.setup_spack_repro_version(repro_dir, c2, c1) ret = ci.setup_spack_repro_version(repro_dir, c2, c1)
out, err = capfd.readouterr() out, err = capfd.readouterr()
@ -208,39 +208,39 @@ def __call__(self, *args, **kwargs):
git_cmd = mock_git_cmd() git_cmd = mock_git_cmd()
monkeypatch.setattr(spack.util.executable, "which", lambda cmd: git_cmd) monkeypatch.setattr(spack.util.git, "git", lambda: git_cmd)
git_cmd.check = lambda *a, **k: 1 if len(a) > 2 and a[2] == c2 else 0 git_cmd.check = lambda *a, **k: 1 if len(a) > 2 and a[2] == c2 else 0
ret = ci.setup_spack_repro_version(repro_dir, c2, c1) ret = ci.setup_spack_repro_version(repro_dir, c2, c1)
out, err = capfd.readouterr() _, err = capfd.readouterr()
assert not ret assert not ret
assert "Missing commit: {0}".format(c2) in err assert "Missing commit: {0}".format(c2) in err
git_cmd.check = lambda *a, **k: 1 if len(a) > 2 and a[2] == c1 else 0 git_cmd.check = lambda *a, **k: 1 if len(a) > 2 and a[2] == c1 else 0
ret = ci.setup_spack_repro_version(repro_dir, c2, c1) ret = ci.setup_spack_repro_version(repro_dir, c2, c1)
out, err = capfd.readouterr() _, err = capfd.readouterr()
assert not ret assert not ret
assert "Missing commit: {0}".format(c1) in err assert "Missing commit: {0}".format(c1) in err
git_cmd.check = lambda *a, **k: 1 if a[0] == "clone" else 0 git_cmd.check = lambda *a, **k: 1 if a[0] == "clone" else 0
ret = ci.setup_spack_repro_version(repro_dir, c2, c1) ret = ci.setup_spack_repro_version(repro_dir, c2, c1)
out, err = capfd.readouterr() _, err = capfd.readouterr()
assert not ret assert not ret
assert "Unable to clone" in err assert "Unable to clone" in err
git_cmd.check = lambda *a, **k: 1 if a[0] == "checkout" else 0 git_cmd.check = lambda *a, **k: 1 if a[0] == "checkout" else 0
ret = ci.setup_spack_repro_version(repro_dir, c2, c1) ret = ci.setup_spack_repro_version(repro_dir, c2, c1)
out, err = capfd.readouterr() _, err = capfd.readouterr()
assert not ret assert not ret
assert "Unable to checkout" in err assert "Unable to checkout" in err
git_cmd.check = lambda *a, **k: 1 if "merge" in a else 0 git_cmd.check = lambda *a, **k: 1 if "merge" in a else 0
ret = ci.setup_spack_repro_version(repro_dir, c2, c1) ret = ci.setup_spack_repro_version(repro_dir, c2, c1)
out, err = capfd.readouterr() _, err = capfd.readouterr()
assert not ret assert not ret
assert "Unable to merge {0}".format(c1) in err assert "Unable to merge {0}".format(c1) in err

View file

@ -13,11 +13,8 @@
import spack.paths import spack.paths
import spack.util.spack_json as sjson import spack.util.spack_json as sjson
from spack.main import SpackCommand from spack.main import SpackCommand
from spack.util.executable import which
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.usefixtures("git")
not which("git") or not spack.cmd.spack_is_git_repo(), reason="needs git"
)
blame = SpackCommand("blame") blame = SpackCommand("blame")

View file

@ -31,7 +31,6 @@
from spack.schema.database_index import schema as db_idx_schema from spack.schema.database_index import schema as db_idx_schema
from spack.schema.gitlab_ci import schema as gitlab_ci_schema from spack.schema.gitlab_ci import schema as gitlab_ci_schema
from spack.spec import CompilerSpec, Spec from spack.spec import CompilerSpec, Spec
from spack.util.executable import which
from spack.util.pattern import Bunch from spack.util.pattern import Bunch
ci_cmd = spack.main.SpackCommand("ci") ci_cmd = spack.main.SpackCommand("ci")
@ -54,14 +53,13 @@ def ci_base_environment(working_env, tmpdir):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mock_git_repo(tmpdir): def mock_git_repo(git, tmpdir):
"""Create a mock git repo with two commits, the last one creating """Create a mock git repo with two commits, the last one creating
a .gitlab-ci.yml""" a .gitlab-ci.yml"""
repo_path = tmpdir.join("mockspackrepo").strpath repo_path = tmpdir.join("mockspackrepo").strpath
mkdirp(repo_path) mkdirp(repo_path)
git = which("git", required=True)
with working_dir(repo_path): with working_dir(repo_path):
git("init") git("init")

View file

@ -13,37 +13,21 @@
from llnl.util.filesystem import mkdirp, working_dir from llnl.util.filesystem import mkdirp, working_dir
import spack import spack
from spack.util.executable import which
from spack.version import ver from spack.version import ver
git = which("git")
git_required_version = "2.17.0"
def check_git_version():
"""Check if git version is new enough for worktree functionality.
Return True if requirements are met.
The latest required functionality is `worktree remove` which was only added
in 2.17.0.
Refer:
https://github.com/git/git/commit/cc73385cf6c5c229458775bc92e7dbbe24d11611
"""
git_version = spack.fetch_strategy.GitFetchStrategy.version_from_git(git)
return git_version >= ver(git_required_version)
pytestmark = pytest.mark.skipif(
not git or not check_git_version(), reason="we need git to test if we are in a git repo"
)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def git_tmp_worktree(tmpdir, mock_git_version_info): def git_tmp_worktree(git, tmpdir, mock_git_version_info):
"""Create new worktree in a temporary folder and monkeypatch """Create new worktree in a temporary folder and monkeypatch
spack.paths.prefix to point to it. spack.paths.prefix to point to it.
""" """
# We need `git worktree remove` for this fixture, which was added in 2.17.0.
# See https://github.com/git/git/commit/cc73385cf6c5c229458775bc92e7dbbe24d11611
git_version = spack.fetch_strategy.GitFetchStrategy.version_from_git(git)
if git_version < ver("2.17.0"):
pytest.skip("git_tmp_worktree requires git v2.17.0")
with working_dir(mock_git_version_info[0]): with working_dir(mock_git_version_info[0]):
# TODO: This is fragile and should be high priority for # TODO: This is fragile and should be high priority for
# follow up fixes. 27021 # follow up fixes. 27021

View file

@ -16,9 +16,6 @@
import spack.cmd.pkg import spack.cmd.pkg
import spack.main import spack.main
import spack.repo import spack.repo
from spack.util.executable import which
pytestmark = pytest.mark.skipif(not which("git"), reason="spack pkg tests require git")
#: new fake package template #: new fake package template
pkg_template = """\ pkg_template = """\
@ -40,7 +37,7 @@ def install(self, spec, prefix):
# Force all tests to use a git repository *in* the mock packages repo. # Force all tests to use a git repository *in* the mock packages repo.
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def mock_pkg_git_repo(tmpdir_factory): def mock_pkg_git_repo(git, tmpdir_factory):
"""Copy the builtin.mock repo and make a mutable git repo inside it.""" """Copy the builtin.mock repo and make a mutable git repo inside it."""
tmproot = tmpdir_factory.mktemp("mock_pkg_git_repo") tmproot = tmpdir_factory.mktemp("mock_pkg_git_repo")
repo_path = tmproot.join("builtin.mock") repo_path = tmproot.join("builtin.mock")
@ -49,7 +46,6 @@ def mock_pkg_git_repo(tmpdir_factory):
mock_repo = spack.repo.RepoPath(str(repo_path)) mock_repo = spack.repo.RepoPath(str(repo_path))
mock_repo_packages = mock_repo.repos[0].packages_path mock_repo_packages = mock_repo.repos[0].packages_path
git = which("git", required=True)
with working_dir(mock_repo_packages): with working_dir(mock_repo_packages):
git("init") git("init")
@ -110,7 +106,7 @@ def test_mock_packages_path(mock_packages):
assert spack.repo.packages_path() == spack.repo.path.get_repo("builtin.mock").packages_path assert spack.repo.packages_path() == spack.repo.path.get_repo("builtin.mock").packages_path
def test_pkg_add(mock_pkg_git_repo): def test_pkg_add(git, mock_pkg_git_repo):
with working_dir(mock_pkg_git_repo): with working_dir(mock_pkg_git_repo):
mkdirp("pkg-e") mkdirp("pkg-e")
with open("pkg-e/package.py", "w") as f: with open("pkg-e/package.py", "w") as f:
@ -118,7 +114,6 @@ def test_pkg_add(mock_pkg_git_repo):
pkg("add", "pkg-e") pkg("add", "pkg-e")
git = which("git", required=True)
with working_dir(mock_pkg_git_repo): with working_dir(mock_pkg_git_repo):
try: try:
assert "A pkg-e/package.py" in git("status", "--short", output=str) assert "A pkg-e/package.py" in git("status", "--short", output=str)

View file

@ -24,18 +24,12 @@
style = spack.main.SpackCommand("style") style = spack.main.SpackCommand("style")
def has_develop_branch(): @pytest.fixture(autouse=True)
git = which("git") def has_develop_branch(git):
if not git: """spack style requires git and a develop branch to run -- skip if we're missing either."""
return False
git("show-ref", "--verify", "--quiet", "refs/heads/develop", fail_on_error=False) git("show-ref", "--verify", "--quiet", "refs/heads/develop", fail_on_error=False)
return git.returncode == 0 if git.returncode != 0:
pytest.skip("requires git and a develop branch")
# spack style requires git to run -- skip the tests if it's not there
pytestmark = pytest.mark.skipif(
not has_develop_branch(), reason="requires git with develop branch"
)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -77,9 +71,8 @@ def flake8_package_with_errors(scope="function"):
yield tmp yield tmp
def test_changed_files_from_git_rev_base(tmpdir, capfd): def test_changed_files_from_git_rev_base(git, tmpdir, capfd):
"""Test arbitrary git ref as base.""" """Test arbitrary git ref as base."""
git = which("git", required=True)
with tmpdir.as_cwd(): with tmpdir.as_cwd():
git("init") git("init")
git("checkout", "-b", "main") git("checkout", "-b", "main")
@ -97,10 +90,9 @@ def test_changed_files_from_git_rev_base(tmpdir, capfd):
assert changed_files(base="HEAD~") == ["bin/spack"] assert changed_files(base="HEAD~") == ["bin/spack"]
def test_changed_no_base(tmpdir, capfd): def test_changed_no_base(git, tmpdir, capfd):
"""Ensure that we fail gracefully with no base branch.""" """Ensure that we fail gracefully with no base branch."""
tmpdir.join("bin").ensure("spack") tmpdir.join("bin").ensure("spack")
git = which("git", required=True)
with tmpdir.as_cwd(): with tmpdir.as_cwd():
git("init") git("init")
git("config", "user.name", "test user") git("config", "user.name", "test user")
@ -165,10 +157,8 @@ def test_style_is_package(tmpdir):
@pytest.fixture @pytest.fixture
def external_style_root(flake8_package_with_errors, tmpdir): def external_style_root(git, flake8_package_with_errors, tmpdir):
"""Create a mock git repository for running spack style.""" """Create a mock git repository for running spack style."""
git = which("git", required=True)
# create a sort-of spack-looking directory # create a sort-of spack-looking directory
script = tmpdir / "bin" / "spack" script = tmpdir / "bin" / "spack"
script.ensure() script.ensure()

View file

@ -46,6 +46,7 @@
import spack.subprocess_context import spack.subprocess_context
import spack.test.cray_manifest import spack.test.cray_manifest
import spack.util.executable import spack.util.executable
import spack.util.git
import spack.util.gpg import spack.util.gpg
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
import spack.util.url as url_util import spack.util.url as url_util
@ -66,12 +67,20 @@ def ensure_configuration_fixture_run_before(request):
request.getfixturevalue("mutable_config") request.getfixturevalue("mutable_config")
@pytest.fixture(scope="session")
def git():
"""Fixture for tests that use git."""
if not spack.util.git.git():
pytest.skip("requires git to be installed")
return spack.util.git.git(required=True)
# #
# Return list of shas for latest two git commits in local spack repo # Return list of shas for latest two git commits in local spack repo
# #
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def last_two_git_commits(): def last_two_git_commits(git):
git = spack.util.executable.which("git", required=True)
spack_git_path = spack.paths.prefix spack_git_path = spack.paths.prefix
with working_dir(spack_git_path): with working_dir(spack_git_path):
git_log_out = git("log", "-n", "2", output=str, error=os.devnull) git_log_out = git("log", "-n", "2", output=str, error=os.devnull)
@ -98,7 +107,7 @@ def override_git_repos_cache_path(tmpdir):
@pytest.fixture @pytest.fixture
def mock_git_version_info(tmpdir, override_git_repos_cache_path): def mock_git_version_info(git, tmpdir, override_git_repos_cache_path):
"""Create a mock git repo with known structure """Create a mock git repo with known structure
The structure of commits in this repo is as follows:: The structure of commits in this repo is as follows::
@ -123,7 +132,6 @@ def mock_git_version_info(tmpdir, override_git_repos_cache_path):
version tags on multiple branches, and version order is not equal to time version tags on multiple branches, and version order is not equal to time
order or topological order. order or topological order.
""" """
git = spack.util.executable.which("git", required=True)
repo_path = str(tmpdir.mkdir("git_repo")) repo_path = str(tmpdir.mkdir("git_repo"))
filename = "file.txt" filename = "file.txt"
@ -1100,7 +1108,9 @@ def mock_archive(request, tmpdir_factory):
"""Creates a very simple archive directory with a configure script and a """Creates a very simple archive directory with a configure script and a
makefile that installs to a prefix. Tars it up into an archive. makefile that installs to a prefix. Tars it up into an archive.
""" """
tar = spack.util.executable.which("tar", required=True) tar = spack.util.executable.which("tar")
if not tar:
pytest.skip("requires tar to be installed")
tmpdir = tmpdir_factory.mktemp("mock-archive-dir") tmpdir = tmpdir_factory.mktemp("mock-archive-dir")
tmpdir.ensure(spack.stage._source_path_subdir, dir=True) tmpdir.ensure(spack.stage._source_path_subdir, dir=True)
@ -1299,7 +1309,7 @@ def get_date():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def mock_git_repository(tmpdir_factory): def mock_git_repository(git, tmpdir_factory):
"""Creates a git repository multiple commits, branches, submodules, and """Creates a git repository multiple commits, branches, submodules, and
a tag. Visual representation of the commit history (starting with the a tag. Visual representation of the commit history (starting with the
earliest commit at c0):: earliest commit at c0)::
@ -1323,8 +1333,6 @@ def mock_git_repository(tmpdir_factory):
associated builtin.mock package 'git-test'. c3 is a commit in the associated builtin.mock package 'git-test'. c3 is a commit in the
repository but does not have an associated explicit package version. repository but does not have an associated explicit package version.
""" """
git = spack.util.executable.which("git", required=True)
suburls = [] suburls = []
# Create two git repositories which will be used as submodules in the # Create two git repositories which will be used as submodules in the
# main repository # main repository
@ -1452,7 +1460,9 @@ def mock_git_repository(tmpdir_factory):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def mock_hg_repository(tmpdir_factory): def mock_hg_repository(tmpdir_factory):
"""Creates a very simple hg repository with two commits.""" """Creates a very simple hg repository with two commits."""
hg = spack.util.executable.which("hg", required=True) hg = spack.util.executable.which("hg")
if not hg:
pytest.skip("requires mercurial to be installed")
tmpdir = tmpdir_factory.mktemp("mock-hg-repo-dir") tmpdir = tmpdir_factory.mktemp("mock-hg-repo-dir")
tmpdir.ensure(spack.stage._source_path_subdir, dir=True) tmpdir.ensure(spack.stage._source_path_subdir, dir=True)
@ -1490,7 +1500,10 @@ def mock_hg_repository(tmpdir_factory):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def mock_svn_repository(tmpdir_factory): def mock_svn_repository(tmpdir_factory):
"""Creates a very simple svn repository with two commits.""" """Creates a very simple svn repository with two commits."""
svn = spack.util.executable.which("svn", required=True) svn = spack.util.executable.which("svn")
if not svn:
pytest.skip("requires svn to be installed")
svnadmin = spack.util.executable.which("svnadmin", required=True) svnadmin = spack.util.executable.which("svnadmin", required=True)
tmpdir = tmpdir_factory.mktemp("mock-svn-stage") tmpdir = tmpdir_factory.mktemp("mock-svn-stage")

View file

@ -16,17 +16,13 @@
from spack.fetch_strategy import GitFetchStrategy from spack.fetch_strategy import GitFetchStrategy
from spack.spec import Spec from spack.spec import Spec
from spack.stage import Stage from spack.stage import Stage
from spack.util.executable import which
from spack.version import ver from spack.version import ver
pytestmark = pytest.mark.skipif(not which("git"), reason="requires git to be installed")
_mock_transport_error = "Mock HTTP transport error" _mock_transport_error = "Mock HTTP transport error"
@pytest.fixture(params=[None, "1.8.5.2", "1.8.5.1", "1.7.10", "1.7.1", "1.7.0"]) @pytest.fixture(params=[None, "1.8.5.2", "1.8.5.1", "1.7.10", "1.7.1", "1.7.0"])
def git_version(request, monkeypatch): def git_version(git, request, monkeypatch):
"""Tests GitFetchStrategy behavior for different git versions. """Tests GitFetchStrategy behavior for different git versions.
GitFetchStrategy tries to optimize using features of newer git GitFetchStrategy tries to optimize using features of newer git
@ -34,7 +30,6 @@ def git_version(request, monkeypatch):
paths for old versions still work, we fake it out here and make it paths for old versions still work, we fake it out here and make it
use the backward-compatibility code paths with newer git versions. use the backward-compatibility code paths with newer git versions.
""" """
git = which("git", required=True)
real_git_version = spack.fetch_strategy.GitFetchStrategy.version_from_git(git) real_git_version = spack.fetch_strategy.GitFetchStrategy.version_from_git(git)
if request.param is None: if request.param is None:
@ -83,6 +78,7 @@ def test_bad_git(tmpdir, mock_bad_git):
@pytest.mark.parametrize("type_of_test", ["default", "branch", "tag", "commit"]) @pytest.mark.parametrize("type_of_test", ["default", "branch", "tag", "commit"])
@pytest.mark.parametrize("secure", [True, False]) @pytest.mark.parametrize("secure", [True, False])
def test_fetch( def test_fetch(
git,
type_of_test, type_of_test,
secure, secure,
mock_git_repository, mock_git_repository,
@ -217,7 +213,7 @@ def test_debug_fetch(
assert os.path.isdir(s.package.stage.source_path) assert os.path.isdir(s.package.stage.source_path)
def test_git_extra_fetch(tmpdir): def test_git_extra_fetch(git, tmpdir):
"""Ensure a fetch after 'expanding' is effectively a no-op.""" """Ensure a fetch after 'expanding' is effectively a no-op."""
testpath = str(tmpdir) testpath = str(tmpdir)
@ -228,7 +224,7 @@ def test_git_extra_fetch(tmpdir):
shutil.rmtree(stage.source_path) shutil.rmtree(stage.source_path)
def test_needs_stage(): def test_needs_stage(git):
"""Trigger a NoStageError when attempt a fetch without a stage.""" """Trigger a NoStageError when attempt a fetch without a stage."""
with pytest.raises( with pytest.raises(
spack.fetch_strategy.NoStageError, match=r"set_stage.*before calling fetch" spack.fetch_strategy.NoStageError, match=r"set_stage.*before calling fetch"

View file

@ -3,7 +3,6 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import sys import sys
import pytest import pytest
@ -11,6 +10,8 @@
import llnl.util.filesystem as fs import llnl.util.filesystem as fs
import spack.paths import spack.paths
import spack.util.executable as exe
import spack.util.git
from spack.main import get_version, main from spack.main import get_version, main
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(
@ -18,7 +19,7 @@
) )
def test_version_git_nonsense_output(tmpdir, working_env): def test_version_git_nonsense_output(tmpdir, working_env, monkeypatch):
git = str(tmpdir.join("git")) git = str(tmpdir.join("git"))
with open(git, "w") as f: with open(git, "w") as f:
f.write( f.write(
@ -28,11 +29,11 @@ def test_version_git_nonsense_output(tmpdir, working_env):
) )
fs.set_executable(git) fs.set_executable(git)
os.environ["PATH"] = str(tmpdir) monkeypatch.setattr(spack.util.git, "git", lambda: exe.which(git))
assert spack.spack_version == get_version() assert spack.spack_version == get_version()
def test_version_git_fails(tmpdir, working_env): def test_version_git_fails(tmpdir, working_env, monkeypatch):
git = str(tmpdir.join("git")) git = str(tmpdir.join("git"))
with open(git, "w") as f: with open(git, "w") as f:
f.write( f.write(
@ -43,11 +44,11 @@ def test_version_git_fails(tmpdir, working_env):
) )
fs.set_executable(git) fs.set_executable(git)
os.environ["PATH"] = str(tmpdir) monkeypatch.setattr(spack.util.git, "git", lambda: exe.which(git))
assert spack.spack_version == get_version() assert spack.spack_version == get_version()
def test_git_sha_output(tmpdir, working_env): def test_git_sha_output(tmpdir, working_env, monkeypatch):
git = str(tmpdir.join("git")) git = str(tmpdir.join("git"))
sha = "26552533be04e83e66be2c28e0eb5011cb54e8fa" sha = "26552533be04e83e66be2c28e0eb5011cb54e8fa"
with open(git, "w") as f: with open(git, "w") as f:
@ -60,7 +61,7 @@ def test_git_sha_output(tmpdir, working_env):
) )
fs.set_executable(git) fs.set_executable(git)
os.environ["PATH"] = str(tmpdir) monkeypatch.setattr(spack.util.git, "git", lambda: exe.which(git))
expected = "{0} ({1})".format(spack.spack_version, sha) expected = "{0} ({1})".format(spack.spack_version, sha)
assert expected == get_version() assert expected == get_version()
@ -70,18 +71,22 @@ def test_get_version_no_repo(tmpdir, monkeypatch):
assert spack.spack_version == get_version() assert spack.spack_version == get_version()
def test_get_version_no_git(tmpdir, working_env): def test_get_version_no_git(tmpdir, working_env, monkeypatch):
os.environ["PATH"] = str(tmpdir) monkeypatch.setattr(spack.util.git, "git", lambda: None)
assert spack.spack_version == get_version() assert spack.spack_version == get_version()
def test_main_calls_get_version(tmpdir, capsys, working_env): def test_main_calls_get_version(tmpdir, capsys, working_env, monkeypatch):
os.environ["PATH"] = str(tmpdir) # act like git is not found in the PATH
monkeypatch.setattr(spack.util.git, "git", lambda: None)
# make sure we get a bare version (without commit) when this happens
main(["-V"]) main(["-V"])
assert spack.spack_version == capsys.readouterr()[0].strip() out, err = capsys.readouterr()
assert spack.spack_version == out.strip()
def test_get_version_bad_git(tmpdir, working_env): def test_get_version_bad_git(tmpdir, working_env, monkeypatch):
bad_git = str(tmpdir.join("git")) bad_git = str(tmpdir.join("git"))
with open(bad_git, "w") as f: with open(bad_git, "w") as f:
f.write( f.write(
@ -91,5 +96,5 @@ def test_get_version_bad_git(tmpdir, working_env):
) )
fs.set_executable(bad_git) fs.set_executable(bad_git)
os.environ["PATH"] = str(tmpdir) monkeypatch.setattr(spack.util.git, "git", lambda: exe.which(bad_git))
assert spack.spack_version == get_version() assert spack.spack_version == get_version()

View file

@ -104,33 +104,24 @@ def test_url_mirror(mock_archive):
repos.clear() repos.clear()
@pytest.mark.skipif(not which("git"), reason="requires git to be installed") def test_git_mirror(git, mock_git_repository):
def test_git_mirror(mock_git_repository):
set_up_package("git-test", mock_git_repository, "git") set_up_package("git-test", mock_git_repository, "git")
check_mirror() check_mirror()
repos.clear() repos.clear()
@pytest.mark.skipif(
not which("svn") or not which("svnadmin"), reason="requires subversion to be installed"
)
def test_svn_mirror(mock_svn_repository): def test_svn_mirror(mock_svn_repository):
set_up_package("svn-test", mock_svn_repository, "svn") set_up_package("svn-test", mock_svn_repository, "svn")
check_mirror() check_mirror()
repos.clear() repos.clear()
@pytest.mark.skipif(not which("hg"), reason="requires mercurial to be installed")
def test_hg_mirror(mock_hg_repository): def test_hg_mirror(mock_hg_repository):
set_up_package("hg-test", mock_hg_repository, "hg") set_up_package("hg-test", mock_hg_repository, "hg")
check_mirror() check_mirror()
repos.clear() repos.clear()
@pytest.mark.skipif(
not all([which("svn"), which("hg"), which("git")]),
reason="requires subversion, git, and mercurial to be installed",
)
def test_all_mirror(mock_git_repository, mock_svn_repository, mock_hg_repository, mock_archive): def test_all_mirror(mock_git_repository, mock_svn_repository, mock_hg_repository, mock_archive):
set_up_package("git-test", mock_git_repository, "git") set_up_package("git-test", mock_git_repository, "git")

View file

@ -16,7 +16,6 @@
import spack.package_base import spack.package_base
import spack.spec import spack.spec
from spack.util.executable import which
from spack.version import ( from spack.version import (
GitVersion, GitVersion,
Version, Version,
@ -593,7 +592,7 @@ def test_invalid_versions(version_str):
@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)")
def test_versions_from_git(mock_git_version_info, monkeypatch, mock_packages): def test_versions_from_git(git, mock_git_version_info, monkeypatch, mock_packages):
repo_path, filename, commits = mock_git_version_info repo_path, filename, commits = mock_git_version_info
monkeypatch.setattr( monkeypatch.setattr(
spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False
@ -607,7 +606,7 @@ def test_versions_from_git(mock_git_version_info, monkeypatch, mock_packages):
] ]
with working_dir(repo_path): with working_dir(repo_path):
which("git")("checkout", commit) git("checkout", commit)
with open(os.path.join(repo_path, filename), "r") as f: with open(os.path.join(repo_path, filename), "r") as f:
expected = f.read() expected = f.read()

View file

@ -0,0 +1,30 @@
# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Single util module where Spack should get a git executable."""
import sys
from typing import Optional
import llnl.util.lang
import spack.util.executable as exe
@llnl.util.lang.memoized
def git(required: bool = False):
"""Get a git executable.
Arguments:
required: if ``True``, fail if ``git`` is not found. By default return ``None``.
"""
git: Optional[exe.Executable] = exe.which("git", required=required)
# If we're running under pytest, add this to ignore the fix for CVE-2022-39253 in
# git 2.38.1+. Do this in one place; we need git to do this in all parts of Spack.
if git and "pytest" in sys.modules:
git.add_default_arg("-c")
git.add_default_arg("protocol.file.allow=always")
return git