Skip to main content

cli/
version.rs

1use std::sync::OnceLock;
2
3static SEMVER: OnceLock<String> = OnceLock::new();
4static DISPLAY_VERSION: OnceLock<String> = OnceLock::new();
5
6pub fn semver() -> &'static str {
7    SEMVER
8        .get_or_init(|| {
9            format_semver(
10                env!("CARGO_PKG_VERSION"),
11                option_env!("SHINE_VERSION_METADATA"),
12            )
13        })
14        .as_str()
15}
16
17pub fn display() -> &'static str {
18    DISPLAY_VERSION
19        .get_or_init(|| {
20            format_display(
21                semver(),
22                option_env!("SHINE_GIT_SHA"),
23                option_env!("SHINE_GIT_DATE"),
24            )
25        })
26        .as_str()
27}
28
29pub fn package() -> &'static str {
30    env!("CARGO_PKG_VERSION")
31}
32
33pub fn preview_commit() -> Option<&'static str> {
34    is_preview()
35        .then_some(option_env!("SHINE_GIT_SHA"))
36        .flatten()
37}
38
39fn is_preview() -> bool {
40    option_env!("SHINE_VERSION_METADATA").is_some_and(|metadata| {
41        let metadata = metadata.trim();
42        metadata == "preview" || metadata.starts_with("preview.")
43    })
44}
45
46fn format_semver(package_version: &str, metadata: Option<&str>) -> String {
47    match metadata
48        .map(str::trim)
49        .filter(|metadata| !metadata.is_empty())
50    {
51        Some("preview") => format!("{package_version}-preview"),
52        Some(metadata) if metadata.starts_with("preview.") => {
53            format!("{package_version}-preview")
54        }
55        Some(metadata) => format!("{package_version}+{metadata}"),
56        None => package_version.to_string(),
57    }
58}
59
60fn format_display(semver: &str, sha: Option<&str>, date: Option<&str>) -> String {
61    match (sha.and_then(trimmed), date.and_then(trimmed)) {
62        (Some(sha), Some(date)) => format!("{semver} ({sha} {date})"),
63        _ => semver.to_string(),
64    }
65}
66
67fn trimmed(value: &str) -> Option<&str> {
68    let value = value.trim();
69    (!value.is_empty()).then_some(value)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::{format_display, format_semver};
75
76    #[test]
77    fn format_version_without_metadata_returns_package_version() {
78        assert_eq!(format_semver("1.0.0", None), "1.0.0");
79        assert_eq!(format_semver("1.0.0", Some("")), "1.0.0");
80    }
81
82    #[test]
83    fn preview_uses_prerelease_label_without_duplicate_commit() {
84        assert_eq!(format_semver("1.0.0", Some("preview")), "1.0.0-preview");
85        assert_eq!(
86            format_semver("1.0.0", Some("preview.abc1234")),
87            "1.0.0-preview"
88        );
89    }
90
91    #[test]
92    fn display_matches_cargo_style_when_provenance_is_available() {
93        assert_eq!(
94            format_display("1.0.0", Some("30a34c682"), Some("2026-05-25")),
95            "1.0.0 (30a34c682 2026-05-25)"
96        );
97        assert_eq!(
98            format_display("1.0.0-preview", Some("30a34c682"), Some("2026-05-25")),
99            "1.0.0-preview (30a34c682 2026-05-25)"
100        );
101    }
102
103    #[test]
104    fn display_falls_back_to_semver_when_provenance_is_incomplete() {
105        assert_eq!(format_display("1.0.0", None, None), "1.0.0");
106        assert_eq!(format_display("1.0.0", Some("30a34c682"), None), "1.0.0");
107    }
108}