Summary
If .graphifyignore contains any negation pattern (!…), directory-level
pruning is switched off for the entire detect() walk. The walk then
descends into every ignored directory — node_modules/, bin/, obj/,
.git/, graphify-out/, etc. — even though those are still excluded from the
output. On a large repo this is a major, silent slowdown.
Output stays correct (the per-file _is_ignored filter still drops those
files), so this is a walk-performance bug, not a correctness bug — but the
detection pass can take dramatically longer than it should.
Where it happens
graphify/detect.py (verified on v0.8.37 / current v8 HEAD):
detect.py:1047 — has_negation = any(p.startswith("!") for _, p in ignore_patterns)
detect.py:1051 — directory kept if (has_negation or not _is_ignored(...))
Because has_negation is computed once for the whole walk, one unrelated !
rule keeps every ignored directory in the traversal.
Why it matters
The natural way to do a scoped build is a repo-wide exclude plus a re-include —
e.g. ignore everything but docs/:
node_modules/
*.md
!docs/**
That single !docs/** rule silently makes the scan walk the whole tree,
including all of node_modules/. We hit this building a docs-only graph: the
scoped extract appeared to hang because it was descending into every
node_modules/.
Repro
mkdir -p proj/bin/Debug proj/docs && echo x=1 > proj/bin/Debug/a.py
printf 'bin/\n*.md\n!docs/**\n' > proj/.graphifyignore
# detection descends into bin/ (a pattern-ignored, non-noise dir) despite the bin/ rule
Expected
A negation should re-include matching files without disabling pruning of
unrelated ignored directories. _is_ignored already honours negations
correctly — last-match-wins lets !dir/ un-ignore a directory (so it isn't
pruned), and gitignore's parent-exclusion rule means a ! cannot rescue a file
beneath an excluded directory — so descending an ignored directory to find a
re-included file is never necessary. Pruning can rely purely on _is_noise_dir
Workaround (until fixed)
Keep .graphifyignore negation-free for large/whole-repo scans; for a scoped
build, place an ancestor-scoped inner ignore (e.g. a docs/.graphifyignore
with !*.md) instead of a root-level re-include.
I have a fix + regression test ready and will open a PR shortly.
Summary
If
.graphifyignorecontains any negation pattern (!…), directory-levelpruning is switched off for the entire
detect()walk. The walk thendescends into every ignored directory —
node_modules/,bin/,obj/,.git/,graphify-out/, etc. — even though those are still excluded from theoutput. On a large repo this is a major, silent slowdown.
Output stays correct (the per-file
_is_ignoredfilter still drops thosefiles), so this is a walk-performance bug, not a correctness bug — but the
detection pass can take dramatically longer than it should.
Where it happens
graphify/detect.py(verified onv0.8.37/ currentv8HEAD):detect.py:1047—has_negation = any(p.startswith("!") for _, p in ignore_patterns)detect.py:1051— directory kept if(has_negation or not _is_ignored(...))Because
has_negationis computed once for the whole walk, one unrelated!rule keeps every ignored directory in the traversal.
Why it matters
The natural way to do a scoped build is a repo-wide exclude plus a re-include —
e.g. ignore everything but
docs/:That single
!docs/**rule silently makes the scan walk the whole tree,including all of
node_modules/. We hit this building a docs-only graph: thescoped extract appeared to hang because it was descending into every
node_modules/.Repro
Expected
A negation should re-include matching files without disabling pruning of
unrelated ignored directories.
_is_ignoredalready honours negationscorrectly — last-match-wins lets
!dir/un-ignore a directory (so it isn'tpruned), and gitignore's parent-exclusion rule means a
!cannot rescue a filebeneath an excluded directory — so descending an ignored directory to find a
re-included file is never necessary. Pruning can rely purely on
_is_noise_dir_is_ignored.Workaround (until fixed)
Keep
.graphifyignorenegation-free for large/whole-repo scans; for a scopedbuild, place an ancestor-scoped inner ignore (e.g. a
docs/.graphifyignorewith
!*.md) instead of a root-level re-include.I have a fix + regression test ready and will open a PR shortly.