Skip to main content

tix/commands/
version.rs

1//! `tix version` — the crate version, and **how** this binary was built.
2//!
3//! `tix --version` answers "which release is this?". This answers the question a
4//! developer actually has when a change seems not to have taken effect: *did the
5//! binary I just ran come from the code on disk?* A version number cannot answer
6//! that — it only moves when someone bumps it — so the build stamp baked in by
7//! `build.rs` (the commit, whether the tree was dirty, and when it was built)
8//! does. Rebuilding on unchanged sources prints the same stamp; rebuilding after
9//! an edit prints a new one.
10//!
11//! Read-only and store-free: it needs no `tix.toml` and no issue tree, so it can
12//! be run from anywhere — including a directory where the rest of the CLI would
13//! refuse to start.
14
15/// The commit this binary was built from, `-dirty` when the working tree had
16/// uncommitted changes, and `unknown` when `git` was unavailable at build time.
17pub const COMMIT: &str = env!("TIX_BUILD_COMMIT");
18
19/// Print the version and the build provenance.
20pub fn print() {
21    println!("tix {}", env!("CARGO_PKG_VERSION"));
22    println!("commit: {}", COMMIT);
23    println!("built:  {}", built_at());
24}
25
26/// [`env!("TIX_BUILD_UNIX")`] rendered as a UTC timestamp.
27///
28/// Falls back to the raw seconds when the value cannot be read as a timestamp —
29/// a stamp that cannot be rendered is still worth printing, and never worth
30/// panicking over.
31fn built_at() -> String {
32    let raw = env!("TIX_BUILD_UNIX");
33    let Ok(seconds) = raw.parse::<i64>() else {
34        return raw.to_string();
35    };
36    match time::OffsetDateTime::from_unix_timestamp(seconds) {
37        Ok(at) => format!(
38            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
39            at.year(),
40            at.month() as u8,
41            at.day(),
42            at.hour(),
43            at.minute(),
44            at.second()
45        ),
46        Err(_) => raw.to_string(),
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    #[allow(clippy::wildcard_imports)]
53    use super::*;
54
55    #[test]
56    fn the_commit_stamp_is_never_empty() {
57        // `unknown` is the documented fallback, so the field is always printable —
58        // a blank line would read as "no stamp" rather than "no git here".
59        assert_ne!(COMMIT.trim(), "");
60    }
61
62    #[test]
63    fn the_build_time_renders_as_an_iso_utc_timestamp() {
64        let rendered = built_at();
65        assert_eq!(rendered.len(), 20, "unexpected stamp: {rendered}");
66        assert!(rendered.ends_with('Z'), "unexpected stamp: {rendered}");
67        let (date, time) = rendered.split_once('T').expect("an ISO stamp has a `T`");
68        assert_eq!(date.split('-').count(), 3, "unexpected date: {date}");
69        assert_eq!(time.split(':').count(), 3, "unexpected time: {time}");
70        assert!(
71            rendered
72                .chars()
73                .all(|c| c.is_ascii_digit() || "T-:Z".contains(c)),
74            "unexpected stamp: {rendered}"
75        );
76    }
77
78    #[test]
79    fn the_crate_version_is_semver_shaped() {
80        // The same shape `bin/tix-sync-version.sh` parses out of `Cargo.toml`, so a
81        // version this command prints is one the release tooling can read.
82        let version = env!("CARGO_PKG_VERSION");
83        let parts: Vec<&str> = version.split('.').collect();
84        assert_eq!(parts.len(), 3, "expected a semver version, got {version}");
85        assert!(
86            parts.iter().all(|part| part.parse::<u64>().is_ok()),
87            "expected numeric components, got {version}"
88        );
89    }
90}