Skip to content
Merged
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: 2 additions & 0 deletions CHANGES_1.in.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ New features:

Bug fixes:

- Patterns ending in ``/**`` no longer match their bare parent directory, preserving traversal to re-included children (issue #137, part A).

- `Pull #123`_: Ignore invalid gitignore bracket ranges for `GitIgnoreSpec`.
- `Pull #128`_: Support POSIX character classes (e.g. `[[:alpha:]]`) in gitignore bracket expressions.
- `Issue #129`_ / `Pull #132`_: Fix GitIgnoreSpec re-including files under an excluded directory
Expand Down
11 changes: 7 additions & 4 deletions pathspec/patterns/gitignore/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def pattern_to_regex(
elif pattern_segs is not None:
# Build regular expression from pattern.
try:
regex_parts = cls.__translate_segments(pattern_segs)
regex_parts = cls.__translate_segments(is_dir_pattern, pattern_segs)
except ValueError as e:
raise GitIgnorePatternError((
f"Invalid git pattern: {original_pattern!r}"
Expand All @@ -245,10 +245,13 @@ def pattern_to_regex(
return (out_regex, include)

@classmethod
def __translate_segments(cls, pattern_segs: list[str]) -> list[str]:
def __translate_segments(cls, is_dir_pattern: bool, pattern_segs: list[str]) -> list[str]:
"""
Translate the pattern segments to regular expressions.

*is_dir_pattern* (:class:`bool`) is whether the original pattern ends
with a slash.

*pattern_segs* (:class:`list` of :class:`str`) contains the pattern
segments.

Expand Down Expand Up @@ -276,8 +279,8 @@ def __translate_segments(cls, pattern_segs: list[str]) -> list[str]:
else:
assert i == end, (i, end)
# A normalized pattern ending with double-asterisks ('**') will match
# any trailing path segments.
out_parts.append('/')
# nonempty trailing path segments, not the parent directory itself.
out_parts.append('/' if is_dir_pattern else '/[^/]')

else:
# Match path segment.
Expand Down
4 changes: 2 additions & 2 deletions pathspec/patterns/gitignore/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,11 @@ def __translate_segments(
else:
assert i == end, (i, end)
# A normalized pattern ending with double-asterisks ('**') will match
# any trailing path segments.
# nonempty trailing path segments, not the parent directory itself.
if is_dir_pattern:
out_parts.append(_DIR_MARK_CG)
else:
out_parts.append('/')
out_parts.append('/[^/]')

else:
# Match path segment.
Expand Down
7 changes: 4 additions & 3 deletions tests/test_03_gitignore_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,12 @@ def test_03_child_double_asterisk(self):
"""
regex, include = GitIgnoreBasicPattern.pattern_to_regex('spam/**')
self.assertTrue(include)
self.assertEqual(regex, '^spam/')
self.assertEqual(regex, '^spam/[^/]')

pattern = GitIgnoreBasicPattern(re.compile(regex), include)
results = set(filter(pattern.match_file, [
'spam/bar',
'spam/',
'foo/spam/bar',
]))
self.assertEqual(results, {'spam/bar'})
Expand Down Expand Up @@ -363,7 +364,7 @@ def test_03_duplicate_leading_double_asterisk_edge_case(self):

regex, include = GitIgnoreBasicPattern.pattern_to_regex('**/api/**')
self.assertTrue(include)
self.assertEqual(regex, '^(?s:.+/)?api/')
self.assertEqual(regex, '^(?s:.+/)?api/[^/]')

equiv_regex, include = GitIgnoreBasicPattern.pattern_to_regex('**/**/api/**/**')
self.assertTrue(include)
Expand Down Expand Up @@ -843,7 +844,7 @@ def test_14_issue_81_a(self):
"""
pattern = GitIgnoreBasicPattern('!libfoo/**')

self.assertEqual(pattern.regex.pattern, '^libfoo/')
self.assertEqual(pattern.regex.pattern, '^libfoo/[^/]')
self.assertIs(pattern.include, False)
self.assertTrue(pattern.match_file('libfoo/__init__.py'))

Expand Down
7 changes: 4 additions & 3 deletions tests/test_04_gitignore_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,12 @@ def test_03_child_double_asterisk(self):
"""
regex, include = GitIgnoreSpecPattern.pattern_to_regex('spam/**')
self.assertTrue(include)
self.assertEqual(regex, '^spam/')
self.assertEqual(regex, '^spam/[^/]')

pattern = GitIgnoreSpecPattern(re.compile(regex), include)
results = set(filter(pattern.match_file, [
'spam/bar',
'spam/',
'foo/spam/bar',
]))
self.assertEqual(results, {'spam/bar'})
Expand Down Expand Up @@ -362,7 +363,7 @@ def test_03_duplicate_leading_double_asterisk_edge_case(self):

regex, include = GitIgnoreSpecPattern.pattern_to_regex('**/api/**')
self.assertTrue(include)
self.assertEqual(regex, '^(?s:.+/)?api/')
self.assertEqual(regex, '^(?s:.+/)?api/[^/]')

equiv_regex, include = GitIgnoreSpecPattern.pattern_to_regex('**/**/api/**/**')
self.assertTrue(include)
Expand Down Expand Up @@ -876,7 +877,7 @@ def test_14_issue_81_a(self):
"""
pattern = GitIgnoreSpecPattern('!libfoo/**')

self.assertEqual(pattern.regex.pattern, '^libfoo/')
self.assertEqual(pattern.regex.pattern, '^libfoo/[^/]')
self.assertIs(pattern.include, False)
self.assertTrue(pattern.match_file('libfoo/__init__.py'))

Expand Down
15 changes: 14 additions & 1 deletion tests/test_06_gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,19 @@ def test_11_issue_134(self):
"node_modules/leaf.txt",
}, debug)

def test_12_issue_139(self):

def test_12_issue_137_a(self):
"""
Test that trailing glob-stars do not ignore parent.
"""
for sub_test in self.parameterize_from_lines(["d/**"]):
with sub_test() as spec:
self.assertFalse(spec.match_file("d/"))
self.assertTrue(spec.match_file("d/file"))
self.assertTrue(spec.match_file("d/child/"))
self.assertTrue(spec.match_file("d/\nfile"))

def test_13_issue_139(self):
"""
Test that glob-stars match newlines in names.
"""
Expand All @@ -924,3 +936,4 @@ def test_12_issue_139(self):
for sub_test in self.parameterize_from_lines([pattern]):
with sub_test() as spec:
self.assertTrue(spec.match_file(path))

Loading