From 835659702f338ed615b1b2114a287007aca9cdd2 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Thu, 17 Sep 2026 20:10:57 +0530 Subject: [PATCH 1/3] Fall back to the main checkout's graph from a git worktree A worktree shares history with the main checkout but has no graphify out directory of its own, so a bare query, explain, path, affected, or god nodes run failed with "graph file not found" even though the graph exists one worktree away. AI agent workflows that run every task in its own worktree hit this constantly, and previously needed a manual cd wrapper carried in project rules. The default graph path now checks the usual location relative to the current directory first, and only when that is absent, resolves git's shared common directory and looks for the graph in its parent, the main checkout's root. This is read only and degrades silently to the ordinary path on any failure: not a git repository, git missing from PATH, or no graph anywhere. A worktree that has its own graph built directly in it still uses that one, not the fallback. Co-Authored-By: Claude Sonnet 5 --- graphify/cli.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/graphify/cli.py b/graphify/cli.py index 5503a9185a..521821ff8c 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -81,8 +81,43 @@ ) +def _worktree_graph_fallback() -> Path | None: + """Locate the main checkout's graph.json from inside a git worktree (#2008). + + A worktree has its own working tree but no graphify-out/ of its own — the + graph lives in whichever checkout built it, almost always the main one. + ``git rev-parse --git-common-dir`` resolves to the shared ``.git`` both the + main checkout and every worktree point at; its parent is the main + checkout's root in both cases, so this is a no-op (returns the same path) + when run from the main checkout itself. Read-only: never writes here, and + silently returns None on any failure (not a git repo, git not on PATH, a + bare repo with no working tree) so a plain "graph file not found" error + still surfaces from the normal path. + """ + import subprocess as _sp + + try: + result = _sp.run( + ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], + capture_output=True, text=True, timeout=5, + ) + except (OSError, _sp.SubprocessError): + return None + if result.returncode != 0: + return None + common_dir = result.stdout.strip() + if not common_dir: + return None + candidate = Path(common_dir).parent / _GRAPHIFY_OUT / "graph.json" + return candidate if candidate.is_file() else None + + def _default_graph_path() -> str: - return str(Path(_GRAPHIFY_OUT) / "graph.json") + candidate = Path(_GRAPHIFY_OUT) / "graph.json" + if candidate.exists(): + return str(candidate) + fallback = _worktree_graph_fallback() + return str(fallback) if fallback is not None else str(candidate) def _stamped_manifest_files( From 763291cd84e9cb5937325f37b818195810a20de1 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Thu, 17 Sep 2026 20:11:01 +0530 Subject: [PATCH 2/3] Add regression tests for the worktree graph fallback Builds real git repos and worktrees under tmp_path rather than mocking subprocess, since the point is git's own common directory resolution behaving as expected. Covers the worktree fallback firing, a main checkout with no graph anywhere returning the plain default, a worktree with no graph anywhere doing the same, a directory that is not a git repository at all, and a worktree that has its own graph preferring that one over the fallback. Co-Authored-By: Claude Sonnet 5 --- tests/test_worktree_graph_fallback.py | 107 ++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/test_worktree_graph_fallback.py diff --git a/tests/test_worktree_graph_fallback.py b/tests/test_worktree_graph_fallback.py new file mode 100644 index 0000000000..c359812338 --- /dev/null +++ b/tests/test_worktree_graph_fallback.py @@ -0,0 +1,107 @@ +"""`_default_graph_path` falls back to the main checkout's graph from a git +worktree (#2008). + +A worktree shares history with the main checkout but has no graphify-out/ of +its own, so a bare `graphify explain`/`query`/etc. run from inside one used +to fail with "graph file not found" even though the graph exists one git +worktree away. These tests build real git repos and worktrees under tmp_path +rather than mocking subprocess, since the whole point is git's own +`--git-common-dir` resolution behaving as expected. +""" +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest + +from graphify.cli import _default_graph_path, _worktree_graph_fallback + +pytestmark = pytest.mark.skipif( + subprocess.run(["git", "--version"], capture_output=True).returncode != 0, + reason="git not available", +) + + +def _git(*args, cwd): + subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True) + + +def _make_repo(root: Path) -> Path: + root.mkdir(parents=True, exist_ok=True) + _git("init", "-q", cwd=root) + _git("config", "user.email", "test@test.com", cwd=root) + _git("config", "user.name", "test", cwd=root) + (root / "README.md").write_text("x") + _git("add", ".", cwd=root) + _git("commit", "-q", "-m", "init", cwd=root) + return root + + +@pytest.fixture +def cwd_guard(): + old = os.getcwd() + yield + os.chdir(old) + + +def test_worktree_falls_back_to_main_checkout_graph(tmp_path, cwd_guard): + main = _make_repo(tmp_path / "main") + (main / "graphify-out").mkdir() + graph = main / "graphify-out" / "graph.json" + graph.write_text('{"nodes": [], "links": []}') + + worktree = tmp_path / "wt" + _git("worktree", "add", "--detach", str(worktree), "HEAD", cwd=main) + + os.chdir(worktree) + result = _worktree_graph_fallback() + assert result is not None + assert result.resolve() == graph.resolve() + assert _default_graph_path() == str(result) + + +def test_main_checkout_with_no_graph_returns_plain_default(tmp_path, cwd_guard): + """Not a worktree scenario at all: no graphify-out/ anywhere. Must not + crash, and must return the ordinary (non-existent) default path.""" + main = _make_repo(tmp_path / "solo") + os.chdir(main) + assert _worktree_graph_fallback() is None + assert _default_graph_path() == str(Path("graphify-out") / "graph.json") + + +def test_worktree_with_no_graph_anywhere_returns_none(tmp_path, cwd_guard): + main = _make_repo(tmp_path / "main2") + worktree = tmp_path / "wt2" + _git("worktree", "add", "--detach", str(worktree), "HEAD", cwd=main) + + os.chdir(worktree) + assert _worktree_graph_fallback() is None + assert _default_graph_path() == str(Path("graphify-out") / "graph.json") + + +def test_not_a_git_repo_returns_none(tmp_path, cwd_guard): + plain = tmp_path / "not_a_repo" + plain.mkdir() + os.chdir(plain) + assert _worktree_graph_fallback() is None + assert _default_graph_path() == str(Path("graphify-out") / "graph.json") + + +def test_worktree_with_its_own_graph_prefers_it(tmp_path, cwd_guard): + """If the worktree DOES have its own graphify-out/graph.json (built + there directly), _default_graph_path must use that one, not fall back -- + the fallback only fires when the local candidate is absent.""" + main = _make_repo(tmp_path / "main3") + (main / "graphify-out").mkdir() + (main / "graphify-out" / "graph.json").write_text('{"nodes": [], "links": []}') + + worktree = tmp_path / "wt3" + _git("worktree", "add", "--detach", str(worktree), "HEAD", cwd=main) + (worktree / "graphify-out").mkdir() + own_graph = worktree / "graphify-out" / "graph.json" + own_graph.write_text('{"nodes": [], "links": []}') + + os.chdir(worktree) + assert _default_graph_path() == str(Path("graphify-out") / "graph.json") From a018da4d67d2d77fdc7ec64717f2e4fedfbdf4b5 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Thu, 17 Sep 2026 20:11:01 +0530 Subject: [PATCH 3/3] Add changelog entry for issue 2008 Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a0daf515f..04c8b5a000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.63 (2026-09-16) +- Feature: `query`/`explain`/`path`/`affected`/`god-nodes` and other read-only graph commands now resolve `graphify-out/graph.json` from the main checkout when run inside a git worktree, instead of failing with "graph file not found". A worktree shares history but has no `graphify-out/` of its own; the fallback resolves git's shared common directory and looks in its parent, degrading silently to the ordinary path on any failure (#2008, thanks @uitholland). + - Feature: Elixir `alias`/`import`/`require`/`use` targets now resolve onto the module's `defmodule` node across files, so the internal module dependency graph is no longer dropped as dangling. Only top-level modules are indexed (a nested `defmodule`, labeled with its bare inner name, cannot capture an unrelated `use ` from another file), and a same-file reference is left unresolved so it cannot clobber the structural `contains` edge (#3603, thanks @ayushcodes10). - Feature: a Rust `self.method()` call now resolves to a method defined on the same type in another file (the common split-`impl`-block layout), pooling methods across every `impl` of one type and refusing to link when two unrelated types share a bare name (#3602, thanks @ayushcodes10). - Feature: a Ruby member call `obj.foo` on a known-type receiver now resolves to a method `foo` inherited from a superclass, including across files, using the same conservative promotion as the implicit-self resolver — a single owning class, matching method kind, and one unambiguous ancestry chain, or it stays dangling (#3585, thanks @oleksii-tumanov).