diff --git a/pathspec/patterns/gitignore/base.py b/pathspec/patterns/gitignore/base.py index 7373a4e..02cdb42 100644 --- a/pathspec/patterns/gitignore/base.py +++ b/pathspec/patterns/gitignore/base.py @@ -105,6 +105,14 @@ def escape(s: AnyStr) -> AnyStr: # Reference: https://git-scm.com/docs/gitignore#_pattern_format out_string = ''.join((f"\\{x}" if x in '\\[]!*#?' else x) for x in string) + # EDGE CASE: Git strips trailing spaces from a pattern unless they are + # escaped with a backslash. Escape them so an escaped filename that ends + # with a space still matches that file. + stripped = out_string.rstrip(' ') + trailing = len(out_string) - len(stripped) + if trailing: + out_string = stripped + '\\ ' * trailing + if return_type is bytes: out_bytes = out_string.encode(_BYTES_ENCODING) return out_bytes # type: ignore[return-value] diff --git a/tests/test_02_gitignore_base.py b/tests/test_02_gitignore_base.py index f315194..3612b65 100644 --- a/tests/test_02_gitignore_base.py +++ b/tests/test_02_gitignore_base.py @@ -20,7 +20,7 @@ def test_01_escape_bytes(self): Test escaping binary strings. """ byte_to_escaped = {__b: b'\\' + __b for __b in ( - __c.encode(_BYTES_ENCODING) for __c in '\\[]!*#?' + __c.encode(_BYTES_ENCODING) for __c in '\\[]!*#? ' )} for char_ord in range(256): char_byte = chr(char_ord).encode(_BYTES_ENCODING) @@ -32,7 +32,7 @@ def test_01_escape_str(self): """ Test escaping unicode strings. """ - char_to_escaped = {__c: f"\\{__c}" for __c in '\\[]!*#?'} + char_to_escaped = {__c: f"\\{__c}" for __c in '\\[]!*#? '} for char_ord in range(128): char = chr(char_ord) escape_val = _GitIgnoreBasePattern.escape(char) diff --git a/tests/test_04_gitignore_spec.py b/tests/test_04_gitignore_spec.py index 9851c60..618f658 100644 --- a/tests/test_04_gitignore_spec.py +++ b/tests/test_04_gitignore_spec.py @@ -621,6 +621,18 @@ def test_08_escape(self): result = GitIgnoreSpecPattern.escape(fname) self.assertEqual(result, escaped) + def test_08_escape_trailing_space(self): + """ + Test that escaping a filename with a trailing space keeps the space, so + the escaped pattern still matches the file. Git strips unescaped trailing + spaces from a pattern. + """ + for fname in ['foo ', 'trailing ', ' ']: + escaped = GitIgnoreSpecPattern.escape(fname) + self.assertTrue(escaped.endswith('\\ '), (fname, escaped)) + pattern = GitIgnoreSpecPattern(escaped) + self.assertEqual(set(filter(pattern.match_file, [fname])), {fname}, (fname, escaped)) + def test_09_single_escape_fail(self): """ Test an escape on a line by itself.