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
22 changes: 16 additions & 6 deletions graphify/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,18 @@ def _bail():
_watchdog.daemon = True
_watchdog.start()
_force = os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes')
_root = Path('.')
_worktree_root = Path.cwd().resolve()
_root = _worktree_root
_out = os.environ.get('GRAPHIFY_OUT', 'graphify-out')
_saved = Path(_out) / '.graphify_root'
if _saved.exists():
try:
_txt = _saved.read_text(encoding='utf-8-sig').strip()
if _txt:
_root = Path(_txt)
_saved_root = Path(_txt).resolve()
_saved_root.relative_to(_worktree_root)
_root = _saved_root
except (OSError, RuntimeError, ValueError):
pass
_rebuild_code(_root, changed_paths=changed, force=_force)
# Refresh the work-memory lessons doc when saved Q&A outcomes exist
# (best-effort; never fails the hook).
Expand Down Expand Up @@ -210,13 +215,18 @@ def _bail():
# post-checkout: branch switch can touch arbitrary files; full rebuild path
# (no changed_paths) is correct here. The flock inside _rebuild_code still
# prevents pile-ups when commit + checkout fire back-to-back.
_root = Path('.')
_worktree_root = Path.cwd().resolve()
_root = _worktree_root
_out = os.environ.get('GRAPHIFY_OUT', 'graphify-out')
_saved = Path(_out) / '.graphify_root'
if _saved.exists():
try:
_txt = _saved.read_text(encoding='utf-8-sig').strip()
if _txt:
_root = Path(_txt)
_saved_root = Path(_txt).resolve()
_saved_root.relative_to(_worktree_root)
_root = _saved_root
except (OSError, RuntimeError, ValueError):
pass
_rebuild_code(_root, force=_force)
# Refresh the work-memory lessons doc when saved Q&A outcomes exist
# (best-effort; never fails the hook).
Expand Down
69 changes: 68 additions & 1 deletion tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import os
import shutil
import subprocess
from types import SimpleNamespace
import sys
from types import ModuleType, SimpleNamespace
from pathlib import Path
import pytest
from graphify.hooks import install, uninstall, status, _hooks_dir, _HOOK_MARKER, _CHECKOUT_MARKER
Expand Down Expand Up @@ -332,6 +333,72 @@ def test_rebuild_bodies_with_graphify_root_are_valid_python():
ast.parse(body)


@pytest.mark.parametrize(
"name,body",
[("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)],
)
@pytest.mark.parametrize("marker_kind", ("external", "empty", "malformed"))
def test_rebuild_bodies_anchor_untrusted_roots_to_worktree(
tmp_path, monkeypatch, capsys, name, body, marker_kind,
):
"""Automatic hooks must never re-anchor a rebuild beyond their worktree."""
repo = tmp_path / "repo"
repo.mkdir()
out = repo / "graphify-out"
out.mkdir()
external = tmp_path / "external"
external.mkdir()
marker = out / ".graphify_root"
marker.write_text(
str(external) if marker_kind == "external" else "\0" if marker_kind == "malformed" else "",
encoding="utf-8",
)
rebuilt: list[Path] = []
watch = ModuleType("graphify.watch")
watch._apply_resource_limits = lambda: None
watch._rebuild_code = lambda root, **kwargs: rebuilt.append(root)
monkeypatch.setitem(sys.modules, "graphify.watch", watch)
monkeypatch.chdir(repo)
monkeypatch.delenv("GRAPHIFY_OUT", raising=False)
monkeypatch.setenv("GRAPHIFY_REBUILD_TIMEOUT", "0")
if name == "post-commit":
monkeypatch.setenv("GRAPHIFY_CHANGED", "src/module.py")

exec(body)

assert rebuilt == [repo.resolve()]
captured = capsys.readouterr()
assert str(external) not in captured.out + captured.err


@pytest.mark.parametrize(
"name,body",
[("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)],
)
def test_rebuild_bodies_accept_saved_subdirectory_roots(tmp_path, monkeypatch, name, body):
"""A valid saved subdirectory scan must remain available to automatic hooks."""
repo = tmp_path / "repo"
source = repo / "src"
source.mkdir(parents=True)
out = repo / "graphify-out"
out.mkdir()
(out / ".graphify_root").write_text(str(source), encoding="utf-8")
rebuilt: list[Path] = []
watch = ModuleType("graphify.watch")
watch._apply_resource_limits = lambda: None
watch._rebuild_code = lambda root, **kwargs: rebuilt.append(root)
monkeypatch.setitem(sys.modules, "graphify.watch", watch)
monkeypatch.chdir(repo)
monkeypatch.delenv("GRAPHIFY_OUT", raising=False)
monkeypatch.setenv("GRAPHIFY_REBUILD_TIMEOUT", "0")
if name == "post-commit":
monkeypatch.setenv("GRAPHIFY_CHANGED", "src/module.py")

exec(body)

assert rebuilt == [source.resolve()]


@pytest.mark.parametrize(
"name,body",
[("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)],
Expand Down