fix(export,cli): stamp graph provenance from the analysed repo, not the shell cwd (#2534 family)

This commit is contained in:
C0KERNEL
2026-08-13 13:30:24 +01:00
committed by safishamsi
parent a995921026
commit cc0ee60c40
3 changed files with 65 additions and 5 deletions
+11 -1
View File
@@ -3924,7 +3924,17 @@ def dispatch_command(cmd: str) -> None:
# passing --allow-partial (the good graph is preserved and the manifest
# is not stamped, so the retry re-extracts).
_force_write = cli_allow_partial or not _extraction_incomplete
_wrote = _to_json(G, communities, str(graph_json_path), force=_force_write)
# Stamp provenance from the ANALYSED repo, not the shell's cwd: without
# this, to_json's fallback asks `git rev-parse HEAD` in whatever repo the
# command was invoked from, so `graphify extract <target>` run from
# another repo's root stamped the invoker's commit into the target's
# graph.json — and cluster then propagates that stamp into
# GRAPH_REPORT.md (#2534 keeps the extract-time stamp by design). Same
# cwd-anchoring mistake #2316 fixed for watch/update, surviving in the
# extract path.
from graphify.watch import _git_head as _gh_target
_wrote = _to_json(G, communities, str(graph_json_path), force=_force_write,
built_at_commit=_gh_target(cwd=Path(target).resolve()))
if not _wrote:
# The shrink guard refused: this partial build is smaller than the
# existing graph. Exit before writing the manifest/marker below, which
+17 -4
View File
@@ -170,11 +170,21 @@ def attach_hyperedges(G: nx.Graph, hyperedges: list) -> None:
G.graph["hyperedges"] = existing
def _git_head() -> str | None:
"""Return the current git HEAD commit hash, or None if not in a git repo."""
def _git_head(cwd: "str | Path | None" = None) -> str | None:
"""Return git HEAD for the repo containing ``cwd``, or None outside a repo.
``cwd`` selects the repository to ask, exactly as in watch._git_head
(#2316). Without it the command inherits the caller's working directory,
which stamps the *invoking* repo's commit when the graph being written
describes a different repo — provenance must come from the repo the graph
describes, so callers pass the graph's own location.
"""
import subprocess as _sp
try:
r = _sp.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=3)
r = _sp.run(
["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=3,
cwd=str(cwd) if cwd is not None else None,
)
return r.stdout.strip() if r.returncode == 0 else None
except Exception:
return None
@@ -349,7 +359,10 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *,
if isinstance(data.get("graph"), dict) and "hyperedges" in data["graph"]:
data["graph"]["hyperedges"] = hyperedges
data["hyperedges"] = hyperedges
commit = built_at_commit if built_at_commit is not None else _git_head()
# Fallback provenance comes from the repo the graph is being written INTO
# (output_path lives in <target>/graphify-out/), never the shell's cwd —
# the same cwd-anchoring mistake #2316 fixed for `update`.
commit = built_at_commit if built_at_commit is not None else _git_head(Path(output_path).resolve().parent)
if commit:
data["built_at_commit"] = commit
from graphify.paths import write_json_atomic
+37
View File
@@ -72,6 +72,43 @@ def test_to_json_sorts_graph_collections_across_insertion_order(tmp_path):
assert outputs[0].read_bytes() == outputs[1].read_bytes()
def test_to_json_commit_fallback_uses_output_repo_not_cwd(tmp_path, monkeypatch):
# Without an explicit built_at_commit, provenance must come from the repo
# the graph is written into, not from whatever repo the shell happens to
# be in — running `graphify extract <target>` from another repo's root
# used to stamp the invoker's HEAD into the target's graph.json.
import subprocess
import networkx as nx
def git(cwd, *args):
subprocess.run(
["git", "-c", "user.email=t@t", "-c", "user.name=t", *args],
cwd=cwd, check=True, capture_output=True,
)
target = tmp_path / "target"
(target / "graphify-out").mkdir(parents=True)
git(target, "init")
git(target, "commit", "--allow-empty", "-m", "target")
target_head = subprocess.run(
["git", "rev-parse", "HEAD"], cwd=target, check=True,
capture_output=True, text=True,
).stdout.strip()
invoker = tmp_path / "invoker"
invoker.mkdir()
git(invoker, "init")
git(invoker, "commit", "--allow-empty", "-m", "invoker")
monkeypatch.chdir(invoker)
G = nx.Graph()
G.add_node("n1", label="n1")
out = target / "graphify-out" / "graph.json"
assert to_json(G, {0: ["n1"]}, str(out), force=True)
assert json.loads(out.read_text())["built_at_commit"] == target_head
def test_to_cypher_creates_file():
G = make_graph()
with tempfile.TemporaryDirectory() as tmp: