|
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,99 @@ 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 | + if osp.basename(git_executable).lower() not in ("git", "git.exe"): |
| 140 | + return None |
| 141 | + |
| 142 | + executable_dir = osp.dirname(git_executable) |
| 143 | + directory_name = osp.basename(executable_dir).lower() |
| 144 | + if directory_name == "cmd": |
| 145 | + # The normal system-wide PATH entry is <git-root>/cmd. |
| 146 | + return osp.dirname(executable_dir) |
| 147 | + if directory_name == "bin": |
| 148 | + prefix = osp.dirname(executable_dir) |
| 149 | + if osp.basename(prefix).lower() in _GIT_FOR_WINDOWS_PREFIXES: |
| 150 | + # Git Bash commonly exposes <git-root>/<platform>/bin/git.exe. |
| 151 | + return osp.dirname(prefix) |
| 152 | + if osp.basename(prefix).lower() != "usr": |
| 153 | + # An explicitly configured Git may be the root-level bin/git.exe. Do |
| 154 | + # not make the same inference from usr/bin: unlike the recognized |
| 155 | + # platform prefixes, "usr" has no reliably bounded parent layout. |
| 156 | + return prefix |
| 157 | + return None |
| 158 | + |
| 159 | + |
| 160 | +def _git_for_windows_bash() -> Union[str, None]: |
| 161 | + """Return Bash from the Git for Windows installation selected by GitPython.""" |
| 162 | + git_root = _git_for_windows_root() |
| 163 | + if git_root is None: |
| 164 | + return None |
| 165 | + |
| 166 | + # Match gix-path's precedence: prefer the lightweight bin shim, then the |
| 167 | + # underlying usr/bin executable. Both belong to the same installation as Git. |
| 168 | + for relative_path in ("bin/bash.exe", "usr/bin/bash.exe"): |
| 169 | + candidate = osp.join(git_root, *relative_path.split("/")) |
| 170 | + if osp.isfile(candidate) and os.access(candidate, os.X_OK): |
| 171 | + return candidate |
| 172 | + return None |
| 173 | + |
| 174 | + |
82 | 175 | def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: |
83 | 176 | """Run the commit hook of the given name. Silently ignore hooks that do not exist. |
84 | 177 |
|
@@ -112,7 +205,12 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: |
112 | 205 | # an absolute path in this form, although a relative path is preferable |
113 | 206 | # because it also works with the Windows Subsystem for Linux wrapper. |
114 | 207 | bash_hp = hp |
115 | | - cmd = ["bash.exe", Path(bash_hp).as_posix()] |
| 208 | + # Prefer Bash associated with GitPython's selected Git installation. If |
| 209 | + # that layout is not recognized, use an explicitly configured non-system |
| 210 | + # PATH entry. Preserve the bare fallback for installations that previously |
| 211 | + # relied on WSL or another CreateProcess-resolved Bash. |
| 212 | + bash_executable = _git_for_windows_bash() or _which_from_path("bash.exe") or "bash.exe" |
| 213 | + cmd = [bash_executable, Path(bash_hp).as_posix()] |
116 | 214 |
|
117 | 215 | process = safer_popen( |
118 | 216 | cmd + list(args), |
|
0 commit comments