Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/poetry/vcs/git/backend.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextlib
import dataclasses
import logging
import os
Expand All @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
134 changes: 134 additions & 0 deletions tests/vcs/git/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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", encoding="utf-8")
porcelain.add(repo, str(test_file))
expected_commit_sha = porcelain.commit(
repo,
message=b"Initial commit",
author=b"Test <test@example.com>",
committer=b"Test <test@example.com>",
)

# Create an annotated tag
porcelain.tag_create(
repo,
tag=b"v1.0.0",
message=b"Release 1.0.0",
author=b"Test <test@example.com>",
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(encoding="utf-8") == "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", encoding="utf-8")
porcelain.add(repo, paths=[b"test.txt"])
commit_sha = porcelain.commit(
repo,
message=b"Initial commit",
committer=b"Test <test@example.com>",
author=b"Test <test@example.com>",
)

# 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 <test@example.com>"
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 <test@example.com>"
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(encoding="utf-8") == "nested tag test"