|
|
@@ -37,7 +37,7 @@ from dulwich.sparse_patterns import (
|
|
|
compute_included_paths_cone,
|
|
|
compute_included_paths_full,
|
|
|
determine_included_paths,
|
|
|
- match_gitignore_patterns,
|
|
|
+ match_sparse_patterns,
|
|
|
parse_sparse_patterns,
|
|
|
)
|
|
|
|
|
|
@@ -57,44 +57,41 @@ class ParseSparsePatternsTests(TestCase):
|
|
|
parsed = parse_sparse_patterns(lines)
|
|
|
self.assertEqual(parsed, [])
|
|
|
|
|
|
- def test_simple_patterns(self):
|
|
|
+ def test_sparse_pattern_combos(self):
|
|
|
lines = [
|
|
|
- "*.py",
|
|
|
- "!*.md",
|
|
|
- "/docs/",
|
|
|
- "!/docs/images/",
|
|
|
- ]
|
|
|
- parsed = parse_sparse_patterns(lines)
|
|
|
- self.assertEqual(len(parsed), 4)
|
|
|
-
|
|
|
- self.assertEqual(parsed[0], ("*.py", False, False, False)) # include *.py
|
|
|
- self.assertEqual(parsed[1], ("*.md", True, False, False)) # exclude *.md
|
|
|
- self.assertEqual(parsed[2], ("docs", False, True, True)) # anchored, dir_only
|
|
|
- self.assertEqual(parsed[3], ("docs/images", True, True, True))
|
|
|
-
|
|
|
- def test_trailing_slash_dir(self):
|
|
|
- lines = [
|
|
|
- "src/",
|
|
|
+ "*.py", # Python files anywhere
|
|
|
+ "!*.md", # markdown files anywhere
|
|
|
+ "/docs/", # root docs dir
|
|
|
+ "!/docs/images/", # no root docs/images subdir
|
|
|
+ "src/", # src dir anywhere
|
|
|
+ "/*.toml", # root TOML files
|
|
|
+ "!/*.bak", # no root backup files
|
|
|
+ "!data/", # no data dirs anywhere
|
|
|
]
|
|
|
parsed = parse_sparse_patterns(lines)
|
|
|
- # "src/" => (pattern="src", negation=False, dir_only=True, anchored=False)
|
|
|
- self.assertEqual(parsed, [("src", False, True, False)])
|
|
|
+ self.assertEqual(len(parsed), 8)
|
|
|
|
|
|
- def test_negation_anchor(self):
|
|
|
- lines = [
|
|
|
- "!/foo.txt",
|
|
|
- ]
|
|
|
- parsed = parse_sparse_patterns(lines)
|
|
|
- # => (pattern="foo.txt", negation=True, dir_only=False, anchored=True)
|
|
|
- self.assertEqual(parsed, [("foo.txt", True, False, True)])
|
|
|
+ # Returns a 4-tuple of: (pattern, negation, dir_only, anchored)
|
|
|
+ self.assertEqual(parsed[0], ("*.py", False, False, False)) # _,_,_
|
|
|
+ self.assertEqual(parsed[1], ("*.md", True, False, False)) # N,_,_
|
|
|
+ self.assertEqual(parsed[2], ("docs", False, True, True)) # _,D,A
|
|
|
+ self.assertEqual(parsed[3], ("docs/images", True, True, True)) # N,D,A
|
|
|
+ self.assertEqual(parsed[4], ("src", False, True, False)) # _,D,_
|
|
|
+ self.assertEqual(parsed[5], ("*.toml", False, False, True)) # _,_,A
|
|
|
+ self.assertEqual(parsed[6], ("*.bak", True, False, True)) # N,_,A
|
|
|
+ self.assertEqual(parsed[7], ("data", True, True, False)) # N,D,_
|
|
|
|
|
|
|
|
|
-class MatchGitignorePatternsTests(TestCase):
|
|
|
- """Test the match_gitignore_patterns function."""
|
|
|
+class MatchSparsePatternsTests(TestCase):
|
|
|
+ """Test the match_sparse_patterns function."""
|
|
|
|
|
|
+ # def match_sparse_patterns(path_str, parsed_patterns, path_is_dir=False):
|
|
|
def test_no_patterns_returns_excluded(self):
|
|
|
"""If no patterns are provided, by default we treat the path as excluded."""
|
|
|
- self.assertFalse(match_gitignore_patterns("anyfile.py", []))
|
|
|
+ self.assertFalse(match_sparse_patterns("foo.py", [], path_is_dir=False))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/", [], path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B/", [], path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B/bar.md", [], path_is_dir=False))
|
|
|
|
|
|
def test_last_match_wins(self):
|
|
|
"""Checks that the last pattern to match determines included vs excluded."""
|
|
|
@@ -106,78 +103,95 @@ class MatchGitignorePatternsTests(TestCase):
|
|
|
)
|
|
|
# "foo.py" matches first pattern => included
|
|
|
# then matches second pattern => excluded
|
|
|
- self.assertFalse(match_gitignore_patterns("foo.py", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("foo.py", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/foo.py", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B/foo.py", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("bar.py", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/bar.py", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B/bar.py", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("bar.md", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/bar.md", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns(".cache", parsed, path_is_dir=True))
|
|
|
|
|
|
def test_dir_only(self):
|
|
|
"""A pattern with a trailing slash should only match directories and subdirectories."""
|
|
|
parsed = parse_sparse_patterns(["docs/"])
|
|
|
- # Because we set path_is_dir=False, it won't match
|
|
|
- self.assertTrue(
|
|
|
- match_gitignore_patterns("docs/readme.md", parsed, path_is_dir=False)
|
|
|
- )
|
|
|
- self.assertTrue(match_gitignore_patterns("docs", parsed, path_is_dir=True))
|
|
|
+ # The directory pattern is not rooted, so can be at any level
|
|
|
+ self.assertTrue(match_sparse_patterns("docs", parsed, path_is_dir=True))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/docs", parsed, path_is_dir=True))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B/docs", parsed, path_is_dir=True))
|
|
|
# Even if the path name is "docs", if it's a file, won't match:
|
|
|
- self.assertFalse(match_gitignore_patterns("docs", parsed, path_is_dir=False))
|
|
|
+ self.assertFalse(match_sparse_patterns("docs", parsed, path_is_dir=False))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/docs", parsed, path_is_dir=False))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B/docs", parsed, path_is_dir=False))
|
|
|
+ # Subfiles and subdirs of the included dir should match
|
|
|
+ self.assertTrue(match_sparse_patterns("docs/x.md", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("docs/A/x.md", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("docs/A/B/x.md", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("docs/A", parsed, path_is_dir=True))
|
|
|
+ self.assertTrue(match_sparse_patterns("docs/A/B", parsed, path_is_dir=True))
|
|
|
+ self.assertTrue(match_sparse_patterns("docs", parsed, path_is_dir=True))
|
|
|
|
|
|
def test_anchored(self):
|
|
|
"""Anchored patterns match from the start of the path only."""
|
|
|
- parsed = parse_sparse_patterns(["/foo"])
|
|
|
- self.assertTrue(match_gitignore_patterns("foo", parsed))
|
|
|
+ parsed = parse_sparse_patterns(["/foo"]) # Can be file or dir, must be at root
|
|
|
+ self.assertTrue(match_sparse_patterns("foo", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("foo", parsed, path_is_dir=True))
|
|
|
# But "some/foo" doesn't match because anchored requires start
|
|
|
- self.assertFalse(match_gitignore_patterns("some/foo", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/foo", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/foo", parsed, path_is_dir=True))
|
|
|
|
|
|
- def test_unanchored_uses_fnmatch(self):
|
|
|
+ def test_unanchored(self):
|
|
|
parsed = parse_sparse_patterns(["foo"])
|
|
|
- self.assertTrue(match_gitignore_patterns("some/foo", parsed))
|
|
|
- self.assertFalse(match_gitignore_patterns("some/bar", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("foo", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("foo", parsed, path_is_dir=True))
|
|
|
+ # But "some/foo" doesn't match because anchored requires start
|
|
|
+ self.assertTrue(match_sparse_patterns("A/foo", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/foo", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("bar", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/bar", parsed))
|
|
|
|
|
|
def test_anchored_empty_pattern(self):
|
|
|
"""Test handling of empty pattern with anchoring (e.g., '/')."""
|
|
|
+ # `/` should be recursive match of all files
|
|
|
parsed = parse_sparse_patterns(["/"])
|
|
|
- # Check the structure of the parsed empty pattern first
|
|
|
- self.assertEqual(parsed, [("", False, False, True)])
|
|
|
- # When the pattern is empty with anchoring, it's continued (skipped) in match_gitignore_patterns
|
|
|
- # for non-empty paths but for empty string it might match due to empty string comparisons
|
|
|
- self.assertFalse(match_gitignore_patterns("foo", parsed))
|
|
|
- # An empty string with empty pattern will match (implementation detail)
|
|
|
- self.assertTrue(match_gitignore_patterns("", parsed))
|
|
|
-
|
|
|
- def test_anchored_dir_only_exact_match(self):
|
|
|
- """Test anchored directory-only patterns with exact matching."""
|
|
|
+ self.assertEqual(parsed, [("", False, False, True)]) # anchored
|
|
|
+ self.assertTrue(match_sparse_patterns("", parsed, path_is_dir=True))
|
|
|
+ self.assertTrue(match_sparse_patterns("A", parsed, path_is_dir=True))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B", parsed, path_is_dir=True))
|
|
|
+ self.assertTrue(match_sparse_patterns("foo", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/foo", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B/foo", parsed))
|
|
|
+
|
|
|
+ def test_anchored_dir_only(self):
|
|
|
+ """Test anchored directory-only patterns."""
|
|
|
parsed = parse_sparse_patterns(["/docs/"])
|
|
|
- # Test with exact match "docs" and path_is_dir=True
|
|
|
- self.assertTrue(match_gitignore_patterns("docs", parsed, path_is_dir=True))
|
|
|
- # Test with "docs/" (exact match + trailing slash)
|
|
|
- self.assertTrue(match_gitignore_patterns("docs/", parsed, path_is_dir=True))
|
|
|
-
|
|
|
- def test_complex_anchored_patterns(self):
|
|
|
- """Test more complex anchored pattern matching."""
|
|
|
- parsed = parse_sparse_patterns(["/dir/subdir"])
|
|
|
- # Test exact match
|
|
|
- self.assertTrue(match_gitignore_patterns("dir/subdir", parsed))
|
|
|
- # Test subdirectory path
|
|
|
- self.assertTrue(match_gitignore_patterns("dir/subdir/file.txt", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("docs", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("docs", parsed)) # file named docs
|
|
|
+ self.assertFalse(match_sparse_patterns("A", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/docs", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/docs", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B/docs", parsed))
|
|
|
+ self.assertFalse(match_sparse_patterns("A/B/docs", parsed, path_is_dir=True))
|
|
|
+
|
|
|
+ def test_anchored_subpath(self):
|
|
|
+ """Test anchored subpath pattern matching."""
|
|
|
+ parsed = parse_sparse_patterns(["/A/B"])
|
|
|
+ # TODO: should this also match the dir "A" (positively?)
|
|
|
+ # self.assertTrue(match_sparse_patterns("A", parsed, path_is_dir=True))
|
|
|
+ # self.assertFalse(match_sparse_patterns("A", parsed, path_is_dir=False))
|
|
|
+ # Test exact match (both as file and dir, not dir-only pattern)
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B", parsed, path_is_dir=True))
|
|
|
+ # Test subdirectory path (file and dir)
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B/file.txt", parsed))
|
|
|
+ self.assertTrue(match_sparse_patterns("A/B/C", parsed, path_is_dir=True))
|
|
|
# Test non-matching path
|
|
|
- self.assertFalse(match_gitignore_patterns("otherdir/subdir", parsed))
|
|
|
-
|
|
|
- def test_pattern_matching_edge_cases(self):
|
|
|
- """Test various edge cases in pattern matching."""
|
|
|
- # Test exact equality with an anchored pattern
|
|
|
- parsed = parse_sparse_patterns(["/foo"])
|
|
|
- self.assertTrue(match_gitignore_patterns("foo", parsed))
|
|
|
-
|
|
|
- # Test with path_is_dir=True
|
|
|
- self.assertTrue(match_gitignore_patterns("foo", parsed, path_is_dir=True))
|
|
|
-
|
|
|
- # Test exact match with pattern with dir_only=True
|
|
|
- parsed = parse_sparse_patterns(["/bar/"])
|
|
|
- self.assertTrue(match_gitignore_patterns("bar", parsed, path_is_dir=True))
|
|
|
-
|
|
|
- # Test startswith match for anchored pattern
|
|
|
- parsed = parse_sparse_patterns(["/prefix"])
|
|
|
- self.assertTrue(
|
|
|
- match_gitignore_patterns("prefix/subdirectory/file.txt", parsed)
|
|
|
- )
|
|
|
+ self.assertFalse(match_sparse_patterns("X", parsed, path_is_dir=True))
|
|
|
+ self.assertFalse(match_sparse_patterns("X/Y", parsed, path_is_dir=True))
|
|
|
|
|
|
|
|
|
class ComputeIncludedPathsFullTests(TestCase):
|