Skip to content
Open
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
12 changes: 10 additions & 2 deletions pathspec/patterns/gitignore/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@
This regular expression matches the optional directory marker and sub-path.
"""

_MATCH_ALL = f'^(?:.+/)?[^/]+{_DIR_MARK_OPT}'
"""
This regular expression matches every path. It is the expansion of the patterns
"*" and "**" (i.e., "**/{any name}"), and it has to capture the directory marker
like any other pattern so that :class:`.GitIgnoreSpec` can tell a directory
match from a file match.
"""


class GitIgnoreSpecPattern(_GitIgnoreBasePattern):
"""
Expand Down Expand Up @@ -121,7 +129,7 @@ def __normalize_segments(
return (None, _DIR_MARK_CG)
else:
# The pattern "**" will match every path. Special case this pattern.
return (None, '.')
return (None, _MATCH_ALL)

elif (
seg_count == 2
Expand All @@ -130,7 +138,7 @@ def __normalize_segments(
):
# The pattern "*" will be normalized to "**/*" and will match every
# path. Special case this pattern for efficiency.
return (None, '.')
return (None, _MATCH_ALL)

elif (
seg_count == 3
Expand Down
26 changes: 22 additions & 4 deletions tests/test_04_gitignore_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
_BYTES_ENCODING)
from pathspec.patterns.gitignore.spec import (
GitIgnoreSpecPattern,
_DIR_MARK,
_DIR_MARK_CG,
_DIR_MARK_OPT)
_DIR_MARK_OPT,
_MATCH_ALL)
from pathspec.patterns.gitwildmatch import (
GitWildMatchPattern)
from pathspec.util import (
Expand Down Expand Up @@ -275,7 +277,7 @@ def test_03_only_double_asterisk(self):
"""
regex, include = GitIgnoreSpecPattern.pattern_to_regex('**')
self.assertTrue(include)
self.assertEqual(regex, '.')
self.assertEqual(regex, _MATCH_ALL)

pattern = GitIgnoreSpecPattern(re.compile(regex), include)
results = set(filter(pattern.match_file, [
Expand Down Expand Up @@ -332,7 +334,7 @@ def test_03_duplicate_leading_double_asterisk_edge_case(self):
"""
regex, include = GitIgnoreSpecPattern.pattern_to_regex('**')
self.assertTrue(include)
self.assertEqual(regex, '.')
self.assertEqual(regex, _MATCH_ALL)

equiv_regex, include = GitIgnoreSpecPattern.pattern_to_regex('**/**')
self.assertTrue(include)
Expand Down Expand Up @@ -753,7 +755,23 @@ def test_12_asterisk_1_regex(self):
"""
regex, include = GitIgnoreSpecPattern.pattern_to_regex('*')
self.assertTrue(include)
self.assertEqual(regex, '.')
self.assertEqual(regex, _MATCH_ALL)

def test_12_asterisk_1b_regex_marks_directories(self):
"""
Test that the relative asterisk path pattern captures the directory marker.

Without the marker, "*" outranks a later directory-only pattern (e.g.
"!*/") and :class:`.GitIgnoreSpec` reports a directory as ignored where Git
does not.
"""
regex, include = GitIgnoreSpecPattern.pattern_to_regex('*')
self.assertTrue(include)

compiled = re.compile(regex)
self.assertIsNotNone(compiled.search('dirA/').group(_DIR_MARK))
self.assertIsNone(compiled.search('fileA').group(_DIR_MARK))
self.assertIsNone(compiled.search('dirA/fileB').group(_DIR_MARK))

def test_12_asterisk_2_regex_equivalent(self):
"""
Expand Down
41 changes: 41 additions & 0 deletions tests/test_06_gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,47 @@ def test_02_dir_exclusions(self):
'test2/b.bin',
}, debug)

def test_02_dir_reinclusion_whitelist(self):
"""
Test that a directory re-included by a directory-only pattern is not
reported as ignored.

The whitelist idiom ("*" ignores everything, "!*/" keeps descending into
directories, "!*.py" keeps the files of interest) only works if asking
about the directory answers what Git answers. A consumer asks about the
directory precisely to decide whether to descend, so reporting "sub/" as
ignored silently drops every file below it.
"""
for sub_test in self.parameterize_from_lines([
'*',
'!*/',
'!*.py',
]):
with sub_test() as spec:
# Confirmed results with git check-ignore (v2.55.0).
dirs = {
'sub/',
'sub/d/',
}
self.assertEqual({_dir for _dir in dirs if spec.match_file(_dir)}, set())

files = {
'a.py',
'a.txt',
'sub/b.py',
'sub/b.txt',
'sub/d/c.py',
}

results = list(spec.check_files(files))
ignores = get_includes(results)
debug = debug_results(spec, results)

self.assertEqual(ignores, {
'a.txt',
'sub/b.txt',
}, debug)

def test_02_file_exclusions(self):
"""
Test file exclusions.
Expand Down