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}