From 51cfc450068799dcc9293f9e5485f02b7741a171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 1 Feb 2026 01:54:50 +0000 Subject: [PATCH 1/2] Fix AssertionError when cloning at annotated tag When cloning a Git repository at an annotated tag, if the peeled tag reference (refs/tags/v1.0.0^{}) is not available in the fetch result, Poetry would set HEAD to the tag object SHA instead of the commit SHA. This caused reset_index() to fail with: AssertionError: assert isinstance(obj, Commit) The fix peels tag objects recursively to extract the underlying commit SHA before setting HEAD. This ensures HEAD always points to a Commit object, not a Tag object. Fixes #10658 --- src/poetry/vcs/git/backend.py | 15 +++- tests/vcs/git/test_backend.py | 134 ++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/src/poetry/vcs/git/backend.py b/src/poetry/vcs/git/backend.py index d0b028de639..dad4867cbbc 100644 --- a/src/poetry/vcs/git/backend.py +++ b/src/poetry/vcs/git/backend.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import dataclasses import logging import os @@ -20,6 +21,7 @@ from dulwich.errors import NotGitRepository from dulwich.file import FileLocked from dulwich.index import IndexEntry +from dulwich.object_store import peel_sha from dulwich.objects import ObjectID from dulwich.protocol import PEELED_TAG_SUFFIX from dulwich.refs import Ref @@ -96,7 +98,7 @@ def resolve(self, remote_refs: FetchPackResult, repo: Repo) -> None: Resolve the ref using the provided remote refs. """ self._normalise(remote_refs=remote_refs, repo=repo) - self._set_head(remote_refs=remote_refs) + self._set_head(remote_refs=remote_refs, repo=repo) def _normalise(self, remote_refs: FetchPackResult, repo: Repo) -> None: """ @@ -142,7 +144,7 @@ def _normalise(self, remote_refs: FetchPackResult, repo: Repo) -> None: self.revision = sha.decode("utf-8") return - def _set_head(self, remote_refs: FetchPackResult) -> None: + def _set_head(self, remote_refs: FetchPackResult, repo: Repo) -> None: """ Internal helper method to populate ref and set it's sha as the remote's head and default ref. @@ -165,6 +167,15 @@ def _set_head(self, remote_refs: FetchPackResult) -> None: ) head = remote_refs.refs[self.ref] + # Peel tag objects to get the underlying commit SHA. + # Annotated tags are Tag objects, not Commit objects. Operations like + # reset_index() expect HEAD to point to a Commit, so we must peel tags + # to extract the commit SHA they reference. + # Object not in store yet will be handled during fetch + if head is not None: + with contextlib.suppress(KeyError): + head = peel_sha(repo.object_store, head)[1].id + remote_refs.refs[self.ref] = remote_refs.refs[Ref(b"HEAD")] = head @property diff --git a/tests/vcs/git/test_backend.py b/tests/vcs/git/test_backend.py index 2531f76d466..5a9480a991b 100644 --- a/tests/vcs/git/test_backend.py +++ b/tests/vcs/git/test_backend.py @@ -8,6 +8,8 @@ import pytest from dulwich.client import FetchPackResult +from dulwich.refs import HEADREF +from dulwich.refs import Ref from dulwich.repo import Repo from poetry.console.exceptions import PoetryRuntimeError @@ -290,3 +292,135 @@ def test_clone_existing_locked_tag(tmp_path: Path, temp_repo: TempRepoFixture) - f"Try again later or remove the {tag_ref_lock} manually" " if you are sure no other process is holding it." ) + + +@pytest.mark.skip_git_mock +def test_clone_annotated_tag(tmp_path: Path) -> None: + """Test cloning at an annotated tag (issue #10658).""" + from dulwich import porcelain + from dulwich.objects import Commit + + # Create a source repository with an annotated tag + source_path = tmp_path / "source-repo" + source_path.mkdir() + repo = Repo.init(str(source_path)) + + # Create initial commit + test_file = source_path / "test.txt" + test_file.write_text("test content") + porcelain.add(repo, str(test_file)) + expected_commit_sha = porcelain.commit( + repo, + message=b"Initial commit", + author=b"Test ", + committer=b"Test ", + ) + + # Create an annotated tag + porcelain.tag_create( + repo, + tag=b"v1.0.0", + message=b"Release 1.0.0", + author=b"Test ", + annotated=True, + ) + + # Clone at the annotated tag + source_root_dir = tmp_path / "clone-root" + source_root_dir.mkdir() + cloned_repo = Git.clone( + url=source_path.as_uri(), + source_root=source_root_dir, + name="clone-test", + tag="v1.0.0", + ) + + # Verify HEAD points to a commit, not a tag object + head_sha = cloned_repo.refs[HEADREF] + head_obj = cloned_repo.object_store[head_sha] + assert isinstance(head_obj, Commit), ( + f"HEAD should point to a Commit, got {type(head_obj).__name__}" + ) + # Verify it's the correct commit + assert head_sha == expected_commit_sha, ( + f"HEAD should point to the expected commit {expected_commit_sha.hex()}, " + f"got {head_sha.hex()}" + ) + + # Verify the clone succeeded and files are present + clone_dir = source_root_dir / "clone-test" + assert (clone_dir / ".git").is_dir() + assert (clone_dir / "test.txt").exists() + assert (clone_dir / "test.txt").read_text() == "test content" + + +@pytest.mark.skip_git_mock +def test_clone_nested_annotated_tags(tmp_path: Path) -> None: + """Test cloning at a tag that points to another tag (nested tags).""" + from dulwich import porcelain + from dulwich.objects import Commit + from dulwich.objects import Tag + + # Create a source repository with nested annotated tags + source_path = tmp_path / "source-repo" + source_path.mkdir() + repo = Repo.init(str(source_path)) + + # Create initial commit + test_file = source_path / "test.txt" + test_file.write_text("nested tag test") + porcelain.add(repo, paths=[b"test.txt"]) + commit_sha = porcelain.commit( + repo, + message=b"Initial commit", + committer=b"Test ", + author=b"Test ", + ) + + # Create first annotated tag pointing to the commit + tag1 = Tag() + tag1.name = b"v1.0.0" + tag1.object = (Commit, commit_sha) + tag1.message = b"First tag" + tag1.tag_time = 1234567890 + tag1.tag_timezone = 0 + tag1.tagger = b"Test " + repo.object_store.add_object(tag1) + repo.refs[Ref(b"refs/tags/v1.0.0")] = tag1.id + + # Create second annotated tag pointing to the first tag + tag2 = Tag() + tag2.name = b"v1.0.0-release" + tag2.object = (Tag, tag1.id) + tag2.message = b"Second tag (points to first tag)" + tag2.tag_time = 1234567891 + tag2.tag_timezone = 0 + tag2.tagger = b"Test " + repo.object_store.add_object(tag2) + repo.refs[Ref(b"refs/tags/v1.0.0-release")] = tag2.id + + # Clone at the nested tag + source_root_dir = tmp_path / "clone-root" + source_root_dir.mkdir() + cloned_repo = Git.clone( + url=source_path.as_uri(), + source_root=source_root_dir, + name="clone-test", + tag="v1.0.0-release", + ) + + # Verify HEAD points to a commit, not a tag object + head_sha = cloned_repo.refs[HEADREF] + head_obj = cloned_repo.object_store[head_sha] + assert isinstance(head_obj, Commit), ( + f"HEAD should point to a Commit (peeling nested tags), got {type(head_obj).__name__}" + ) + + # Verify it's the correct commit + assert head_sha == commit_sha + + # Verify the clone succeeded and files are present + clone_dir = source_root_dir / "clone-test" + assert (clone_dir / ".git").is_dir() + assert (clone_dir / "test.txt").exists() + assert (clone_dir / "test.txt").read_text() == "nested tag test" From c042d81331fc339231e1a61456d111fc92346520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sat, 14 Feb 2026 10:57:14 +0100 Subject: [PATCH 2/2] fix encoding warnings --- tests/vcs/git/test_backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/vcs/git/test_backend.py b/tests/vcs/git/test_backend.py index 5a9480a991b..b2c28aa93e7 100644 --- a/tests/vcs/git/test_backend.py +++ b/tests/vcs/git/test_backend.py @@ -307,7 +307,7 @@ def test_clone_annotated_tag(tmp_path: Path) -> None: # Create initial commit test_file = source_path / "test.txt" - test_file.write_text("test content") + test_file.write_text("test content", encoding="utf-8") porcelain.add(repo, str(test_file)) expected_commit_sha = porcelain.commit( repo, @@ -351,7 +351,7 @@ def test_clone_annotated_tag(tmp_path: Path) -> None: clone_dir = source_root_dir / "clone-test" assert (clone_dir / ".git").is_dir() assert (clone_dir / "test.txt").exists() - assert (clone_dir / "test.txt").read_text() == "test content" + assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "test content" @pytest.mark.skip_git_mock @@ -368,7 +368,7 @@ def test_clone_nested_annotated_tags(tmp_path: Path) -> None: # Create initial commit test_file = source_path / "test.txt" - test_file.write_text("nested tag test") + test_file.write_text("nested tag test", encoding="utf-8") porcelain.add(repo, paths=[b"test.txt"]) commit_sha = porcelain.commit( repo, @@ -423,4 +423,4 @@ def test_clone_nested_annotated_tags(tmp_path: Path) -> None: clone_dir = source_root_dir / "clone-test" assert (clone_dir / ".git").is_dir() assert (clone_dir / "test.txt").exists() - assert (clone_dir / "test.txt").read_text() == "nested tag test" + assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "nested tag test"