at 8645f5a
use chrono::{DateTime, Utc}; use miette::{IntoDiagnostic, Result, miette}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::{VersionExtractor, VersionInfo}; fn default_dirty_suffix() -> String { "-dirty".to_owned() } /// Configuration for [`GitVersion`]. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] pub struct GitVersionConfig { /// Strip this prefix from the tag name before returning the version. /// For example, `"v"` turns `"v1.2.3"` into `"1.2.3"`. pub tag_prefix: Option<String>, /// Suffix appended to the version when the working tree has uncommitted /// changes. Forwarded verbatim as `--dirty=<suffix>` to `git describe`. /// Defaults to `"-dirty"`. #[serde(default = "default_dirty_suffix")] pub dirty_suffix: String, } impl Default for GitVersionConfig { fn default() -> Self { Self { tag_prefix: None, dirty_suffix: default_dirty_suffix(), } } } /// Extracts the version by running `git describe --tags --always`. pub struct GitVersion; /// Run `git log -1 --format=%cI <refspec>` and parse the result as a UTC /// [`DateTime`]. Returns `None` on any failure so callers can treat the date /// as optional. async fn git_commit_date(refspec: &str) -> Option<DateTime<Utc>> { let output = tokio::process::Command::new("git") .args(["log", "-1", "--format=%cI", refspec]) .output() .await .ok()?; if !output.status.success() { return None; } let raw = String::from_utf8(output.stdout).ok()?; DateTime::parse_from_rfc3339(raw.trim()) .ok() .map(|dt| dt.with_timezone(&Utc)) } impl VersionExtractor for GitVersion { type ConfigType = GitVersionConfig; async fn get_last_version(&self, config: Self::ConfigType) -> Result<VersionInfo> { let dirty_arg = format!("--dirty={}", config.dirty_suffix); let output = tokio::process::Command::new("git") .args(["describe", "--tags", "--always", &dirty_arg]) .output() .await .into_diagnostic()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(miette!("git describe failed:\n{stderr}")); } let raw = String::from_utf8(output.stdout).into_diagnostic()?; let version_raw = raw.trim(); let version = match &config.tag_prefix { Some(prefix) => version_raw .strip_prefix(prefix.as_str()) .unwrap_or(version_raw), None => version_raw, } .to_owned(); // Use the HEAD commit date as the "release date" for the current build. let date = git_commit_date("HEAD").await; Ok(VersionInfo { version, date }) } /// Return all Git tags as [`VersionInfo`] entries, each carrying the /// tag's creator date (annotated tag date, or commit date for lightweight /// tags). Uses a single `git for-each-ref` invocation. async fn get_all_versions(&self, config: Self::ConfigType) -> Result<Vec<VersionInfo>> { // %(creatordate:iso-strict) works for both annotated and lightweight tags. let output = tokio::process::Command::new("git") .args([ "for-each-ref", "--sort=version:refname", "--format=%(refname:short)\t%(creatordate:iso-strict)", "refs/tags", ]) .output() .await .into_diagnostic()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(miette!("git for-each-ref failed:\n{stderr}")); } let raw = String::from_utf8(output.stdout).into_diagnostic()?; let versions = raw .lines() .filter(|s| !s.is_empty()) .map(|line| { // Split on the first tab; anything after is the date string. let (tag, date_str) = line.split_once('\t').unwrap_or((line, "")); let version = match &config.tag_prefix { Some(prefix) => tag.strip_prefix(prefix.as_str()).unwrap_or(tag), None => tag, } .to_owned(); let date = DateTime::parse_from_rfc3339(date_str.trim()) .ok() .map(|dt| dt.with_timezone(&Utc)); VersionInfo { version, date } }) .collect(); Ok(versions) } }