diff --git a/CHANGES.rst b/CHANGES.rst index c043455..aa443c9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,10 +11,12 @@ Bug fixes: - Fix documentation on `pathspec.pattern.RegexPattern.match_file()`. - `Issue #60`_: Remove redundant wheel dep from pyproject.toml. - `Issue #61`_: Dist failure for Fedora, CentOS, EPEL. +- `Issue #62`_: Since version 0.10.0 pure wildcard does not work in some cases. .. _`Issue #60`: https://github.com/cpburnz/python-pathspec/pull/60 .. _`Issue #61`: https://github.com/cpburnz/python-pathspec/issues/61 +.. _`Issue #62`: https://github.com/cpburnz/python-pathspec/issues/62 0.10.0 (2022-08-30) diff --git a/pathspec/_meta.py b/pathspec/_meta.py index 95a6e26..799b316 100644 --- a/pathspec/_meta.py +++ b/pathspec/_meta.py @@ -45,6 +45,7 @@ "ichard26 ", "jack1142 ", "mgorny ", + "bzakdd ", ] __license__ = "MPL 2.0" __version__ = "0.10.1.dev1" diff --git a/pathspec/patterns/gitwildmatch.py b/pathspec/patterns/gitwildmatch.py index a7d8869..c6b6fb7 100644 --- a/pathspec/patterns/gitwildmatch.py +++ b/pathspec/patterns/gitwildmatch.py @@ -41,7 +41,7 @@ class GitWildMatchPattern(RegexPattern): def pattern_to_regex( cls, pattern: AnyStr, - ) -> Tuple[Optional[AnyStr], Optional[bool]]: # TODO: Change to NamedTuple + ) -> Tuple[Optional[AnyStr], Optional[bool]]: """ Convert the pattern into a regular expression. @@ -143,14 +143,6 @@ def pattern_to_regex( # all. This must be because the pattern is invalid. raise GitWildMatchPatternError(f"Invalid git pattern: {original_pattern!r}") - if pattern_segs[-1] == '*' and len(pattern_segs) > 1: - # A pattern ending with "/*" should match all descendant paths - # of the directory, not just direct children. This is equivalent - # to "{pattern}/**". This behavior of git contradicts its - # documentation. So, set last segment to a double-asterisk to - # include all descendants. - pattern_segs[-1] = '**' - if not pattern_segs[-1] and len(pattern_segs) > 1: # A pattern ending with a slash ('/') will match all descendant # paths if it is a directory but not if it is a regular file. @@ -188,7 +180,15 @@ def pattern_to_regex( # Match single path segment. if need_slash: output.append('/') + output.append('[^/]+') + + if i == end: + # A pattern ending without a slash ('/') will match a file + # or a directory (with paths underneath it). E.g., "foo" + # matches "foo", "foo/bar", "foo/bar/baz", etc. + output.append('(?:(?P/).*)?') + need_slash = True else: diff --git a/tests/test_gitignore.py b/tests/test_gitignore.py index 75dba79..04902d2 100644 --- a/tests/test_gitignore.py +++ b/tests/test_gitignore.py @@ -294,3 +294,20 @@ def test_03_issue_19_c(self): 'dirD/fileE', 'dirD/fileF', }) + + def test_04_issue_62(self): + """ + Test including all files and excluding a directory. + """ + spec = GitIgnoreSpec.from_lines([ + '*', + '!product_dir/', + ]) + results = set(spec.match_files([ + 'anydir/file.txt', + 'product_dir/file.txt', + ])) + self.assertEqual(results, { + 'anydir/file.txt', + 'product_dir/file.txt', + }) diff --git a/tests/test_gitwildmatch.py b/tests/test_gitwildmatch.py index 46f4155..8563d95 100644 --- a/tests/test_gitwildmatch.py +++ b/tests/test_gitwildmatch.py @@ -705,3 +705,48 @@ def test_11_match_sub_directory_3(self): 'dirG/dirH/fileJ', 'dirG/fileO', }) + + def test_12_asterisk_1_regex(self): + """ + Test a relative asterisk path pattern's regular expression. + """ + regex, include = GitWildMatchPattern.pattern_to_regex('*') + self.assertTrue(include) + self.assertEqual(regex, f'^(?:.+/)?[^/]+{RE_SUB}$') + + def test_12_asterisk_2_regex_equivalent(self): + """ + Test a path pattern equivalent to the relative asterisk using double + asterisk. + """ + regex, include = GitWildMatchPattern.pattern_to_regex('*') + self.assertTrue(include) + + equiv_regex, include = GitWildMatchPattern.pattern_to_regex('**/*') + self.assertTrue(include) + + self.assertEqual(regex, equiv_regex) + + def test_12_asterisk_3_child(self): + """ + Test a relative asterisk path pattern matching a direct child path. + """ + pattern = GitWildMatchPattern('*') + results = set(filter(pattern.match_file, [ + 'file.txt', + ])) + self.assertEqual(results, { + 'file.txt', + }) + + def test_12_asterisk_4_descendant(self): + """ + Test a relative asterisk path pattern matching a descendant path. + """ + pattern = GitWildMatchPattern('*') + results = set(filter(pattern.match_file, [ + 'anydir/file.txt', + ])) + self.assertEqual(results, { + 'anydir/file.txt', + }) diff --git a/tests/test_pathspec.py b/tests/test_pathspec.py index 4ff59e5..cb6c476 100644 --- a/tests/test_pathspec.py +++ b/tests/test_pathspec.py @@ -521,3 +521,19 @@ def test_06_issue_41_c(self): 'dir/file.sql', 'dir/index.txt', }) + + def test_07_issue_62(self): + """ + Test including all files and excluding a directory. + """ + spec = PathSpec.from_lines('gitwildmatch', [ + '*', + '!product_dir/', + ]) + results = set(spec.match_files([ + 'anydir/file.txt', + 'product_dir/file.txt' + ])) + self.assertEqual(results, { + 'anydir/file.txt', + })