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/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ def _apply_gitignores(query: IngestionQuery) -> None:

"""
for fname in (".gitignore", ".gitingestignore"):
query.ignore_patterns.update(load_ignore_patterns(query.local_path, filename=fname))
query.ignore_patterns = [*query.ignore_patterns, *load_ignore_patterns(query.local_path, filename=fname)]


@asynccontextmanager
Expand Down
22 changes: 11 additions & 11 deletions src/gitingest/utils/ignore_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
}


def load_ignore_patterns(root: Path, filename: str) -> set[str]:
def load_ignore_patterns(root: Path, filename: str) -> list[str]:
"""Load ignore patterns from ``filename`` found under ``root``.

The loader walks the directory tree, looks for the supplied ``filename``,
Expand All @@ -185,20 +185,20 @@ def load_ignore_patterns(root: Path, filename: str) -> set[str]:

Returns
-------
set[str]
A set of ignore patterns extracted from the ``filename`` file found under the ``root`` directory.
list[str]
Ignore patterns extracted from the ``filename`` file found under the ``root`` directory.

"""
patterns: set[str] = set()
patterns: list[str] = []

for ignore_file in root.rglob(filename):
if ignore_file.is_file():
patterns.update(_parse_ignore_file(ignore_file, root))
patterns.extend(_parse_ignore_file(ignore_file, root))
return patterns


def _parse_ignore_file(ignore_file: Path, root: Path) -> set[str]:
"""Parse an ignore file and return a set of ignore patterns.
def _parse_ignore_file(ignore_file: Path, root: Path) -> list[str]:
"""Parse an ignore file and return ignore patterns.

Parameters
----------
Expand All @@ -209,11 +209,11 @@ def _parse_ignore_file(ignore_file: Path, root: Path) -> set[str]:

Returns
-------
set[str]
A set of ignore patterns.
list[str]
Ignore patterns in file order.

"""
patterns: set[str] = set()
patterns: list[str] = []

# Path of the ignore file relative to the repository root
rel_dir = ignore_file.parent.relative_to(root)
Expand All @@ -235,6 +235,6 @@ def _parse_ignore_file(ignore_file: Path, root: Path) -> set[str]:
line = line.lstrip("/")

pattern_body = (base_dir / line).as_posix()
patterns.add(f"!{pattern_body}" if negated else pattern_body)
patterns.append(f"!{pattern_body}" if negated else pattern_body)

return patterns
3 changes: 2 additions & 1 deletion src/gitingest/utils/ingestion_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathspec import PathSpec

if TYPE_CHECKING:
from collections.abc import Iterable
from pathlib import Path


Expand Down Expand Up @@ -40,7 +41,7 @@ def _should_include(path: Path, base_path: Path, include_patterns: set[str]) ->
return spec.match_file(str(rel_path))


def _should_exclude(path: Path, base_path: Path, ignore_patterns: set[str]) -> bool:
def _should_exclude(path: Path, base_path: Path, ignore_patterns: Iterable[str]) -> bool:
"""Return ``True`` if ``path`` matches any of ``ignore_patterns``.

Parameters
Expand Down
26 changes: 26 additions & 0 deletions tests/test_gitignore_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ def test_load_gitignore_patterns(tmp_path: Path) -> None:
assert not pattern.startswith("#")


def test_load_gitignore_patterns_preserves_negation_order(tmp_path: Path) -> None:
"""Test that negated gitignore patterns keep their file order."""
gitignore = tmp_path / ".gitignore"
gitignore.write_text("*.log\n!important.log\n")

assert load_ignore_patterns(tmp_path, filename=".gitignore") == ["*.log", "!important.log"]


@pytest.mark.asyncio
async def test_ingest_with_gitignore(repo_path: Path) -> None:
"""Integration test for ``ingest_async()`` respecting ``.gitignore`` rules.
Expand All @@ -67,3 +75,21 @@ async def test_ingest_with_gitignore(repo_path: Path) -> None:
# Now both files should be present.
assert "This file should be excluded." in content_without_ignore
assert "This file should be included." in content_without_ignore


@pytest.mark.asyncio
async def test_ingest_with_gitignore_negation(tmp_path: Path) -> None:
"""Test that later negated gitignore rules can re-include files."""
gitignore = tmp_path / ".gitignore"
gitignore.write_text("*.log\n!important.log\n")

ignored_file = tmp_path / "ignored.log"
ignored_file.write_text("This log should be ignored.")

important_file = tmp_path / "important.log"
important_file.write_text("This log should be included.")

_, _, content = await ingest_async(source=str(tmp_path))

assert "This log should be ignored." not in content
assert "This log should be included." in content