Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/kimi_cli/utils/file_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,12 @@ def git_index_mtime(root: Path) -> float | None:
return None


def _parse_ls_files_output(stdout: str, *, filter_ignored: bool = True) -> list[str]:
def _parse_ls_files_output(
stdout: str,
*,
filter_ignored: bool = True,
allowed_ignored_names: frozenset[str] = frozenset(),
) -> list[str]:
"""Parse NUL-delimited ``git ls-files -z`` output into paths with synthesised dirs.

When *filter_ignored* is *True*, paths whose segments match
Expand All @@ -168,7 +173,7 @@ def _parse_ls_files_output(stdout: str, *, filter_ignored: bool = True) -> list[
if prefix in ignored_prefixes:
skip = True
break
if is_ignored(part):
if is_ignored(part) and part not in allowed_ignored_names:
ignored_prefixes.add(prefix)
skip = True
break
Expand Down Expand Up @@ -244,7 +249,13 @@ def list_files_git(
return None

deleted = _git_deleted_files(root, scope)
paths = _parse_ls_files_output(result.stdout)
# ``vendor`` often contains checked-in project sources (for example Go
# dependencies), so honor those explicit index entries. Other generated
# directories remain filtered, as does the untracked pass below.
paths = _parse_ls_files_output(
result.stdout,
allowed_ignored_names=frozenset(("vendor",)),
)
if deleted:
paths = [p for p in paths if p.endswith("/") or p not in deleted]

Expand Down
27 changes: 14 additions & 13 deletions tests/ui_and_conv/test_file_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,8 @@ def _init_git_repo(work_dir: Path) -> None:
subprocess.run(cmd, cwd=work_dir, capture_output=True, check=True)


def test_tracked_ignored_dirs_filtered_in_git_mode(tmp_path: Path):
"""Tracked ``node_modules/`` and ``vendor/`` must still be filtered.

Regression test: ``git ls-files`` returns all tracked paths, so
directories in ``_IGNORED_NAMES`` were surfacing in completion when
they happened to be committed.
"""
def test_tracked_ignored_dirs_are_indexed_without_scanning_untracked_tree(tmp_path: Path):
"""Tracked vendor files are explicit project inputs; untracked ones stay filtered."""
(tmp_path / "src").mkdir()
(tmp_path / "src" / "app.py").write_text("# app")
nm = tmp_path / "node_modules" / "pkg"
Expand All @@ -181,14 +176,20 @@ def test_tracked_ignored_dirs_filtered_in_git_mode(tmp_path: Path):
completer = LocalFileMentionCompleter(tmp_path)

texts = _completion_texts(completer, "@nod")
assert not any("node_modules" in t for t in texts), (
f"node_modules should be filtered even if tracked, got: {texts}"
)
assert not any("node_modules" in text for text in texts)

texts = _completion_texts(completer, "@ven")
assert not any("vendor" in t for t in texts), (
f"vendor should be filtered even if tracked, got: {texts}"
)
assert "vendor/dep.py" in texts

# Adding a large untracked dependency subtree must not expand candidates.
untracked = vendor / "generated"
untracked.mkdir()
for i in range(100):
(untracked / f"generated_{i}.py").write_text("# generated")

completer = LocalFileMentionCompleter(tmp_path)
texts = _completion_texts(completer, "@generated")
assert not any("generated_" in text for text in texts)


def test_unstaged_rename_hides_deleted_path(tmp_path: Path):
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/test_file_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class TestIgnoredDirFiltering:
"""Tracked ignored dirs must not leak into git results."""

@pytest.mark.parametrize(
"dirname", ["node_modules", "vendor", "__pycache__", ".vscode", "dist"]
"dirname", ["node_modules", "__pycache__", ".vscode", "dist"]
)
def test_tracked_ignored_dir_filtered(self, tmp_path: Path, dirname: str) -> None:
(tmp_path / "keep.py").write_text("")
Expand Down
Loading