From 814ee81c42e0d17e30a61a6317627308f24201b4 Mon Sep 17 00:00:00 2001 From: Antonio Ossa Guerra Date: Mon, 17 Oct 2022 14:59:04 -0300 Subject: [PATCH] Apply .gitignore files considering their location When a .gitignore file contains the special rule to ignore every subfolder content (`*/*`) and the file is located in a subfolder relative to where the command is executed (root), the rule is incorrectly applied and ignores every file at the same level of the .gitignore file. The reason for this is that the `gitignore` variable accumulates the rules found in each .gitignore while traversing files and directories recursively. This makes sense and, in general, works as expected. The problem is that the gitignore rules are applied using as the root of the target directory as a reference. This is the cause of the bug. The implemented solution keeps track of every .gitignore file found while traversing the targets and the location of each .gitignore. Then, when matching files to the .gitignore rules, compare each set of rules with the appropiate relative path to the candidate target file. To make this possible, we changed the single `gitignore` object with a dictionary of similar objects, where the corresponding key is the path to the folder that contains that .gitignore file. This required changing the signature of the `get_sources` function. Also, we introduce a `is_ignored` function that compares a file with every set of rules. Finally, some tests required an update to pass the gitignore object in the new format. Signed-off-by: Antonio Ossa Guerra --- src/black/__init__.py | 12 +++++++----- src/black/files.py | 23 +++++++++++++++++++---- tests/test_black.py | 6 +++--- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 5293796aea1..ed16522fee4 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -625,6 +625,8 @@ def get_sources( sources: Set[Path] = set() root = ctx.obj["root"] + gitignore = None + for s in src: if s == "-" and stdin_filename: p = Path(stdin_filename) @@ -660,14 +662,14 @@ def get_sources( elif p.is_dir(): if exclude is None: exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) - gitignore = get_gitignore(root) + root_gitignore = get_gitignore(root) p_gitignore = get_gitignore(p) # No need to use p's gitignore if it is identical to root's gitignore # (i.e. root and p point to the same directory). - if gitignore != p_gitignore: - gitignore += p_gitignore - else: - gitignore = None + if root_gitignore != p_gitignore: + gitignore = {root: root_gitignore, p: p_gitignore} + else: + gitignore = {root: root_gitignore} sources.update( gen_python_files( p.iterdir(), diff --git a/src/black/files.py b/src/black/files.py index ed503f5fec7..8701e870383 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -198,7 +198,7 @@ def gen_python_files( extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: Report, - gitignore: Optional[PathSpec], + gitignore: Optional[Dict[Path, PathSpec]], *, verbose: bool, quiet: bool, @@ -211,6 +211,17 @@ def gen_python_files( `report` is where output about exclusions goes. """ + + def is_ignored(gitignore_dict, child, report): + for _dir, _gitignore in gitignore_dict.items(): + relative_path = normalize_path_maybe_ignore(child, _dir, report) + if relative_path is None: + continue + if _gitignore is not None and _gitignore.match_file(relative_path): + report.path_ignored(child, "matches the .gitignore file content") + return True + return False + assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" for child in paths: normalized_path = normalize_path_maybe_ignore(child, root, report) @@ -218,8 +229,7 @@ def gen_python_files( continue # First ignore files matching .gitignore, if passed - if gitignore is not None and gitignore.match_file(normalized_path): - report.path_ignored(child, "matches the .gitignore file content") + if gitignore is not None and is_ignored(gitignore, child, report): continue # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. @@ -244,6 +254,11 @@ def gen_python_files( if child.is_dir(): # If gitignore is None, gitignore usage is disabled, while a Falsey # gitignore is when the directory doesn't have a .gitignore file. + if gitignore is None: + _gitignore = None + else: + _gitignore = gitignore.copy() + _gitignore[child] = get_gitignore(child) yield from gen_python_files( child.iterdir(), root, @@ -252,7 +267,7 @@ def gen_python_files( extend_exclude, force_exclude, report, - gitignore + get_gitignore(child) if gitignore is not None else None, + _gitignore, verbose=verbose, quiet=quiet, ) diff --git a/tests/test_black.py b/tests/test_black.py index 5d0175d9d66..83c705a869f 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1989,7 +1989,7 @@ def test_gitignore_exclude(self) -> None: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, ) @@ -2018,7 +2018,7 @@ def test_nested_gitignore(self) -> None: None, None, report, - root_gitignore, + {path: root_gitignore}, verbose=False, quiet=False, ) @@ -2110,7 +2110,7 @@ def test_symlink_out_of_root_directory(self) -> None: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, )