Skip to content

Commit 721f804

Browse files
authored
Fix: do not resolve symlinks
1 parent 37ecfd5 commit 721f804

2 files changed

Lines changed: 38 additions & 5 deletions

File tree

gitignore_parser.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import re
44

5-
from os.path import dirname
5+
from os.path import abspath, dirname
66
from pathlib import Path
77
from typing import Reversible, Union
88

@@ -102,7 +102,7 @@ def rule_from_pattern(pattern, base_path=None, source=None):
102102
negation=negation,
103103
directory_only=directory_only,
104104
anchored=anchored,
105-
base_path=Path(base_path) if base_path else None,
105+
base_path=_normalize_path(base_path) if base_path else None,
106106
source=source
107107
)
108108

@@ -125,9 +125,9 @@ def __repr__(self):
125125
def match(self, abs_path: Union[str, Path]):
126126
matched = False
127127
if self.base_path:
128-
rel_path = str(Path(abs_path).resolve().relative_to(self.base_path))
128+
rel_path = str(_normalize_path(abs_path).relative_to(self.base_path))
129129
else:
130-
rel_path = str(Path(abs_path))
130+
rel_path = str(_normalize_path(abs_path))
131131
# Path() strips the trailing slash, so we need to preserve it
132132
# in case of directory-only negation
133133
if self.negation and type(abs_path) == str and abs_path[-1] == '/':
@@ -208,3 +208,13 @@ def fnmatch_pathname_to_regex(
208208
else:
209209
res.append('($|\\/)')
210210
return ''.join(res)
211+
212+
213+
def _normalize_path(path: Union[str, Path]) -> Path:
214+
"""Normalize a path without resolving symlinks.
215+
216+
This is equivalent to `Path.resolve()` except that it does not resolve symlinks.
217+
Note that this simplifies paths by removing double slashes, `..`, `.` etc. like
218+
`Path.resolve()` does.
219+
"""
220+
return Path(abspath(path))

tests.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from unittest.mock import patch, mock_open
22
from pathlib import Path
3+
from tempfile import TemporaryDirectory
34

45
from gitignore_parser import parse_gitignore
56

@@ -178,10 +179,32 @@ def test_slash_in_range_does_not_match_dirs(self):
178179
self.assertFalse(matches('/home/michael/abc/def'))
179180
self.assertFalse(matches('/home/michael/abcXYZdef'))
180181

182+
def test_symlink_to_another_directory(self):
183+
"""Test the behavior of a symlink to another directory.
184+
185+
The issue https://github.com/mherrmann/gitignore_parser/issues/29 describes how
186+
a symlink to another directory caused an exception to be raised during matching.
187+
188+
This test ensures that the issue is now fixed.
189+
"""
190+
with TemporaryDirectory() as project_dir, TemporaryDirectory() as another_dir:
191+
matches = _parse_gitignore_string('link', fake_base_dir=project_dir)
192+
193+
# Create a symlink to another directory.
194+
link = Path(project_dir, 'link')
195+
target = Path(another_dir, 'target')
196+
link.symlink_to(target)
197+
198+
# Check the intended behavior according to
199+
# https://git-scm.com/docs/gitignore#_notes:
200+
# Symbolic links are not followed and are matched as if they were regular
201+
# files.
202+
self.assertTrue(matches(link))
203+
181204
def _parse_gitignore_string(data: str, fake_base_dir: str = None):
182205
with patch('builtins.open', mock_open(read_data=data)):
183206
success = parse_gitignore(f'{fake_base_dir}/.gitignore', fake_base_dir)
184207
return success
185208

186209
if __name__ == '__main__':
187-
main()
210+
main()

0 commit comments

Comments
 (0)