|
13 | 13 | import logging |
14 | 14 | import os |
15 | 15 | import re |
| 16 | +import shutil |
16 | 17 | import signal |
17 | 18 | import subprocess |
18 | 19 | import sys |
@@ -699,6 +700,9 @@ class Git(metaclass=_GitMeta): |
699 | 700 | "_repo", |
700 | 701 | ) |
701 | 702 |
|
| 703 | + _version_info: Optional[Tuple[int, ...]] |
| 704 | + _version_info_token: object |
| 705 | + |
702 | 706 | # Match Git's leading transport selector, including an empty helper name. |
703 | 707 | # Git also selects the command-executing ext helper for an ext:// URL. |
704 | 708 | re_unsafe_protocol = re.compile(r"([A-Za-z0-9][A-Za-z0-9+.-]*|)::|ext://") |
@@ -791,6 +795,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None: |
791 | 795 | """ |
792 | 796 |
|
793 | 797 | _refresh_token = object() # Since None would match an initial _version_info_token. |
| 798 | + _version_check_cache: Dict[Tuple[Any, ...], Tuple[int, ...]] = {} |
794 | 799 |
|
795 | 800 | @classmethod |
796 | 801 | def refresh(cls, path: Union[None, PathLike] = None) -> bool: |
@@ -1145,8 +1150,61 @@ def _check_operand(value: Any, label: str = "operand") -> str: |
1145 | 1150 | return value |
1146 | 1151 |
|
1147 | 1152 | def _require_version(self) -> None: |
1148 | | - if self.version_info < (2, 52): |
| 1153 | + key = None |
| 1154 | + if self._version_info_token is not self._refresh_token: |
| 1155 | + key = self._version_check_key() |
| 1156 | + if key is not None: |
| 1157 | + version = self._version_check_cache.get(key) |
| 1158 | + if version is not None: |
| 1159 | + self._version_info = version |
| 1160 | + self._version_info_token = key[0] |
| 1161 | + version = self.version_info |
| 1162 | + if version < (2, 52): |
1149 | 1163 | raise UnsupportedOperation("GitPython requires Git 2.52 or newer for repository operations") |
| 1164 | + if key is not None and key not in self._version_check_cache: |
| 1165 | + # ponytail: clear at 128 contexts; use LRU if diverse environments churn. |
| 1166 | + if len(self._version_check_cache) >= 128: |
| 1167 | + self._version_check_cache.clear() |
| 1168 | + self._version_check_cache[key] = version |
| 1169 | + |
| 1170 | + def _version_check_key(self) -> Optional[Tuple[Any, ...]]: |
| 1171 | + """Identify the executable and context of a minimum-version probe.""" |
| 1172 | + executable = self.GIT_PYTHON_GIT_EXECUTABLE |
| 1173 | + if not executable or self._git_options or self._persistent_git_options: |
| 1174 | + return None |
| 1175 | + environment = {**os.environ, "LANGUAGE": "C", "LC_ALL": "C", **self._environment} |
| 1176 | + try: |
| 1177 | + cwd = os.path.realpath(self._working_dir or os.getcwd()) |
| 1178 | + if not os.access(cwd, os.X_OK): |
| 1179 | + return None |
| 1180 | + resolved: Optional[str] |
| 1181 | + if os.path.isabs(executable): |
| 1182 | + resolved = executable |
| 1183 | + elif sys.platform == "win32": |
| 1184 | + # CreateProcess searches in the parent context, unlike execvpe. |
| 1185 | + return None |
| 1186 | + elif os.path.dirname(executable): |
| 1187 | + resolved = os.path.join(cwd, executable) |
| 1188 | + else: |
| 1189 | + path = environment.get("PATH") |
| 1190 | + path = os.defpath if path is None else path |
| 1191 | + resolved = shutil.which( |
| 1192 | + executable, path=os.pathsep.join(os.path.join(cwd, p) for p in path.split(os.pathsep)) |
| 1193 | + ) |
| 1194 | + if resolved is None: |
| 1195 | + return None |
| 1196 | + info = os.stat(resolved) |
| 1197 | + except OSError: |
| 1198 | + return None |
| 1199 | + return ( |
| 1200 | + self._refresh_token, |
| 1201 | + type(self), |
| 1202 | + executable, |
| 1203 | + resolved, |
| 1204 | + cwd, |
| 1205 | + tuple(sorted(environment.items())), |
| 1206 | + (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns), |
| 1207 | + ) |
1150 | 1208 |
|
1151 | 1209 | def _call_process_safe( |
1152 | 1210 | self, |
@@ -1235,8 +1293,8 @@ def __init__(self, working_dir: Union[None, PathLike] = None) -> None: |
1235 | 1293 | self._repo: Any = None # Weak reference; the Repo owns native resources. |
1236 | 1294 |
|
1237 | 1295 | # Cached version slots |
1238 | | - self._version_info: Union[Tuple[int, ...], None] = None |
1239 | | - self._version_info_token: object = None |
| 1296 | + self._version_info = None |
| 1297 | + self._version_info_token = None |
1240 | 1298 |
|
1241 | 1299 | # Cached command slots |
1242 | 1300 | self.cat_file_header: Union[None, TBD] = None |
|
0 commit comments