From 93e8b4b31e20a98bc1f4d83299ff8919ad95c806 Mon Sep 17 00:00:00 2001 From: 9Kun <91123978+9Kun@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:05:08 +0800 Subject: [PATCH] Match directory entries with directory-only patterns --- pathspec/pathspec.py | 4 +++ tests/test_05_pathspec.py | 70 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pathspec/pathspec.py b/pathspec/pathspec.py index 3e3872a..a0e727e 100644 --- a/pathspec/pathspec.py +++ b/pathspec/pathspec.py @@ -359,6 +359,8 @@ def match_entries( *entries* (:class:`~collections.abc.Iterable` of :class:`.TreeEntry`) contains the entries to be matched against :attr:`self.patterns <.PathSpec.patterns>`. + Directory entries are matched with a trailing path separator, without + changing their :attr:`~.TreeEntry.path` attributes. *separators* (:class:`~collections.abc.Collection` of :class:`str`; or :data:`None`) optionally contains the path separators to normalize. See @@ -377,6 +379,8 @@ def match_entries( for entry in entries: norm_file = normalize_file(entry.path, separators) + if entry.is_dir() and not norm_file.endswith('/'): + norm_file += '/' include, _index = self._backend.match_file(norm_file) if negate: diff --git a/tests/test_05_pathspec.py b/tests/test_05_pathspec.py index ba22fb0..542ac3d 100644 --- a/tests/test_05_pathspec.py +++ b/tests/test_05_pathspec.py @@ -3,6 +3,7 @@ """ import os +import re import shutil import tempfile import unittest @@ -26,7 +27,9 @@ SkipTest) from pathspec import ( - PathSpec) + GitIgnoreSpec, + PathSpec, + RegexPattern) from pathspec.backend import ( BackendNamesHint, _Backend) @@ -773,6 +776,71 @@ def test_05_match_entries(self): 'Y/Z/c.txt', ]))) + def test_05_match_entries_directories(self): + """ + Directory-only patterns match directory entries without changing paths. + """ + lines = ['build/', '!keep/build/'] + for sub_test in self.parameterize_from_lines('gitignore', lines): + with sub_test() as spec: + self.make_dirs([ + 'build', + 'build/nested', + 'empty', + 'empty/build', + 'keep', + 'keep/build', + 'other', + ]) + self.make_files([ + 'build/file.txt', + 'keep/build/file.txt', + 'other/build', + ]) + entries = list(iter_tree_entries(self.temp_dir)) + original_paths = [entry.path for entry in entries] + expected = set(map(ospath, [ + 'build', + 'build/nested', + 'build/file.txt', + 'empty/build', + ])) + for check_spec in ( + spec, + GitIgnoreSpec.from_lines(lines, backend=spec._backend_name), + ): + with self.subTest(spec=type(check_spec).__name__): + matched = list(check_spec.match_entries(entries)) + self.assertEqual(get_paths_from_entries(matched), expected) + self.assertEqual( + get_paths_from_entries(check_spec.match_entries(entries, negate=True)), + set(original_paths) - expected) + self.assertEqual( + get_paths_from_entries(check_spec.match_tree_entries(self.temp_dir)), + expected) + self.assertEqual( + set(check_spec.match_tree_files(self.temp_dir)), + {ospath('build/file.txt')}) + self.assertEqual([entry.path for entry in entries], original_paths) + self.assertEqual(matched, [entry for entry in entries if entry.path in expected]) + + def test_05_match_entries_directory_separators(self): + """ + Append exactly one normalized separator to directory matching paths. + """ + self.make_dirs(['build']) + entry, = iter_tree_entries(self.temp_dir) + spec = PathSpec([RegexPattern(re.compile(r'^build/$'), include=True)], backend='simple') + for path, separators in ( + ('build', None), + ('build/', None), + ('build:', (':',)), + ): + with self.subTest(path=path): + entry.path = path + self.assertEqual(list(spec.match_entries([entry], separators)), [entry]) + self.assertEqual(entry.path, path) + def test_05_match_file(self): """ Test matching files individually.