Skip to content

Commit 9406716

Browse files
authored
fix: do not immediately fail if git is not available
Copier has lots of git-related stuff, but it also has features that work without it. It is now a lazy requirement. Fixes #312
1 parent 5b33f0b commit 9406716

File tree

5 files changed

+34
-18
lines changed

5 files changed

+34
-18
lines changed

copier/main.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from pathspec import PathSpec
3232
from plumbum import ProcessExecutionError, colors
3333
from plumbum.cli.terminal import ask
34-
from plumbum.cmd import git
3534
from plumbum.machines import local
3635
from pydantic import ConfigDict, PositiveInt
3736
from pydantic.dataclasses import dataclass
@@ -57,6 +56,7 @@
5756
StrSeq,
5857
)
5958
from .user_data import DEFAULT_DATA, AnswersMap, Question
59+
from .vcs import get_git
6060

6161

6262
@dataclass(config=ConfigDict(extra="forbid"))
@@ -815,6 +815,7 @@ def run_update(self) -> None:
815815
self._print_message(self.template.message_after_update)
816816

817817
def _apply_update(self):
818+
git = get_git()
818819
subproject_top = Path(
819820
git(
820821
"-C",
@@ -931,6 +932,7 @@ def _apply_update(self):
931932

932933
def _git_initialize_repo(self):
933934
"""Initialize a git repository in the current directory."""
935+
git = get_git()
934936
git("init", retcode=None)
935937
git("add", ".")
936938
git("config", "user.name", "Copier")

copier/subproject.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99
from typing import Callable, List, Optional
1010

1111
import yaml
12-
from plumbum.cmd import git
1312
from plumbum.machines import local
1413
from pydantic.dataclasses import dataclass
1514

1615
from .template import Template
1716
from .types import AbsolutePath, AnyByStrDict, VCSTypes
18-
from .vcs import is_in_git_repo
17+
from .vcs import get_git, is_in_git_repo
1918

2019

2120
@dataclass
@@ -42,7 +41,7 @@ def is_dirty(self) -> bool:
4241
"""
4342
if self.vcs == "git":
4443
with local.cwd(self.local_abspath):
45-
return bool(git("status", "--porcelain").strip())
44+
return bool(get_git()("status", "--porcelain").strip())
4645
return False
4746

4847
def _cleanup(self):

copier/template.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import yaml
1515
from funcy import lflatten
1616
from packaging.version import Version, parse
17-
from plumbum.cmd import git
1817
from plumbum.machines import local
1918
from pydantic.dataclasses import dataclass
2019
from yamlinclude import YamlIncludeConstructor
@@ -28,7 +27,7 @@
2827
)
2928
from .tools import copier_version, handle_remove_readonly
3029
from .types import AnyByStrDict, Env, OptStr, StrSeq, Union, VCSTypes
31-
from .vcs import checkout_latest_tag, clone, get_repo
30+
from .vcs import checkout_latest_tag, clone, get_git, get_repo
3231

3332
# Default list of files in the template to exclude from the rendered project
3433
DEFAULT_EXCLUDE: Tuple[str, ...] = (
@@ -249,13 +248,13 @@ def commit(self) -> OptStr:
249248
"""If the template is VCS-tracked, get its commit description."""
250249
if self.vcs == "git":
251250
with local.cwd(self.local_abspath):
252-
return git("describe", "--tags", "--always").strip()
251+
return get_git()("describe", "--tags", "--always").strip()
253252

254253
@cached_property
255254
def commit_hash(self) -> OptStr:
256255
"""If the template is VCS-tracked, get its commit full hash."""
257256
if self.vcs == "git":
258-
return git("-C", self.local_abspath, "rev-parse", "HEAD").strip()
257+
return get_git()("-C", self.local_abspath, "rev-parse", "HEAD").strip()
259258

260259
@cached_property
261260
def config_data(self) -> AnyByStrDict:

copier/vcs.py

+24-8
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,24 @@
1010
from packaging import version
1111
from packaging.version import InvalidVersion, Version
1212
from plumbum import TF, ProcessExecutionError, colors, local
13-
from plumbum.cmd import git
1413

1514
from .errors import DirtyLocalWarning, ShallowCloneWarning
1615
from .types import OptBool, OptStr, StrOrPath
1716

17+
18+
def get_git():
19+
"""Gets `git` command, or fails if it's not available"""
20+
return local["git"]
21+
22+
23+
def get_git_version():
24+
git = get_git()
25+
26+
return Version(re.findall(r"\d+\.\d+\.\d+", git("version"))[0])
27+
28+
1829
GIT_PREFIX = ("git@", "git://", "git+", "https://github.com/", "https://gitlab.com/")
1930
GIT_POSTFIX = ".git"
20-
GIT_VERSION = Version(re.findall(r"\d+\.\d+\.\d+", git("version"))[0])
2131
REPLACEMENTS = (
2232
(re.compile(r"^gh:/?(.*\.git)$"), r"https://github.com/\1"),
2333
(re.compile(r"^gh:/?(.*)$"), r"https://github.com/\1.git"),
@@ -30,15 +40,15 @@ def is_git_repo_root(path: StrOrPath) -> bool:
3040
"""Indicate if a given path is a git repo root directory."""
3141
try:
3242
with local.cwd(Path(path, ".git")):
33-
return git("rev-parse", "--is-inside-git-dir").strip() == "true"
43+
return get_git()("rev-parse", "--is-inside-git-dir").strip() == "true"
3444
except OSError:
3545
return False
3646

3747

3848
def is_in_git_repo(path: StrOrPath) -> bool:
3949
"""Indicate if a given path is in a git repo directory."""
4050
try:
41-
git("-C", path, "rev-parse", "--show-toplevel")
51+
get_git()("-C", path, "rev-parse", "--show-toplevel")
4252
return True
4353
except (OSError, ProcessExecutionError):
4454
return False
@@ -47,7 +57,10 @@ def is_in_git_repo(path: StrOrPath) -> bool:
4757
def is_git_shallow_repo(path: StrOrPath) -> bool:
4858
"""Indicate if a given path is a git shallow repo directory."""
4959
try:
50-
return git("-C", path, "rev-parse", "--is-shallow-repository").strip() == "true"
60+
return (
61+
get_git()("-C", path, "rev-parse", "--is-shallow-repository").strip()
62+
== "true"
63+
)
5164
except (OSError, ProcessExecutionError):
5265
return False
5366

@@ -58,8 +71,8 @@ def is_git_bundle(path: Path) -> bool:
5871
path = path.resolve()
5972
with TemporaryDirectory(prefix=f"{__name__}.is_git_bundle.") as dirname:
6073
with local.cwd(dirname):
61-
git("init")
62-
return bool(git["bundle", "verify", path] & TF)
74+
get_git()("init")
75+
return bool(get_git()["bundle", "verify", path] & TF)
6376

6477

6578
def get_repo(url: str) -> OptStr:
@@ -107,6 +120,7 @@ def checkout_latest_tag(local_repo: StrOrPath, use_prereleases: OptBool = False)
107120
use_prereleases:
108121
If `False`, skip prerelease git tags.
109122
"""
123+
git = get_git()
110124
with local.cwd(local_repo):
111125
all_tags = filter(valid_version, git("tag").split())
112126
if not use_prereleases:
@@ -140,10 +154,12 @@ def clone(url: str, ref: OptStr = None) -> str:
140154
ref:
141155
Reference to checkout. For Git repos, defaults to `HEAD`.
142156
"""
157+
git = get_git()
158+
git_version = get_git_version()
143159
location = mkdtemp(prefix=f"{__name__}.clone.")
144160
_clone = git["clone", "--no-checkout", url, location]
145161
# Faster clones if possible
146-
if GIT_VERSION >= Version("2.27"):
162+
if git_version >= Version("2.27"):
147163
url_match = re.match("(file://)?(.*)", url)
148164
if url_match is not None:
149165
file_url = url_match.groups()[-1]

tests/test_vcs.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from copier import Worker, run_copy, run_update
1313
from copier.errors import ShallowCloneWarning
14-
from copier.vcs import GIT_VERSION, checkout_latest_tag, clone, get_repo
14+
from copier.vcs import checkout_latest_tag, clone, get_git_version, get_repo
1515

1616

1717
def test_get_repo() -> None:
@@ -93,7 +93,7 @@ def test_shallow_clone(tmp_path: Path, recwarn: pytest.WarningsRecorder) -> None
9393
git("clone", "--depth=2", "https://github.com/copier-org/autopretty.git", src_path)
9494
assert Path(src_path, "README.md").exists()
9595

96-
if GIT_VERSION >= Version("2.27"):
96+
if get_git_version() >= Version("2.27"):
9797
with pytest.warns(ShallowCloneWarning):
9898
local_tmp = clone(str(src_path))
9999
else:

0 commit comments

Comments
 (0)