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
1 change: 1 addition & 0 deletions CHANGES_1.in.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ New features:

Bug fixes:

- Ignore gitignore patterns ending with an unmatched backslash.
- `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
8 changes: 8 additions & 0 deletions pathspec/patterns/gitignore/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ def pattern_to_regex(
# a literal hash (i.e., '\#').
return (None, None)

elif original_pattern.rstrip('\r\n').endswith('\\'):
pattern_line = original_pattern.rstrip('\r\n')
trailing_backslashes = len(pattern_line) - len(pattern_line.rstrip('\\'))
if trailing_backslashes % 2:
# A pattern ending with an unmatched backslash is invalid and never
# matches.
return (None, None)

if pattern_str.startswith('!'):
# A pattern starting with an exclamation mark ('!') negates the pattern
# (exclude instead of include). Escape the exclamation mark with a back
Expand Down
8 changes: 8 additions & 0 deletions pathspec/patterns/gitignore/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ def pattern_to_regex(
# a literal hash (i.e., '\#').
return (None, None)

elif original_pattern.rstrip('\r\n').endswith('\\'):
pattern_line = original_pattern.rstrip('\r\n')
trailing_backslashes = len(pattern_line) - len(pattern_line.rstrip('\\'))
if trailing_backslashes % 2:
# A pattern ending with an unmatched backslash is invalid and never
# matches.
return (None, None)

elif pattern_str == '/':
# EDGE CASE: According to `git check-ignore` (v2.4.1), a single '/' does
# not match any file.
Expand Down
25 changes: 22 additions & 3 deletions tests/test_03_gitignore_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,15 @@ def test_08_escape(self):
result = GitIgnoreBasicPattern.escape(fname)
self.assertEqual(result, escaped)

def test_09_single_escape_fail(self):
def test_09_single_backslash(self):
"""
Test an escape on a line by itself.
Test that a lone backslash is an invalid pattern that never matches. Git
treats invalid gitignore patterns as null patterns instead of rejecting the
entire set of patterns.
"""
self._check_invalid_pattern('\\')
pattern = GitIgnoreBasicPattern('\\')
self.assertIs(pattern.include, None)
self.assertIs(pattern.regex, None)

def test_09_single_exclamation_mark_fail(self):
"""
Expand Down Expand Up @@ -984,6 +988,21 @@ def test_16_repr_str(self):
self.assertEqual(repr(pattern), "GitIgnoreBasicPattern(pattern='*.py', include=True)")
self.assertEqual(str(pattern), '*.py')

def test_17_trailing_backslash(self):
"""
Test that a pattern ending in one unmatched backslash is invalid, while
two trailing backslashes encode one literal backslash.
"""
for raw_pattern in ['fileA\\', 'fileA\\\n']:
with self.subTest(f"p={raw_pattern!r}"):
pattern = GitIgnoreBasicPattern(raw_pattern)
self.assertIs(pattern.include, None)
self.assertIs(pattern.regex, None)

pattern = GitIgnoreBasicPattern('fileA\\\\')
self.assertIs(pattern.include, True)
self.assertIsNotNone(pattern.match_file('fileA\\'))

def test_globstars_match_newlines(self):
for pattern, path in [
("target", "line\nbreak/target"),
Expand Down
25 changes: 22 additions & 3 deletions tests/test_04_gitignore_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,11 +634,15 @@ def test_08_escape_trailing_space(self):
pattern = GitIgnoreSpecPattern(escaped)
self.assertEqual(set(filter(pattern.match_file, [fname])), {fname}, (fname, escaped))

def test_09_single_escape_fail(self):
def test_09_single_backslash(self):
"""
Test an escape on a line by itself.
Test that a lone backslash is an invalid pattern that never matches. Git
treats invalid gitignore patterns as null patterns instead of rejecting the
entire set of patterns.
"""
self._check_invalid_pattern('\\')
pattern = GitIgnoreSpecPattern('\\')
self.assertIs(pattern.include, None)
self.assertIs(pattern.regex, None)

def test_09_single_exclamation_mark_fail(self):
"""
Expand Down Expand Up @@ -1111,3 +1115,18 @@ def test_16_posix_class_e_invalid(self):
pattern = GitIgnoreSpecPattern(raw_pattern)
self.assertIs(pattern.include, None)
self.assertIs(pattern.regex, None)

def test_17_trailing_backslash(self):
"""
Test that a pattern ending in one unmatched backslash is invalid, while
two trailing backslashes encode one literal backslash.
"""
for raw_pattern in ['fileA\\', 'fileA\\\n']:
with self.subTest(f"p={raw_pattern!r}"):
pattern = GitIgnoreSpecPattern(raw_pattern)
self.assertIs(pattern.include, None)
self.assertIs(pattern.regex, None)

pattern = GitIgnoreSpecPattern('fileA\\\\')
self.assertIs(pattern.include, True)
self.assertIsNotNone(pattern.match_file('fileA\\'))
17 changes: 17 additions & 0 deletions tests/test_05_pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,3 +1132,20 @@ def test_11_repr(self):
repr(spec),
"PathSpec(patterns=[GitIgnoreBasicPattern(pattern='*.py', include=True)], backend='simple')",
)

def test_12_trailing_backslash(self):
"""
Test that an invalid trailing-backslash pattern does not prevent a later
valid pattern from being used.
"""
spec = PathSpec.from_lines('gitignore', [
'fileA\\',
'*.log',
], backend='simple')

self.assertIs(spec.patterns[0].include, None)
self.assertIs(spec.patterns[0].regex, None)
self.assertIs(spec.match_file('fileA'), False)
self.assertIs(spec.match_file('fileA\\'), False)
self.assertIs(spec.match_file('fileA.log'), True)
self.assertIs(spec.match_file('fileA.txt'), False)
17 changes: 17 additions & 0 deletions tests/test_06_gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -955,3 +955,20 @@ def test_13_issue_139(self):
for sub_test in self.parameterize_from_lines([pattern]):
with sub_test() as spec:
self.assertTrue(spec.match_file(path))

def test_14_trailing_backslash(self):
"""
Test that an invalid trailing-backslash pattern does not prevent a later
valid pattern from being used.
"""
spec = GitIgnoreSpec.from_lines([
'fileA\\',
'*.log',
], backend='simple')

self.assertIs(spec.patterns[0].include, None)
self.assertIs(spec.patterns[0].regex, None)
self.assertIs(spec.match_file('fileA'), False)
self.assertIs(spec.match_file('fileA\\'), False)
self.assertIs(spec.match_file('fileA.log'), True)
self.assertIs(spec.match_file('fileA.txt'), False)