Skip to content

Commit 354eb2f

Browse files
authored
Merge pull request #2159 from Siesta0217/fix-core-hooks-path-commit-hooks
Fix commit hooks respecting core.hooksPath
2 parents 5bc2560 + 9bc287a commit 354eb2f

2 files changed

Lines changed: 75 additions & 6 deletions

File tree

git/index/fun.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ def hook_path(name: str, git_dir: PathLike) -> str:
6464
return osp.join(git_dir, "hooks", name)
6565

6666

67+
def _commit_hook_path(name: str, index: "IndexFile") -> str:
68+
""":return: path to the named commit hook, respecting Git's core.hooksPath."""
69+
with index.repo.config_reader() as config:
70+
hooks_dir = config.get("core", "hooksPath", fallback="")
71+
72+
if not hooks_dir:
73+
return hook_path(name, index.repo.git_dir)
74+
75+
return osp.abspath(osp.join(index.repo.working_dir, osp.expanduser(hooks_dir), name))
76+
77+
6778
def _has_file_extension(path: str) -> str:
6879
return osp.splitext(path)[1]
6980

@@ -82,7 +93,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
8293
8394
:raise git.exc.HookExecutionError:
8495
"""
85-
hp = hook_path(name, index.repo.git_dir)
96+
hp = _commit_hook_path(name, index)
8697
if not os.access(hp, os.X_OK):
8798
return
8899

@@ -94,8 +105,14 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
94105
if sys.platform == "win32" and not _has_file_extension(hp):
95106
# Windows only uses extensions to determine how to open files
96107
# (doesn't understand shebangs). Try using bash to run the hook.
97-
relative_hp = Path(hp).relative_to(index.repo.working_dir).as_posix()
98-
cmd = ["bash.exe", relative_hp]
108+
try:
109+
bash_hp = osp.relpath(hp, index.repo.working_dir)
110+
except ValueError:
111+
# Different drives have no relative path on Windows. Git Bash accepts
112+
# an absolute path in this form, although a relative path is preferable
113+
# because it also works with the Windows Subsystem for Linux wrapper.
114+
bash_hp = hp
115+
cmd = ["bash.exe", Path(bash_hp).as_posix()]
99116

100117
process = safer_popen(
101118
cmd + list(args),

test/test_index.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,9 +1099,35 @@ class Mocked:
10991099
def test_run_commit_hook(self, rw_repo):
11001100
index = rw_repo.index
11011101
_make_hook(index.repo.git_dir, "fake-hook", "echo 'ran fake hook' >output.txt")
1102-
run_commit_hook("fake-hook", index)
1103-
output = Path(rw_repo.git_dir, "output.txt").read_text(encoding="utf-8")
1104-
self.assertEqual(output, "ran fake hook\n")
1102+
output = Path(rw_repo.git_dir, "output.txt")
1103+
with mock.patch.object(Repo, "config_level", ("repository",)):
1104+
with mock.patch.object(Git, "execute", side_effect=AssertionError("hook lookup must not run git")):
1105+
run_commit_hook("fake-hook", index)
1106+
self.assertEqual(output.read_text(encoding="utf-8"), "ran fake hook\n")
1107+
1108+
output.unlink()
1109+
with index.repo.config_writer() as writer:
1110+
writer.set_value("core", "hooksPath", "")
1111+
run_commit_hook("fake-hook", index)
1112+
1113+
self.assertEqual(output.read_text(encoding="utf-8"), "ran fake hook\n")
1114+
1115+
@with_rw_directory
1116+
def test_run_commit_hook_outside_worktree_on_windows(self, rw_dir):
1117+
root = Path(rw_dir).resolve()
1118+
repo = Repo.init(root / "repo")
1119+
hooks_dir = root / "hooks"
1120+
_make_hook(root, "fake-hook", "exit 0")
1121+
with repo.config_writer() as writer:
1122+
writer.set_value("core", "hooksPath", str(hooks_dir))
1123+
1124+
with mock.patch("git.index.fun.sys.platform", "win32"):
1125+
with mock.patch("git.index.fun.safer_popen") as popen, mock.patch("git.index.fun.handle_process_output"):
1126+
popen.return_value.returncode = 0
1127+
run_commit_hook("fake-hook", repo.index)
1128+
1129+
command = popen.call_args[0][0]
1130+
self.assertEqual(command, ["bash.exe", "../hooks/fake-hook"])
11051131

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

1189+
@pytest.mark.xfail(
1190+
type(_win_bash_status) is WinBashStatus.Absent,
1191+
reason="Can't run a hook on Windows without bash.exe.",
1192+
raises=HookExecutionError,
1193+
)
1194+
@pytest.mark.xfail(
1195+
type(_win_bash_status) is WinBashStatus.WslNoDistro,
1196+
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",
1197+
raises=HookExecutionError,
1198+
)
1199+
@with_rw_repo("HEAD")
1200+
def test_pre_commit_hook_respects_core_hooks_path(self, rw_repo):
1201+
index = rw_repo.index
1202+
hooks_dir = Path(index.repo.working_dir, "custom-hooks")
1203+
hooks_dir.mkdir()
1204+
hp = hooks_dir / "pre-commit"
1205+
hp.write_text(HOOKS_SHEBANG + "echo 'ran custom hook' >custom-hook-output.txt", encoding="utf-8")
1206+
os.chmod(hp, 0o744)
1207+
1208+
with index.repo.config_writer() as writer:
1209+
writer.set_value("core", "hooksPath", "custom-hooks")
1210+
1211+
index.commit("This should run the custom hook")
1212+
output = Path(rw_repo.working_dir, "custom-hook-output.txt").read_text(encoding="utf-8")
1213+
self.assertEqual(output, "ran custom hook\n")
1214+
11631215
@pytest.mark.xfail(
11641216
type(_win_bash_status) is WinBashStatus.WslNoDistro,
11651217
reason="Currently uses the bash.exe of WSL, even with no WSL distro installed",

0 commit comments

Comments
 (0)