perf(detect): skip the git ls-files probe when no .gitignore contributes (#2759)
#2766 ran `git ls-files` on every scan of a git repo. Gate it on .gitignore actually contributing patterns beyond the explicit (.graphifyignore/--exclude) set — with no .gitignore in play nothing can be gitignore-dropped, so the tracked-file exemption is moot and an ordinary corpus pays no subprocess cost. Adds a spy test asserting the probe is skipped without a .gitignore and runs once with one. Also adds the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
95ad0adcaa
commit
3460500fff
@@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu
|
||||
|
||||
## 0.9.44 (unreleased)
|
||||
|
||||
- Fix: a git-tracked file that also matches a `.gitignore` pattern (a committed file later added to `.gitignore`, or a force-added one) is no longer dropped from the corpus, matching git's own behavior of never un-tracking such a file; `.graphifyignore`/`--exclude` stay authoritative and a non-git corpus is unaffected (#2759, thanks @NithishKumar04). The `git ls-files` probe is skipped entirely when no `.gitignore` is in play, so ordinary corpora pay nothing for it.
|
||||
|
||||
- Fix: doctest/Catch2 string-named test cases (`TEST_CASE("...")`, `SCENARIO`, `TEST_CASE_TEMPLATE`), which tree-sitter-cpp drops as ERROR nodes, are recovered as callable nodes contained by the file (#2594, thanks @ousamabenyounes); a punctuation-only test name gets a distinct line-positional id instead of collapsing onto the file-stem id.
|
||||
- Fix: `graphify affected` resolves an absolute-path seed against the repo root derived from the graph's own location instead of the current working directory, so a blast-radius query with an absolute seed run from anywhere (an editor, a script) no longer silently returns nothing; a seed outside the root still misses cleanly (#2706, thanks @ousamabenyounes).
|
||||
- Fix: a lazy CommonJS `require(...)` inside a function body (the idiom for breaking circular dependencies) now emits the same `imports_from`/`imports` dependency edges as a top-level require, attributed to the enclosing function, instead of being silently dropped; a dynamic `require(variable)` is still skipped (#2700, thanks @rajanpanth).
|
||||
|
||||
+12
-2
@@ -1372,8 +1372,14 @@ def ignored_predicate(
|
||||
root = root.resolve()
|
||||
patterns = _load_graphifyignore(root, gitignore=gitignore)
|
||||
explicit_patterns = _load_graphifyignore(root, gitignore=False)
|
||||
# Only shell out to git when .gitignore actually contributes patterns beyond
|
||||
# the explicit (.graphifyignore/--exclude) set: with no .gitignore in play,
|
||||
# nothing is dropped by gitignore and the tracked-file exemption is moot, so a
|
||||
# non-.gitignore corpus pays no `git ls-files` cost.
|
||||
tracked_files, tracked_dirs = (
|
||||
_git_tracked_path_keys(root) if gitignore else (set(), set())
|
||||
_git_tracked_path_keys(root)
|
||||
if gitignore and len(patterns) > len(explicit_patterns)
|
||||
else (set(), set())
|
||||
)
|
||||
if extra_excludes:
|
||||
for pat in extra_excludes:
|
||||
@@ -1503,8 +1509,12 @@ def detect(root: Path, *, follow_symlinks: bool | None = None, google_workspace:
|
||||
pruned_noise: list[str] = []
|
||||
ignore_patterns = _load_graphifyignore(root, gitignore=gitignore)
|
||||
explicit_ignore_patterns = _load_graphifyignore(root, gitignore=False)
|
||||
# See ignored_predicate: skip the `git ls-files` subprocess when .gitignore
|
||||
# contributes no patterns, so a non-.gitignore corpus pays nothing for it.
|
||||
tracked_files, tracked_dirs = (
|
||||
_git_tracked_path_keys(root) if gitignore else (set(), set())
|
||||
_git_tracked_path_keys(root)
|
||||
if gitignore and len(ignore_patterns) > len(explicit_ignore_patterns)
|
||||
else (set(), set())
|
||||
)
|
||||
ignore_cache: dict[Path, bool] = {} # shared across all _is_ignored calls in this scan
|
||||
explicit_ignore_cache: dict[Path, bool] = {}
|
||||
|
||||
@@ -539,6 +539,35 @@ def test_git_tracking_probe_failure_preserves_ignore_behavior(
|
||||
assert str(ignored_file) in result["ignored"]
|
||||
|
||||
|
||||
def test_git_lsfiles_skipped_when_no_gitignore_contributes(tmp_path, monkeypatch):
|
||||
"""Optimization (#2759): a git repo with no .gitignore in play must not pay
|
||||
the `git ls-files` subprocess — nothing can be gitignore-dropped, so the
|
||||
tracked-exemption is moot. A .gitignore that DOES contribute still probes."""
|
||||
_git(tmp_path, "init", "-q")
|
||||
(tmp_path / "app.py").write_text("value = 1\n", encoding="utf-8")
|
||||
_git(tmp_path, "add", "app.py")
|
||||
|
||||
real_run = detect_mod.subprocess.run
|
||||
calls = {"ls_files": 0}
|
||||
|
||||
def _spy(args, *a, **k):
|
||||
if isinstance(args, list) and "ls-files" in args:
|
||||
calls["ls_files"] += 1
|
||||
return real_run(args, *a, **k)
|
||||
|
||||
monkeypatch.setattr(detect_mod.subprocess, "run", _spy)
|
||||
|
||||
# No .gitignore anywhere -> gitignore contributes nothing -> no probe.
|
||||
detect(tmp_path)
|
||||
assert calls["ls_files"] == 0, "git ls-files ran despite no .gitignore in play"
|
||||
|
||||
# Add a .gitignore -> gitignore now contributes -> probe happens (once).
|
||||
(tmp_path / ".gitignore").write_text("build/\n", encoding="utf-8")
|
||||
calls["ls_files"] = 0
|
||||
detect(tmp_path)
|
||||
assert calls["ls_files"] >= 1, "git ls-files skipped even though .gitignore is present"
|
||||
|
||||
|
||||
def test_gitignore_nested_below_root_prunes_whole_directory(tmp_path):
|
||||
"""A nested .gitignore excluding a directory prevents descending into it."""
|
||||
sub = tmp_path / "vendor" / "sub"
|
||||
|
||||
Reference in New Issue
Block a user