diff --git a/poetry/core/vcs/git.py b/poetry/core/vcs/git.py index 1987f1974..5d9321309 100644 --- a/poetry/core/vcs/git.py +++ b/poetry/core/vcs/git.py @@ -6,6 +6,8 @@ from typing import Any from typing import Optional +from poetry.core.utils._compat import WINDOWS + pattern_formats = { "protocol": r"\w+", @@ -183,13 +185,48 @@ def __str__(self) -> str: GitUrl = namedtuple("GitUrl", ["url", "revision", "subdirectory"]) +_executable: Optional[str] = None + + +def executable(): + global _executable + + if _executable is not None: + return _executable + + if WINDOWS: + # Finding git via where.exe + where = "%WINDIR%\\System32\\where.exe" + paths = subprocess.check_output( + [where, "git"], shell=True, encoding="oem" + ).split("\n") + for path in paths: + if not path: + continue + + path = Path(path.strip()) + try: + path.relative_to(Path.cwd()) + except ValueError: + _executable = str(path) + + break + else: + _executable = "git" + + if _executable is None: + raise RuntimeError("Unable to find a valid git executable") + + return _executable + + class GitConfig: def __init__(self, requires_git_presence: bool = False) -> None: self._config = {} try: config_list = subprocess.check_output( - ["git", "config", "-l"], stderr=subprocess.STDOUT + [executable(), "config", "-l"], stderr=subprocess.STDOUT ).decode() m = re.findall("(?ms)^([^=]+)=(.*?)$", config_list) @@ -344,7 +381,9 @@ def run(self, *args: Any, **kwargs: Any) -> str: ) + args return ( - subprocess.check_output(["git"] + list(args), stderr=subprocess.STDOUT) + subprocess.check_output( + [executable()] + list(args), stderr=subprocess.STDOUT + ) .decode() .strip() ) diff --git a/tests/vcs/test_vcs.py b/tests/vcs/test_vcs.py index 4d22c9fa2..e0e9dc629 100644 --- a/tests/vcs/test_vcs.py +++ b/tests/vcs/test_vcs.py @@ -1,7 +1,10 @@ +import subprocess + from pathlib import Path import pytest +from poetry.core.utils._compat import WINDOWS from poetry.core.vcs.git import Git from poetry.core.vcs.git import GitError from poetry.core.vcs.git import GitUrl @@ -366,3 +369,44 @@ def test_git_checkout_raises_error_on_invalid_repository(): def test_git_rev_parse_raises_error_on_invalid_repository(): with pytest.raises(GitError): Git().rev_parse("-u./payload") + + +@pytest.mark.skipif( + not WINDOWS, + reason="Retrieving the complete path to git is only necessary on Windows, for security reasons", +) +def test_ensure_absolute_path_to_git(mocker): + def checkout_output(cmd, *args, **kwargs): + if Path(cmd[0]).name == "where.exe": + return "\n".join( + [ + str(Path.cwd().joinpath("git.exe")), + "C:\\Git\\cmd\\git.exe", + ] + ) + + return b"" + + mock = mocker.patch.object(subprocess, "check_output", side_effect=checkout_output) + + Git().run("config") + + assert mock.call_args_list[-1][0][0] == [ + "C:\\Git\\cmd\\git.exe", + "config", + ] + + +@pytest.mark.skipif( + not WINDOWS, + reason="Retrieving the complete path to git is only necessary on Windows, for security reasons", +) +def test_ensure_existing_git_executable_is_found(mocker): + mock = mocker.patch.object(subprocess, "check_output", return_value=b"") + + Git().run("config") + + cmd = Path(mock.call_args_list[-1][0][0][0]) + + assert cmd.is_absolute() + assert cmd.name == "git.exe"