uv_cli/
version.rs

1//! Code for representing uv's release version number.
2// See also <https://github.com/astral-sh/ruff/blob/8118d29419055b779719cc96cdf3dacb29ac47c9/crates/ruff/src/version.rs>
3use std::fmt;
4
5use serde::Serialize;
6
7use uv_normalize::PackageName;
8use uv_pep508::uv_pep440::Version;
9
10/// Information about the git repository where uv was built from.
11#[derive(Serialize)]
12pub(crate) struct CommitInfo {
13    short_commit_hash: String,
14    commit_hash: String,
15    commit_date: String,
16    last_tag: Option<String>,
17    commits_since_last_tag: u32,
18}
19
20/// uv's version.
21#[derive(Serialize)]
22pub struct VersionInfo {
23    /// Name of the package (or "uv" if printing uv's own version)
24    pub package_name: Option<String>,
25    /// version, such as "0.5.1"
26    version: String,
27    /// Information about the git commit we may have been built from.
28    ///
29    /// `None` if not built from a git repo or if retrieval failed.
30    commit_info: Option<CommitInfo>,
31}
32
33impl VersionInfo {
34    pub fn new(package_name: Option<&PackageName>, version: &Version) -> Self {
35        Self {
36            package_name: package_name.map(ToString::to_string),
37            version: version.to_string(),
38            commit_info: None,
39        }
40    }
41}
42
43impl fmt::Display for VersionInfo {
44    /// Formatted version information: "<version>[+<commits>] (<commit> <date>)"
45    ///
46    /// This is intended for consumption by `clap` to provide `uv --version`,
47    /// and intentionally omits the name of the package
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write!(f, "{}", self.version)?;
50        if let Some(ci) = &self.commit_info {
51            write!(f, "{ci}")?;
52        }
53        Ok(())
54    }
55}
56
57impl fmt::Display for CommitInfo {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        if self.commits_since_last_tag > 0 {
60            write!(f, "+{}", self.commits_since_last_tag)?;
61        }
62        write!(f, " ({} {})", self.short_commit_hash, self.commit_date)?;
63        Ok(())
64    }
65}
66
67impl From<VersionInfo> for clap::builder::Str {
68    fn from(val: VersionInfo) -> Self {
69        val.to_string().into()
70    }
71}
72
73/// Returns information about uv's version.
74pub fn uv_self_version() -> VersionInfo {
75    // Environment variables are only read at compile-time
76    macro_rules! option_env_str {
77        ($name:expr) => {
78            option_env!($name).map(|s| s.to_string())
79        };
80    }
81
82    // This version is pulled from Cargo.toml and set by Cargo
83    let version = uv_version::version().to_string();
84
85    // Commit info is pulled from git and set by `build.rs`
86    let commit_info = option_env_str!("UV_COMMIT_HASH").map(|commit_hash| CommitInfo {
87        short_commit_hash: option_env_str!("UV_COMMIT_SHORT_HASH").unwrap(),
88        commit_hash,
89        commit_date: option_env_str!("UV_COMMIT_DATE").unwrap(),
90        last_tag: option_env_str!("UV_LAST_TAG"),
91        commits_since_last_tag: option_env_str!("UV_LAST_TAG_DISTANCE")
92            .as_deref()
93            .map_or(0, |value| value.parse::<u32>().unwrap_or(0)),
94    });
95
96    VersionInfo {
97        package_name: Some("uv".to_owned()),
98        version,
99        commit_info,
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use insta::{assert_json_snapshot, assert_snapshot};
106
107    use super::{CommitInfo, VersionInfo};
108
109    #[test]
110    fn version_formatting() {
111        let version = VersionInfo {
112            package_name: Some("uv".to_string()),
113            version: "0.0.0".to_string(),
114            commit_info: None,
115        };
116        assert_snapshot!(version, @"0.0.0");
117    }
118
119    #[test]
120    fn version_formatting_with_commit_info() {
121        let version = VersionInfo {
122            package_name: Some("uv".to_string()),
123            version: "0.0.0".to_string(),
124            commit_info: Some(CommitInfo {
125                short_commit_hash: "53b0f5d92".to_string(),
126                commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
127                last_tag: Some("v0.0.1".to_string()),
128                commit_date: "2023-10-19".to_string(),
129                commits_since_last_tag: 0,
130            }),
131        };
132        assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)");
133    }
134
135    #[test]
136    fn version_formatting_with_commits_since_last_tag() {
137        let version = VersionInfo {
138            package_name: Some("uv".to_string()),
139            version: "0.0.0".to_string(),
140            commit_info: Some(CommitInfo {
141                short_commit_hash: "53b0f5d92".to_string(),
142                commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
143                last_tag: Some("v0.0.1".to_string()),
144                commit_date: "2023-10-19".to_string(),
145                commits_since_last_tag: 24,
146            }),
147        };
148        assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)");
149    }
150
151    #[test]
152    fn version_serializable() {
153        let version = VersionInfo {
154            package_name: Some("uv".to_string()),
155            version: "0.0.0".to_string(),
156            commit_info: Some(CommitInfo {
157                short_commit_hash: "53b0f5d92".to_string(),
158                commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
159                last_tag: Some("v0.0.1".to_string()),
160                commit_date: "2023-10-19".to_string(),
161                commits_since_last_tag: 0,
162            }),
163        };
164        assert_json_snapshot!(version, @r#"
165    {
166      "package_name": "uv",
167      "version": "0.0.0",
168      "commit_info": {
169        "short_commit_hash": "53b0f5d92",
170        "commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7",
171        "commit_date": "2023-10-19",
172        "last_tag": "v0.0.1",
173        "commits_since_last_tag": 0
174      }
175    }
176    "#);
177    }
178}