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
2 changes: 1 addition & 1 deletion src/gitingest/utils/ingestion_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def _should_include(path: Path, base_path: Path, include_patterns: set[str]) ->
rel_path = _relative_or_none(path, base_path)
if rel_path is None: # outside repo → do *not* include
return False
if path.is_dir(): # keep directories so children are visited
if path.is_dir() and not path.is_symlink(): # keep directories so children are visited
return True

spec = PathSpec.from_lines("gitwildmatch", include_patterns)
Expand Down
46 changes: 46 additions & 0 deletions tests/test_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

import pytest

from gitingest import output_formatter
from gitingest.ingestion import ingest_query
from gitingest.schemas import filesystem

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -233,3 +235,47 @@ def test_include_ignore_patterns(
# check non-presence of non-included directories in structure
for expected_not_structure_item in pattern_scenario["expected_not_structure"]:
assert expected_not_structure_item not in structure


def test_include_pattern_excludes_symlink_to_directory(
temp_directory: Path,
sample_query: IngestionQuery,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify directory symlinks are filtered by include patterns."""
real_dir = temp_directory / "real_dir"
real_dir.mkdir()
(real_dir / "real_file.txt").write_text("real content")

symlink_path = temp_directory / "should_exclude_me"
try:
symlink_path.symlink_to(real_dir, target_is_directory=True)
except (NotImplementedError, OSError):
symlink_path.mkdir()
# Fall back to simulating a directory symlink on platforms that require symlink privileges.
original_is_symlink = type(symlink_path).is_symlink

def fake_is_symlink(self: Path) -> bool:
return self == symlink_path or original_is_symlink(self)

original_readlink = output_formatter.readlink

def fake_readlink(path: Path) -> Path:
if path == symlink_path:
return real_dir
return original_readlink(path)

monkeypatch.setattr(type(symlink_path), "is_symlink", fake_is_symlink)
monkeypatch.setattr(output_formatter, "readlink", fake_readlink)
monkeypatch.setattr(filesystem, "readlink", fake_readlink)

sample_query.local_path = temp_directory
sample_query.subpath = "/"
sample_query.type = None
sample_query.include_patterns = {"real_dir/*"}

_, structure, content = ingest_query(sample_query)

assert "should_exclude_me" not in structure
assert "real_dir/" in structure
assert "real_dir/real_file.txt" in content