|
8 | 8 | import pytest |
9 | 9 |
|
10 | 10 | from dulwich.client import FetchPackResult |
| 11 | +from dulwich.refs import HEADREF |
| 12 | +from dulwich.refs import Ref |
11 | 13 | from dulwich.repo import Repo |
12 | 14 |
|
13 | 15 | from poetry.console.exceptions import PoetryRuntimeError |
@@ -290,3 +292,135 @@ def test_clone_existing_locked_tag(tmp_path: Path, temp_repo: TempRepoFixture) - |
290 | 292 | f"Try again later or remove the {tag_ref_lock} manually" |
291 | 293 | " if you are sure no other process is holding it." |
292 | 294 | ) |
| 295 | + |
| 296 | + |
| 297 | +@pytest.mark.skip_git_mock |
| 298 | +def test_clone_annotated_tag(tmp_path: Path) -> None: |
| 299 | + """Test cloning at an annotated tag (issue #10658).""" |
| 300 | + from dulwich import porcelain |
| 301 | + from dulwich.objects import Commit |
| 302 | + |
| 303 | + # Create a source repository with an annotated tag |
| 304 | + source_path = tmp_path / "source-repo" |
| 305 | + source_path.mkdir() |
| 306 | + repo = Repo.init(str(source_path)) |
| 307 | + |
| 308 | + # Create initial commit |
| 309 | + test_file = source_path / "test.txt" |
| 310 | + test_file.write_text("test content", encoding="utf-8") |
| 311 | + porcelain.add(repo, str(test_file)) |
| 312 | + expected_commit_sha = porcelain.commit( |
| 313 | + repo, |
| 314 | + message=b"Initial commit", |
| 315 | + author=b"Test <test@example.com>", |
| 316 | + committer=b"Test <test@example.com>", |
| 317 | + ) |
| 318 | + |
| 319 | + # Create an annotated tag |
| 320 | + porcelain.tag_create( |
| 321 | + repo, |
| 322 | + tag=b"v1.0.0", |
| 323 | + message=b"Release 1.0.0", |
| 324 | + author=b"Test <test@example.com>", |
| 325 | + annotated=True, |
| 326 | + ) |
| 327 | + |
| 328 | + # Clone at the annotated tag |
| 329 | + source_root_dir = tmp_path / "clone-root" |
| 330 | + source_root_dir.mkdir() |
| 331 | + cloned_repo = Git.clone( |
| 332 | + url=source_path.as_uri(), |
| 333 | + source_root=source_root_dir, |
| 334 | + name="clone-test", |
| 335 | + tag="v1.0.0", |
| 336 | + ) |
| 337 | + |
| 338 | + # Verify HEAD points to a commit, not a tag object |
| 339 | + head_sha = cloned_repo.refs[HEADREF] |
| 340 | + head_obj = cloned_repo.object_store[head_sha] |
| 341 | + assert isinstance(head_obj, Commit), ( |
| 342 | + f"HEAD should point to a Commit, got {type(head_obj).__name__}" |
| 343 | + ) |
| 344 | + # Verify it's the correct commit |
| 345 | + assert head_sha == expected_commit_sha, ( |
| 346 | + f"HEAD should point to the expected commit {expected_commit_sha.hex()}, " |
| 347 | + f"got {head_sha.hex()}" |
| 348 | + ) |
| 349 | + |
| 350 | + # Verify the clone succeeded and files are present |
| 351 | + clone_dir = source_root_dir / "clone-test" |
| 352 | + assert (clone_dir / ".git").is_dir() |
| 353 | + assert (clone_dir / "test.txt").exists() |
| 354 | + assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "test content" |
| 355 | + |
| 356 | + |
| 357 | +@pytest.mark.skip_git_mock |
| 358 | +def test_clone_nested_annotated_tags(tmp_path: Path) -> None: |
| 359 | + """Test cloning at a tag that points to another tag (nested tags).""" |
| 360 | + from dulwich import porcelain |
| 361 | + from dulwich.objects import Commit |
| 362 | + from dulwich.objects import Tag |
| 363 | + |
| 364 | + # Create a source repository with nested annotated tags |
| 365 | + source_path = tmp_path / "source-repo" |
| 366 | + source_path.mkdir() |
| 367 | + repo = Repo.init(str(source_path)) |
| 368 | + |
| 369 | + # Create initial commit |
| 370 | + test_file = source_path / "test.txt" |
| 371 | + test_file.write_text("nested tag test", encoding="utf-8") |
| 372 | + porcelain.add(repo, paths=[b"test.txt"]) |
| 373 | + commit_sha = porcelain.commit( |
| 374 | + repo, |
| 375 | + message=b"Initial commit", |
| 376 | + committer=b"Test <test@example.com>", |
| 377 | + author=b"Test <test@example.com>", |
| 378 | + ) |
| 379 | + |
| 380 | + # Create first annotated tag pointing to the commit |
| 381 | + tag1 = Tag() |
| 382 | + tag1.name = b"v1.0.0" |
| 383 | + tag1.object = (Commit, commit_sha) |
| 384 | + tag1.message = b"First tag" |
| 385 | + tag1.tag_time = 1234567890 |
| 386 | + tag1.tag_timezone = 0 |
| 387 | + tag1.tagger = b"Test <test@example.com>" |
| 388 | + repo.object_store.add_object(tag1) |
| 389 | + repo.refs[Ref(b"refs/tags/v1.0.0")] = tag1.id |
| 390 | + |
| 391 | + # Create second annotated tag pointing to the first tag |
| 392 | + tag2 = Tag() |
| 393 | + tag2.name = b"v1.0.0-release" |
| 394 | + tag2.object = (Tag, tag1.id) |
| 395 | + tag2.message = b"Second tag (points to first tag)" |
| 396 | + tag2.tag_time = 1234567891 |
| 397 | + tag2.tag_timezone = 0 |
| 398 | + tag2.tagger = b"Test <test@example.com>" |
| 399 | + repo.object_store.add_object(tag2) |
| 400 | + repo.refs[Ref(b"refs/tags/v1.0.0-release")] = tag2.id |
| 401 | + |
| 402 | + # Clone at the nested tag |
| 403 | + source_root_dir = tmp_path / "clone-root" |
| 404 | + source_root_dir.mkdir() |
| 405 | + cloned_repo = Git.clone( |
| 406 | + url=source_path.as_uri(), |
| 407 | + source_root=source_root_dir, |
| 408 | + name="clone-test", |
| 409 | + tag="v1.0.0-release", |
| 410 | + ) |
| 411 | + |
| 412 | + # Verify HEAD points to a commit, not a tag object |
| 413 | + head_sha = cloned_repo.refs[HEADREF] |
| 414 | + head_obj = cloned_repo.object_store[head_sha] |
| 415 | + assert isinstance(head_obj, Commit), ( |
| 416 | + f"HEAD should point to a Commit (peeling nested tags), got {type(head_obj).__name__}" |
| 417 | + ) |
| 418 | + |
| 419 | + # Verify it's the correct commit |
| 420 | + assert head_sha == commit_sha |
| 421 | + |
| 422 | + # Verify the clone succeeded and files are present |
| 423 | + clone_dir = source_root_dir / "clone-test" |
| 424 | + assert (clone_dir / ".git").is_dir() |
| 425 | + assert (clone_dir / "test.txt").exists() |
| 426 | + assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "nested tag test" |
0 commit comments