Skip to content

Commit 06bb47c

Browse files
authored
Merge pull request #138 from jackwalkerlabs/fix/trailing-globstar-directory
Keep trailing globstars from matching their parent directory
2 parents 7d9fd76 + 760ee4c commit 06bb47c

6 files changed

Lines changed: 33 additions & 13 deletions

File tree

‎CHANGES_1.in.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ New features:
1616

1717
Bug fixes:
1818

19+
- Patterns ending in ``/**`` no longer match their bare parent directory, preserving traversal to re-included children (issue #137, part A).
20+
1921
- `Pull #123`_: Ignore invalid gitignore bracket ranges for `GitIgnoreSpec`.
2022
- `Pull #128`_: Support POSIX character classes (e.g. `[[:alpha:]]`) in gitignore bracket expressions.
2123
- `Issue #129`_ / `Pull #132`_: Fix GitIgnoreSpec re-including files under an excluded directory

‎pathspec/patterns/gitignore/basic.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def pattern_to_regex(
221221
elif pattern_segs is not None:
222222
# Build regular expression from pattern.
223223
try:
224-
regex_parts = cls.__translate_segments(pattern_segs)
224+
regex_parts = cls.__translate_segments(is_dir_pattern, pattern_segs)
225225
except ValueError as e:
226226
raise GitIgnorePatternError((
227227
f"Invalid git pattern: {original_pattern!r}"
@@ -245,10 +245,13 @@ def pattern_to_regex(
245245
return (out_regex, include)
246246

247247
@classmethod
248-
def __translate_segments(cls, pattern_segs: list[str]) -> list[str]:
248+
def __translate_segments(cls, is_dir_pattern: bool, pattern_segs: list[str]) -> list[str]:
249249
"""
250250
Translate the pattern segments to regular expressions.
251251
252+
*is_dir_pattern* (:class:`bool`) is whether the original pattern ends
253+
with a slash.
254+
252255
*pattern_segs* (:class:`list` of :class:`str`) contains the pattern
253256
segments.
254257
@@ -276,8 +279,8 @@ def __translate_segments(cls, pattern_segs: list[str]) -> list[str]:
276279
else:
277280
assert i == end, (i, end)
278281
# A normalized pattern ending with double-asterisks ('**') will match
279-
# any trailing path segments.
280-
out_parts.append('/')
282+
# nonempty trailing path segments, not the parent directory itself.
283+
out_parts.append('/' if is_dir_pattern else '/[^/]')
281284

282285
else:
283286
# Match path segment.

‎pathspec/patterns/gitignore/spec.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,11 @@ def __translate_segments(
317317
else:
318318
assert i == end, (i, end)
319319
# A normalized pattern ending with double-asterisks ('**') will match
320-
# any trailing path segments.
320+
# nonempty trailing path segments, not the parent directory itself.
321321
if is_dir_pattern:
322322
out_parts.append(_DIR_MARK_CG)
323323
else:
324-
out_parts.append('/')
324+
out_parts.append('/[^/]')
325325

326326
else:
327327
# Match path segment.

‎tests/test_03_gitignore_basic.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,12 @@ def test_03_child_double_asterisk(self):
229229
"""
230230
regex, include = GitIgnoreBasicPattern.pattern_to_regex('spam/**')
231231
self.assertTrue(include)
232-
self.assertEqual(regex, '^spam/')
232+
self.assertEqual(regex, '^spam/[^/]')
233233

234234
pattern = GitIgnoreBasicPattern(re.compile(regex), include)
235235
results = set(filter(pattern.match_file, [
236236
'spam/bar',
237+
'spam/',
237238
'foo/spam/bar',
238239
]))
239240
self.assertEqual(results, {'spam/bar'})
@@ -363,7 +364,7 @@ def test_03_duplicate_leading_double_asterisk_edge_case(self):
363364

364365
regex, include = GitIgnoreBasicPattern.pattern_to_regex('**/api/**')
365366
self.assertTrue(include)
366-
self.assertEqual(regex, '^(?s:.+/)?api/')
367+
self.assertEqual(regex, '^(?s:.+/)?api/[^/]')
367368

368369
equiv_regex, include = GitIgnoreBasicPattern.pattern_to_regex('**/**/api/**/**')
369370
self.assertTrue(include)
@@ -843,7 +844,7 @@ def test_14_issue_81_a(self):
843844
"""
844845
pattern = GitIgnoreBasicPattern('!libfoo/**')
845846

846-
self.assertEqual(pattern.regex.pattern, '^libfoo/')
847+
self.assertEqual(pattern.regex.pattern, '^libfoo/[^/]')
847848
self.assertIs(pattern.include, False)
848849
self.assertTrue(pattern.match_file('libfoo/__init__.py'))
849850

‎tests/test_04_gitignore_spec.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,12 @@ def test_03_child_double_asterisk(self):
228228
"""
229229
regex, include = GitIgnoreSpecPattern.pattern_to_regex('spam/**')
230230
self.assertTrue(include)
231-
self.assertEqual(regex, '^spam/')
231+
self.assertEqual(regex, '^spam/[^/]')
232232

233233
pattern = GitIgnoreSpecPattern(re.compile(regex), include)
234234
results = set(filter(pattern.match_file, [
235235
'spam/bar',
236+
'spam/',
236237
'foo/spam/bar',
237238
]))
238239
self.assertEqual(results, {'spam/bar'})
@@ -362,7 +363,7 @@ def test_03_duplicate_leading_double_asterisk_edge_case(self):
362363

363364
regex, include = GitIgnoreSpecPattern.pattern_to_regex('**/api/**')
364365
self.assertTrue(include)
365-
self.assertEqual(regex, '^(?s:.+/)?api/')
366+
self.assertEqual(regex, '^(?s:.+/)?api/[^/]')
366367

367368
equiv_regex, include = GitIgnoreSpecPattern.pattern_to_regex('**/**/api/**/**')
368369
self.assertTrue(include)
@@ -876,7 +877,7 @@ def test_14_issue_81_a(self):
876877
"""
877878
pattern = GitIgnoreSpecPattern('!libfoo/**')
878879

879-
self.assertEqual(pattern.regex.pattern, '^libfoo/')
880+
self.assertEqual(pattern.regex.pattern, '^libfoo/[^/]')
880881
self.assertIs(pattern.include, False)
881882
self.assertTrue(pattern.match_file('libfoo/__init__.py'))
882883

‎tests/test_06_gitignore.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,19 @@ def test_11_issue_134(self):
908908
"node_modules/leaf.txt",
909909
}, debug)
910910

911-
def test_12_issue_139(self):
911+
912+
def test_12_issue_137_a(self):
913+
"""
914+
Test that trailing glob-stars do not ignore parent.
915+
"""
916+
for sub_test in self.parameterize_from_lines(["d/**"]):
917+
with sub_test() as spec:
918+
self.assertFalse(spec.match_file("d/"))
919+
self.assertTrue(spec.match_file("d/file"))
920+
self.assertTrue(spec.match_file("d/child/"))
921+
self.assertTrue(spec.match_file("d/\nfile"))
922+
923+
def test_13_issue_139(self):
912924
"""
913925
Test that glob-stars match newlines in names.
914926
"""
@@ -924,3 +936,4 @@ def test_12_issue_139(self):
924936
for sub_test in self.parameterize_from_lines([pattern]):
925937
with sub_test() as spec:
926938
self.assertTrue(spec.match_file(path))
939+

0 commit comments

Comments
 (0)