1use serde::{Deserialize, Serialize};
13
14pub const GIT_SHA: Option<&str> = option_env!("OMNI_DEV_GIT_SHA");
17
18pub const GIT_SHA_SHORT: Option<&str> = option_env!("OMNI_DEV_GIT_SHA_SHORT");
20
21pub const GIT_COMMIT_DATE: Option<&str> = option_env!("OMNI_DEV_GIT_COMMIT_DATE");
24
25const GIT_DIRTY: Option<&str> = option_env!("OMNI_DEV_GIT_DIRTY");
28
29const BUILD_EPOCH: Option<&str> = option_env!("OMNI_DEV_BUILD_EPOCH");
32
33#[must_use]
37pub fn git_dirty() -> Option<bool> {
38 match GIT_DIRTY {
39 Some("true") => Some(true),
40 Some("false") => Some(false),
41 _ => None,
42 }
43}
44
45#[must_use]
48pub fn build_timestamp() -> Option<String> {
49 let secs: i64 = BUILD_EPOCH?.parse().ok()?;
50 chrono::DateTime::from_timestamp(secs, 0).map(|dt| dt.to_rfc3339())
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct Provenance {
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub commit: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub commit_long: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub commit_date: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub dirty: Option<bool>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub build_timestamp: Option<String>,
76}
77
78#[must_use]
82pub fn provenance() -> Provenance {
83 Provenance {
84 commit: GIT_SHA_SHORT.map(str::to_string),
85 commit_long: GIT_SHA.map(str::to_string),
86 commit_date: GIT_COMMIT_DATE.map(str::to_string),
87 dirty: git_dirty(),
88 build_timestamp: build_timestamp(),
89 }
90}
91
92#[must_use]
99pub fn long_version() -> &'static str {
100 static LONG_VERSION: std::sync::LazyLock<String> =
101 std::sync::LazyLock::new(compute_long_version);
102 LONG_VERSION.as_str()
103}
104
105fn compute_long_version() -> String {
107 let mut suffix = String::new();
108 if let Some(commit) = GIT_SHA_SHORT {
109 suffix.push_str(commit);
110 }
111 if let Some(date) = GIT_COMMIT_DATE {
112 let day = date.split('T').next().unwrap_or(date);
114 if !suffix.is_empty() {
115 suffix.push(' ');
116 }
117 suffix.push_str(day);
118 }
119 if git_dirty() == Some(true) {
120 if !suffix.is_empty() {
121 suffix.push_str(", ");
122 }
123 suffix.push_str("dirty");
124 }
125 if suffix.is_empty() {
126 crate::VERSION.to_string()
127 } else {
128 format!("{} ({suffix})", crate::VERSION)
129 }
130}
131
132#[cfg(test)]
133#[allow(clippy::unwrap_used, clippy::expect_used)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn git_dirty_maps_the_raw_flag() {
139 assert_eq!(interpret_dirty(Some("true")), Some(true));
143 assert_eq!(interpret_dirty(Some("false")), Some(false));
144 assert_eq!(interpret_dirty(None), None);
145 assert_eq!(interpret_dirty(Some("garbage")), None);
146 }
147
148 fn interpret_dirty(raw: Option<&str>) -> Option<bool> {
151 match raw {
152 Some("true") => Some(true),
153 Some("false") => Some(false),
154 _ => None,
155 }
156 }
157
158 #[test]
159 fn build_timestamp_formats_epoch_seconds() {
160 let dt = chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap();
163 assert_eq!(dt.to_rfc3339(), "2023-11-14T22:13:20+00:00");
164 }
165
166 #[test]
167 fn long_version_always_starts_with_the_crate_version() {
168 let v = long_version();
169 assert!(v.starts_with(crate::VERSION), "{v}");
170 }
171
172 #[test]
173 fn provenance_round_trips_and_omits_absent_fields() {
174 let empty = serde_json::to_string(&Provenance::default()).unwrap();
177 assert_eq!(empty, "{}");
178
179 let full = Provenance {
180 commit: Some("a6d304fd".to_string()),
181 commit_long: Some("a6d304fddeadbeef".to_string()),
182 commit_date: Some("2026-07-20T15:33:17+10:00".to_string()),
183 dirty: Some(true),
184 build_timestamp: Some("2026-07-20T05:33:17+00:00".to_string()),
185 };
186 let line = serde_json::to_string(&full).unwrap();
187 let back: Provenance = serde_json::from_str(&line).unwrap();
188 assert_eq!(back.commit.as_deref(), Some("a6d304fd"));
189 assert_eq!(back.dirty, Some(true));
190 }
191}