| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 6e61b1d commit 3d57b2f
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,7 +26,7 @@ | |||
| 26 | 26 | from gitdb.base import IStream | |
| 27 | 27 | from gitdb.typ import str_tree_type | |
| 28 | 28 | ||
| 29 | - from git.cmd import handle_process_output, safer_popen | ||
| 29 | + from git.cmd import Git, handle_process_output, safer_popen | ||
| 30 | 30 | from git.compat import defenc, force_bytes, force_text, safe_decode | |
| 31 | 31 | from git.exc import HookExecutionError, UnmergedEntriesError | |
| 32 | 32 | from git.objects.fun import ( | |
@@ -79,6 +79,97 @@ def _has_file_extension(path: str) -> str: | |||
| 79 | 79 | return osp.splitext(path)[1] | |
| 80 | 80 | ||
| 81 | 81 | ||
| 82 | + def _is_in_windows_system_root(path: str) -> bool: | ||
| 83 | + """Return whether ``path`` is inside the Windows installation directory.""" | ||
| 84 | + system_root = os.environ.get("SystemRoot") | ||
| 85 | + if not system_root: | ||
| 86 | + return False | ||
| 87 | + | ||
| 88 | + system_root = osp.normcase(osp.realpath(system_root)) | ||
| 89 | + path = osp.normcase(osp.realpath(path)) | ||
| 90 | + try: | ||
| 91 | + return osp.commonpath((system_root, path)) == system_root | ||
| 92 | + except ValueError: | ||
| 93 | + # Paths on different drives have no common path on Windows. | ||
| 94 | + return False | ||
| 95 | + | ||
| 96 | + | ||
| 97 | + def _which_from_path(command: str) -> Union[str, None]: | ||
| 98 | + """Resolve ``command`` from PATH, excluding the Windows installation.""" | ||
| 99 | + for directory in os.get_exec_path(): | ||
| 100 | + # Unlike POSIX, Windows does not define an empty PATH entry as the current | ||
| 101 | + # directory. Skip it rather than letting abspath() turn it into one. | ||
| 102 | + if not directory: | ||
| 103 | + continue | ||
| 104 | + directory = osp.abspath(directory) | ||
| 105 | + candidate = osp.join(directory, command) | ||
| 106 | + # SystemRoot contains the WSL launcher stubs. They are valid executables but | ||
| 107 | + # not suitable for running a Windows Git hook: the hook path and environment | ||
| 108 | + # were prepared for Git for Windows, and WSL may have no distribution at all. | ||
| 109 | + if _is_in_windows_system_root(candidate): | ||
| 110 | + continue | ||
| 111 | + if osp.isfile(candidate) and os.access(candidate, os.X_OK): | ||
| 112 | + return candidate | ||
| 113 | + return None | ||
| 114 | + | ||
| 115 | + | ||
| 116 | + _GIT_FOR_WINDOWS_PREFIXES = ("mingw64", "mingw32", "clangarm64", "clang64", "clang32", "ucrt64") | ||
| 117 | + | ||
| 118 | + | ||
| 119 | + def _git_for_windows_root() -> Union[str, None]: | ||
| 120 | + """Infer a standard Git for Windows root from GitPython's selected executable.""" | ||
| 121 | + git_executable = os.fspath(Git.GIT_PYTHON_GIT_EXECUTABLE or Git.git_exec_name) | ||
| 122 | + if osp.dirname(git_executable): | ||
| 123 | + # CreateProcess resolves a relative executable path containing a directory | ||
| 124 | + # from the parent process cwd, even when Popen supplies a different child cwd. | ||
| 125 | + git_executable = osp.abspath(git_executable) | ||
| 126 | + else: | ||
| 127 | + # GitPython deliberately retains a bare executable name so later PATH changes | ||
| 128 | + # affect Git commands. Resolve it with the same PATH snapshot used for Bash. | ||
| 129 | + names = (git_executable,) if _has_file_extension(git_executable) else (git_executable, f"{git_executable}.exe") | ||
| 130 | + for name in names: | ||
| 131 | + resolved = _which_from_path(name) | ||
| 132 | + if resolved is not None: | ||
| 133 | + git_executable = resolved | ||
| 134 | + break | ||
| 135 | + else: | ||
| 136 | + git_executable = "" | ||
| 137 | + if not git_executable: | ||
| 138 | + return None | ||
| 139 | + | ||
| 140 | + executable_dir = osp.dirname(git_executable) | ||
| 141 | + directory_name = osp.basename(executable_dir).lower() | ||
| 142 | + if directory_name == "cmd": | ||
| 143 | + # The normal system-wide PATH entry is <git-root>/cmd. | ||
| 144 | + return osp.dirname(executable_dir) | ||
| 145 | + if directory_name == "bin": | ||
| 146 | + prefix = osp.dirname(executable_dir) | ||
| 147 | + if osp.basename(prefix).lower() in _GIT_FOR_WINDOWS_PREFIXES: | ||
| 148 | + # Git Bash commonly exposes <git-root>/<platform>/bin/git.exe. | ||
| 149 | + return osp.dirname(prefix) | ||
| 150 | + if osp.basename(prefix).lower() != "usr": | ||
| 151 | + # An explicitly configured Git may be the root-level bin/git.exe. Do | ||
| 152 | + # not make the same inference from usr/bin: unlike the recognized | ||
| 153 | + # platform prefixes, "usr" has no reliably bounded parent layout. | ||
| 154 | + return prefix | ||
| 155 | + return None | ||
| 156 | + | ||
| 157 | + | ||
| 158 | + def _git_for_windows_bash() -> Union[str, None]: | ||
| 159 | + """Return Bash from the Git for Windows installation selected by GitPython.""" | ||
| 160 | + git_root = _git_for_windows_root() | ||
| 161 | + if git_root is None: | ||
| 162 | + return None | ||
| 163 | + | ||
| 164 | + # Match gix-path's precedence: prefer the lightweight bin shim, then the | ||
| 165 | + # underlying usr/bin executable. Both belong to the same installation as Git. | ||
| 166 | + for relative_path in ("bin/bash.exe", "usr/bin/bash.exe"): | ||
| 167 | + candidate = osp.join(git_root, *relative_path.split("/")) | ||
| 168 | + if osp.isfile(candidate) and os.access(candidate, os.X_OK): | ||
| 169 | + return candidate | ||
| 170 | + return None | ||
| 171 | + | ||
| 172 | + | ||
| 82 | 173 | def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: | |
| 83 | 174 | """Run the commit hook of the given name. Silently ignore hooks that do not exist. | |
| 84 | 175 | ||
@@ -112,7 +203,12 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: | |||
| 112 | 203 | # an absolute path in this form, although a relative path is preferable | |
| 113 | 204 | # because it also works with the Windows Subsystem for Linux wrapper. | |
| 114 | 205 | bash_hp = hp | |
| 115 | - cmd = ["bash.exe", Path(bash_hp).as_posix()] | ||
| 206 | + # Prefer Bash associated with GitPython's selected Git installation. If | ||
| 207 | + # that layout is not recognized, use an explicitly configured non-system | ||
| 208 | + # PATH entry. Preserve the bare fallback for installations that previously | ||
| 209 | + # relied on WSL or another CreateProcess-resolved Bash. | ||
| 210 | + bash_executable = _git_for_windows_bash() or _which_from_path("bash.exe") or "bash.exe" | ||
| 211 | + cmd = [bash_executable, Path(bash_hp).as_posix()] | ||
| 116 | 212 | ||
| 117 | 213 | process = safer_popen( | |
| 118 | 214 | cmd + list(args), | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,7 +33,7 @@ | |||
| 33 | 33 | UnmergedEntriesError, | |
| 34 | 34 | UnsafeOptionError, | |
| 35 | 35 | ) | |
| 36 | - from git.index.fun import hook_path, run_commit_hook | ||
| 36 | + from git.index.fun import _git_for_windows_bash, _which_from_path, hook_path, run_commit_hook | ||
| 37 | 37 | from git.index.typ import BaseIndexEntry, IndexEntry | |
| 38 | 38 | from git.index.util import TemporaryFileSwap | |
| 39 | 39 | from git.objects import Blob | |
@@ -1128,16 +1128,61 @@ def test_run_commit_hook_outside_worktree_on_windows(self, rw_dir): | |||
| 1128 | 1128 | repo = Repo.init(root / "repo") | |
| 1129 | 1129 | hooks_dir = root / "hooks" | |
| 1130 | 1130 | _make_hook(root, "fake-hook", "exit 0") | |
| 1131 | + system_root = root / "Windows" | ||
| 1132 | + system_bash = system_root / "System32" / "bash.exe" | ||
| 1133 | + git_executable = root / "Git" / "cmd" / "git.exe" | ||
| 1134 | + git_bash = root / "Git" / "bin" / "bash.exe" | ||
| 1135 | + for executable in (system_bash, git_executable, git_bash): | ||
| 1136 | + executable.parent.mkdir(parents=True) | ||
| 1137 | + executable.touch() | ||
| 1138 | + executable.chmod(0o755) | ||
| 1131 | 1139 | with repo.config_writer() as writer: | |
| 1132 | 1140 | writer.set_value("core", "hooksPath", str(hooks_dir)) | |
| 1133 | 1141 | ||
| 1134 | - with mock.patch("git.index.fun.sys.platform", "win32"): | ||
| 1142 | + # Model a normal Windows PATH: System32 (containing the WSL launcher) comes | ||
| 1143 | + # before Git's cmd directory, while Git's Bash is not itself on PATH. This | ||
| 1144 | + # exercises both Git-installation discovery and shell selection without | ||
| 1145 | + # mocking either resolver's answer. | ||
| 1146 | + with mock.patch("git.index.fun.sys.platform", "win32"), mock.patch.object( | ||
| 1147 | + Git, "GIT_PYTHON_GIT_EXECUTABLE", "git" | ||
| 1148 | + ), mock.patch.dict(os.environ, {"SystemRoot": str(system_root)}), mock.patch( | ||
| 1149 | + "git.index.fun.os.get_exec_path", return_value=["", str(system_bash.parent), str(git_executable.parent)] | ||
| 1150 | + ): | ||
| 1135 | 1151 | with mock.patch("git.index.fun.safer_popen") as popen, mock.patch("git.index.fun.handle_process_output"): | |
| 1136 | 1152 | popen.return_value.returncode = 0 | |
| 1137 | 1153 | run_commit_hook("fake-hook", repo.index) | |
| 1138 | 1154 | ||
| 1139 | 1155 | command = popen.call_args[0][0] | |
| 1140 | - self.assertEqual(command, ["bash.exe", "../hooks/fake-hook"]) | ||
| 1156 | + self.assertEqual(command, [str(git_bash), "../hooks/fake-hook"]) | ||
| 1157 | + | ||
| 1158 | + @with_rw_directory | ||
| 1159 | + def test_windows_bash_lookup_respects_explicit_current_directory_in_path(self, rw_dir): | ||
| 1160 | + root = Path(rw_dir).resolve() | ||
| 1161 | + bash = root / "bash.exe" | ||
| 1162 | + bash.touch() | ||
| 1163 | + bash.chmod(0o755) | ||
| 1164 | + | ||
| 1165 | + # An explicitly listed directory is trusted PATH configuration, even when | ||
| 1166 | + # it happens to be the current directory. This differs from an empty entry, | ||
| 1167 | + # which Windows requires PATH lookup to ignore. | ||
| 1168 | + with cwd(root), mock.patch("git.index.fun.os.get_exec_path", return_value=[str(root)]): | ||
| 1169 | + self.assertEqual(_which_from_path("bash.exe"), str(bash)) | ||
| 1170 | + | ||
| 1171 | + @with_rw_directory | ||
| 1172 | + def test_windows_bash_lookup_from_explicit_git_bin(self, rw_dir): | ||
| 1173 | + git_root = Path(rw_dir).resolve() / "Git" | ||
| 1174 | + git_executable = git_root / "bin" / "git.exe" | ||
| 1175 | + bash = git_root / "bin" / "bash.exe" | ||
| 1176 | + git_executable.parent.mkdir(parents=True) | ||
| 1177 | + for executable in (git_executable, bash): | ||
| 1178 | + executable.touch() | ||
| 1179 | + executable.chmod(0o755) | ||
| 1180 | + | ||
| 1181 | + # A relative executable containing a directory is resolved by CreateProcess | ||
| 1182 | + # from the parent process cwd, not the separately supplied child cwd. | ||
| 1183 | + relative_git = osp.relpath(git_executable, os.curdir) | ||
| 1184 | + with mock.patch.object(Git, "GIT_PYTHON_GIT_EXECUTABLE", relative_git): | ||
| 1185 | + self.assertEqual(_git_for_windows_bash(), str(bash)) | ||
| 1141 | 1186 | ||
| 1142 | 1187 | @ddt.data((False,), (True,)) | |
| 1143 | 1188 | @with_rw_directory | |
| Back | FazBrowse Home | New Git URL |
0 commit comments