Add long-form version output (#1930)

Similar to https://github.com/astral-sh/ruff/pull/8034

Adds more version information so it's clear what revision the user is on

```
❯ cargo run -q -- --version
uv 0.1.10 (daa8565a7 2024-02-23)
❯ cargo run -q -- -V
uv 0.1.10
❯ cargo run -q -- version
uv 0.1.10 (daa8565a7 2024-02-23)
❯ cargo run -q -- version --output-format json
{
  "version": "0.1.10",
  "commit_info": {
    "short_commit_hash": "daa8565a7",
    "commit_hash": "daa8565a75249305821fdc34ace085060c082ba3",
    "commit_date": "2024-02-23",
    "last_tag": null,
    "commits_since_last_tag": 0
  }
}
```
This commit is contained in:
Zanie Blue 2024-02-23 13:45:01 -06:00 committed by GitHub
parent ba9c788680
commit af39bbde75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 274 additions and 5 deletions

3
Cargo.lock generated
View File

@ -1572,6 +1572,7 @@ dependencies = [
"lazy_static",
"linked-hash-map",
"regex",
"serde",
"similar",
"yaml-rust",
]
@ -4168,6 +4169,8 @@ dependencies = [
"requirements-txt",
"reqwest",
"rustc-hash",
"serde",
"serde_json",
"tempfile",
"textwrap",
"thiserror",

View File

@ -41,7 +41,7 @@ anstream = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive"] }
clap = { workspace = true, features = ["derive", "string"] }
clap_complete_command = { workspace = true }
console = { workspace = true }
ctrlc = { workspace = true }
@ -56,6 +56,8 @@ owo-colors = { workspace = true }
pubgrub = { workspace = true }
pyproject-toml = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
textwrap = { workspace = true }
thiserror = { workspace = true }
@ -79,7 +81,7 @@ assert_cmd = { version = "2.0.12" }
assert_fs = { version = "1.1.0" }
filetime = { version = "0.2.23" }
indoc = { version = "2.0.4" }
insta = { version = "1.34.0", features = ["filters"] }
insta = { version = "1.34.0", features = ["filters", "json"] }
predicates = { version = "3.0.4" }
regex = { version = "1.10.3" }
reqwest = { version = "0.11.23", features = ["blocking"], default-features = false }
@ -94,4 +96,3 @@ pypi = []
git = []
# Introduces a dependency on Maturin.
maturin = []

80
crates/uv/build.rs Normal file
View File

@ -0,0 +1,80 @@
use std::{fs, path::Path, process::Command};
fn main() {
// The workspace root directory is not available without walking up the tree
// https://github.com/rust-lang/cargo/issues/3946
let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("..")
.join("..");
commit_info(&workspace_root);
#[allow(clippy::disallowed_methods)]
let target = std::env::var("TARGET").unwrap();
println!("cargo:rustc-env=RUST_HOST_TARGET={target}");
}
fn commit_info(workspace_root: &Path) {
// If not in a git repository, do not attempt to retrieve commit information
let git_dir = workspace_root.join(".git");
if !git_dir.exists() {
return;
}
let git_head_path = git_dir.join("HEAD");
println!(
"cargo:rerun-if-changed={}",
git_head_path.as_path().display()
);
let git_head_contents = fs::read_to_string(git_head_path);
if let Ok(git_head_contents) = git_head_contents {
// The contents are either a commit or a reference in the following formats
// - "<commit>" when the head is detached
// - "ref <ref>" when working on a branch
// If a commit, checking if the HEAD file has changed is sufficient
// If a ref, we need to add the head file for that ref to rebuild on commit
let mut git_ref_parts = git_head_contents.split_whitespace();
git_ref_parts.next();
if let Some(git_ref) = git_ref_parts.next() {
let git_ref_path = git_dir.join(git_ref);
println!(
"cargo:rerun-if-changed={}",
git_ref_path.as_path().display()
);
}
}
let output = match Command::new("git")
.arg("log")
.arg("-1")
.arg("--date=short")
.arg("--abbrev=9")
.arg("--format=%H %h %cd %(describe)")
.output()
{
Ok(output) if output.status.success() => output,
_ => return,
};
let stdout = String::from_utf8(output.stdout).unwrap();
let mut parts = stdout.split_whitespace();
let mut next = || parts.next().unwrap();
println!("cargo:rustc-env=UV_COMMIT_HASH={}", next());
println!("cargo:rustc-env=UV_COMMIT_SHORT_HASH={}", next());
println!("cargo:rustc-env=UV_COMMIT_DATE={}", next());
// Describe can fail for some commits
// https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem
if let Some(describe) = parts.next() {
let mut describe_parts = describe.split('-');
println!(
"cargo:rustc-env=UV_LAST_TAG={}",
describe_parts.next().unwrap()
);
// If this is the tagged commit, this component will be missing
println!(
"cargo:rustc-env=UV_LAST_TAG_DISTANCE={}",
describe_parts.next().unwrap_or("0")
);
}
}

View File

@ -10,6 +10,7 @@ pub(crate) use pip_install::pip_install;
pub(crate) use pip_sync::pip_sync;
pub(crate) use pip_uninstall::pip_uninstall;
pub(crate) use venv::venv;
pub(crate) use version::version;
mod cache_clean;
mod cache_dir;
@ -20,6 +21,7 @@ mod pip_sync;
mod pip_uninstall;
mod reporters;
mod venv;
mod version;
#[derive(Copy, Clone)]
pub(crate) enum ExitStatus {
@ -72,3 +74,9 @@ pub(super) struct ChangeEvent<T: InstalledMetadata> {
dist: T,
kind: ChangeEventKind,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub(crate) enum VersionFormat {
Text,
Json,
}

View File

@ -0,0 +1,20 @@
use anyhow::Result;
use crate::commands::VersionFormat;
/// Display version information
pub(crate) fn version(output_format: VersionFormat, buffer: &mut dyn std::io::Write) -> Result<()> {
let version_info = crate::version::version();
match output_format {
VersionFormat::Text => {
writeln!(buffer, "uv {}", &version_info)?;
}
VersionFormat::Json => {
serde_json::to_writer_pretty(&mut *buffer, &version_info)?;
// Add a trailing newline
writeln!(buffer)?;
}
};
Ok(())
}

View File

@ -24,7 +24,7 @@ use uv_traits::{
ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, SetupPyStrategy,
};
use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade};
use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade, VersionFormat};
use crate::compat::CompatArgs;
use crate::requirements::RequirementsSource;
@ -50,11 +50,12 @@ mod confirm;
mod logging;
mod printer;
mod requirements;
mod version;
const DEFAULT_VENV_NAME: &str = ".venv";
#[derive(Parser)]
#[command(author, version, about)]
#[command(author, version, long_version = crate::version::version(), about)]
#[command(propagate_version = true)]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
@ -122,6 +123,11 @@ enum Commands {
/// Remove all items from the cache.
#[clap(hide = true)]
Clean(CleanArgs),
/// Display uv's version
Version {
#[arg(long, value_enum, default_value = "text")]
output_format: VersionFormat,
},
/// Generate shell completion
#[clap(alias = "--generate-shell-completion", hide = true)]
GenerateShellCompletion { shell: clap_complete_command::Shell },
@ -1126,6 +1132,10 @@ async fn run() -> Result<ExitStatus> {
)
.await
}
Commands::Version { output_format } => {
commands::version(output_format, &mut stdout())?;
Ok(ExitStatus::Success)
}
Commands::GenerateShellCompletion { shell } => {
shell.generate(&mut Cli::command(), &mut stdout());
Ok(ExitStatus::Success)

147
crates/uv/src/version.rs Normal file
View File

@ -0,0 +1,147 @@
//! Code for representing uv's release version number.
// See also <https://github.com/astral-sh/ruff/blob/8118d29419055b779719cc96cdf3dacb29ac47c9/crates/ruff/src/version.rs>
use serde::Serialize;
use std::fmt;
/// Information about the git repository where uv was built from.
#[derive(Serialize)]
pub(crate) struct CommitInfo {
short_commit_hash: String,
commit_hash: String,
commit_date: String,
last_tag: Option<String>,
commits_since_last_tag: u32,
}
/// uv's version.
#[derive(Serialize)]
pub(crate) struct VersionInfo {
/// uv's version, such as "0.5.1"
version: String,
/// Information about the git commit we may have been built from.
///
/// `None` if not built from a git repo or if retrieval failed.
commit_info: Option<CommitInfo>,
}
impl fmt::Display for VersionInfo {
/// Formatted version information: "<version>[+<commits>] (<commit> <date>)"
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.version)?;
if let Some(ref ci) = self.commit_info {
if ci.commits_since_last_tag > 0 {
write!(f, "+{}", ci.commits_since_last_tag)?;
}
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
}
Ok(())
}
}
impl From<VersionInfo> for clap::builder::Str {
fn from(val: VersionInfo) -> Self {
val.to_string().into()
}
}
/// Returns information about uv's version.
pub(crate) fn version() -> VersionInfo {
// Environment variables are only read at compile-time
macro_rules! option_env_str {
($name:expr) => {
option_env!($name).map(|s| s.to_string())
};
}
// This version is pulled from Cargo.toml and set by Cargo
let version = option_env_str!("CARGO_PKG_VERSION").unwrap();
// Commit info is pulled from git and set by `build.rs`
let commit_info = option_env_str!("UV_COMMIT_HASH").map(|commit_hash| CommitInfo {
short_commit_hash: option_env_str!("UV_COMMIT_SHORT_HASH").unwrap(),
commit_hash,
commit_date: option_env_str!("UV_COMMIT_DATE").unwrap(),
last_tag: option_env_str!("UV_LAST_TAG"),
commits_since_last_tag: option_env_str!("UV_LAST_TAG_DISTANCE")
.as_deref()
.map_or(0, |value| value.parse::<u32>().unwrap_or(0)),
});
VersionInfo {
version,
commit_info,
}
}
#[cfg(test)]
mod tests {
use insta::{assert_display_snapshot, assert_json_snapshot};
use super::{CommitInfo, VersionInfo};
#[test]
fn version_formatting() {
let version = VersionInfo {
version: "0.0.0".to_string(),
commit_info: None,
};
assert_display_snapshot!(version, @"0.0.0");
}
#[test]
fn version_formatting_with_commit_info() {
let version = VersionInfo {
version: "0.0.0".to_string(),
commit_info: Some(CommitInfo {
short_commit_hash: "53b0f5d92".to_string(),
commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
last_tag: Some("v0.0.1".to_string()),
commit_date: "2023-10-19".to_string(),
commits_since_last_tag: 0,
}),
};
assert_display_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)");
}
#[test]
fn version_formatting_with_commits_since_last_tag() {
let version = VersionInfo {
version: "0.0.0".to_string(),
commit_info: Some(CommitInfo {
short_commit_hash: "53b0f5d92".to_string(),
commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
last_tag: Some("v0.0.1".to_string()),
commit_date: "2023-10-19".to_string(),
commits_since_last_tag: 24,
}),
};
assert_display_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)");
}
#[test]
fn version_serializable() {
let version = VersionInfo {
version: "0.0.0".to_string(),
commit_info: Some(CommitInfo {
short_commit_hash: "53b0f5d92".to_string(),
commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(),
last_tag: Some("v0.0.1".to_string()),
commit_date: "2023-10-19".to_string(),
commits_since_last_tag: 0,
}),
};
assert_json_snapshot!(version, @r###"
{
"version": "0.0.0",
"commit_info": {
"short_commit_hash": "53b0f5d92",
"commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7",
"commit_date": "2023-10-19",
"last_tag": "v0.0.1",
"commits_since_last_tag": 0
}
}
"###);
}
}