c6af9e284a
Tests / catch-all (windows-latest) (push) Has been cancelled
Tests / jvm (macos-latest) (push) Has been cancelled
Tests / jvm (ubuntu-latest) (push) Has been cancelled
Tests / jvm (windows-latest) (push) Has been cancelled
Tests / native (macos-latest) (push) Has been cancelled
Tests / native (ubuntu-latest) (push) Has been cancelled
Tests / native (windows-latest) (push) Has been cancelled
Tests / niche (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (macos-latest) (push) Has been cancelled
Tests / other-langs (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (windows-latest) (push) Has been cancelled
Tests / catch-all (macos-latest) (push) Has been cancelled
Tests / catch-all (ubuntu-latest) (push) Has been cancelled
Docs Build / build (push) Has been cancelled
Docs Build / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Build and Push Docker Images / build-and-push (push) Has been cancelled
756 lines
26 KiB
Python
756 lines
26 KiB
Python
import os
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from pathspec import PathSpec
|
|
|
|
from serena.util.file_system import GitignoreParser, GitignoreSpec, match_path
|
|
|
|
|
|
class TestGitignoreParser:
|
|
"""Test class for GitignoreParser functionality."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test environment before each test method."""
|
|
# Create a temporary directory for testing
|
|
self.test_dir = tempfile.mkdtemp()
|
|
self.repo_path = Path(self.test_dir)
|
|
|
|
# Create test repository structure
|
|
self._create_repo_structure()
|
|
|
|
def teardown_method(self):
|
|
"""Clean up test environment after each test method."""
|
|
# Remove the temporary directory
|
|
shutil.rmtree(self.test_dir)
|
|
|
|
def _create_repo_structure(self):
|
|
"""
|
|
Create a test repository structure with multiple gitignore files.
|
|
|
|
Structure:
|
|
repo/
|
|
├── .gitignore
|
|
├── file1.txt
|
|
├── test.log
|
|
├── src/
|
|
│ ├── .gitignore
|
|
│ ├── main.py
|
|
│ ├── test.log
|
|
│ ├── build/
|
|
│ │ └── output.o
|
|
│ └── lib/
|
|
│ ├── .gitignore
|
|
│ └── cache.tmp
|
|
└── docs/
|
|
├── .gitignore
|
|
├── api.md
|
|
└── temp/
|
|
└── draft.md
|
|
"""
|
|
# Create directories
|
|
(self.repo_path / "src").mkdir()
|
|
(self.repo_path / "src" / "build").mkdir()
|
|
(self.repo_path / "src" / "lib").mkdir()
|
|
(self.repo_path / "docs").mkdir()
|
|
(self.repo_path / "docs" / "temp").mkdir()
|
|
|
|
# Create files
|
|
(self.repo_path / "file1.txt").touch()
|
|
(self.repo_path / "test.log").touch()
|
|
(self.repo_path / "src" / "main.py").touch()
|
|
(self.repo_path / "src" / "test.log").touch()
|
|
(self.repo_path / "src" / "build" / "output.o").touch()
|
|
(self.repo_path / "src" / "lib" / "cache.tmp").touch()
|
|
(self.repo_path / "docs" / "api.md").touch()
|
|
(self.repo_path / "docs" / "temp" / "draft.md").touch()
|
|
|
|
# Create root .gitignore
|
|
root_gitignore = self.repo_path / ".gitignore"
|
|
root_gitignore.write_text(
|
|
"""# Root gitignore
|
|
*.log
|
|
/build/
|
|
"""
|
|
)
|
|
|
|
# Create src/.gitignore
|
|
src_gitignore = self.repo_path / "src" / ".gitignore"
|
|
src_gitignore.write_text(
|
|
"""# Source gitignore
|
|
*.o
|
|
build/
|
|
!important.log
|
|
"""
|
|
)
|
|
|
|
# Create src/lib/.gitignore (deeply nested)
|
|
src_lib_gitignore = self.repo_path / "src" / "lib" / ".gitignore"
|
|
src_lib_gitignore.write_text(
|
|
"""# Library gitignore
|
|
*.tmp
|
|
*.cache
|
|
"""
|
|
)
|
|
|
|
# Create docs/.gitignore
|
|
docs_gitignore = self.repo_path / "docs" / ".gitignore"
|
|
docs_gitignore.write_text(
|
|
"""# Docs gitignore
|
|
temp/
|
|
*.tmp
|
|
"""
|
|
)
|
|
|
|
def test_initialization(self):
|
|
"""Test GitignoreParser initialization."""
|
|
parser = GitignoreParser(str(self.repo_path))
|
|
|
|
assert parser.repo_root == str(self.repo_path.absolute())
|
|
assert len(parser.get_ignore_specs()) == 4
|
|
|
|
def test_find_gitignore_files(self):
|
|
"""Test finding all gitignore files in repository, including deeply nested ones."""
|
|
parser = GitignoreParser(str(self.repo_path))
|
|
|
|
# Get file paths from specs
|
|
gitignore_files = [spec.file_path for spec in parser.get_ignore_specs()]
|
|
|
|
# Convert to relative paths for easier testing
|
|
rel_paths = [os.path.relpath(f, self.repo_path) for f in gitignore_files]
|
|
rel_paths.sort()
|
|
|
|
assert len(rel_paths) == 4
|
|
assert ".gitignore" in rel_paths
|
|
assert os.path.join("src", ".gitignore") in rel_paths
|
|
assert os.path.join("src", "lib", ".gitignore") in rel_paths # Deeply nested
|
|
assert os.path.join("docs", ".gitignore") in rel_paths
|
|
|
|
def test_parse_patterns_root_directory(self):
|
|
"""Test parsing gitignore patterns in root directory."""
|
|
# Create a simple test case with only root gitignore
|
|
test_dir = self.repo_path / "test_root"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""*.log
|
|
build/
|
|
/temp.txt
|
|
"""
|
|
)
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
specs = parser.get_ignore_specs()
|
|
|
|
assert len(specs) == 1
|
|
patterns = specs[0].patterns
|
|
|
|
assert "*.log" in patterns
|
|
assert "build/" in patterns
|
|
assert "/temp.txt" in patterns
|
|
|
|
def test_parse_patterns_subdirectory(self):
|
|
"""Test parsing gitignore patterns in subdirectory."""
|
|
# Create a test case with subdirectory gitignore
|
|
test_dir = self.repo_path / "test_sub"
|
|
test_dir.mkdir()
|
|
subdir = test_dir / "src"
|
|
subdir.mkdir()
|
|
|
|
gitignore = subdir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""*.o
|
|
/build/
|
|
test.log
|
|
"""
|
|
)
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
specs = parser.get_ignore_specs()
|
|
|
|
assert len(specs) == 1
|
|
patterns = specs[0].patterns
|
|
|
|
# Non-anchored pattern should get ** prefix
|
|
assert "src/**/*.o" in patterns
|
|
# Anchored pattern should not get ** prefix
|
|
assert "src/build/" in patterns
|
|
# Non-anchored pattern without slash
|
|
assert "src/**/test.log" in patterns
|
|
|
|
def test_should_ignore_root_patterns(self):
|
|
"""Test ignoring files based on root .gitignore."""
|
|
parser = GitignoreParser(str(self.repo_path))
|
|
|
|
# Files that should be ignored
|
|
assert parser.should_ignore("test.log")
|
|
assert parser.should_ignore(str(self.repo_path / "test.log"))
|
|
|
|
# Files that should NOT be ignored
|
|
assert not parser.should_ignore("file1.txt")
|
|
assert not parser.should_ignore("src/main.py")
|
|
|
|
def test_match_path_root_directory(self):
|
|
"""Root directory should never be ignored by pathspec patterns."""
|
|
spec = PathSpec.from_lines("gitwildmatch", ["/.*/"])
|
|
|
|
assert not match_path(".", spec, root_path=str(self.repo_path))
|
|
assert not match_path("", spec, root_path=str(self.repo_path))
|
|
|
|
def test_should_ignore_subdirectory_patterns(self):
|
|
"""Test ignoring files based on subdirectory .gitignore files."""
|
|
parser = GitignoreParser(str(self.repo_path))
|
|
|
|
# .o files in src should be ignored
|
|
assert parser.should_ignore("src/build/output.o")
|
|
|
|
# build/ directory in src should be ignored
|
|
assert parser.should_ignore("src/build/")
|
|
|
|
# temp/ directory in docs should be ignored
|
|
assert parser.should_ignore("docs/temp/draft.md")
|
|
|
|
# But temp/ outside docs should not be ignored by docs/.gitignore
|
|
assert not parser.should_ignore("temp/file.txt")
|
|
|
|
# Test deeply nested .gitignore in src/lib/
|
|
# .tmp files in src/lib should be ignored
|
|
assert parser.should_ignore("src/lib/cache.tmp")
|
|
|
|
# .cache files in src/lib should also be ignored
|
|
assert parser.should_ignore("src/lib/data.cache")
|
|
|
|
# But .tmp files outside src/lib should not be ignored by src/lib/.gitignore
|
|
assert not parser.should_ignore("src/other.tmp")
|
|
|
|
def test_anchored_vs_non_anchored_patterns(self):
|
|
"""Test the difference between anchored and non-anchored patterns."""
|
|
# Create new test structure
|
|
test_dir = self.repo_path / "test_anchored"
|
|
test_dir.mkdir()
|
|
(test_dir / "src").mkdir()
|
|
(test_dir / "src" / "subdir").mkdir()
|
|
(test_dir / "src" / "subdir" / "deep").mkdir()
|
|
|
|
# Create src/.gitignore with both anchored and non-anchored patterns
|
|
gitignore = test_dir / "src" / ".gitignore"
|
|
gitignore.write_text(
|
|
"""/temp.txt
|
|
data.json
|
|
"""
|
|
)
|
|
|
|
# Create test files
|
|
(test_dir / "src" / "temp.txt").touch()
|
|
(test_dir / "src" / "data.json").touch()
|
|
(test_dir / "src" / "subdir" / "temp.txt").touch()
|
|
(test_dir / "src" / "subdir" / "data.json").touch()
|
|
(test_dir / "src" / "subdir" / "deep" / "data.json").touch()
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# Anchored pattern /temp.txt should only match in src/
|
|
assert parser.should_ignore("src/temp.txt")
|
|
assert not parser.should_ignore("src/subdir/temp.txt")
|
|
|
|
# Non-anchored pattern data.json should match anywhere under src/
|
|
assert parser.should_ignore("src/data.json")
|
|
assert parser.should_ignore("src/subdir/data.json")
|
|
assert parser.should_ignore("src/subdir/deep/data.json")
|
|
|
|
def test_root_anchored_patterns(self):
|
|
"""Test anchored patterns in root .gitignore only match root-level files."""
|
|
# Create new test structure for root anchored patterns
|
|
test_dir = self.repo_path / "test_root_anchored"
|
|
test_dir.mkdir()
|
|
(test_dir / "src").mkdir()
|
|
(test_dir / "docs").mkdir()
|
|
(test_dir / "src" / "nested").mkdir()
|
|
|
|
# Create root .gitignore with anchored patterns
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""/config.json
|
|
/temp.log
|
|
/build
|
|
*.pyc
|
|
"""
|
|
)
|
|
|
|
# Create test files at root level
|
|
(test_dir / "config.json").touch()
|
|
(test_dir / "temp.log").touch()
|
|
(test_dir / "build").mkdir()
|
|
(test_dir / "file.pyc").touch()
|
|
|
|
# Create same-named files in subdirectories
|
|
(test_dir / "src" / "config.json").touch()
|
|
(test_dir / "src" / "temp.log").touch()
|
|
(test_dir / "src" / "build").mkdir()
|
|
(test_dir / "src" / "file.pyc").touch()
|
|
(test_dir / "docs" / "config.json").touch()
|
|
(test_dir / "docs" / "temp.log").touch()
|
|
(test_dir / "src" / "nested" / "config.json").touch()
|
|
(test_dir / "src" / "nested" / "temp.log").touch()
|
|
(test_dir / "src" / "nested" / "build").mkdir()
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# Anchored patterns should only match root-level files
|
|
assert parser.should_ignore("config.json")
|
|
assert not parser.should_ignore("src/config.json")
|
|
assert not parser.should_ignore("docs/config.json")
|
|
assert not parser.should_ignore("src/nested/config.json")
|
|
|
|
assert parser.should_ignore("temp.log")
|
|
assert not parser.should_ignore("src/temp.log")
|
|
assert not parser.should_ignore("docs/temp.log")
|
|
assert not parser.should_ignore("src/nested/temp.log")
|
|
|
|
assert parser.should_ignore("build")
|
|
assert not parser.should_ignore("src/build")
|
|
assert not parser.should_ignore("src/nested/build")
|
|
|
|
# Non-anchored patterns should match everywhere
|
|
assert parser.should_ignore("file.pyc")
|
|
assert parser.should_ignore("src/file.pyc")
|
|
|
|
def test_mixed_anchored_and_non_anchored_root_patterns(self):
|
|
"""Test mix of anchored and non-anchored patterns in root .gitignore."""
|
|
test_dir = self.repo_path / "test_mixed_patterns"
|
|
test_dir.mkdir()
|
|
(test_dir / "app").mkdir()
|
|
(test_dir / "tests").mkdir()
|
|
(test_dir / "app" / "modules").mkdir()
|
|
|
|
# Create root .gitignore with mixed patterns
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""/secrets.env
|
|
/dist/
|
|
node_modules/
|
|
*.tmp
|
|
/app/local.config
|
|
debug.log
|
|
"""
|
|
)
|
|
|
|
# Create test files and directories
|
|
(test_dir / "secrets.env").touch()
|
|
(test_dir / "dist").mkdir()
|
|
(test_dir / "node_modules").mkdir()
|
|
(test_dir / "file.tmp").touch()
|
|
(test_dir / "app" / "local.config").touch()
|
|
(test_dir / "debug.log").touch()
|
|
|
|
# Create same files in subdirectories
|
|
(test_dir / "app" / "secrets.env").touch()
|
|
(test_dir / "app" / "dist").mkdir()
|
|
(test_dir / "app" / "node_modules").mkdir()
|
|
(test_dir / "app" / "file.tmp").touch()
|
|
(test_dir / "app" / "debug.log").touch()
|
|
(test_dir / "tests" / "secrets.env").touch()
|
|
(test_dir / "tests" / "node_modules").mkdir()
|
|
(test_dir / "tests" / "debug.log").touch()
|
|
(test_dir / "app" / "modules" / "local.config").touch()
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# Anchored patterns should only match at root
|
|
assert parser.should_ignore("secrets.env")
|
|
assert not parser.should_ignore("app/secrets.env")
|
|
assert not parser.should_ignore("tests/secrets.env")
|
|
|
|
assert parser.should_ignore("dist")
|
|
assert not parser.should_ignore("app/dist")
|
|
|
|
assert parser.should_ignore("app/local.config")
|
|
assert not parser.should_ignore("app/modules/local.config")
|
|
|
|
# Non-anchored patterns should match everywhere
|
|
assert parser.should_ignore("node_modules")
|
|
assert parser.should_ignore("app/node_modules")
|
|
assert parser.should_ignore("tests/node_modules")
|
|
|
|
assert parser.should_ignore("file.tmp")
|
|
assert parser.should_ignore("app/file.tmp")
|
|
|
|
assert parser.should_ignore("debug.log")
|
|
assert parser.should_ignore("app/debug.log")
|
|
assert parser.should_ignore("tests/debug.log")
|
|
|
|
def test_negation_patterns(self):
|
|
"""Test negation patterns are parsed correctly."""
|
|
test_dir = self.repo_path / "test_negation"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""*.log
|
|
!important.log
|
|
!src/keep.log
|
|
"""
|
|
)
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
specs = parser.get_ignore_specs()
|
|
|
|
assert len(specs) == 1
|
|
patterns = specs[0].patterns
|
|
|
|
assert "*.log" in patterns
|
|
assert "!important.log" in patterns
|
|
assert "!src/keep.log" in patterns
|
|
|
|
def test_comments_and_empty_lines(self):
|
|
"""Test that comments and empty lines are ignored."""
|
|
test_dir = self.repo_path / "test_comments"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""# This is a comment
|
|
*.log
|
|
|
|
# Another comment
|
|
# Indented comment
|
|
|
|
build/
|
|
"""
|
|
)
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
specs = parser.get_ignore_specs()
|
|
|
|
assert len(specs) == 1
|
|
patterns = specs[0].patterns
|
|
|
|
assert len(patterns) == 2
|
|
assert "*.log" in patterns
|
|
assert "build/" in patterns
|
|
|
|
def test_escaped_characters(self):
|
|
"""Test escaped special characters."""
|
|
test_dir = self.repo_path / "test_escaped"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""\\#not-a-comment.txt
|
|
\\!not-negation.txt
|
|
"""
|
|
)
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
specs = parser.get_ignore_specs()
|
|
|
|
assert len(specs) == 1
|
|
patterns = specs[0].patterns
|
|
|
|
assert "#not-a-comment.txt" in patterns
|
|
assert "!not-negation.txt" in patterns
|
|
|
|
def test_escaped_negation_patterns(self):
|
|
test_dir = self.repo_path / "test_escaped_negation"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""*.log
|
|
\\!not-negation.log
|
|
!actual-negation.log
|
|
"""
|
|
)
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
specs = parser.get_ignore_specs()
|
|
|
|
assert len(specs) == 1
|
|
patterns = specs[0].patterns
|
|
|
|
# Key assertions: escaped exclamation becomes literal, real negation preserved
|
|
assert "!not-negation.log" in patterns # escaped -> literal
|
|
assert "!actual-negation.log" in patterns # real negation preserved
|
|
|
|
# Test the actual behavioral difference between escaped and real negation:
|
|
# *.log pattern should ignore test.log
|
|
assert parser.should_ignore("test.log")
|
|
|
|
# Escaped negation file should still be ignored by *.log pattern
|
|
assert parser.should_ignore("!not-negation.log")
|
|
|
|
# Actual negation should override the *.log pattern
|
|
assert not parser.should_ignore("actual-negation.log")
|
|
|
|
def test_glob_patterns(self):
|
|
"""Test various glob patterns work correctly."""
|
|
test_dir = self.repo_path / "test_glob"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""*.pyc
|
|
**/*.tmp
|
|
src/*.o
|
|
!src/important.o
|
|
[Tt]est*
|
|
"""
|
|
)
|
|
|
|
# Create test files
|
|
(test_dir / "src").mkdir()
|
|
(test_dir / "src" / "nested").mkdir()
|
|
(test_dir / "file.pyc").touch()
|
|
(test_dir / "src" / "file.pyc").touch()
|
|
(test_dir / "file.tmp").touch()
|
|
(test_dir / "src" / "nested" / "file.tmp").touch()
|
|
(test_dir / "src" / "file.o").touch()
|
|
(test_dir / "src" / "important.o").touch()
|
|
(test_dir / "Test.txt").touch()
|
|
(test_dir / "test.log").touch()
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# *.pyc should match everywhere
|
|
assert parser.should_ignore("file.pyc")
|
|
assert parser.should_ignore("src/file.pyc")
|
|
|
|
# **/*.tmp should match all .tmp files
|
|
assert parser.should_ignore("file.tmp")
|
|
assert parser.should_ignore("src/nested/file.tmp")
|
|
|
|
# src/*.o should only match .o files directly in src/
|
|
assert parser.should_ignore("src/file.o")
|
|
|
|
# Character class patterns
|
|
assert parser.should_ignore("Test.txt")
|
|
assert parser.should_ignore("test.log")
|
|
|
|
def test_empty_gitignore(self):
|
|
"""Test handling of empty gitignore files."""
|
|
test_dir = self.repo_path / "test_empty"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text("")
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# Should not crash and should return empty list
|
|
assert len(parser.get_ignore_specs()) == 0
|
|
|
|
def test_malformed_gitignore(self):
|
|
"""Test handling of malformed gitignore content."""
|
|
test_dir = self.repo_path / "test_malformed"
|
|
test_dir.mkdir()
|
|
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text(
|
|
"""# Only comments and empty lines
|
|
|
|
# More comments
|
|
|
|
"""
|
|
)
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# Should handle gracefully
|
|
assert len(parser.get_ignore_specs()) == 0
|
|
|
|
def test_reload(self):
|
|
"""Test reloading gitignore files."""
|
|
test_dir = self.repo_path / "test_reload"
|
|
test_dir.mkdir()
|
|
|
|
# Create initial gitignore
|
|
gitignore = test_dir / ".gitignore"
|
|
gitignore.write_text("*.log")
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
assert len(parser.get_ignore_specs()) == 1
|
|
assert parser.should_ignore("test.log")
|
|
|
|
# Modify gitignore
|
|
gitignore.write_text("*.tmp")
|
|
|
|
# Without reload, should still use old patterns
|
|
assert parser.should_ignore("test.log")
|
|
assert not parser.should_ignore("test.tmp")
|
|
|
|
# After reload, should use new patterns
|
|
parser.reload()
|
|
assert not parser.should_ignore("test.log")
|
|
assert parser.should_ignore("test.tmp")
|
|
|
|
def test_gitignore_spec_matches(self):
|
|
"""Test GitignoreSpec.matches method."""
|
|
spec = GitignoreSpec("/path/to/.gitignore", ["*.log", "build/", "!important.log"])
|
|
|
|
assert spec.matches("test.log")
|
|
assert spec.matches("build/output.o")
|
|
assert spec.matches("src/test.log")
|
|
|
|
# Note: Negation patterns in pathspec work differently than in git
|
|
# This is a limitation of the pathspec library
|
|
|
|
def test_subdirectory_gitignore_pattern_scoping(self):
|
|
"""Test that subdirectory .gitignore patterns are scoped correctly."""
|
|
# Create test structure: foo/ with subdirectory bar/
|
|
test_dir = self.repo_path / "test_subdir_scoping"
|
|
test_dir.mkdir()
|
|
(test_dir / "foo").mkdir()
|
|
(test_dir / "foo" / "bar").mkdir()
|
|
|
|
# Create files in various locations
|
|
(test_dir / "foo.txt").touch() # root level
|
|
(test_dir / "foo" / "foo.txt").touch() # in foo/
|
|
(test_dir / "foo" / "bar" / "foo.txt").touch() # in foo/bar/
|
|
|
|
# Test case 1: foo.txt in foo/.gitignore should only ignore in foo/ subtree
|
|
gitignore = test_dir / "foo" / ".gitignore"
|
|
gitignore.write_text("foo.txt\n")
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# foo.txt at root should NOT be ignored by foo/.gitignore
|
|
assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored by foo/.gitignore"
|
|
|
|
# foo.txt in foo/ should be ignored
|
|
assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored"
|
|
|
|
# foo.txt in foo/bar/ should be ignored (within foo/ subtree)
|
|
assert parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should be ignored"
|
|
|
|
def test_anchored_pattern_in_subdirectory(self):
|
|
"""Test that anchored patterns in subdirectory only match immediate children."""
|
|
test_dir = self.repo_path / "test_anchored_subdir"
|
|
test_dir.mkdir()
|
|
(test_dir / "foo").mkdir()
|
|
(test_dir / "foo" / "bar").mkdir()
|
|
|
|
# Create files
|
|
(test_dir / "foo.txt").touch() # root level
|
|
(test_dir / "foo" / "foo.txt").touch() # in foo/
|
|
(test_dir / "foo" / "bar" / "foo.txt").touch() # in foo/bar/
|
|
|
|
# Test case 2: /foo.txt in foo/.gitignore should only match foo/foo.txt
|
|
gitignore = test_dir / "foo" / ".gitignore"
|
|
gitignore.write_text("/foo.txt\n")
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# foo.txt at root should NOT be ignored
|
|
assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored"
|
|
|
|
# foo.txt directly in foo/ should be ignored
|
|
assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored by /foo.txt pattern"
|
|
|
|
# foo.txt in foo/bar/ should NOT be ignored (anchored pattern only matches immediate children)
|
|
assert not parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should NOT be ignored by /foo.txt pattern"
|
|
|
|
def test_double_star_pattern_scoping(self):
|
|
"""Test that **/pattern in subdirectory only applies within that subtree."""
|
|
test_dir = self.repo_path / "test_doublestar_scope"
|
|
test_dir.mkdir()
|
|
(test_dir / "foo").mkdir()
|
|
(test_dir / "foo" / "bar").mkdir()
|
|
(test_dir / "other").mkdir()
|
|
|
|
# Create files
|
|
(test_dir / "foo.txt").touch() # root level
|
|
(test_dir / "foo" / "foo.txt").touch() # in foo/
|
|
(test_dir / "foo" / "bar" / "foo.txt").touch() # in foo/bar/
|
|
(test_dir / "other" / "foo.txt").touch() # in other/
|
|
|
|
# Test case 3: **/foo.txt in foo/.gitignore should only ignore within foo/ subtree
|
|
gitignore = test_dir / "foo" / ".gitignore"
|
|
gitignore.write_text("**/foo.txt\n")
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# foo.txt at root should NOT be ignored
|
|
assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored by foo/.gitignore"
|
|
|
|
# foo.txt in foo/ should be ignored
|
|
assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored"
|
|
|
|
# foo.txt in foo/bar/ should be ignored (within foo/ subtree)
|
|
assert parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should be ignored"
|
|
|
|
# foo.txt in other/ should NOT be ignored (outside foo/ subtree)
|
|
assert not parser.should_ignore("other/foo.txt"), "other/foo.txt should NOT be ignored by foo/.gitignore"
|
|
|
|
def test_anchored_double_star_pattern(self):
|
|
"""Test that /**/pattern in subdirectory works correctly."""
|
|
test_dir = self.repo_path / "test_anchored_doublestar"
|
|
test_dir.mkdir()
|
|
(test_dir / "foo").mkdir()
|
|
(test_dir / "foo" / "bar").mkdir()
|
|
(test_dir / "other").mkdir()
|
|
|
|
# Create files
|
|
(test_dir / "foo.txt").touch() # root level
|
|
(test_dir / "foo" / "foo.txt").touch() # in foo/
|
|
(test_dir / "foo" / "bar" / "foo.txt").touch() # in foo/bar/
|
|
(test_dir / "other" / "foo.txt").touch() # in other/
|
|
|
|
# Test case 4: /**/foo.txt in foo/.gitignore should correctly ignore only within foo/ subtree
|
|
gitignore = test_dir / "foo" / ".gitignore"
|
|
gitignore.write_text("/**/foo.txt\n")
|
|
|
|
parser = GitignoreParser(str(test_dir))
|
|
|
|
# foo.txt at root should NOT be ignored
|
|
assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored"
|
|
|
|
# foo.txt in foo/ should be ignored
|
|
assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored"
|
|
|
|
# foo.txt in foo/bar/ should be ignored (within foo/ subtree)
|
|
assert parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should be ignored"
|
|
|
|
# foo.txt in other/ should NOT be ignored (outside foo/ subtree)
|
|
assert not parser.should_ignore("other/foo.txt"), "other/foo.txt should NOT be ignored by foo/.gitignore"
|
|
|
|
|
|
class TestGitignoreParserPermissionError:
|
|
"""Test PermissionError handling in GitignoreParser."""
|
|
|
|
def setup_method(self):
|
|
self.test_dir = tempfile.mkdtemp()
|
|
self.repo_path = Path(self.test_dir)
|
|
|
|
def teardown_method(self):
|
|
shutil.rmtree(self.test_dir, ignore_errors=True)
|
|
|
|
def test_scandir_permission_error_on_subdirectory(self):
|
|
"""
|
|
Test that GitignoreParser does not crash when a subdirectory
|
|
is not readable (PermissionError on os.scandir).
|
|
|
|
Regression test for https://github.com/oraios/serena/issues/1624
|
|
"""
|
|
# Create a root .gitignore
|
|
gitignore = self.repo_path / ".gitignore"
|
|
gitignore.write_text("*.log\n")
|
|
|
|
# Create an unreadable subdirectory
|
|
unreadable = self.repo_path / "unreadable_dir"
|
|
unreadable.mkdir()
|
|
|
|
# Remove read permissions (Unix only; on Windows this is a no-op)
|
|
old_mode = os.stat(unreadable).st_mode
|
|
os.chmod(unreadable, 0o000)
|
|
|
|
try:
|
|
# This should not raise PermissionError
|
|
parser = GitignoreParser(str(self.repo_path))
|
|
# Parser should still function
|
|
assert parser.should_ignore("test.log")
|
|
finally:
|
|
# Restore permissions so teardown can clean up
|
|
os.chmod(unreadable, old_mode)
|