pub(crate) fn version_string(describe: &str, crate_version: &str) -> String {
let Some(rest) = describe.strip_prefix("fontc-v") else {
return crate_version.to_string();
};
let (rest, dirty) = match rest.strip_suffix("-dirty") {
Some(clean) => (clean, true),
None => (rest, false),
};
if let Some((tag_and_distance, sha)) = rest.rsplit_once("-g")
&& let Some((tag, distance)) = tag_and_distance.rsplit_once('-')
&& !distance.is_empty()
&& distance.bytes().all(|b| b.is_ascii_digit())
{
let base = if tag.contains('-') {
format!("{tag}.dev")
} else {
format!("{}-dev", bump_patch(tag))
};
let mut version = format!("{base}.{distance}+g{sha}");
if dirty {
version.push_str(".dirty");
}
return version;
}
if dirty {
format!("{rest}+dirty")
} else {
rest.to_string()
}
}
fn bump_patch(tag: &str) -> String {
let release = tag.split('-').next().unwrap_or(tag);
let parts: Vec<&str> = release.split('.').collect();
if let Some(patch) = parts.last().and_then(|p| p.parse::<u64>().ok()) {
let prefix = &parts[..parts.len() - 1];
if prefix.is_empty() {
return (patch + 1).to_string();
}
return format!("{}.{}", prefix.join("."), patch + 1);
}
tag.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn past_a_tag() {
assert_eq!(
version_string("fontc-v0.6.0-394-gd62ba016", "0.6.0"),
"0.6.1-dev.394+gd62ba016"
);
assert_eq!(
version_string("fontc-v0.6.0-394-gd62ba016-dirty", "0.6.0"),
"0.6.1-dev.394+gd62ba016.dirty"
);
}
#[test]
fn on_a_release_tag() {
assert_eq!(version_string("fontc-v0.6.0", "0.6.0"), "0.6.0");
assert_eq!(version_string("fontc-v0.6.0-dirty", "0.6.0"), "0.6.0+dirty");
}
#[test]
fn past_a_prerelease_tag() {
assert_eq!(
version_string("fontc-v1.0.0-rc.1-5-gabc1234", "1.0.0-rc.1"),
"1.0.0-rc.1.dev.5+gabc1234"
);
assert_eq!(
version_string("fontc-v1.0.0-rc.1-5-gabc1234-dirty", "1.0.0-rc.1"),
"1.0.0-rc.1.dev.5+gabc1234.dirty"
);
}
#[test]
fn on_a_prerelease_tag() {
assert_eq!(
version_string("fontc-v1.0.0-rc.1", "1.0.0-rc.1"),
"1.0.0-rc.1"
);
assert_eq!(
version_string("fontc-v1.0.0-rc.1-dirty", "1.0.0-rc.1"),
"1.0.0-rc.1+dirty"
);
}
#[test]
fn without_git() {
for describe in ["", "VERGEN_IDEMPOTENT_OUTPUT"] {
let v = version_string(describe, "0.6.0");
assert_eq!(v, "0.6.0");
assert!(!v.contains("VERGEN"), "{v:?}");
}
}
const EMITTED: &[&str] = &[
"0.6.1-dev.394+gd62ba016", "0.6.1-dev.394+gd62ba016.dirty", "0.6.0", "0.6.0+dirty", "1.0.0-rc.1.dev.5+gabc1234", "1.0.0-rc.1.dev.5+gabc1234.dirty", "1.0.0-rc.1", "1.0.0-rc.1+dirty", ];
#[test]
fn emitted_versions_are_valid_semver() {
for s in EMITTED {
let v = semver::Version::parse(s)
.unwrap_or_else(|e| panic!("{s:?} is not valid semver: {e}"));
assert_eq!(&v.to_string(), s, "did not round-trip");
}
let v = semver::Version::parse("0.6.1-dev.394+gd62ba016.dirty").unwrap();
assert_eq!((v.major, v.minor, v.patch), (0, 6, 1));
assert_eq!(v.pre.as_str(), "dev.394");
assert_eq!(v.build.as_str(), "gd62ba016.dirty");
}
#[test]
fn emitted_versions_sort_per_semver() {
use std::cmp::Ordering;
let v = |s: &str| semver::Version::parse(s).unwrap();
let snapshot = v("0.6.1-dev.394+gd62ba016");
assert_eq!(v("0.6.0").cmp_precedence(&snapshot), Ordering::Less);
assert_eq!(snapshot.cmp_precedence(&v("0.6.1")), Ordering::Less);
assert_eq!(snapshot.cmp_precedence(&v("0.7.0")), Ordering::Less);
assert_eq!(
v("0.6.1-dev.9+gd62ba016").cmp_precedence(&snapshot),
Ordering::Less
);
assert_eq!(
snapshot.cmp_precedence(&v("0.6.1-dev.1000+gd62ba016")),
Ordering::Less
);
assert_eq!(
snapshot.cmp_precedence(&v("0.6.1-dev.394+gd62ba016.dirty")),
Ordering::Equal
);
let rc_dev = v("1.0.0-rc.1.dev.5+gabc1234");
assert_eq!(v("1.0.0-rc.1").cmp_precedence(&rc_dev), Ordering::Less);
assert_eq!(rc_dev.cmp_precedence(&v("1.0.0-rc.2")), Ordering::Less);
assert_eq!(rc_dev.cmp_precedence(&v("1.0.0")), Ordering::Less);
}
#[test]
fn patch_bump() {
assert_eq!(bump_patch("0.6.0"), "0.6.1");
assert_eq!(bump_patch("0.6.9"), "0.6.10");
assert_eq!(bump_patch("1.0.0"), "1.0.1");
assert_eq!(bump_patch("0.6.0-rc.1"), "0.6.1");
}
}