diff --git a/tests/test_detect.py b/tests/test_detect.py index 6f71fe4..37ffc6f 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -269,23 +269,42 @@ def test_gitignore_nested_below_root_prunes_whole_directory(tmp_path): def test_gitignore_nested_negation_overrides_broader_root_rule(tmp_path): """A closer (nested) .gitignore's `!` re-include wins over a root exclude, - matching git's closer-file-wins precedence.""" - (tmp_path / ".gitignore").write_text("*.txt\n") + matching git's closer-file-wins precedence. Uses .py so classification lands + in the deterministic `code` bucket.""" + (tmp_path / ".gitignore").write_text("*.py\n") sub = tmp_path / "vendor" / "sub" sub.mkdir(parents=True) - (sub / ".gitignore").write_text("!important.txt\n") - (tmp_path / "root.txt").write_text("a") - (sub / "important.txt").write_text("b") - (sub / "other.txt").write_text("c") + (sub / ".gitignore").write_text("!important.py\n") + (tmp_path / "root.py").write_text("a = 1") + (sub / "important.py").write_text("b = 1") + (sub / "other.py").write_text("c = 1") result = detect(tmp_path) - # .txt is not a code extension so check the document bucket instead - doc_files = result["files"]["document"] if "document" in result["files"] else [] - unclassified = result.get("unclassified", []) - all_seen = doc_files + unclassified - assert any("important.txt" in f for f in all_seen) - assert not any(f.endswith("root.txt") for f in all_seen) - assert not any(f.endswith("other.txt") for f in all_seen) + code = result["files"]["code"] + # nested `!important.py` re-includes it despite the root `*.py` exclude... + assert any("vendor/sub/important.py" in f for f in code) + # ...while the root-excluded and non-re-included files stay out + assert not any(f.endswith("root.py") for f in code) + assert not any(f.endswith("other.py") for f in code) + + +def test_nested_ignore_overrides_git_info_exclude_and_root(tmp_path): + """Precedence across all three sources: a nested `.gitignore` `!` re-include + outranks both a root `.gitignore` and `.git/info/exclude` (lowest, from + #1810), while an info/exclude-only file with no re-include stays out.""" + (tmp_path / ".git" / "info").mkdir(parents=True) + (tmp_path / ".git" / "info" / "exclude").write_text("*.py\n") + (tmp_path / ".gitignore").write_text("keep.py\n") # root also excludes it + sub = tmp_path / "a" / "b" + sub.mkdir(parents=True) + (sub / ".gitignore").write_text("!keep.py\n") # nearest wins -> re-included + (sub / "keep.py").write_text("x = 1") + (tmp_path / "drop.py").write_text("y = 1") # only info/exclude -> excluded + + result = detect(tmp_path) + code = result["files"]["code"] + assert any("a/b/keep.py" in f for f in code), "nested ! must beat root + info/exclude" + assert not any(f.endswith("drop.py") for f in code) def test_detect_handles_circular_symlinks(tmp_path):