From 6f43612766da4a2f275b575af0802c3e73b6ed83 Mon Sep 17 00:00:00 2001
From: cobalt <61329810+cobaltt7@users.noreply.github.com>
Date: Thu, 15 Jan 2026 21:07:25 -0600
Subject: [PATCH] Handle pathspec v1 changes (#4958)

---
 CHANGES.md                                    | 22 ++++++
 pyproject.toml                                |  2 +-
 src/black/__init__.py                         |  8 +-
 src/black/files.py                            | 16 ++--
 .../b/exclude/still_exclude/a.pie             |  0
 .../b/exclude/still_exclude/a.py              |  0
 .../b/exclude/still_exclude/a.pyi             |  0
 tests/test_black.py                           | 77 ++++++++++++++++++-
 8 files changed, 108 insertions(+), 17 deletions(-)
 create mode 100644 tests/data/include_exclude_tests/b/exclude/still_exclude/a.pie
 create mode 100644 tests/data/include_exclude_tests/b/exclude/still_exclude/a.py
 create mode 100644 tests/data/include_exclude_tests/b/exclude/still_exclude/a.pyi

Index: black-25.1.0/pyproject.toml
===================================================================
--- black-25.1.0.orig/pyproject.toml
+++ black-25.1.0/pyproject.toml
@@ -67,7 +67,7 @@ dependencies = [
   "click>=8.0.0",
   "mypy_extensions>=0.4.3",
   "packaging>=22.0",
-  "pathspec>=0.9.0",
+  "pathspec>=1.0.0",
   "platformdirs>=2",
   "tomli>=1.1.0; python_version < '3.11'",
   "typing_extensions>=4.0.1; python_version < '3.11'",
Index: black-25.1.0/src/black/__init__.py
===================================================================
--- black-25.1.0.orig/src/black/__init__.py
+++ black-25.1.0/src/black/__init__.py
@@ -25,8 +25,8 @@ from typing import Any, Optional, Union
 import click
 from click.core import ParameterSource
 from mypy_extensions import mypyc_attr
-from pathspec import PathSpec
-from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
+from pathspec import GitIgnoreSpec
+from pathspec.patterns.gitignore import GitIgnorePatternError
 
 from _black_version import version as __version__
 from black.cache import Cache
@@ -680,7 +680,7 @@ def main(  # noqa: C901
                 report=report,
                 stdin_filename=stdin_filename,
             )
-        except GitWildMatchPatternError:
+        except GitIgnorePatternError:
             ctx.exit(1)
 
         path_empty(
@@ -743,7 +743,7 @@ def get_sources(
     assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
     using_default_exclude = exclude is None
     exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude
-    gitignore: Optional[dict[Path, PathSpec]] = None
+    gitignore: Optional[dict[Path, GitIgnoreSpec]] = None
     root_gitignore = get_gitignore(root)
 
     for s in src:
Index: black-25.1.0/src/black/files.py
===================================================================
--- black-25.1.0.orig/src/black/files.py
+++ black-25.1.0/src/black/files.py
@@ -10,8 +10,8 @@ from typing import TYPE_CHECKING, Any, O
 from mypy_extensions import mypyc_attr
 from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
 from packaging.version import InvalidVersion, Version
-from pathspec import PathSpec
-from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
+from pathspec import GitIgnoreSpec
+from pathspec.patterns.gitignore import GitIgnorePatternError
 
 if sys.version_info >= (3, 11):
     try:
@@ -238,16 +238,16 @@ def find_user_pyproject_toml() -> Path:
 
 
 @lru_cache
-def get_gitignore(root: Path) -> PathSpec:
-    """Return a PathSpec matching gitignore content if present."""
+def get_gitignore(root: Path) -> GitIgnoreSpec:
+    """Return a GitIgnoreSpec matching gitignore content if present."""
     gitignore = root / ".gitignore"
     lines: list[str] = []
     if gitignore.is_file():
         with gitignore.open(encoding="utf-8") as gf:
             lines = gf.readlines()
     try:
-        return PathSpec.from_lines("gitwildmatch", lines)
-    except GitWildMatchPatternError as e:
+        return GitIgnoreSpec.from_lines(lines)
+    except GitIgnorePatternError as e:
         err(f"Could not parse {gitignore}: {e}")
         raise
 
@@ -292,7 +292,7 @@ def best_effort_relative_path(path: Path
 def _path_is_ignored(
     root_relative_path: str,
     root: Path,
-    gitignore_dict: dict[Path, PathSpec],
+    gitignore_dict: dict[Path, GitIgnoreSpec],
 ) -> bool:
     path = root / root_relative_path
     # Note that this logic is sensitive to the ordering of gitignore_dict. Callers must
@@ -325,7 +325,7 @@ def gen_python_files(
     extend_exclude: Optional[Pattern[str]],
     force_exclude: Optional[Pattern[str]],
     report: Report,
-    gitignore_dict: Optional[dict[Path, PathSpec]],
+    gitignore_dict: Optional[dict[Path, GitIgnoreSpec]],
     *,
     verbose: bool,
     quiet: bool,
Index: black-25.1.0/tests/test_black.py
===================================================================
--- black-25.1.0.orig/tests/test_black.py
+++ black-25.1.0/tests/test_black.py
@@ -27,7 +27,7 @@ import pytest
 from click import unstyle
 from click.testing import CliRunner
 from packaging.version import Version
-from pathspec import PathSpec
+from pathspec import GitIgnoreSpec
 
 import black
 import black.files
@@ -2457,8 +2457,8 @@ class TestFileCollection:
         include = re.compile(r"\.pyi?$")
         exclude = re.compile(r"")
         report = black.Report()
-        gitignore = PathSpec.from_lines(
-            "gitwildmatch", ["exclude/", ".definitely_exclude"]
+        gitignore = GitIgnoreSpec.from_lines(
+            ["exclude/", ".definitely_exclude", "!exclude/still_exclude/"]
         )
         sources: list[Path] = []
         expected = [
@@ -2482,6 +2482,66 @@ class TestFileCollection:
         )
         assert sorted(expected) == sorted(sources)
 
+    def test_gitignore_reinclude(self) -> None:
+        path = THIS_DIR / "data" / "include_exclude_tests"
+        include = re.compile(r"\.pyi?$")
+        exclude = re.compile(r"")
+        report = black.Report()
+        gitignore = GitIgnoreSpec.from_lines(
+            ["*/exclude/*", ".definitely_exclude", "!*/exclude/still_exclude/"]
+        )
+        sources: list[Path] = []
+        expected = [
+            Path(path / "b/dont_exclude/a.py"),
+            Path(path / "b/dont_exclude/a.pyi"),
+        ]
+        this_abs = THIS_DIR.resolve()
+        sources.extend(
+            black.gen_python_files(
+                path.iterdir(),
+                this_abs,
+                include,
+                exclude,
+                None,
+                None,
+                report,
+                {path: gitignore},
+                verbose=False,
+                quiet=False,
+            )
+        )
+        assert sorted(expected) == sorted(sources)
+
+    def test_gitignore_reinclude_root(self) -> None:
+        path = THIS_DIR / "data" / "include_exclude_tests" / "b"
+        include = re.compile(r"\.pyi?$")
+        exclude = re.compile(r"")
+        report = black.Report()
+        gitignore = GitIgnoreSpec.from_lines(
+            ["exclude/*", ".definitely_exclude", "!exclude/still_exclude/"]
+        )
+        sources: list[Path] = []
+        expected = [
+            Path(path / "dont_exclude/a.py"),
+            Path(path / "dont_exclude/a.pyi"),
+        ]
+        this_abs = THIS_DIR.resolve()
+        sources.extend(
+            black.gen_python_files(
+                path.iterdir(),
+                this_abs,
+                include,
+                exclude,
+                None,
+                None,
+                report,
+                {path: gitignore},
+                verbose=False,
+                quiet=False,
+            )
+        )
+        assert sorted(expected) == sorted(sources)
+
     def test_nested_gitignore(self) -> None:
         path = Path(THIS_DIR / "data" / "nested_gitignore_tests")
         include = re.compile(r"\.pyi?$")
@@ -2633,7 +2693,7 @@ class TestFileCollection:
         include = re.compile(black.DEFAULT_INCLUDES)
         exclude = re.compile(black.DEFAULT_EXCLUDES)
         report = black.Report()
-        gitignore = PathSpec.from_lines("gitwildmatch", [])
+        gitignore = GitIgnoreSpec.from_lines([])
 
         regular = MagicMock()
         regular.relative_to.return_value = Path("regular.py")
