diff --git a/git/index/fun.py b/git/index/fun.py index 5d52486f9..7bd2e4bf6 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -79,6 +79,21 @@ def _has_file_extension(path: str) -> str: return osp.splitext(path)[1] +def _which_from_path(command: str) -> Union[str, None]: + """Resolve an executable 'command' from PATH without considering the current directory.""" + cwd = osp.normcase(osp.abspath(os.curdir)) + for directory in os.get_exec_path(): + if not directory: + continue + directory = osp.abspath(directory) + if osp.normcase(directory) == cwd: + continue + candidate = osp.join(directory, command) + if osp.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return None + + def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: """Run the commit hook of the given name. Silently ignore hooks that do not exist. @@ -112,7 +127,11 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: # an absolute path in this form, although a relative path is preferable # because it also works with the Windows Subsystem for Linux wrapper. bash_hp = hp - cmd = ["bash.exe", Path(bash_hp).as_posix()] + # Resolve through PATH before spawning. On Windows, CreateProcess searches + # the current and system directories before PATH for a bare executable name, + # which can otherwise select an impostor or the WSL launcher instead of Git + # for Windows' Bash. + cmd = [_which_from_path("bash.exe") or "bash.exe", Path(bash_hp).as_posix()] process = safer_popen( cmd + list(args), diff --git a/test/test_index.py b/test/test_index.py index 3ad5a457f..d902bf58d 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -1128,16 +1128,20 @@ def test_run_commit_hook_outside_worktree_on_windows(self, rw_dir): repo = Repo.init(root / "repo") hooks_dir = root / "hooks" _make_hook(root, "fake-hook", "exit 0") + bash = root / "git" / "bin" / "bash.exe" with repo.config_writer() as writer: writer.set_value("core", "hooksPath", str(hooks_dir)) - with mock.patch("git.index.fun.sys.platform", "win32"): + with mock.patch("git.index.fun.sys.platform", "win32"), mock.patch( + "git.index.fun._which_from_path", return_value=str(bash) + ) as which: with mock.patch("git.index.fun.safer_popen") as popen, mock.patch("git.index.fun.handle_process_output"): popen.return_value.returncode = 0 run_commit_hook("fake-hook", repo.index) + which.assert_called_once_with("bash.exe") command = popen.call_args[0][0] - self.assertEqual(command, ["bash.exe", "../hooks/fake-hook"]) + self.assertEqual(command, [str(bash), "../hooks/fake-hook"]) @ddt.data((False,), (True,)) @with_rw_directory