diff --git a/crates/infrastructure/src/git/mod.rs b/crates/infrastructure/src/git/mod.rs index 9300fad3..236e48b7 100644 --- a/crates/infrastructure/src/git/mod.rs +++ b/crates/infrastructure/src/git/mod.rs @@ -207,8 +207,9 @@ impl GitPort for Git2Repository { let mut ref_map: HashMap> = HashMap::new(); if let Ok(references) = repo.references() { for reference in references.flatten() { - // Only handle symbolic and direct refs; skip errors. - let target_oid = reference.resolve().ok().and_then(|r| r.target()); + // Only handle refs that resolve to commits; annotated tags point at + // tag objects, so `target()` alone would never match revwalk commits. + let target_oid = reference_commit_oid(&reference); if let Some(oid) = target_oid { let label = match reference.shorthand() { Some(name) => { @@ -269,3 +270,16 @@ impl GitPort for Git2Repository { )) } } + +fn reference_commit_oid(reference: &git2::Reference<'_>) -> Option { + reference + .peel(git2::ObjectType::Commit) + .ok() + .map(|object| object.id()) + .or_else(|| { + reference + .resolve() + .ok() + .and_then(|resolved| resolved.target()) + }) +} diff --git a/crates/infrastructure/tests/git_repository.rs b/crates/infrastructure/tests/git_repository.rs index ab5e49fb..b1328ec4 100644 --- a/crates/infrastructure/tests/git_repository.rs +++ b/crates/infrastructure/tests/git_repository.rs @@ -111,3 +111,45 @@ async fn unstage_after_first_commit_resets_index() { let a = st.iter().find(|s| s.path == "a.txt").unwrap(); assert!(!a.staged, "unstaged change is no longer in the index"); } + +#[tokio::test] +async fn git_graph_labels_annotated_tags_on_their_target_commit() { + let tmp = TempDir::new(); + let root = tmp.root(); + let git = Git2Repository::new(); + git.init(&root).await.unwrap(); + + tmp.write("a.txt", "v1"); + git.stage(&root, "a.txt").await.unwrap(); + let tagged = git.commit(&root, "tagged commit").await.unwrap(); + + tmp.write("a.txt", "v2"); + git.stage(&root, "a.txt").await.unwrap(); + git.commit(&root, "head commit").await.unwrap(); + + let repo = git2::Repository::open(root.as_str()).unwrap(); + let target = repo + .find_object( + git2::Oid::from_str(&tagged.hash).unwrap(), + Some(git2::ObjectType::Commit), + ) + .unwrap(); + let sig = repo + .signature() + .or_else(|_| git2::Signature::now("IdeA", "idea@localhost")) + .unwrap(); + repo.tag("v0.13.0", &target, &sig, "release v0.13.0", false) + .unwrap(); + + let graph = git.log_graph(&root, 10).await.unwrap(); + let commit = graph + .iter() + .find(|commit| commit.hash == tagged.hash) + .expect("tagged ancestor commit is included in graph"); + + assert!( + commit.refs.iter().any(|r| r == "tag: v0.13.0"), + "annotated tag must be attached to target commit refs, got {:?}", + commit.refs + ); +}