mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 22:16:23 +00:00
Fix gitignore pattern scoping bug for **/pattern in subdirectories
Previously, when a subdirectory .gitignore file contained patterns starting with "**/", these patterns were incorrectly applied globally instead of being scoped to that subdirectory's tree. For example, if foo/.gitignore contained "**/test.txt", it would incorrectly ignore test.txt files everywhere in the repository, including at the root level and in other subdirectories like bar/test.txt. The bug was in _parse_gitignore_content() where patterns starting with "**/" were left unchanged (line 228: adjusted_pattern = line), making them global instead of scoped. Fixed by ensuring that even "**/pattern" gets the subdirectory prefix: - Before: "**/test.txt" → "**/test.txt" (global) - After: "**/test.txt" → "foo/**/test.txt" (scoped to foo/ subtree) Added comprehensive test cases covering all gitignore pattern scenarios: - Non-anchored patterns (foo.txt) - already worked correctly - Anchored patterns (/foo.txt) - already worked correctly - Double-star patterns (**/foo.txt) - was broken, now fixed - Anchored double-star (/**/foo.txt) - already worked correctly 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
c82fab97c1
commit
de173ce078
2 files changed
+121
-1
No files matched your search
@@ -225,7 +225,8 @@ class GitignoreParser:
|
||||
# Non-anchored patterns can match anywhere below the gitignore directory
|
||||
# We need to preserve this behavior
|
||||
if line.startswith("**/"):
|
||||
adjusted_pattern = line
|
||||
# Even if pattern starts with **, it should still be scoped to the subdirectory
|
||||
adjusted_pattern = os.path.join(rel_dir, line)
|
||||
else:
|
||||
# Add the directory prefix but also allow matching in subdirectories
|
||||
adjusted_pattern = os.path.join(rel_dir, "**", line)
|
||||
|
||||
@@ -587,3 +587,122 @@ src/*.o
|
||||
|
||||
# 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"
|
||||
Reference in new issue
Block a user