Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Fix: the incremental rebuild no longer purges AST nodes it just reported as fail-closed "kept" — the eviction pass re-checks the kept set, so a moved-file/symlink layout can't deadlock the shrink guard into refusing every update (#3697, #3695, thanks @hopstreax).
- Fix: `graph.html` no longer crashes vis-network with a stack overflow on large graphs — nodes are seeded on a spiral before physics runs so overlap-avoidance can't blow the layout recursion (#3699, thanks @sanjaiyan-dev).
- Fix: node and edge tooltips now show special characters literally (C++ templates like `vector<int>`, generics, `&`, quotes) instead of raw HTML entities, while the HTML sinks that need escaping keep it (#3686, #3664, thanks @hopstreax).
- Fix: `graphify extract --cargo` no longer aborts the whole extraction, discarding the AST pass that already completed, when no `Cargo.toml` exists at the scan root — an ordinary condition for any repo whose manifest lives in a subdirectory. It now prints a note and continues with an empty cargo result instead, matching the merge step's existing handling of that shape (#3677, thanks @ExhibitJ).
- Docs: repository links now point at `Graphify-Labs/graphify` instead of the old account (including in generated wiki output), translated READMEs use the current logo, GitHub issue/PR templates were added, and the Enterprise link was corrected (#3692, #3694, #3693, thanks @Abdul535).

## 0.9.64 (2026-09-18)
Expand Down
8 changes: 8 additions & 0 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4182,6 +4182,14 @@ def _progress(idx: int, total: int, _result: dict) -> None:
print("[graphify extract] introspecting Cargo workspace...")
try:
cargo_result = introspect_cargo(target)
except FileNotFoundError:
# No Cargo.toml at the scan root is an ordinary condition
# (e.g. Tauri keeps its manifest under src-tauri/), not a
# failure — the AST pass already completed and cargo_result
# is already the empty, handled shape the merge below
# expects, so degrade instead of discarding that work (#3677).
print("[graphify extract] --cargo: no Cargo.toml at scan root, "
"skipping crate edges")
except (ConnectionError, ImportError, OSError) as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
Expand Down
71 changes: 71 additions & 0 deletions tests/test_cargo_missing_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""`graphify extract --cargo` must degrade, not abort, when no Cargo.toml
exists at the scan root (#3677).

A missing root manifest is an ordinary condition (e.g. a Tauri app keeps its
manifest under src-tauri/), not a failure. Before the fix, FileNotFoundError
(a subclass of OSError) was caught by a handler meant for ImportError/
ConnectionError and the whole process exited, discarding the AST pass that
had already completed and never writing graph.json.
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

PYTHON = sys.executable
_KEY_VARS = ("GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL",
"ANTHROPIC_API_KEY", "MOONSHOT_API_KEY", "DEEPSEEK_API_KEY")


def _run(repo: Path, *extra: str):
env = {k: v for k, v in os.environ.items() if k not in _KEY_VARS}
env["GRAPHIFY_OUT"] = str(repo / "graphify-out")
return subprocess.run(
[PYTHON, "-m", "graphify", "extract", ".", "--code-only", "--cargo", *extra],
cwd=repo, capture_output=True, text=True, env=env,
)


def test_cargo_flag_without_a_manifest_still_writes_the_graph(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
(repo / "app.py").write_text("def hello():\n return 1\n", encoding="utf-8")

r = _run(repo)

assert r.returncode == 0, (
f"a missing Cargo.toml must not abort the whole extraction: {r.stderr}"
)
out = r.stdout + r.stderr
assert "no Cargo.toml at scan root" in out, (
f"the missing manifest should be reported as a skip, not silently dropped: {out}"
)
graph = repo / "graphify-out" / "graph.json"
assert graph.exists(), "the AST work already done must still be written to graph.json"
g = json.loads(graph.read_text(encoding="utf-8"))
labels = [n.get("label") for n in g["nodes"]]
assert any(str(l).startswith("hello") for l in labels), "code was indexed"


def test_cargo_flag_with_a_manifest_still_adds_crate_nodes(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
(repo / "app.py").write_text("def hello():\n return 1\n", encoding="utf-8")
(repo / "Cargo.toml").write_text(
'[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n',
encoding="utf-8",
)

r = _run(repo)

assert r.returncode == 0, f"a valid manifest must extract cleanly: {r.stderr}"
graph = repo / "graphify-out" / "graph.json"
assert graph.exists()
g = json.loads(graph.read_text(encoding="utf-8"))
node_ids = {n.get("id") for n in g["nodes"]}
assert any("app" in str(i) for i in node_ids), (
f"a real Cargo.toml should still contribute a crate node; got {sorted(node_ids)}"
)
Loading