diff --git a/CHANGES.md b/CHANGES.md index a4b8e01fb93..3a96029bf5c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Add support for formatting Jupyter Notebook files (#2357) - Move from `appdirs` dependency to `platformdirs` (#2375) +- Present a more user-friendly error if .gitignore is invalid (#2414) ### Integrations diff --git a/setup.py b/setup.py index 92b78f1abe1..215fa6cff61 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def get_long_description() -> str: "tomli>=0.2.6,<2.0.0", "typed-ast>=1.4.2; python_version < '3.8'", "regex>=2020.1.8", - "pathspec>=0.8.1, <1", + "pathspec>=0.9.0, <1", "dataclasses>=0.6; python_version < '3.7'", "typing_extensions>=3.10.0.0; python_version < '3.10'", "mypy_extensions>=0.4.3", diff --git a/src/black/__init__.py b/src/black/__init__.py index 29fb244f8b7..60f4fa34e9d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -9,6 +9,7 @@ from multiprocessing import Manager, freeze_support import os from pathlib import Path +from pathspec.patterns.gitwildmatch import GitWildMatchPatternError import regex as re import signal import sys @@ -428,18 +429,21 @@ def main( content=code, fast=fast, write_back=write_back, mode=mode, report=report ) else: - sources = get_sources( - ctx=ctx, - src=src, - quiet=quiet, - verbose=verbose, - include=include, - exclude=exclude, - extend_exclude=extend_exclude, - force_exclude=force_exclude, - report=report, - stdin_filename=stdin_filename, - ) + try: + sources = get_sources( + ctx=ctx, + src=src, + quiet=quiet, + verbose=verbose, + include=include, + exclude=exclude, + extend_exclude=extend_exclude, + force_exclude=force_exclude, + report=report, + stdin_filename=stdin_filename, + ) + except GitWildMatchPatternError: + ctx.exit(1) path_empty( sources, diff --git a/src/black/files.py b/src/black/files.py index ba60c84a275..4d7b47aaa9f 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -18,6 +18,7 @@ ) from pathspec import PathSpec +from pathspec.patterns.gitwildmatch import GitWildMatchPatternError import tomli from black.output import err @@ -122,7 +123,11 @@ def get_gitignore(root: Path) -> PathSpec: if gitignore.is_file(): with gitignore.open(encoding="utf-8") as gf: lines = gf.readlines() - return PathSpec.from_lines("gitwildmatch", lines) + try: + return PathSpec.from_lines("gitwildmatch", lines) + except GitWildMatchPatternError as e: + err(f"Could not parse {gitignore}: {e}") + raise def normalize_path_maybe_ignore( diff --git a/tests/data/invalid_gitignore_tests/.gitignore b/tests/data/invalid_gitignore_tests/.gitignore new file mode 100644 index 00000000000..cdf4cb4feba --- /dev/null +++ b/tests/data/invalid_gitignore_tests/.gitignore @@ -0,0 +1 @@ +! diff --git a/tests/data/invalid_gitignore_tests/a.py b/tests/data/invalid_gitignore_tests/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/invalid_gitignore_tests/pyproject.toml b/tests/data/invalid_gitignore_tests/pyproject.toml new file mode 100644 index 00000000000..3908e457a9e --- /dev/null +++ b/tests/data/invalid_gitignore_tests/pyproject.toml @@ -0,0 +1 @@ +# Empty configuration file; used in tests to avoid interference from Black's own config. diff --git a/tests/data/invalid_nested_gitignore_tests/a.py b/tests/data/invalid_nested_gitignore_tests/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/invalid_nested_gitignore_tests/a/.gitignore b/tests/data/invalid_nested_gitignore_tests/a/.gitignore new file mode 100644 index 00000000000..cdf4cb4feba --- /dev/null +++ b/tests/data/invalid_nested_gitignore_tests/a/.gitignore @@ -0,0 +1 @@ +! diff --git a/tests/data/invalid_nested_gitignore_tests/a/a.py b/tests/data/invalid_nested_gitignore_tests/a/a.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/invalid_nested_gitignore_tests/pyproject.toml b/tests/data/invalid_nested_gitignore_tests/pyproject.toml new file mode 100644 index 00000000000..3908e457a9e --- /dev/null +++ b/tests/data/invalid_nested_gitignore_tests/pyproject.toml @@ -0,0 +1 @@ +# Empty configuration file; used in tests to avoid interference from Black's own config. diff --git a/tests/test_black.py b/tests/test_black.py index 942446ec343..5c720507216 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1727,6 +1727,30 @@ def test_nested_gitignore(self) -> None: ) self.assertEqual(sorted(expected), sorted(sources)) + def test_invalid_gitignore(self) -> None: + path = THIS_DIR / "data" / "invalid_gitignore_tests" + empty_config = path / "pyproject.toml" + result = BlackRunner().invoke( + black.main, ["--verbose", "--config", str(empty_config), str(path)] + ) + assert result.exit_code == 1 + assert result.stderr_bytes is not None + + gitignore = path / ".gitignore" + assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + + def test_invalid_nested_gitignore(self) -> None: + path = THIS_DIR / "data" / "invalid_nested_gitignore_tests" + empty_config = path / "pyproject.toml" + result = BlackRunner().invoke( + black.main, ["--verbose", "--config", str(empty_config), str(path)] + ) + assert result.exit_code == 1 + assert result.stderr_bytes is not None + + gitignore = path / "a" / ".gitignore" + assert f"Could not parse {gitignore}" in result.stderr_bytes.decode() + def test_empty_include(self) -> None: path = THIS_DIR / "data" / "include_exclude_tests" report = black.Report()