use std::{error::Error, fs};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Semver {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl Semver {
pub fn parse(version: &str) -> Result<Self, Box<dyn Error>> {
let version = version
.trim()
.trim_start_matches('v')
.split(['-', '+'])
.next()
.ok_or("missing version")?;
let mut parts = version.split('.');
let major = parts.next().ok_or("missing major version")?.parse()?;
let minor = parts.next().ok_or("missing minor version")?.parse()?;
let patch = parts.next().ok_or("missing patch version")?.parse()?;
if parts.next().is_some() {
return Err("version must contain three numeric components".into());
}
Ok(Self {
major,
minor,
patch,
})
}
pub fn bump(&mut self, bump: Bump) {
match bump {
Bump::Major => {
self.major += 1;
self.minor = 0;
self.patch = 0;
}
Bump::Minor => {
self.minor += 1;
self.patch = 0;
}
Bump::Patch | Bump::Auto => self.patch += 1,
}
}
}
impl std::fmt::Display for Semver {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bump {
Auto,
Major,
Minor,
Patch,
}
#[derive(Debug, Clone)]
pub struct VersionInfo {
pub semver: Semver,
pub autoincrement: usize,
pub commit_date: String,
pub commit_hash: String,
}
/// Read the version from this project's `[package]` section, not a dependency.
pub fn current_version() -> Result<Semver, Box<dyn Error>> {
let manifest = fs::read_to_string("Cargo.toml")?;
let mut in_package = false;
for line in manifest.lines() {
if line.trim() == "[package]" {
in_package = true;
continue;
}
if line.trim_start().starts_with('[') {
in_package = false;
}
if in_package
&& let Some(value) = line
.trim()
.strip_prefix("version =")
.map(str::trim)
.and_then(|value| value.strip_prefix('"'))
.and_then(|value| value.strip_suffix('"'))
{
return Semver::parse(value);
}
}
Err("Cargo.toml has no [package].version".into())
}
pub fn version_info() -> Result<VersionInfo, Box<dyn Error>> {
let repo = git2::Repository::discover(".")?;
let head = repo.head()?.peel_to_commit()?;
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let autoincrement = revwalk.count();
let commit_date = time::OffsetDateTime::from_unix_timestamp(head.time().seconds())?
.date()
.to_string();
Ok(VersionInfo {
semver: current_version()?,
autoincrement,
commit_date,
commit_hash: head.id().to_string(),
})
}
pub fn run() -> Result<(), Box<dyn Error>> {
let info = version_info()?;
println!("semver: {}", info.semver);
println!("autoincrement: {}", info.autoincrement);
println!("commit date: {}", info.commit_date);
println!("commit hash: {}", info.commit_hash);
Ok(())
}
#[cfg(test)]
mod tests {
use super::{Bump, Semver};
#[test]
fn parses_versions_with_optional_v_prefix_and_metadata() {
assert_eq!(
Semver::parse("v1.2.3-rc.1+build").unwrap(),
Semver {
major: 1,
minor: 2,
patch: 3,
}
);
}
#[test]
fn bumps_and_resets_lower_components() {
let mut version = Semver {
major: 1,
minor: 2,
patch: 3,
};
version.bump(Bump::Minor);
assert_eq!(version.to_string(), "1.3.0");
version.bump(Bump::Major);
assert_eq!(version.to_string(), "2.0.0");
}
}