Files
Koushik-Salammagari e1947826fc fix(hooks): expand ~ in core.hooksPath before resolving install target (#554)
When a user configures core.hooksPath = ~/gitconfig/hooks in .gitconfig,
_hooks_dir() was constructing Path("~/gitconfig/hooks") without calling
expanduser(), so hooks were installed into <repo>/~/gitconfig/hooks instead
of the intended absolute path.

Add Path.expanduser() call immediately after reading the raw string from
git config, before the is_absolute() / root-relative fallback logic.

Fixes #547
2026-05-02 14:14:46 +01:00

153 lines
4.5 KiB
Python

"""Tests for hooks.py - git hook install/uninstall."""
import os
import subprocess
from pathlib import Path
import pytest
from graphify.hooks import install, uninstall, status, _HOOK_MARKER, _CHECKOUT_MARKER
def _make_git_repo(tmp_path: Path) -> Path:
subprocess.run(["git", "init", str(tmp_path)], check=True, capture_output=True)
return tmp_path
def test_install_creates_hook(tmp_path):
repo = _make_git_repo(tmp_path)
result = install(repo)
hook = repo / ".git" / "hooks" / "post-commit"
assert hook.exists()
assert _HOOK_MARKER in hook.read_text()
assert "installed" in result
def test_install_is_executable(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
hook = repo / ".git" / "hooks" / "post-commit"
if os.name == "nt":
assert hook.read_text(encoding="utf-8").startswith("#!/bin/sh\n")
else:
assert hook.stat().st_mode & 0o111 # executable bit set
def test_install_idempotent(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
result = install(repo)
assert "already installed" in result
# marker appears only once
hook = repo / ".git" / "hooks" / "post-commit"
assert hook.read_text().count(_HOOK_MARKER) == 1
def test_install_appends_to_existing_hook(tmp_path):
repo = _make_git_repo(tmp_path)
hook = repo / ".git" / "hooks" / "post-commit"
hook.write_text("#!/bin/bash\necho existing\n")
hook.chmod(0o755)
install(repo)
content = hook.read_text()
assert "existing" in content
assert _HOOK_MARKER in content
def test_uninstall_removes_hook(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
result = uninstall(repo)
hook = repo / ".git" / "hooks" / "post-commit"
assert not hook.exists()
assert "removed" in result.lower()
def test_uninstall_no_hook(tmp_path):
repo = _make_git_repo(tmp_path)
result = uninstall(repo)
assert "nothing to remove" in result
def test_status_installed(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
result = status(repo)
assert "installed" in result
def test_status_not_installed(tmp_path):
repo = _make_git_repo(tmp_path)
result = status(repo)
assert "not installed" in result
def test_no_git_repo_raises(tmp_path):
with pytest.raises(RuntimeError, match="No git repository"):
install(tmp_path / "not_a_repo")
def test_install_creates_post_checkout_hook(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
hook = repo / ".git" / "hooks" / "post-checkout"
assert hook.exists()
assert _CHECKOUT_MARKER in hook.read_text()
def test_install_post_checkout_is_executable(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
hook = repo / ".git" / "hooks" / "post-checkout"
if os.name == "nt":
assert hook.read_text(encoding="utf-8").startswith("#!/bin/sh\n")
else:
assert hook.stat().st_mode & 0o111
def test_uninstall_removes_post_checkout_hook(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
uninstall(repo)
hook = repo / ".git" / "hooks" / "post-checkout"
assert not hook.exists()
def test_status_shows_both_hooks(tmp_path):
repo = _make_git_repo(tmp_path)
install(repo)
result = status(repo)
assert "post-commit" in result
assert "post-checkout" in result
assert result.count("installed") >= 2
def test_hook_skips_head_on_exe():
"""Hook script must skip shebang extraction for .exe binaries (Windows)."""
from graphify.hooks import _PYTHON_DETECT
assert "*.exe) _SHEBANG=" in _PYTHON_DETECT or '*.exe)' in _PYTHON_DETECT
def test_hooks_dir_expands_tilde(tmp_path, monkeypatch):
"""_hooks_dir must call expanduser() so ~/... paths in core.hooksPath resolve correctly."""
from graphify.hooks import _hooks_dir
import types
repo = _make_git_repo(tmp_path)
custom_hooks = tmp_path / "custom_hooks"
custom_hooks.mkdir()
# expanduser() reads $HOME, so override it to make ~/custom_hooks predictable
monkeypatch.setenv("HOME", str(tmp_path))
real_run = subprocess.run
def fake_run(cmd, **kwargs):
if isinstance(cmd, list) and "config" in cmd and "core.hooksPath" in cmd:
return types.SimpleNamespace(returncode=0, stdout="~/custom_hooks")
return real_run(cmd, **kwargs)
monkeypatch.setattr(subprocess, "run", fake_run)
resolved = _hooks_dir(repo)
assert resolved == custom_hooks, (
f"expected {custom_hooks}, got {resolved} — tilde was not expanded"
)