diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b26df9b..a52dba71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.16 (unreleased) +- Fix (regression from 0.9.15): a nested `.gitignore`/`.graphifyignore` no longer applies its patterns outside its own directory (#1873 / #1887 / #1885, thanks @Alwyn93). When 0.9.15 started reading nested ignore files, a non-anchored pattern was still matched against the path relative to the scan root, so a nested bare `*` (a common "ignore this scratch dir" idiom, e.g. what the hypothesis library writes into `.hypothesis/`) matched every file and `detect()` returned zero files, silently producing an empty graph. Patterns are now matched relative to the directory whose ignore file defined them and only apply within that subtree; the anchor directory itself is exempt. + +- Fix (regression from 0.9.15): `graphify update` no longer emits 0 nodes and refuses to overwrite an existing `graph.json` when the source tree contains a nested broad `.gitignore` (#1880). This was the update-layer symptom of the scoping bug above: the zeroed re-scan produced an empty rebuild, which the shrink-guard then correctly refused. With subtree scoping fixed the rebuild sees the real files again; the shrink-guard is unchanged. + - Fix: `detect_incremental` re-extracts a legacy-manifest file when its mtime moves backwards, not just forwards (#1859 / #1862, thanks @thejesh23). The legacy float-schema branch used a strict `current_mtime > stored`, so a file restored to an older timestamp (a `git checkout` of an older commit, a tarball restore, `rsync --times`) was treated as unchanged and never re-extracted, leaving the graph reflecting newer content than the corpus on disk. It now compares with `!=`, matching the dict-schema branch; with no stored hash to verify, any mtime delta forces a re-extract, and the next save promotes the entry to the hash-verified dict schema. - Fix: the dedup summary line reports the fuzzy-merge count even when there were no exact merges (#1857 / #1860, thanks @thejesh23). The fuzzy branch was nested inside `if exact_merges`, so a doc- or semantic-heavy run that merged only via the cross-file fuzzy pass printed a bare `Deduplicated N node(s).` with no breakdown. Both counts are now reported whenever non-zero. diff --git a/tests/test_watch.py b/tests/test_watch.py index 67c68639..8047499d 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -188,6 +188,37 @@ def test_rebuild_code_writes_community_name(tmp_path): ) +def test_update_rebuilds_with_nested_star_gitignore(tmp_path): + """#1880: `graphify update` must not emit 0 nodes (and then refuse to + overwrite) just because the source tree has a nested `.gitignore` with a + broad pattern. This was the 0.9.15 symptom of the #1847/#1873 subtree-scoping + bug: a nested bare `*` zeroed the re-scan, update built 0 nodes, and the + shrink-guard refused. With scoping fixed the rebuild sees the real files.""" + import json + from graphify.watch import _rebuild_code + + corpus = tmp_path / "corpus" + (corpus / "src").mkdir(parents=True) + (corpus / "src" / "a.py").write_text( + "from src.b import Base\nclass App(Base):\n def run(self): return 1\n", encoding="utf-8" + ) + (corpus / "src" / "b.py").write_text("class Base: pass\n", encoding="utf-8") + (corpus / "main.py").write_text("def top(): return 2\n", encoding="utf-8") + # a common scratch-dir idiom deeper in the tree: ignore everything HERE only + (corpus / "scratch").mkdir() + (corpus / "scratch" / ".gitignore").write_text("*\n", encoding="utf-8") + (corpus / "scratch" / "junk.py").write_text("x = 1\n", encoding="utf-8") + + assert _rebuild_code(corpus, acquire_lock=False) is True + + graph = json.loads((corpus / "graphify-out" / "graph.json").read_text(encoding="utf-8")) + sources = {n.get("source_file", "") for n in graph["nodes"]} + assert graph["nodes"], "update produced 0 nodes on a tree with a nested '*' gitignore (#1880)" + assert any("src/a.py" in s for s in sources) and any("main.py" in s for s in sources) + # the nested-ignored scratch file stays out (scoped correctly, not tree-wide) + assert not any("scratch/junk.py" in s for s in sources) + + def test_graphify_root_preserves_absolute_when_user_supplied(tmp_path): """When the caller supplies an absolute path, ``.graphify_root`` stores that absolute form verbatim — preserving explicit-absolute intent."""