Skip to content

Commit

Permalink
Merge branch 'main' into trailing_comma
Browse files Browse the repository at this point in the history
  • Loading branch information
yilei authored Nov 8, 2022
2 parents 2307a95 + ffaaf48 commit 6819dfc
Show file tree
Hide file tree
Showing 13 changed files with 82 additions and 17 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@

<!-- 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)

### Packaging

<!-- Changes to how Black is packaged, such as dependency requirements -->
Expand Down
21 changes: 11 additions & 10 deletions src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import click
from click.core import ParameterSource
from mypy_extensions import mypyc_attr
from pathspec import PathSpec
from pathspec.patterns.gitwildmatch import GitWildMatchPatternError

from _black_version import version as __version__
Expand Down Expand Up @@ -627,6 +628,11 @@ def get_sources(
sources: Set[Path] = set()
root = ctx.obj["root"]

using_default_exclude = exclude is None
exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude
gitignore: Optional[PathSpec] = None
root_gitignore = get_gitignore(root)

for s in src:
if s == "-" and stdin_filename:
p = Path(stdin_filename)
Expand Down Expand Up @@ -660,16 +666,11 @@ def get_sources(

sources.add(p)
elif p.is_dir():
if exclude is None:
exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES)
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 using_default_exclude:
gitignore = {
root: root_gitignore,
root / p: get_gitignore(p),
}
sources.update(
gen_python_files(
p.iterdir(),
Expand Down
28 changes: 24 additions & 4 deletions src/black/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand All @@ -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,
Expand All @@ -211,15 +224,15 @@ 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)
if normalized_path is None:
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.
Expand All @@ -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,
Expand All @@ -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,
)
Expand Down
1 change: 1 addition & 0 deletions tests/data/gitignore_used_on_multiple_sources/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*/*
Empty file.
Empty file.
43 changes: 40 additions & 3 deletions tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,17 @@ def test_gitignore_used_as_default(self) -> None:
ctx.obj["root"] = base
assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")

def test_gitignore_used_on_multiple_sources(self) -> None:
root = Path(DATA_DIR / "gitignore_used_on_multiple_sources")
expected = [
root / "dir1" / "b.py",
root / "dir2" / "b.py",
]
ctx = FakeContext()
ctx.obj["root"] = root
src = [root / "dir1", root / "dir2"]
assert_collected_sources(src, expected, ctx=ctx)

@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_exclude_for_issue_1572(self) -> None:
# Exclude shouldn't touch files that were explicitly given to Black through the
Expand Down Expand Up @@ -2031,7 +2042,7 @@ def test_gitignore_exclude(self) -> None:
None,
None,
report,
gitignore,
{path: gitignore},
verbose=False,
quiet=False,
)
Expand Down Expand Up @@ -2060,7 +2071,7 @@ def test_nested_gitignore(self) -> None:
None,
None,
report,
root_gitignore,
{path: root_gitignore},
verbose=False,
quiet=False,
)
Expand Down Expand Up @@ -2098,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]
Expand Down Expand Up @@ -2152,7 +2189,7 @@ def test_symlink_out_of_root_directory(self) -> None:
None,
None,
report,
gitignore,
{path: gitignore},
verbose=False,
quiet=False,
)
Expand Down

0 comments on commit 6819dfc

Please sign in to comment.