diff --git a/src/git/tags.rs b/src/git/tags.rs index ae6838a5..9efcedc8 100644 --- a/src/git/tags.rs +++ b/src/git/tags.rs @@ -86,7 +86,9 @@ impl TagIndex { let floating = self.floating_tag_names(prefix); self.entries .iter() - .filter(|e| e.reachable && e.name.starts_with(prefix) && !floating.contains(&e.name)) + .filter(|e| { + e.reachable && is_version_tag(&e.name, prefix) && !floating.contains(&e.name) + }) .max_by_key(|e| e.time) .map(|e| e.name.clone()) } @@ -131,7 +133,9 @@ impl TagIndex { let floating = self.floating_tag_names(prefix); self.entries .iter() - .filter(|e| e.reachable && e.name.starts_with(prefix) && !floating.contains(&e.name)) + .filter(|e| { + e.reachable && is_version_tag(&e.name, prefix) && !floating.contains(&e.name) + }) .max_by_key(|e| e.time) .map(|e| e.commit_oid) } @@ -149,7 +153,7 @@ impl TagIndex { .iter() .filter(|e| { e.reachable - && e.name.starts_with(prefix) + && is_version_tag(&e.name, prefix) && !is_prerelease_tag(&e.name, prefix) && !floating.contains(&e.name) }) @@ -302,6 +306,18 @@ pub(super) fn floating_tag_names<'a>( floating } +pub(super) fn is_version_tag(tag_name: &str, prefix: &str) -> bool { + let Some(rest) = tag_name.strip_prefix(prefix) else { + return false; + }; + let rest = rest.strip_prefix('v').unwrap_or(rest); + let core = rest.split(['-', '+']).next().unwrap_or(rest); + !core.is_empty() + && core + .split('.') + .all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())) +} + pub(super) fn is_prerelease_tag(tag_name: &str, prefix: &str) -> bool { let version_part = tag_name.strip_prefix(prefix).unwrap_or(tag_name); version_part.contains('-') @@ -467,8 +483,7 @@ fn prefixed_tags( .flatten() .filter_map(|reference| { let name = String::from_utf8_lossy(reference.name().shorten()).into_owned(); - name.starts_with(prefix) - .then(|| (name, reference.id().detach())) + is_version_tag(&name, prefix).then(|| (name, reference.id().detach())) }) .collect(); let floating = floating_tag_names(tags.iter().map(|(name, _)| name.as_str()), prefix); diff --git a/src/git/tests.rs b/src/git/tests.rs index 77d3ec4a..ea92c04a 100644 --- a/src/git/tests.rs +++ b/src/git/tests.rs @@ -2566,3 +2566,56 @@ fn tag_index_reads_sequential_tags() { Some("v3") ); } + +#[test] +fn prefix_sharing_tag_without_a_version_is_not_the_last_tag() { + let (dir, repo) = init_repo(); + create_commit_in_repo(&repo, dir.path(), "a.txt", "feat: first"); + create_lightweight_tag(&repo, "v1.0.0"); + create_commit_in_repo(&repo, dir.path(), "b.txt", "feat!: breaking"); + create_lightweight_tag(&repo, "vendor-sync"); + create_commit_in_repo(&repo, dir.path(), "c.txt", "fix: third"); + let v1 = git(dir.path(), &["rev-parse", "v1.0.0^{commit}"]); + + let idx = TagIndex::build(&repo).unwrap(); + let last = idx + .find_last_tag_commit("v", OrphanedTagStrategy::Warn) + .unwrap(); + assert_eq!(last.to_string(), v1.trim()); + + let found = find_last_tag_name(&repo, "v", OrphanedTagStrategy::Warn).unwrap(); + assert_eq!(found, Some("v1.0.0".to_string())); +} + +#[test] +fn tag_of_a_sibling_package_is_not_the_last_tag() { + let (dir, repo) = init_repo(); + create_commit_in_repo(&repo, dir.path(), "a.txt", "feat: first"); + create_lightweight_tag(&repo, "core-1.0.0"); + create_commit_in_repo(&repo, dir.path(), "b.txt", "feat(core): second"); + create_commit_in_repo(&repo, dir.path(), "c.txt", "chore: third"); + create_lightweight_tag(&repo, "core-utils-1.0.1"); + let first = git(dir.path(), &["rev-parse", "core-1.0.0^{commit}"]); + + let idx = TagIndex::build(&repo).unwrap(); + let last = idx + .find_last_tag_commit("core-", OrphanedTagStrategy::Warn) + .unwrap(); + assert_eq!(last.to_string(), first.trim()); +} + +#[test] +fn prerelease_and_v_prefixed_versions_still_count_as_tags() { + let (dir, repo) = init_repo(); + create_commit_in_repo(&repo, dir.path(), "a.txt", "feat: first"); + create_lightweight_tag(&repo, "pkg-v1.0.0"); + create_commit_in_repo(&repo, dir.path(), "b.txt", "feat: second"); + create_lightweight_tag(&repo, "pkg-v1.1.0-beta.1"); + let beta = git(dir.path(), &["rev-parse", "pkg-v1.1.0-beta.1^{commit}"]); + + let idx = TagIndex::build(&repo).unwrap(); + let last = idx + .find_last_tag_commit("pkg-", OrphanedTagStrategy::Warn) + .unwrap(); + assert_eq!(last.to_string(), beta.trim()); +}