Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions git/index/fun.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ def hook_path(name: str, git_dir: PathLike) -> str:
return osp.join(git_dir, "hooks", name)


def _commit_hook_path(name: str, index: "IndexFile") -> str:
""":return: path to the named commit hook, respecting Git's core.hooksPath."""
with index.repo.config_reader() as config:
hooks_dir = config.get("core", "hooksPath", fallback="")

if not hooks_dir:
return hook_path(name, index.repo.git_dir)

return osp.abspath(osp.join(index.repo.working_dir, osp.expanduser(hooks_dir), name))


def _has_file_extension(path: str) -> str:
return osp.splitext(path)[1]

Expand All @@ -82,7 +93,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:

:raise git.exc.HookExecutionError:
"""
hp = hook_path(name, index.repo.git_dir)
hp = _commit_hook_path(name, index)
if not os.access(hp, os.X_OK):
return
Comment thread
Byron marked this conversation as resolved.

Expand All @@ -94,8 +105,14 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
if sys.platform == "win32" and not _has_file_extension(hp):
# Windows only uses extensions to determine how to open files
# (doesn't understand shebangs). Try using bash to run the hook.
relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
cmd = ["bash.exe", relative_hp]
try:
bash_hp = osp.relpath(hp, index.repo.working_dir)
except ValueError:
# Different drives have no relative path on Windows. Git Bash accepts
# 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()]

process = safer_popen(
cmd + list(args),
Expand Down
58 changes: 55 additions & 3 deletions test/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,9 +1099,35 @@ class Mocked:
def test_run_commit_hook(self, rw_repo):
index = rw_repo.index
_make_hook(index.repo.git_dir, "fake-hook", "echo 'ran fake hook' >output.txt")
run_commit_hook("fake-hook", index)
output = Path(rw_repo.git_dir, "output.txt").read_text(encoding="utf-8")
self.assertEqual(output, "ran fake hook\n")
output = Path(rw_repo.git_dir, "output.txt")
with mock.patch.object(Repo, "config_level", ("repository",)):
with mock.patch.object(Git, "execute", side_effect=AssertionError("hook lookup must not run git")):
run_commit_hook("fake-hook", index)
self.assertEqual(output.read_text(encoding="utf-8"), "ran fake hook\n")

output.unlink()
with index.repo.config_writer() as writer:
writer.set_value("core", "hooksPath", "")
run_commit_hook("fake-hook", index)

self.assertEqual(output.read_text(encoding="utf-8"), "ran fake hook\n")

@with_rw_directory
def test_run_commit_hook_outside_worktree_on_windows(self, rw_dir):
root = Path(rw_dir).resolve()
repo = Repo.init(root / "repo")
hooks_dir = root / "hooks"
_make_hook(root, "fake-hook", "exit 0")
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.safer_popen") as popen, mock.patch("git.index.fun.handle_process_output"):
popen.return_value.returncode = 0
run_commit_hook("fake-hook", repo.index)

command = popen.call_args[0][0]
self.assertEqual(command, ["bash.exe", "../hooks/fake-hook"])

@ddt.data((False,), (True,))
@with_rw_directory
Expand Down Expand Up @@ -1160,6 +1186,32 @@ def test_pre_commit_hook_success(self, rw_repo):
_make_hook(index.repo.git_dir, "pre-commit", "exit 0")
index.commit("This should not fail")

@pytest.mark.xfail(
type(_win_bash_status) is WinBashStatus.Absent,
reason="Can't run a hook on Windows without bash.exe.",
raises=HookExecutionError,
)
@pytest.mark.xfail(
type(_win_bash_status) is WinBashStatus.WslNoDistro,
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
raises=HookExecutionError,
)
@with_rw_repo("HEAD")
def test_pre_commit_hook_respects_core_hooks_path(self, rw_repo):
index = rw_repo.index
hooks_dir = Path(index.repo.working_dir, "custom-hooks")
hooks_dir.mkdir()
hp = hooks_dir / "pre-commit"
hp.write_text(HOOKS_SHEBANG + "echo 'ran custom hook' >custom-hook-output.txt", encoding="utf-8")
os.chmod(hp, 0o744)

with index.repo.config_writer() as writer:
writer.set_value("core", "hooksPath", "custom-hooks")

index.commit("This should run the custom hook")
output = Path(rw_repo.working_dir, "custom-hook-output.txt").read_text(encoding="utf-8")
self.assertEqual(output, "ran custom hook\n")

@pytest.mark.xfail(
type(_win_bash_status) is WinBashStatus.WslNoDistro,
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
Expand Down
Loading