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
49 changes: 48 additions & 1 deletion graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,54 @@


def _default_graph_path() -> str:
return str(Path(_GRAPHIFY_OUT) / "graph.json")
"""Default graph.json for the read commands (query/explain/path/affected).

Normally ``<GRAPHIFY_OUT>/graph.json`` relative to CWD. When run from a linked
git worktree that has no graph of its own, fall back to the primary checkout's
graph — worktrees share one build and agent workflows increasingly run each
task in its own worktree (#2008). An explicit ``--graph`` still overrides this,
and writers (extract/build) resolve through graphify.paths, so a write is never
redirected.
"""
local = Path(_GRAPHIFY_OUT) / "graph.json"
if local.is_file():
return str(local)
shared = _worktree_primary_graph()
if shared is not None:
return shared
return str(local)


def _worktree_primary_graph() -> str | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice helper function. Would it be worth adding a short docstring explaining why graph.json is the default output path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks !!! Actually both new functions already carry docstrings, If I mention specifically _default_graph_path (lines 85–93) and _worktree_primary_graph (lines 104–113). _default_graph_path explains why <GRAPHIFY_OUT>/graph.json is the default and how the worktree fallback kicks in, and _worktree_primary_graph documents the git-dir vs. git-common-dir check and every case where it returns None. Btw I am happy to extend further if any part seems less explanatory.

"""The primary checkout's ``graph.json`` when CWD is inside a linked git
worktree, else ``None``.

A linked worktree has ``--git-dir`` != the shared ``--git-common-dir`` (whose
parent is the primary checkout) — the same absolute-path compare the commit and
post-checkout hooks use to skip worktrees (#1809). Returns ``None`` when git is
unavailable, we are not in a linked worktree, ``GRAPHIFY_OUT`` is absolute (an
absolute override already points every worktree at one graph), or the primary
checkout has no graph yet.
"""
if os.path.isabs(_GRAPHIFY_OUT):
return None
import subprocess
try:
git_dir = subprocess.run(["git", "rev-parse", "--git-dir"],
capture_output=True, text=True, timeout=3)
common = subprocess.run(["git", "rev-parse", "--git-common-dir"],
capture_output=True, text=True, timeout=3)
except (OSError, subprocess.SubprocessError):
return None
if git_dir.returncode != 0 or common.returncode != 0:
return None
cwd = os.getcwd()
git_dir_abs = Path(cwd, git_dir.stdout.strip()).resolve()
common_abs = Path(cwd, common.stdout.strip()).resolve()
if git_dir_abs == common_abs:
return None # primary checkout (or a subdir of it), not a linked worktree
candidate = common_abs.parent / _GRAPHIFY_OUT / "graph.json"
return str(candidate) if candidate.is_file() else None


def _stamped_manifest_files(
Expand Down
86 changes: 86 additions & 0 deletions tests/test_worktree_graph_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""#2008: read commands resolve the graph from the primary checkout when they
are run from a linked git worktree that has no ``graphify-out/`` of its own.

Agent workflows (Claude Code etc.) increasingly run every task in its own
worktree, where ``graphify explain``/``query``/``path``/``affected`` used to fail

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Path here improves readability. Is there any scenario where _GRAPHIFY_OUT could be missing or invalid? If so, should that be handled explicitly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks !!! Agreed on Path. On _GRAPHIFY_OUT — it isn't a value that gets passed in while the program runs. It's a fixed setting loaded once at the top of the file (line 14), so it's always a valid string (defaults to graphify-out). If it were ever wrong, the program wouldn't even start — it'd fail right at import, not inside this function. The only tricky case is when it's set to an absolute path, and that's already handled at line 114 (the os.path.isabs check). So there's nothing extra to handle here. That said, I'm happy to add a check if you'd still like one just to be safe.

hard with "graph file not found" because the graph lives in the primary checkout.
"""
from __future__ import annotations

import shutil
import subprocess
from pathlib import Path

import pytest


def _git(*args, cwd):
subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True)


def _init_repo(tmp_path) -> Path:
primary = tmp_path / "primary"
primary.mkdir()
_git("init", "-q", ".", cwd=primary)
_git("config", "user.email", "t@t.co", cwd=primary)
_git("config", "user.name", "t", cwd=primary)
(primary / "f.txt").write_text("x", encoding="utf-8")
_git("add", "-A", cwd=primary)
_git("commit", "-qm", "init", cwd=primary)
return primary


def _write_graph(where: Path) -> Path:
graph = where / "graphify-out" / "graph.json"
graph.parent.mkdir(parents=True)
graph.write_text("{}", encoding="utf-8")
return graph


def test_default_graph_path_resolves_from_linked_worktree(tmp_path, monkeypatch):
"""From a linked worktree with no local graphify-out, the read-command default
graph path falls back to the primary checkout's graph."""
if shutil.which("git") is None: # pragma: no cover
pytest.skip("git not available")
primary = _init_repo(tmp_path)
graph = _write_graph(primary)
linked = tmp_path / "linked"
_git("worktree", "add", "-q", str(linked), "-b", "feature", cwd=primary)

monkeypatch.chdir(linked)
from graphify.cli import _default_graph_path
assert Path(_default_graph_path()).resolve() == graph.resolve()


def test_default_graph_path_prefers_local_graph_in_worktree(tmp_path, monkeypatch):
"""A worktree that built its own graph keeps using it (local wins)."""
if shutil.which("git") is None: # pragma: no cover
pytest.skip("git not available")
primary = _init_repo(tmp_path)
_write_graph(primary)
linked = tmp_path / "linked"
_git("worktree", "add", "-q", str(linked), "-b", "feature", cwd=primary)
local_graph = _write_graph(linked)

monkeypatch.chdir(linked)
from graphify.cli import _default_graph_path
assert Path(_default_graph_path()).resolve() == local_graph.resolve()


def test_default_graph_path_primary_checkout_unaffected(tmp_path, monkeypatch):
"""In the primary checkout the default stays the local graphify-out path, even
when the graph does not exist yet (normal 'run extract' error path preserved)."""
if shutil.which("git") is None: # pragma: no cover
pytest.skip("git not available")
primary = _init_repo(tmp_path)

monkeypatch.chdir(primary)
from graphify.cli import _default_graph_path
assert Path(_default_graph_path()) == Path("graphify-out") / "graph.json"


def test_default_graph_path_outside_git_repo_unaffected(tmp_path, monkeypatch):
"""Outside any git repo the resolver must not crash and returns the local default."""
monkeypatch.chdir(tmp_path)
from graphify.cli import _default_graph_path
assert Path(_default_graph_path()) == Path("graphify-out") / "graph.json"
Loading