From 449eb6972cac119da198b9eb84539f2f91469931 Mon Sep 17 00:00:00 2001 From: Ruan Comelli Date: Wed, 4 Oct 2023 21:11:16 -0300 Subject: [PATCH] fix: fix issue #29 by not normalizing symlinks --- gitignore_parser.py | 18 ++++++++++++++---- tests.py | 25 ++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/gitignore_parser.py b/gitignore_parser.py index 54cd21a..00a56aa 100644 --- a/gitignore_parser.py +++ b/gitignore_parser.py @@ -2,7 +2,7 @@ import os import re -from os.path import dirname +from os.path import abspath, dirname from pathlib import Path from typing import Reversible, Union @@ -102,7 +102,7 @@ def rule_from_pattern(pattern, base_path=None, source=None): negation=negation, directory_only=directory_only, anchored=anchored, - base_path=Path(base_path) if base_path else None, + base_path=_normalize_path(base_path) if base_path else None, source=source ) @@ -125,9 +125,9 @@ def __repr__(self): def match(self, abs_path: Union[str, Path]): matched = False if self.base_path: - rel_path = str(Path(abs_path).resolve().relative_to(self.base_path)) + rel_path = str(_normalize_path(abs_path).relative_to(self.base_path)) else: - rel_path = str(Path(abs_path)) + rel_path = str(_normalize_path(abs_path)) # Path() strips the trailing slash, so we need to preserve it # in case of directory-only negation if self.negation and type(abs_path) == str and abs_path[-1] == '/': @@ -208,3 +208,13 @@ def fnmatch_pathname_to_regex( else: res.append('($|\\/)') return ''.join(res) + + +def _normalize_path(path: Union[str, Path]) -> Path: + """Normalize a path without resolving symlinks. + + This is equivalent to `Path.resolve()` except that it does not resolve symlinks. + Note that this simplifies paths by removing double slashes, `..`, `.` etc. like + `Path.resolve()` does. + """ + return Path(abspath(path)) diff --git a/tests.py b/tests.py index 300bfeb..f337b64 100644 --- a/tests.py +++ b/tests.py @@ -1,5 +1,6 @@ from unittest.mock import patch, mock_open from pathlib import Path +from tempfile import TemporaryDirectory from gitignore_parser import parse_gitignore @@ -178,10 +179,32 @@ def test_slash_in_range_does_not_match_dirs(self): self.assertFalse(matches('/home/michael/abc/def')) self.assertFalse(matches('/home/michael/abcXYZdef')) + def test_symlink_to_another_directory(self): + """Test the behavior of a symlink to another directory. + + The issue https://github.com/mherrmann/gitignore_parser/issues/29 describes how + a symlink to another directory caused an exception to be raised during matching. + + This test ensures that the issue is now fixed. + """ + with TemporaryDirectory() as project_dir, TemporaryDirectory() as another_dir: + matches = _parse_gitignore_string('link', fake_base_dir=project_dir) + + # Create a symlink to another directory. + link = Path(project_dir, 'link') + target = Path(another_dir, 'target') + link.symlink_to(target) + + # Check the intended behavior according to + # https://git-scm.com/docs/gitignore#_notes: + # Symbolic links are not followed and are matched as if they were regular + # files. + self.assertTrue(matches(link)) + def _parse_gitignore_string(data: str, fake_base_dir: str = None): with patch('builtins.open', mock_open(read_data=data)): success = parse_gitignore(f'{fake_base_dir}/.gitignore', fake_base_dir) return success if __name__ == '__main__': - main() \ No newline at end of file + main()