Skip to content

Commit 195196b

Browse files
committed
updater: resolve the previous graph when several checkouts left graph files
The dependency graph is named after the checkout directory. With one worktree per revision, each update left a new *_dependency_graph.json next to the older ones, and snapshot_old_graph refused to choose among several, so the update fell back to a full build with 'previous dependency graph not found'. - find_any_graph_file: several candidates resolve to the newest (logged) - prune_superseded_graphs: after a successful update, drop graph files other than the current checkout's, so the next update finds one - tests for both paths
1 parent 7bc15b7 commit 195196b

3 files changed

Lines changed: 126 additions & 9 deletions

File tree

‎codewiki/cli/adapters/doc_generator.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,17 @@ async def _run_backend_generation(self, backend_config: BackendConfig):
227227
backend_config, doc_generator, update_opts, prev_graph_path, components, leaf_nodes
228228
)
229229
if outcome in ("incremental", "no_change"):
230+
# The builder wrote the new graph under this checkout's name. Drop graphs
231+
# left by earlier checkouts so the next update has exactly one to pick.
232+
from codewiki.src.be.updater.graph_store import (
233+
graph_file_path,
234+
prune_superseded_graphs,
235+
)
236+
237+
prune_superseded_graphs(
238+
backend_config.dependency_graph_dir,
239+
keep=graph_file_path(backend_config.dependency_graph_dir, str(self.repo_path)),
240+
)
230241
return
231242
# full_fallback / detector_failure: preserve the old docs, rebuild from scratch
232243
self._move_docs_aside()

‎codewiki/src/be/updater/graph_store.py‎

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,60 @@ def prev_graph_path(dependency_graph_dir: str, repo_path: str) -> str:
3636
return graph_file_path(dependency_graph_dir, repo_path)[: -len(".json")] + PREV_SUFFIX
3737

3838

