diff --git a/copier/main.py b/copier/main.py index d58bcb6fe..bea80fc9b 100644 --- a/copier/main.py +++ b/copier/main.py @@ -31,7 +31,6 @@ from pathspec import PathSpec from plumbum import ProcessExecutionError, colors from plumbum.cli.terminal import ask -from plumbum.cmd import git from plumbum.machines import local from pydantic import ConfigDict, PositiveInt from pydantic.dataclasses import dataclass @@ -57,6 +56,7 @@ StrSeq, ) from .user_data import DEFAULT_DATA, AnswersMap, Question +from .vcs import get_git @dataclass(config=ConfigDict(extra="forbid")) @@ -815,6 +815,7 @@ def run_update(self) -> None: self._print_message(self.template.message_after_update) def _apply_update(self): + git = get_git() subproject_top = Path( git( "-C", @@ -931,6 +932,7 @@ def _apply_update(self): def _git_initialize_repo(self): """Initialize a git repository in the current directory.""" + git = get_git() git("init", retcode=None) git("add", ".") git("config", "user.name", "Copier") diff --git a/copier/subproject.py b/copier/subproject.py index bccec0cd1..3040bebb7 100644 --- a/copier/subproject.py +++ b/copier/subproject.py @@ -9,13 +9,12 @@ from typing import Callable, List, Optional import yaml -from plumbum.cmd import git from plumbum.machines import local from pydantic.dataclasses import dataclass from .template import Template from .types import AbsolutePath, AnyByStrDict, VCSTypes -from .vcs import is_in_git_repo +from .vcs import get_git, is_in_git_repo @dataclass @@ -42,7 +41,7 @@ def is_dirty(self) -> bool: """ if self.vcs == "git": with local.cwd(self.local_abspath): - return bool(git("status", "--porcelain").strip()) + return bool(get_git()("status", "--porcelain").strip()) return False def _cleanup(self): diff --git a/copier/template.py b/copier/template.py index d8768c47e..16779bd34 100644 --- a/copier/template.py +++ b/copier/template.py @@ -14,7 +14,6 @@ import yaml from funcy import lflatten from packaging.version import Version, parse -from plumbum.cmd import git from plumbum.machines import local from pydantic.dataclasses import dataclass from yamlinclude import YamlIncludeConstructor @@ -28,7 +27,7 @@ ) from .tools import copier_version, handle_remove_readonly from .types import AnyByStrDict, Env, OptStr, StrSeq, Union, VCSTypes -from .vcs import checkout_latest_tag, clone, get_repo +from .vcs import checkout_latest_tag, clone, get_git, get_repo # Default list of files in the template to exclude from the rendered project DEFAULT_EXCLUDE: Tuple[str, ...] = ( @@ -249,13 +248,13 @@ def commit(self) -> OptStr: """If the template is VCS-tracked, get its commit description.""" if self.vcs == "git": with local.cwd(self.local_abspath): - return git("describe", "--tags", "--always").strip() + return get_git()("describe", "--tags", "--always").strip() @cached_property def commit_hash(self) -> OptStr: """If the template is VCS-tracked, get its commit full hash.""" if self.vcs == "git": - return git("-C", self.local_abspath, "rev-parse", "HEAD").strip() + return get_git()("-C", self.local_abspath, "rev-parse", "HEAD").strip() @cached_property def config_data(self) -> AnyByStrDict: diff --git a/copier/vcs.py b/copier/vcs.py index 7dee4a3ad..33f649ff0 100644 --- a/copier/vcs.py +++ b/copier/vcs.py @@ -10,14 +10,24 @@ from packaging import version from packaging.version import InvalidVersion, Version from plumbum import TF, ProcessExecutionError, colors, local -from plumbum.cmd import git from .errors import DirtyLocalWarning, ShallowCloneWarning from .types import OptBool, OptStr, StrOrPath + +def get_git(): + """Gets `git` command, or fails if it's not available""" + return local["git"] + + +def get_git_version(): + git = get_git() + + return Version(re.findall(r"\d+\.\d+\.\d+", git("version"))[0]) + + GIT_PREFIX = ("git@", "git://", "git+", "https://github.com/", "https://gitlab.com/") GIT_POSTFIX = ".git" -GIT_VERSION = Version(re.findall(r"\d+\.\d+\.\d+", git("version"))[0]) REPLACEMENTS = ( (re.compile(r"^gh:/?(.*\.git)$"), r"https://github.com/\1"), (re.compile(r"^gh:/?(.*)$"), r"https://github.com/\1.git"), @@ -30,7 +40,7 @@ def is_git_repo_root(path: StrOrPath) -> bool: """Indicate if a given path is a git repo root directory.""" try: with local.cwd(Path(path, ".git")): - return git("rev-parse", "--is-inside-git-dir").strip() == "true" + return get_git()("rev-parse", "--is-inside-git-dir").strip() == "true" except OSError: return False @@ -38,7 +48,7 @@ def is_git_repo_root(path: StrOrPath) -> bool: def is_in_git_repo(path: StrOrPath) -> bool: """Indicate if a given path is in a git repo directory.""" try: - git("-C", path, "rev-parse", "--show-toplevel") + get_git()("-C", path, "rev-parse", "--show-toplevel") return True except (OSError, ProcessExecutionError): return False @@ -47,7 +57,10 @@ def is_in_git_repo(path: StrOrPath) -> bool: def is_git_shallow_repo(path: StrOrPath) -> bool: """Indicate if a given path is a git shallow repo directory.""" try: - return git("-C", path, "rev-parse", "--is-shallow-repository").strip() == "true" + return ( + get_git()("-C", path, "rev-parse", "--is-shallow-repository").strip() + == "true" + ) except (OSError, ProcessExecutionError): return False @@ -58,8 +71,8 @@ def is_git_bundle(path: Path) -> bool: path = path.resolve() with TemporaryDirectory(prefix=f"{__name__}.is_git_bundle.") as dirname: with local.cwd(dirname): - git("init") - return bool(git["bundle", "verify", path] & TF) + get_git()("init") + return bool(get_git()["bundle", "verify", path] & TF) def get_repo(url: str) -> OptStr: @@ -107,6 +120,7 @@ def checkout_latest_tag(local_repo: StrOrPath, use_prereleases: OptBool = False) use_prereleases: If `False`, skip prerelease git tags. """ + git = get_git() with local.cwd(local_repo): all_tags = filter(valid_version, git("tag").split()) if not use_prereleases: @@ -140,10 +154,12 @@ def clone(url: str, ref: OptStr = None) -> str: ref: Reference to checkout. For Git repos, defaults to `HEAD`. """ + git = get_git() + git_version = get_git_version() location = mkdtemp(prefix=f"{__name__}.clone.") _clone = git["clone", "--no-checkout", url, location] # Faster clones if possible - if GIT_VERSION >= Version("2.27"): + if git_version >= Version("2.27"): url_match = re.match("(file://)?(.*)", url) if url_match is not None: file_url = url_match.groups()[-1] diff --git a/tests/test_vcs.py b/tests/test_vcs.py index ce414a5d6..7f7db7c71 100644 --- a/tests/test_vcs.py +++ b/tests/test_vcs.py @@ -11,7 +11,7 @@ from copier import Worker, run_copy, run_update from copier.errors import ShallowCloneWarning -from copier.vcs import GIT_VERSION, checkout_latest_tag, clone, get_repo +from copier.vcs import checkout_latest_tag, clone, get_git_version, get_repo def test_get_repo() -> None: @@ -93,7 +93,7 @@ def test_shallow_clone(tmp_path: Path, recwarn: pytest.WarningsRecorder) -> None git("clone", "--depth=2", "https://github.com/copier-org/autopretty.git", src_path) assert Path(src_path, "README.md").exists() - if GIT_VERSION >= Version("2.27"): + if get_git_version() >= Version("2.27"): with pytest.warns(ShallowCloneWarning): local_tmp = clone(str(src_path)) else: