Skip to content
Open
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
13 changes: 12 additions & 1 deletion git/index/fun.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,19 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
Arguments passed to hook file.

:raise git.exc.HookExecutionError:

:note:
Respects the ``core.hooksPath`` git configuration option. When set, hooks are
resolved relative to that path instead of the default ``.git/hooks`` directory.
"""
hp = hook_path(name, index.repo.git_dir)
hooks_path = index.repo.config_reader().get("core", "hooksPath", fallback=None)
if hooks_path is not None:
hp = osp.join(hooks_path, name)
if not osp.isabs(hooks_path):
# Relative hooksPath is resolved against the working tree root.
hp = osp.join(index.repo.working_dir, hp)
else:
hp = hook_path(name, index.repo.git_dir)
if not os.access(hp, os.X_OK):
return

Expand Down
89 changes: 89 additions & 0 deletions test/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,95 @@ def test_commit_msg_hook_fail(self, rw_repo):
else:
raise AssertionError("Should have caught a HookExecutionError")

# ---- core.hooksPath tests ----

@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", bare=True)
def test_run_commit_hook_respects_absolute_hooks_path(self, rw_repo):
"""Hooks in a custom absolute core.hooksPath are found and executed."""
index = rw_repo.index
custom_hooks_dir = osp.join(rw_repo.working_dir, "custom-hooks")
os.makedirs(custom_hooks_dir, exist_ok=True)
hp = osp.join(custom_hooks_dir, "fake-hook")
# Use a Python script so it works on all platforms (no bash required)
hook_script = "#!" + sys.executable + "\nimport sys; open('output.txt', 'w').write('ran custom hook\\n')\n"
with open(hp, "w", encoding="utf-8") as f:
f.write(hook_script)
os.chmod(hp, 0o755)

rw_repo.config_writer().set_value("core", "hooksPath", custom_hooks_dir).release()
run_commit_hook("fake-hook", index)
output = Path(rw_repo.working_dir, "output.txt").read_text(encoding="utf-8")
self.assertEqual(output, "ran custom hook\n")

@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", bare=True)
def test_run_commit_hook_respects_relative_hooks_path(self, rw_repo):
"""A relative core.hooksPath is resolved against the working tree root."""
index = rw_repo.index
custom_hooks_dir = osp.join(rw_repo.working_dir, "hooks-relative")
os.makedirs(custom_hooks_dir, exist_ok=True)
hp = osp.join(custom_hooks_dir, "fake-hook")
# Use a Python script so it works on all platforms (no bash required)
hook_script = "#!" + sys.executable + "\nimport sys; open('output.txt', 'w').write('ran relative hook\\n')\n"
with open(hp, "w", encoding="utf-8") as f:
f.write(hook_script)
os.chmod(hp, 0o755)

rw_repo.config_writer().set_value("core", "hooksPath", "hooks-relative").release()
run_commit_hook("fake-hook", index)
output = Path(rw_repo.working_dir, "output.txt").read_text(encoding="utf-8")
self.assertEqual(output, "ran relative hook\n")

@with_rw_repo("HEAD", bare=True)
def test_run_commit_hook_falls_back_to_default_without_hooks_path(self, rw_repo):
"""When core.hooksPath is not set, the default .git/hooks path is used."""
index = rw_repo.index
_make_hook(index.repo.git_dir, "fake-hook", "echo 'ran default 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 default hook\n")

@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", bare=True)
def test_run_commit_hook_hooks_path_skips_missing_hook(self, rw_repo):
"""If a hook does not exist in core.hooksPath, no error is raised."""
index = rw_repo.index
custom_hooks_dir = osp.join(rw_repo.working_dir, "empty-hooks")
os.makedirs(custom_hooks_dir, exist_ok=True)
rw_repo.config_writer().set_value("core", "hooksPath", custom_hooks_dir).release()
# Should not raise -- the hook doesn't exist, so run_commit_hook returns silently
run_commit_hook("nonexistent-hook", index)

# ---- end core.hooksPath tests ----

@with_rw_repo("HEAD")
def test_index_add_pathlib(self, rw_repo):
git_dir = Path(rw_repo.git_dir)
Expand Down
Loading