1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
"""Locate and invoke the git executable (ADR-0008). No Python Git library."""
from __future__ import annotations
import os
import shutil
import subprocess
from collections.abc import Mapping, Sequence
from pathlib import Path
from lpip.errors import GitCommandError, GitNotFoundError
from lpip.validation import to_git_remote_url
_WIN_FALLBACK_RELATIVE = Path("Git") / "cmd" / "git.exe"
_WIN_USER_RELATIVE = Path("Programs") / "Git" / "cmd" / "git.exe"
def find_git_executable(
*,
environ: Mapping[str, str] | None = None,
search_windows_fallbacks: bool | None = None,
) -> Path | None:
"""PATH first, then typical Git for Windows locations (ADR-0008)."""
env = environ if environ is not None else os.environ
path_var = env.get("PATH")
found = shutil.which("git", path=path_var)
if found:
return Path(found)
use_windows = os.name == "nt" if search_windows_fallbacks is None else search_windows_fallbacks
if not use_windows:
return None
candidates: list[Path] = []
for key in ("PROGRAMFILES", "PROGRAMFILES(X86)"):
root = env.get(key)
if root:
candidates.append(Path(root) / _WIN_FALLBACK_RELATIVE)
local = env.get("LOCALAPPDATA")
if local:
candidates.append(Path(local) / _WIN_USER_RELATIVE)
for candidate in candidates:
if candidate.is_file():
return candidate
return None
def require_git(
*,
environ: Mapping[str, str] | None = None,
search_windows_fallbacks: bool | None = None,
) -> Path:
found = find_git_executable(
environ=environ,
search_windows_fallbacks=search_windows_fallbacks,
)
if found is None:
raise GitNotFoundError()
return found
def run_git(
args: Sequence[str],
*,
cwd: Path | None = None,
environ: Mapping[str, str] | None = None,
git: Path | None = None,
) -> subprocess.CompletedProcess[str]:
"""Run git. Inherits the environment; does not set GIT_TERMINAL_PROMPT=0."""
exe = git or require_git(environ=environ)
env = dict(environ) if environ is not None else None
completed = subprocess.run( # nosec B603
[str(exe), *args],
cwd=cwd,
env=env,
text=True,
capture_output=True,
check=False,
)
if completed.returncode != 0:
detail = (completed.stderr or completed.stdout or "").strip()
raise GitCommandError(list(args), detail=detail or None)
return completed
def git_ls_remote_tags(url: str, *, environ: Mapping[str, str] | None = None) -> tuple[str, ...]:
"""Tag names from ``git ls-remote --tags`` (ADR-0016). Drops peeled ``^{}`` lines."""
completed = run_git(
["ls-remote", "--tags", "--", to_git_remote_url(url)],
environ=environ,
)
tags: list[str] = []
for line in completed.stdout.splitlines():
parts = line.split()
if len(parts) < 2:
continue
ref = parts[1]
prefix = "refs/tags/"
if not ref.startswith(prefix):
continue
name = ref[len(prefix) :]
if name.endswith("^{}"):
continue
tags.append(name)
return tuple(tags)
def git_clone(url: str, dest: Path, *, environ: Mapping[str, str] | None = None) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
run_git(["clone", "--", to_git_remote_url(url), str(dest)], environ=environ)
def git_fetch(repo: Path, *, environ: Mapping[str, str] | None = None) -> None:
run_git(["fetch", "--all", "--tags"], cwd=repo, environ=environ)
def git_checkout(repo: Path, revision: str, *, environ: Mapping[str, str] | None = None) -> None:
run_git(["checkout", revision], cwd=repo, environ=environ)
def git_current_revision(repo: Path, *, environ: Mapping[str, str] | None = None) -> str:
return run_git(["rev-parse", "HEAD"], cwd=repo, environ=environ).stdout.strip()
|