<!-- Changes to how Black can be configured -->
+- 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)
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:
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(),
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]],
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,
`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)
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.
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,
extend_exclude,
force_exclude,
report,
- gitignore + get_gitignore(child) if gitignore is not None else None,
+ new_gitignore_dict,
verbose=verbose,
quiet=quiet,
)
None,
None,
report,
- gitignore,
+ {path: gitignore},
verbose=False,
quiet=False,
)
None,
None,
report,
- root_gitignore,
+ {path: root_gitignore},
verbose=False,
quiet=False,
)
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]
None,
None,
report,
- gitignore,
+ {path: gitignore},
verbose=False,
quiet=False,
)