diff --git a/CHANGES.md b/CHANGES.md index a1071a8ec7c..a9a1b279ddc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,8 @@ +- Fix incorrectly applied .gitignore rules by considering the .gitignore location and + the relative path to the target file (#3338) - Fix incorrectly ignoring .gitignore presence when more than one source directory is specified (#3336) diff --git a/src/black/__init__.py b/src/black/__init__.py index 6c8d3468583..2786861e9e0 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -628,9 +628,9 @@ def get_sources( sources: Set[Path] = set() root = ctx.obj["root"] - exclude_is_None = exclude is None + using_default_exclude = exclude is None exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude - gitignore = None # type: Optional[PathSpec] + gitignore: Optional[PathSpec] = None root_gitignore = get_gitignore(root) for s in src: @@ -666,14 +666,11 @@ def get_sources( sources.add(p) elif p.is_dir(): - if exclude_is_None: - 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 root_gitignore == p_gitignore: - gitignore = root_gitignore - else: - gitignore = root_gitignore + p_gitignore + if using_default_exclude: + gitignore = { + root: root_gitignore, + root / p: get_gitignore(p), + } sources.update( gen_python_files( p.iterdir(), diff --git a/src/black/files.py b/src/black/files.py index ed503f5fec7..ea517f4ece9 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -182,6 +182,19 @@ def normalize_path_maybe_ignore( return root_relative_path +def path_is_ignored( + path: Path, gitignore_dict: Dict[Path, PathSpec], report: Report +) -> bool: + for gitignore_path, pattern in gitignore_dict.items(): + relative_path = normalize_path_maybe_ignore(path, gitignore_path, report) + if relative_path is None: + break + if pattern.match_file(relative_path): + report.path_ignored(path, "matches a .gitignore file content") + return True + return False + + def path_is_excluded( normalized_path: str, pattern: Optional[Pattern[str]], @@ -198,7 +211,7 @@ def gen_python_files( extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: Report, - gitignore: Optional[PathSpec], + gitignore_dict: Optional[Dict[Path, PathSpec]], *, verbose: bool, quiet: bool, @@ -211,6 +224,7 @@ def gen_python_files( `report` is where output about exclusions goes. """ + 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 +232,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_dict and path_is_ignored(child, gitignore_dict, report): continue # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. @@ -244,6 +257,13 @@ 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_dict is not None: + new_gitignore_dict = { + **gitignore_dict, + root / child: get_gitignore(child), + } + else: + new_gitignore_dict = None yield from gen_python_files( child.iterdir(), root, @@ -252,7 +272,7 @@ def gen_python_files( extend_exclude, force_exclude, report, - gitignore + get_gitignore(child) if gitignore is not None else None, + new_gitignore_dict, verbose=verbose, quiet=quiet, ) diff --git a/tests/data/ignore_subfolders_gitignore_tests/a.py b/tests/data/ignore_subfolders_gitignore_tests/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore b/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore new file mode 100644 index 00000000000..150f68c80f5 --- /dev/null +++ b/tests/data/ignore_subfolders_gitignore_tests/subdir/.gitignore @@ -0,0 +1 @@ +*/* diff --git a/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py b/tests/data/ignore_subfolders_gitignore_tests/subdir/b.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py b/tests/data/ignore_subfolders_gitignore_tests/subdir/subdir/c.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_black.py b/tests/test_black.py index 784eb0dc9ad..a43f05e083b 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2042,7 +2042,7 @@ def test_gitignore_exclude(self) -> None: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, ) @@ -2071,7 +2071,7 @@ def test_nested_gitignore(self) -> None: None, None, report, - root_gitignore, + {path: root_gitignore}, verbose=False, quiet=False, ) @@ -2109,6 +2109,32 @@ def test_invalid_nested_gitignore(self) -> None: gitignore = path / "a" / ".gitignore" assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + def test_gitignore_that_ignores_subfolders(self) -> None: + # If gitignore with */* is in root + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests" / "subdir") + expected = [root / "b.py"] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([root], expected, ctx=ctx) + + # If .gitignore with */* is nested + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") + expected = [ + root / "a.py", + root / "subdir" / "b.py", + ] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([root], expected, ctx=ctx) + + # If command is executed from outer dir + root = Path(DATA_DIR / "ignore_subfolders_gitignore_tests") + target = root / "subdir" + expected = [target / "b.py"] + ctx = FakeContext() + ctx.obj["root"] = root + assert_collected_sources([target], expected, ctx=ctx) + def test_empty_include(self) -> None: path = DATA_DIR / "include_exclude_tests" src = [path] @@ -2163,7 +2189,7 @@ def test_symlink_out_of_root_directory(self) -> None: None, None, report, - gitignore, + {path: gitignore}, verbose=False, quiet=False, )