mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-14 19:37:12 +00:00
e1947826fc
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
153 lines
4.5 KiB
Python
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"
|
|
)
|