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.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ New features:

Bug fixes:

- Fixed subtree traversal from filesystem roots and equivalent Windows paths with different casing.
- `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
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:

- Fixed subtree traversal from filesystem roots and equivalent Windows paths with different casing.
- `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
4 changes: 2 additions & 2 deletions pathspec/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ def _get_sub_path_safe(root: str, sub_dir: StrPath) -> str:
Returns the sub-directory path relative to the root directory (:class:`str`).
"""
sub_abs = os.path.normpath(os.path.join(root, sub_dir))
if sub_abs == root:
if os.path.normcase(sub_abs) == os.path.normcase(root):
return ''
elif sub_abs.startswith(root + os.sep):
elif os.path.commonpath((root, sub_abs)) == root:
return os.path.relpath(sub_abs, root)
else:
raise ValueError(f"{sub_dir=!r} must be relative to {root=!r}.")
Expand Down
33 changes: 33 additions & 0 deletions tests/test_01_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,39 @@ def test_01_files_2_files(self):
'Dir/Inner/f',
])))

def test_subdir_from_filesystem_root(self):
"""Traverse only the requested subtree when the root ends in a separator."""
self.make_files(['a.txt'])
root = Path(self.temp_dir).anchor
expected = {os.path.relpath(os.path.join(self.temp_dir, 'a.txt'), root)}
for subdir in (self.temp_dir, os.path.relpath(self.temp_dir, root)):
with self.subTest(subdir=subdir):
self.assertEqual(set(iter_tree_files(root, subdir=subdir)), expected)
self.assertEqual(
get_paths_from_entries(iter_tree_entries(root, subdir=subdir)), expected)

def test_subdir_same_root(self):
"""Both an absolute root and a dot select the root itself."""
self.make_files(['a.txt'])
for subdir in ('.', self.temp_dir):
with self.subTest(subdir=subdir):
self.assertEqual(set(iter_tree_files(self.temp_dir, subdir=subdir)), {'a.txt'})

def test_subdir_rejects_outside_root(self):
"""Reject parents and sibling paths sharing the root's string prefix."""
for subdir in ('..', os.path.join('..', 'other'), str(self.temp_dir) + '-other'):
for traverse in (iter_tree_files, iter_tree_entries):
with self.subTest(subdir=subdir, traverse=traverse):
with self.assertRaises(ValueError):
list(traverse(self.temp_dir, subdir=subdir))

@unittest.skipUnless(os.path.normcase('A') == os.path.normcase('a'), 'Case-sensitive paths')
def test_subdir_case_insensitive_root(self):
"""Accept equivalent root paths with different casing on Windows."""
self.make_files(['a.txt'])
root = str(self.temp_dir).swapcase()
self.assertEqual(set(iter_tree_files(root, subdir=self.temp_dir)), {'a.txt'})

def test_02_link_1_check_symlink(self):
"""
Tests whether links can be created.
Expand Down