From 860ffb95498b7dcfd56cca9fafc19b997c23ae1b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 20 Oct 2023 14:07:41 -0500 Subject: [PATCH] Add `ruff version` with long version display (#8034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `ruff version` sub-command which displays long version information in the style of `cargo` and `rustc`. We include the number of commits since the last release tag if its a development build, in the style of Python's versioneer. ``` ❯ ruff version ruff 0.1.0+14 (947940e91 2023-10-18) ``` ``` ❯ ruff version --output-format json { "version": "0.1.0", "commit_info": { "short_commit_hash": "947940e91", "commit_hash": "947940e91269f20f6b3f8f8c7c63f8e914680e80", "commit_date": "2023-10-18", "last_tag": "v0.1.0", "commits_since_last_tag": 14 } }% ``` ``` ❯ cargo version cargo 1.72.1 (103a7ff2e 2023-08-15) ``` ## Test plan I've tested this manually locally, but want to at least add unit tests for the message formatting. We'd also want to check the next release to ensure the information is correct. I checked build behavior with a detached head and branches. ## Future work We could include rustc and cargo versions from the build, the current Python version, and other diagnostic information for bug reports. The `--version` and `-V` output is unchanged. However, we could update it to display the long ruff version without the rust and cargo versions (this is what cargo does). We'll need to be careful to ensure this does not break downstream packages which parse our version string. ``` ❯ ruff --version ruff 0.1.0 ``` The LSP should be updated to use `ruff version --output-format json` instead of parsing `ruff --version`. --- crates/ruff_cli/Cargo.toml | 2 +- crates/ruff_cli/build.rs | 80 +++++++++++ crates/ruff_cli/src/args.rs | 5 + crates/ruff_cli/src/commands/mod.rs | 1 + crates/ruff_cli/src/commands/version.rs | 21 +++ crates/ruff_cli/src/lib.rs | 7 + ...i__version__tests__version_formatting.snap | 5 + ...__version_formatting_with_commit_info.snap | 5 + ...ormatting_with_commits_since_last_tag.snap | 5 + ..._version__tests__version_serializable.snap | 14 ++ crates/ruff_cli/src/version.rs | 130 ++++++++++++++++++ docs/configuration.md | 13 +- 12 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 crates/ruff_cli/build.rs create mode 100644 crates/ruff_cli/src/commands/version.rs create mode 100644 crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting.snap create mode 100644 crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commit_info.snap create mode 100644 crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commits_since_last_tag.snap create mode 100644 crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_serializable.snap create mode 100644 crates/ruff_cli/src/version.rs diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 7b8f2df9ff..13a58a7823 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -66,7 +66,7 @@ wild = { version = "2" } assert_cmd = { version = "2.0.8" } # Avoid writing colored snapshots when running tests from the terminal colored = { workspace = true, features = ["no-color"]} -insta = { workspace = true, features = ["filters"] } +insta = { workspace = true, features = ["filters", "json"] } insta-cmd = { version = "0.4.0" } tempfile = "3.6.0" test-case = { workspace = true } diff --git a/crates/ruff_cli/build.rs b/crates/ruff_cli/build.rs new file mode 100644 index 0000000000..c50a033d6d --- /dev/null +++ b/crates/ruff_cli/build.rs @@ -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 + // - "" when the head is detached + // - "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=RUFF_COMMIT_HASH={}", next()); + println!("cargo:rustc-env=RUFF_COMMIT_SHORT_HASH={}", next()); + println!("cargo:rustc-env=RUFF_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=RUFF_LAST_TAG={}", + describe_parts.next().unwrap() + ); + // If this is the tagged commit, this component will be missing + println!( + "cargo:rustc-env=RUFF_LAST_TAG_DISTANCE={}", + describe_parts.next().unwrap_or("0") + ); + } +} diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index f4b7182a2e..156ca6a616 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -69,6 +69,11 @@ pub enum Command { #[doc(hidden)] #[clap(hide = true)] Format(FormatCommand), + /// Display Ruff's version + Version { + #[arg(long, value_enum, default_value = "text")] + output_format: HelpFormat, + }, } // The `Parser` derive is for ruff_dev, for ruff_cli `Args` would be sufficient diff --git a/crates/ruff_cli/src/commands/mod.rs b/crates/ruff_cli/src/commands/mod.rs index 794a58788b..554a7a454a 100644 --- a/crates/ruff_cli/src/commands/mod.rs +++ b/crates/ruff_cli/src/commands/mod.rs @@ -9,3 +9,4 @@ pub(crate) mod linter; pub(crate) mod rule; pub(crate) mod show_files; pub(crate) mod show_settings; +pub(crate) mod version; diff --git a/crates/ruff_cli/src/commands/version.rs b/crates/ruff_cli/src/commands/version.rs new file mode 100644 index 0000000000..729d0f15d5 --- /dev/null +++ b/crates/ruff_cli/src/commands/version.rs @@ -0,0 +1,21 @@ +use std::io::{self, BufWriter, Write}; + +use anyhow::Result; + +use crate::args::HelpFormat; + +/// Display version information +pub(crate) fn version(output_format: HelpFormat) -> Result<()> { + let mut stdout = BufWriter::new(io::stdout().lock()); + let version_info = crate::version::version(); + + match output_format { + HelpFormat::Text => { + writeln!(stdout, "ruff {}", &version_info)?; + } + HelpFormat::Json => { + serde_json::to_writer_pretty(stdout, &version_info)?; + } + }; + Ok(()) +} diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 993f0f8771..75c0041b81 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::print_stdout)] + use std::fs::File; use std::io::{self, stdout, BufWriter, Write}; use std::path::{Path, PathBuf}; @@ -27,6 +29,7 @@ mod panic; mod printer; pub mod resolve; mod stdin; +mod version; #[derive(Copy, Clone)] pub enum ExitStatus { @@ -134,6 +137,10 @@ pub fn run( set_up_logging(&log_level)?; match command { + Command::Version { output_format } => { + commands::version::version(output_format)?; + Ok(ExitStatus::Success) + } Command::Rule { rule, all, format } => { if all { commands::rule::rules(format)?; diff --git a/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting.snap b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting.snap new file mode 100644 index 0000000000..7bd9a393de --- /dev/null +++ b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_cli/src/version.rs +expression: version +--- +0.0.0 diff --git a/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commit_info.snap b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commit_info.snap new file mode 100644 index 0000000000..cdda49684a --- /dev/null +++ b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commit_info.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_cli/src/version.rs +expression: version +--- +0.0.0 (53b0f5d92 2023-10-19) diff --git a/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commits_since_last_tag.snap b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commits_since_last_tag.snap new file mode 100644 index 0000000000..6330751c8a --- /dev/null +++ b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_formatting_with_commits_since_last_tag.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_cli/src/version.rs +expression: version +--- +0.0.0+24 (53b0f5d92 2023-10-19) diff --git a/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_serializable.snap b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_serializable.snap new file mode 100644 index 0000000000..0a63628659 --- /dev/null +++ b/crates/ruff_cli/src/snapshots/ruff_cli__version__tests__version_serializable.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff_cli/src/version.rs +expression: version +--- +{ + "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 + } +} diff --git a/crates/ruff_cli/src/version.rs b/crates/ruff_cli/src/version.rs new file mode 100644 index 0000000000..f79b938f65 --- /dev/null +++ b/crates/ruff_cli/src/version.rs @@ -0,0 +1,130 @@ +//! Code for representing Ruff's release version number. +use serde::Serialize; +use std::fmt; + +/// Information about the git repository where Ruff was built from. +#[derive(Serialize)] +pub(crate) struct CommitInfo { + short_commit_hash: String, + commit_hash: String, + commit_date: String, + last_tag: Option, + commits_since_last_tag: u32, +} + +/// Ruff's version. +#[derive(Serialize)] +pub(crate) struct VersionInfo { + /// Ruff'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, +} + +impl fmt::Display for VersionInfo { + /// Formatted version information: "[+] ( )" + 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(()) + } +} + +/// Returns information about Ruff'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!("RUFF_COMMIT_HASH").map(|commit_hash| CommitInfo { + short_commit_hash: option_env_str!("RUFF_COMMIT_SHORT_HASH").unwrap(), + commit_hash, + commit_date: option_env_str!("RUFF_COMMIT_DATE").unwrap(), + last_tag: option_env_str!("RUFF_LAST_TAG"), + commits_since_last_tag: option_env_str!("RUFF_LAST_TAG_DISTANCE") + .as_deref() + .map_or(0, |value| value.parse::().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); + } + + #[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); + } + + #[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); + } + + #[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); + } +} diff --git a/docs/configuration.md b/docs/configuration.md index 935ddf960c..8ec8992fbe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -156,12 +156,13 @@ Ruff: An extremely fast Python linter. Usage: ruff [OPTIONS] Commands: - check Run Ruff on the given files or directories (default) - rule Explain a rule (or all rules) - config List or describe the available configuration options - linter List all supported upstream linters - clean Clear any caches in the current directory and any subdirectories - help Print this message or the help of the given subcommand(s) + check Run Ruff on the given files or directories (default) + rule Explain a rule (or all rules) + config List or describe the available configuration options + linter List all supported upstream linters + clean Clear any caches in the current directory and any subdirectories + version Display Ruff's version + help Print this message or the help of the given subcommand(s) Options: -h, --help Print help