diff --git a/CHANGES_1.in.rst b/CHANGES_1.in.rst index 67a0bba..29e3126 100644 --- a/CHANGES_1.in.rst +++ b/CHANGES_1.in.rst @@ -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 diff --git a/pathspec/patterns/gitignore/basic.py b/pathspec/patterns/gitignore/basic.py index 44b2400..67d9a02 100644 --- a/pathspec/patterns/gitignore/basic.py +++ b/pathspec/patterns/gitignore/basic.py @@ -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 diff --git a/pathspec/patterns/gitignore/spec.py b/pathspec/patterns/gitignore/spec.py index f8bd159..568bf72 100644 --- a/pathspec/patterns/gitignore/spec.py +++ b/pathspec/patterns/gitignore/spec.py @@ -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. diff --git a/tests/test_03_gitignore_basic.py b/tests/test_03_gitignore_basic.py index 9d4c960..3b05b9c 100644 --- a/tests/test_03_gitignore_basic.py +++ b/tests/test_03_gitignore_basic.py @@ -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): """ @@ -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"), diff --git a/tests/test_04_gitignore_spec.py b/tests/test_04_gitignore_spec.py index f43b32c..cd06d8f 100644 --- a/tests/test_04_gitignore_spec.py +++ b/tests/test_04_gitignore_spec.py @@ -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): """ @@ -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\\')) diff --git a/tests/test_05_pathspec.py b/tests/test_05_pathspec.py index 08a1310..898916e 100644 --- a/tests/test_05_pathspec.py +++ b/tests/test_05_pathspec.py @@ -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) diff --git a/tests/test_06_gitignore.py b/tests/test_06_gitignore.py index aea7ec7..c5e1a2b 100644 --- a/tests/test_06_gitignore.py +++ b/tests/test_06_gitignore.py @@ -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)