File support-dulwich-0_25.patch of Package python-poetry

From 2b0723c277cb2a51c8b030e02e0830eeefc75632 Mon Sep 17 00:00:00 2001
From: David Hotham <david.hotham@blueyonder.co.uk>
Date: Mon, 29 Dec 2025 22:51:23 +0000
Subject: [PATCH] Dulwich upgrade (#10674)

---
 poetry.lock                             | 76 ++++++++++++------------
 pyproject.toml                          |  2 +-
 src/poetry/vcs/git/backend.py           | 78 +++++++++++++++----------
 tests/integration/test_utils_vcs_git.py | 31 ++++++----
 tests/vcs/git/conftest.py               |  3 +
 tests/vcs/git/test_backend.py           | 14 ++---
 6 files changed, 116 insertions(+), 88 deletions(-)

Index: poetry-2.2.1/pyproject.toml
===================================================================
--- poetry-2.2.1.orig/pyproject.toml
+++ poetry-2.2.1/pyproject.toml
@@ -8,7 +8,7 @@ dependencies = [
     "build (>=1.2.1,<2.0.0)",
     "cachecontrol[filecache] (>=0.14.0,<0.15.0)",
     "cleo (>=2.1.0,<3.0.0)",
-    "dulwich (>=0.24.0,<0.25.0)",
+    "dulwich (>=0.25.0,<0.26.0)",
     "fastjsonschema (>=2.18.0,<3.0.0)",
     "importlib-metadata (>=4.4) ; python_version < '3.10'",
     "installer (>=0.7.0,<0.8.0)",
Index: poetry-2.2.1/src/poetry/vcs/git/backend.py
===================================================================
--- poetry-2.2.1.orig/src/poetry/vcs/git/backend.py
+++ poetry-2.2.1/src/poetry/vcs/git/backend.py
@@ -20,7 +20,9 @@ from dulwich.config import parse_submodu
 from dulwich.errors import NotGitRepository
 from dulwich.file import FileLocked
 from dulwich.index import IndexEntry
-from dulwich.refs import ANNOTATED_TAG_SUFFIX
+from dulwich.objects import ObjectID
+from dulwich.protocol import PEELED_TAG_SUFFIX
+from dulwich.refs import Ref
 from dulwich.repo import Repo
 
 from poetry.console.exceptions import PoetryRuntimeError
@@ -76,10 +78,10 @@ def is_revision_sha(revision: str | None
     return re.match(r"^\b[0-9a-f]{5,40}\b$", revision or "") is not None
 
 
-def annotated_tag(ref: str | bytes) -> bytes:
+def peeled_tag(ref: str | bytes) -> Ref:
     if isinstance(ref, str):
         ref = ref.encode("utf-8")
-    return ref + ANNOTATED_TAG_SUFFIX
+    return Ref(ref + PEELED_TAG_SUFFIX)
 
 
 @dataclasses.dataclass
@@ -87,7 +89,7 @@ class GitRefSpec:
     branch: str | None = None
     revision: str | None = None
     tag: str | None = None
-    ref: bytes = dataclasses.field(default_factory=lambda: b"HEAD")
+    ref: Ref = dataclasses.field(default_factory=lambda: Ref(b"HEAD"))
 
     def resolve(self, remote_refs: FetchPackResult, repo: Repo) -> None:
         """
@@ -104,7 +106,7 @@ class GitRefSpec:
         """
         if self.revision:
             ref = f"refs/tags/{self.revision}".encode()
-            if ref in remote_refs.refs or annotated_tag(ref) in remote_refs.refs:
+            if ref in remote_refs.refs or peeled_tag(ref) in remote_refs.refs:
                 # this is a tag, incorrectly specified as a revision, tags take priority
                 self.tag = self.revision
                 self.revision = None
@@ -120,7 +122,7 @@ class GitRefSpec:
             and f"refs/heads/{self.branch}".encode() not in remote_refs.refs
             and (
                 f"refs/tags/{self.branch}".encode() in remote_refs.refs
-                or annotated_tag(f"refs/tags/{self.branch}") in remote_refs.refs
+                or peeled_tag(f"refs/tags/{self.branch}") in remote_refs.refs
             )
         ):
             # this is a tag incorrectly specified as a branch
@@ -131,7 +133,7 @@ class GitRefSpec:
             # revision is a short sha, resolve to full sha
             short_sha = self.revision.encode("utf-8")
             for sha in remote_refs.refs.values():
-                if sha.startswith(short_sha):
+                if sha is not None and sha.startswith(short_sha):
                     self.revision = sha.decode("utf-8")
                     return
 
@@ -145,24 +147,25 @@ class GitRefSpec:
         Internal helper method to populate ref and set it's sha as the remote's head
         and default ref.
         """
-        self.ref = remote_refs.symrefs[b"HEAD"]
+        self.ref = remote_refs.symrefs[Ref(b"HEAD")]
 
+        head: ObjectID | None
         if self.revision:
-            head = self.revision.encode("utf-8")
+            head = ObjectID(self.revision.encode("utf-8"))
         else:
             if self.tag:
-                ref = f"refs/tags/{self.tag}".encode()
-                annotated = annotated_tag(ref)
-                self.ref = annotated if annotated in remote_refs.refs else ref
+                ref = Ref(f"refs/tags/{self.tag}".encode())
+                peeled = peeled_tag(ref)
+                self.ref = peeled if peeled in remote_refs.refs else ref
             elif self.branch:
                 self.ref = (
-                    self.branch.encode("utf-8")
+                    Ref(self.branch.encode("utf-8"))
                     if self.is_ref
-                    else f"refs/heads/{self.branch}".encode()
+                    else Ref(f"refs/heads/{self.branch}".encode())
                 )
             head = remote_refs.refs[self.ref]
 
-        remote_refs.refs[self.ref] = remote_refs.refs[b"HEAD"] = head
+        remote_refs.refs[self.ref] = remote_refs.refs[Ref(b"HEAD")] = head
 
     @property
     def key(self) -> str:
@@ -216,7 +219,7 @@ class Git:
     @staticmethod
     def get_revision(repo: Repo) -> str:
         with repo:
-            return repo.get_peeled(b"HEAD").decode("utf-8")
+            return repo.get_peeled(Ref(b"HEAD")).decode("utf-8")
 
     @classmethod
     def info(cls, repo: Repo | Path) -> GitRepoLocalInfo:
@@ -234,21 +237,24 @@ class Git:
         client: GitClient
         path: str
 
-        kwargs: dict[str, str] = {}
         credentials = get_default_authenticator().get_credentials_for_git_url(url=url)
 
+        username = None
+        password = None
         if credentials.password and credentials.username:
             # we do this conditionally as otherwise, dulwich might complain if these
             # parameters are passed in for an ssh url
-            kwargs["username"] = credentials.username
-            kwargs["password"] = credentials.password
+            username = credentials.username
+            password = credentials.password
 
         config = local.get_config_stack()
-        client, path = get_transport_and_path(url, config=config, **kwargs)
+        client, path = get_transport_and_path(
+            url, config=config, username=username, password=password
+        )
 
         with local:
             result: FetchPackResult = client.fetch(
-                path,
+                path.encode(),
                 local,
                 determine_wants=local.object_store.determine_wants_all,
             )
@@ -335,7 +341,9 @@ class Git:
 
         try:
             # ensure local HEAD matches remote
-            local.refs[b"HEAD"] = remote_refs.refs[b"HEAD"]
+            ref = remote_refs.refs[Ref(b"HEAD")]
+            if ref is not None:
+                local.refs[Ref(b"HEAD")] = ref
         except ValueError:
             raise PoetryRuntimeError.create(
                 reason=f"<error>Failed to clone {url} at '{refspec.key}', verify ref exists on remote.</>",
@@ -349,30 +357,36 @@ class Git:
 
         if refspec.is_ref:
             # set ref to current HEAD
-            local.refs[refspec.ref] = local.refs[b"HEAD"]
+            local.refs[refspec.ref] = local.refs[Ref(b"HEAD")]
 
         for base, prefix in {
-            (b"refs/remotes/origin", b"refs/heads/"),
-            (b"refs/tags", b"refs/tags"),
+            (Ref(b"refs/remotes/origin"), b"refs/heads/"),
+            (Ref(b"refs/tags"), b"refs/tags"),
         }:
             try:
                 local.refs.import_refs(
                     base=base,
                     other={
-                        n[len(prefix) :]: v
+                        Ref(n[len(prefix) :]): v
                         for (n, v) in remote_refs.refs.items()
-                        if n.startswith(prefix) and not n.endswith(ANNOTATED_TAG_SUFFIX)
+                        if n.startswith(prefix)
+                        and not n.endswith(PEELED_TAG_SUFFIX)
+                        and v is not None
                     },
                 )
             except FileLocked as e:
 
-                def to_str(path: bytes) -> str:
-                    return path.decode().replace(os.sep * 2, os.sep)
+                def to_str(path: bytes | str) -> str:
+                    if isinstance(path, bytes):
+                        path = path.decode()
+                    return path.replace(os.sep * 2, os.sep)
 
                 raise PoetryRuntimeError.create(
+                    # <https://github.com/jelmer/dulwich/pull/2045> should clean up the
+                    # ignore.
                     reason=(
                         f"<error>Failed to clone {url} at '{refspec.key}',"
-                        f" unable to acquire file lock for {to_str(e.filename)}.</>"
+                        f" unable to acquire file lock for {to_str(e.filename)}.</>"  # type: ignore[arg-type]
                     ),
                     info=[
                         ERROR_MESSAGE_NOTE,
@@ -519,7 +533,9 @@ class Git:
 
                     with current_repo:
                         # we use peeled sha here to ensure tags are resolved consistently
-                        current_sha = current_repo.get_peeled(b"HEAD").decode("utf-8")
+                        current_sha = current_repo.get_peeled(Ref(b"HEAD")).decode(
+                            "utf-8"
+                        )
                 except (NotGitRepository, AssertionError, KeyError):
                     # something is wrong with the current checkout, clean it
                     remove_directory(target, force=True)
Index: poetry-2.2.1/tests/integration/test_utils_vcs_git.py
===================================================================
--- poetry-2.2.1.orig/tests/integration/test_utils_vcs_git.py
+++ poetry-2.2.1/tests/integration/test_utils_vcs_git.py
@@ -15,7 +15,9 @@ import pytest
 
 from dulwich.client import HTTPUnauthorized
 from dulwich.client import get_transport_and_path
+from dulwich.config import CaseInsensitiveOrderedMultiDict
 from dulwich.config import ConfigFile
+from dulwich.refs import Ref
 from dulwich.repo import Repo
 
 from poetry.console.exceptions import PoetryConsoleError
@@ -108,7 +110,9 @@ def _remote_refs(source_url: str, local_
     path: str
     client, path = get_transport_and_path(source_url)
     return client.fetch(
-        path, local_repo, determine_wants=local_repo.object_store.determine_wants_all
+        path.encode(),
+        local_repo,
+        determine_wants=local_repo.object_store.determine_wants_all,
     )
 
 
@@ -118,8 +122,8 @@ def remote_refs(_remote_refs: FetchPackR
 
 
 @pytest.fixture(scope="module")
-def remote_default_ref(_remote_refs: FetchPackResult) -> bytes:
-    ref: bytes = _remote_refs.symrefs[b"HEAD"]
+def remote_default_ref(_remote_refs: FetchPackResult) -> Ref:
+    ref: Ref = _remote_refs.symrefs[Ref(b"HEAD")]
     return ref
 
 
@@ -136,12 +140,14 @@ def test_use_system_git_client_from_envi
 
 
 def test_git_local_info(
-    source_url: str, remote_refs: FetchPackResult, remote_default_ref: bytes
+    source_url: str, remote_refs: FetchPackResult, remote_default_ref: Ref
 ) -> None:
     with Git.clone(url=source_url) as repo:
         info = Git.info(repo=repo)
         assert info.origin == source_url
-        assert info.revision == remote_refs.refs[remote_default_ref].decode("utf-8")
+        ref = remote_refs.refs[remote_default_ref]
+        assert ref is not None
+        assert info.revision == ref.decode("utf-8")
 
 
 @pytest.mark.parametrize(
@@ -151,7 +157,7 @@ def test_git_clone_default_branch_head(
     specification: GitCloneKwargs,
     source_url: str,
     remote_refs: FetchPackResult,
-    remote_default_ref: bytes,
+    remote_default_ref: Ref,
     mocker: MockerFixture,
 ) -> None:
     spy = mocker.spy(Git, "_clone")
@@ -294,7 +300,7 @@ def test_system_git_fallback_on_http_401
     mocker.patch.object(
         Git,
         "_clone",
-        side_effect=HTTPUnauthorized(None, None),
+        side_effect=HTTPUnauthorized(None, source_url),
     )
 
     # use tmp_path for source_root to get a shorter path,
@@ -307,7 +313,7 @@ def test_system_git_fallback_on_http_401
     spy.assert_called_with(
         url="https://github.com/python-poetry/test-fixture-vcs-repository.git",
         target=path,
-        refspec=GitRefSpec(branch="0.1", revision=None, tag=None, ref=b"HEAD"),
+        refspec=GitRefSpec(branch="0.1", revision=None, tag=None, ref=Ref(b"HEAD")),
     )
     spy.assert_called_once()
 
@@ -388,6 +394,8 @@ def test_username_password_parameter_is_
     spy_get_transport_and_path.assert_called_with(
         location=source_url,
         config=dummy_git_config,
+        username=None,
+        password=None,
     )
     spy_get_transport_and_path.assert_called_once()
 
@@ -411,7 +419,7 @@ def test_system_git_called_when_configur
     spy_legacy.assert_called_with(
         url=source_url,
         target=path,
-        refspec=GitRefSpec(branch="0.1", revision=None, tag=None, ref=b"HEAD"),
+        refspec=GitRefSpec(branch="0.1", revision=None, tag=None, ref=Ref(b"HEAD")),
     )
 
 
@@ -428,9 +436,10 @@ def test_relative_submodules_with_ssh(
     )
 
     # construct fake git config
-    fake_config = ConfigFile(
-        {(b"remote", b"origin"): {b"url": ssh_source_url.encode("utf-8")}}
+    values = CaseInsensitiveOrderedMultiDict.make(
+        {b"url": ssh_source_url.encode("utf-8")}
     )
+    fake_config = ConfigFile({(b"remote", b"origin"): values})
     # trick Git into thinking remote.origin is an ssh url
     mock_get_config = mocker.patch.object(repo_with_unresolved_submodules, "get_config")
     mock_get_config.return_value = fake_config
Index: poetry-2.2.1/tests/vcs/git/conftest.py
===================================================================
--- poetry-2.2.1.orig/tests/vcs/git/conftest.py
+++ poetry-2.2.1/tests/vcs/git/conftest.py
@@ -27,6 +27,7 @@ def temp_repo(tmp_path: Path) -> TempRep
         author=b"User <user@example.com>",
         message=b"init",
         no_verify=True,
+        sign=False,
     )
 
     # one commit which is not "head"
@@ -37,6 +38,7 @@ def temp_repo(tmp_path: Path) -> TempRep
         author=b"User <user@example.com>",
         message=b"extra",
         no_verify=True,
+        sign=False,
     )
 
     # extra commit
@@ -48,6 +50,7 @@ def temp_repo(tmp_path: Path) -> TempRep
         author=b"User <user@example.com>",
         message=b"extra",
         no_verify=True,
+        sign=False,
     )
 
     repo[b"refs/tags/v1"] = head_commit
Index: poetry-2.2.1/tests/vcs/git/test_backend.py
===================================================================
--- poetry-2.2.1.orig/tests/vcs/git/test_backend.py
+++ poetry-2.2.1/tests/vcs/git/test_backend.py
@@ -11,7 +11,7 @@ from dulwich.repo import Repo
 
 from poetry.console.exceptions import PoetryRuntimeError
 from poetry.vcs.git.backend import Git
-from poetry.vcs.git.backend import annotated_tag
+from poetry.vcs.git.backend import peeled_tag
 from poetry.vcs.git.backend import is_revision_sha
 from poetry.vcs.git.backend import urlpathjoin
 
@@ -60,8 +60,8 @@ def test_get_name_from_source_url(url: s
 
 
 @pytest.mark.parametrize(("tag"), ["my-tag", b"my-tag"])
-def test_annotated_tag(tag: str | bytes) -> None:
-    tag = annotated_tag("my-tag")
+def test_peeled_tag(tag: str | bytes) -> None:
+    tag = peeled_tag("my-tag")
     assert tag == b"my-tag^{}"
 
 
openSUSE Build Service is sponsored by