39+
def list_graph_files(dependency_graph_dir: str) -> list[str]:
40+
"""All current ``*_dependency_graph.json`` files in the dir (``.prev.json`` excluded)."""
41+
if not os.path.isdir(dependency_graph_dir):
42+
return []
43+
return sorted(
44+
os.path.join(dependency_graph_dir, f)
45+
for f in os.listdir(dependency_graph_dir)
46+
if f.endswith("_dependency_graph.json")
47+
)
48+
49+
3950
def find_any_graph_file(dependency_graph_dir: str) -> str | None:
40-
"""Return the single ``*_dependency_graph.json`` in the dir, if there is exactly one.
51+
"""Return the previous build's graph when it is not under the current repo name.
4152
4253
The repo may have been analysed from a differently named checkout (a git
43-
worktree, for example), so the sanitized name is not always the same.
54+
worktree per revision, for example), so the sanitized name is not always
55+
the same. With exactly one candidate that is the answer. With several
56+
(each earlier worktree left its own file) the newest by mtime is taken and
57+
the choice is logged; ``prune_superseded_graphs`` keeps this case rare.
4458
"""
45-
if not os.path.isdir(dependency_graph_dir):
59+
candidates = list_graph_files(dependency_graph_dir)
60+
if not candidates:
4661
return None
47-
candidates = [
48-
os.path.join(dependency_graph_dir, f)
49-
for f in os.listdir(dependency_graph_dir)
50-
if f.endswith("_dependency_graph.json")
51-
]
5262
if len(candidates) == 1:
5363
return candidates[0]
54-
return None
64+
newest = max(candidates, key=os.path.getmtime)
65+
logger.warning(
66+
"Several dependency graphs found in %s; using the newest (%s) as the previous graph. "
67+
"Candidates: %s",
68+
dependency_graph_dir,
69+
os.path.basename(newest),
70+
", ".join(os.path.basename(c) for c in candidates),
71+
)
72+
return newest
73+
74+
75+
def prune_superseded_graphs(dependency_graph_dir: str, keep: str) -> list[str]:
76+
"""Delete current-graph files other than ``keep`` so the next update finds one graph.
77+
78+
The previous graph survives as ``*.prev.json``; only stale copies left by
79+
earlier checkouts under other names are removed. Returns the removed paths.
80+
"""
81+
removed = []
82+
keep_abs = os.path.abspath(keep)
83+
for path in list_graph_files(dependency_graph_dir):
84+
if os.path.abspath(path) != keep_abs:
85+
os.remove(path)
86+
removed.append(path)
87+
if removed:
88+
logger.info(
89+
"Removed superseded dependency graph(s): %s",
90+
", ".join(os.path.basename(r) for r in removed),
91+
)
92+
return removed
5593

5694

5795
def snapshot_old_graph(dependency_graph_dir: str, repo_path: str) -> str | None:

‎tests/test_updater_graph_store.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Previous-graph lookup when the repo was analysed from differently named checkouts."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import os
7+
import time
8+
9+
from codewiki.src.be.updater.graph_store import (
10+
find_any_graph_file,
11+
graph_file_path,
12+
prune_superseded_graphs,
13+
snapshot_old_graph,
14+
)
15+
16+
17+
def _write_graph(d, name, payload):
18+
path = os.path.join(d, f"{name}_dependency_graph.json")
19+
with open(path, "w") as f:
20+
json.dump(payload, f)
21+
return path
22+
23+
24+
def test_single_candidate_is_used_whatever_its_name(tmp_path):
25+
d = str(tmp_path)
26+
old = _write_graph(d, "rq3_svelte_anchor", {"a": 1})
27+
assert find_any_graph_file(d) == old
28+
prev = snapshot_old_graph(d, "/repos/rq3-svelte-w1")
29+
assert prev is not None and prev.endswith("rq3_svelte_w1_dependency_graph.prev.json")
30+
assert json.load(open(prev)) == {"a": 1}
31+
32+
33+
def test_several_candidates_pick_newest_not_none(tmp_path):
34+
d = str(tmp_path)
35+
older = _write_graph(d, "rq3_svelte_anchor", {"rev": "anchor"})
36+
time.sleep(0.01)
37+
newer = _write_graph(d, "rq3_svelte_w1", {"rev": "w1"})
38+
os.utime(older, (1, 1)) # make the order unambiguous
39+
assert find_any_graph_file(d) == newer
40+
prev = snapshot_old_graph(d, "/repos/rq3-svelte-w2")
41+
assert json.load(open(prev)) == {"rev": "w1"}
42+
43+
44+
def test_prev_snapshots_are_not_candidates(tmp_path):
45+
d = str(tmp_path)
46+
_write_graph(d, "rq3_svelte_w1", {"rev": "w1"})
47+
with open(os.path.join(d, "rq3_svelte_w1_dependency_graph.prev.json"), "w") as f:
48+
json.dump({"rev": "anchor"}, f)
49+
assert find_any_graph_file(d).endswith("rq3_svelte_w1_dependency_graph.json")
50+
51+
52+
def test_prune_keeps_current_and_prev_only(tmp_path):
53+
d = str(tmp_path)
54+
_write_graph(d, "rq3_svelte_anchor", {})
55+
_write_graph(d, "rq3_svelte_w1", {})
56+
current = _write_graph(d, "rq3_svelte_w2", {})
57+
with open(os.path.join(d, "rq3_svelte_w2_dependency_graph.prev.json"), "w") as f:
58+
json.dump({}, f)
59+
removed = prune_superseded_graphs(d, keep=graph_file_path(d, "/repos/rq3-svelte-w2"))
60+
assert sorted(os.path.basename(r) for r in removed) == [
61+
"rq3_svelte_anchor_dependency_graph.json",
62+
"rq3_svelte_w1_dependency_graph.json",
63+
]
64+
assert sorted(os.listdir(d)) == [
65+
"rq3_svelte_w2_dependency_graph.json",
66+
"rq3_svelte_w2_dependency_graph.prev.json",
67+
]
68+
assert os.path.exists(current)

0 commit comments

Comments
 (0)