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
34 changes: 34 additions & 0 deletions graphify/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,37 @@ def _json_text(data: dict) -> str:
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"


def _stabilize_rebuild_cwd(watch_path: Path) -> bool:
"""Ensure relative rebuild paths have a usable CWD before queue/lock setup.

Detached git hooks can inherit a transient working directory that is deleted
before the background rebuild starts. In that state Path.cwd(),
Path('.').resolve(), and relative graphify-out mkdirs raise FileNotFoundError
before the normal rebuild error handling can run. Hooks that know the repo
root export GRAPHIFY_REPO_ROOT so the rebuild can recover by chdir'ing there.
"""
if watch_path.is_absolute():
return True

repo_root = os.environ.get("GRAPHIFY_REPO_ROOT", "").strip()
if repo_root and Path(repo_root).is_dir():
try:
os.chdir(repo_root)
return True
except OSError:
pass

try:
Path.cwd()
return True
except FileNotFoundError:
print(
"[graphify watch] Rebuild failed: current working directory "
"no longer exists and GRAPHIFY_REPO_ROOT is not set."
)
return False


def _rebuild_code(
watch_path: Path,
*,
Expand Down Expand Up @@ -690,6 +721,9 @@ def _rebuild_code(

Returns True on success, False on error or skipped-due-to-lock.
"""
if not _stabilize_rebuild_cwd(watch_path):
return False

out = watch_path / _GRAPHIFY_OUT
if acquire_lock:
# #1059: incremental (changed_paths is not None) hooks must not drop
Expand Down
50 changes: 50 additions & 0 deletions tests/test_watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,56 @@ def test_graphify_root_preserves_absolute_when_user_supplied(tmp_path):
)


def test_rebuild_code_deleted_cwd_without_repo_root_returns_false(tmp_path, monkeypatch, capsys):
"""Detached hooks can inherit a CWD that no longer exists.

Without GRAPHIFY_REPO_ROOT, the rebuild should fail cleanly before creating
relative graphify-out queue/lock files.
"""
from graphify.watch import _rebuild_code

old_cwd = Path.cwd()
gone = tmp_path / "gone"
gone.mkdir()
monkeypatch.delenv("GRAPHIFY_REPO_ROOT", raising=False)

os.chdir(gone)
gone.rmdir()
try:
assert _rebuild_code(Path("."), changed_paths=[Path("lib.py")]) is False
finally:
os.chdir(old_cwd)

out = capsys.readouterr().out
assert "current working directory no longer exists" in out


def test_rebuild_code_deleted_cwd_uses_graphify_repo_root(tmp_path, monkeypatch):
"""GRAPHIFY_REPO_ROOT lets detached hook rebuilds recover from a deleted CWD."""
from graphify.watch import _rebuild_code

old_cwd = Path.cwd()
corpus = tmp_path / "corpus"
corpus.mkdir()
(corpus / "lib.py").write_text("def f(): pass\n", encoding="utf-8")
gone = tmp_path / "gone"
gone.mkdir()
monkeypatch.setenv("GRAPHIFY_REPO_ROOT", str(corpus))

os.chdir(gone)
gone.rmdir()
try:
assert _rebuild_code(
Path("."),
changed_paths=[Path("lib.py")],
no_cluster=True,
) is True
assert Path.cwd().resolve() == corpus.resolve()
assert (corpus / "graphify-out" / "graph.json").exists()
finally:
os.chdir(old_cwd)


def test_rebuild_code_evicts_nodes_from_deleted_files(tmp_path):
"""#1007: graphify update (_rebuild_code with no changed_paths) must remove
nodes and edges from files deleted since the last run."""
Expand Down