use git2::{Object, ObjectType, Oid, Reference, Repository, Tag};
use regex::Regex;
use semver::Version;
use std::error::Error;
pub fn get_latest_version_tag(
repository: &Repository,
) -> Result<Option<VersionTag>, Box<dyn Error>> {
let references: Vec<Reference> = repository
.references()?
.filter_map(|reference| reference.ok())
.collect();
let version_tags: Vec<VersionTag> = references
.iter()
.filter(|reference| reference.is_tag())
.filter_map(|reference| {
reference.target().and_then(|oid| {
repository
.find_object(oid, None)
.ok()
.map(|object| (reference, object))
})
})
.filter_map(|(reference, object)| {
Tag::from_object(object)
.and_then(|tag| VersionTag::from_annotated_tag(&tag))
.or_else(|| VersionTag::from_lightweight_tag(reference))
})
.collect();
Ok(version_tags.iter().max().cloned())
}
trait AnnotatedTag {
fn from_object(object: Object) -> Option<Tag>;
}
impl AnnotatedTag for Tag<'_> {
fn from_object(object: Object<'_>) -> Option<Tag<'_>> {
object
.peel(ObjectType::Tag)
.ok()
.and_then(|tag_object| tag_object.as_tag().cloned())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct VersionTag {
pub version: Version,
pub commit_oid: Oid,
}
impl VersionTag {
fn from_annotated_tag(tag: &Tag) -> Option<Self> {
let tag_name = tag.name().unwrap();
if !Self::is_valid_version_tag(tag_name) {
return None;
}
let version_number = tag_name.trim_start_matches("v");
Some(Self {
version: Version::parse(version_number).unwrap(),
commit_oid: tag.target_id(),
})
}
fn from_lightweight_tag(reference: &Reference) -> Option<Self> {
let tag_name = reference.shorthand().unwrap();
if !Self::is_valid_version_tag(tag_name) {
return None;
}
let version_number = tag_name.trim_start_matches("v");
Some(Self {
version: Version::parse(version_number).unwrap(),
commit_oid: reference.target().unwrap(),
})
}
fn is_valid_version_tag(tag_name: &str) -> bool {
let version_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap();
version_regex.is_match(tag_name)
}
}
#[cfg(test)]
mod version_tag_tests {
pub use crate::repo::RepositoryExtension;
use crate::test_util::repo_init;
pub use crate::test_util::RepositoryTestExtensions;
use semver::Version;
#[test]
fn repository_does_not_have_tags() {
let (_temp_dir, repository) = repo_init(None);
let result = repository.get_latest_version_tag().unwrap();
assert!(result.is_none(), "Expected None, but got Some")
}
#[test]
fn repository_does_not_have_version_tags() {
let (_temp_dir, repository) = repo_init(Some(vec![":tada: initial release"]));
let commit = repository.find_commit_by_message(":tada: initial release");
repository.add_tag(commit.unwrap(), "tag_1");
let result = repository.get_latest_version_tag().unwrap();
assert!(result.is_none(), "Expected None, but got Some")
}
#[test]
fn repository_has_one_annotated_version_tag() {
let commit_message = ":tada: initial release";
let (_temp_dir, repository) = repo_init(Some(vec![commit_message]));
let commit = repository.find_commit_by_message(commit_message);
repository.add_tag(commit.unwrap(), "v1.0.0");
let result = repository.get_latest_version_tag().unwrap().unwrap();
assert_eq!(result.version, Version::parse("1.0.0").unwrap());
assert_eq!(
result.commit_oid,
repository
.find_commit_by_message(commit_message)
.unwrap()
.id(),
"Object IDs don't match"
);
}
#[test]
fn repository_has_one_not_annotated_version_tag() {
let commit_message = ":tada: initial release";
let (_temp_dir, repository) = repo_init(Some(vec![commit_message]));
let commit = repository.find_commit_by_message(commit_message).unwrap();
repository
.tag_lightweight("v1.0.0", commit.as_object(), false)
.unwrap();
let result = repository.get_latest_version_tag().unwrap().unwrap();
assert_eq!(result.version, Version::parse("1.0.0").unwrap());
assert_eq!(
result.commit_oid,
repository
.find_commit_by_message(commit_message)
.unwrap()
.id(),
"Object IDs don't match"
);
}
#[test]
fn repository_have_multiple_version_tags() {
let commit_messages = vec![
":tada: initial release",
":sparkles: new feature",
":boom: everything is broken",
];
let (_temp_dir, repository) = repo_init(Some(commit_messages.clone()));
let tags = vec!["v1.0.0", "v1.1.0", "v2.0.0"];
commit_messages
.iter()
.map(|commit| repository.find_commit_by_message(commit).unwrap())
.zip(tags)
.for_each(|(commit_id, tag)| repository.add_tag(commit_id, tag));
let result = repository.get_latest_version_tag().unwrap().unwrap();
assert_eq!(result.version, Version::parse("2.0.0").unwrap());
assert_eq!(
result.commit_oid,
repository
.find_commit_by_message(commit_messages.last().unwrap())
.unwrap()
.id(),
"Object IDs don't match"
);
}
}