use serde::{Deserialize, Serialize};
pub const GIT_SHA: Option<&str> = option_env!("OMNI_DEV_GIT_SHA");
pub const GIT_SHA_SHORT: Option<&str> = option_env!("OMNI_DEV_GIT_SHA_SHORT");
pub const GIT_COMMIT_DATE: Option<&str> = option_env!("OMNI_DEV_GIT_COMMIT_DATE");
const GIT_DIRTY: Option<&str> = option_env!("OMNI_DEV_GIT_DIRTY");
const BUILD_EPOCH: Option<&str> = option_env!("OMNI_DEV_BUILD_EPOCH");
#[must_use]
pub fn git_dirty() -> Option<bool> {
match GIT_DIRTY {
Some("true") => Some(true),
Some("false") => Some(false),
_ => None,
}
}
#[must_use]
pub fn build_timestamp() -> Option<String> {
let secs: i64 = BUILD_EPOCH?.parse().ok()?;
chrono::DateTime::from_timestamp(secs, 0).map(|dt| dt.to_rfc3339())
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Provenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit_long: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit_date: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dirty: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_timestamp: Option<String>,
}
#[must_use]
pub fn provenance() -> Provenance {
Provenance {
commit: GIT_SHA_SHORT.map(str::to_string),
commit_long: GIT_SHA.map(str::to_string),
commit_date: GIT_COMMIT_DATE.map(str::to_string),
dirty: git_dirty(),
build_timestamp: build_timestamp(),
}
}
#[must_use]
pub fn long_version() -> &'static str {
static LONG_VERSION: std::sync::LazyLock<String> =
std::sync::LazyLock::new(compute_long_version);
LONG_VERSION.as_str()
}
fn compute_long_version() -> String {
let mut suffix = String::new();
if let Some(commit) = GIT_SHA_SHORT {
suffix.push_str(commit);
}
if let Some(date) = GIT_COMMIT_DATE {
let day = date.split('T').next().unwrap_or(date);
if !suffix.is_empty() {
suffix.push(' ');
}
suffix.push_str(day);
}
if git_dirty() == Some(true) {
if !suffix.is_empty() {
suffix.push_str(", ");
}
suffix.push_str("dirty");
}
if suffix.is_empty() {
crate::VERSION.to_string()
} else {
format!("{} ({suffix})", crate::VERSION)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn git_dirty_maps_the_raw_flag() {
assert_eq!(interpret_dirty(Some("true")), Some(true));
assert_eq!(interpret_dirty(Some("false")), Some(false));
assert_eq!(interpret_dirty(None), None);
assert_eq!(interpret_dirty(Some("garbage")), None);
}
fn interpret_dirty(raw: Option<&str>) -> Option<bool> {
match raw {
Some("true") => Some(true),
Some("false") => Some(false),
_ => None,
}
}
#[test]
fn build_timestamp_formats_epoch_seconds() {
let dt = chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap();
assert_eq!(dt.to_rfc3339(), "2023-11-14T22:13:20+00:00");
}
#[test]
fn long_version_always_starts_with_the_crate_version() {
let v = long_version();
assert!(v.starts_with(crate::VERSION), "{v}");
}
#[test]
fn provenance_round_trips_and_omits_absent_fields() {
let empty = serde_json::to_string(&Provenance::default()).unwrap();
assert_eq!(empty, "{}");
let full = Provenance {
commit: Some("a6d304fd".to_string()),
commit_long: Some("a6d304fddeadbeef".to_string()),
commit_date: Some("2026-07-20T15:33:17+10:00".to_string()),
dirty: Some(true),
build_timestamp: Some("2026-07-20T05:33:17+00:00".to_string()),
};
let line = serde_json::to_string(&full).unwrap();
let back: Provenance = serde_json::from_str(&line).unwrap();
assert_eq!(back.commit.as_deref(), Some("a6d304fd"));
assert_eq!(back.dirty, Some(true));
}
}