Skip to content
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");
    }
}