diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index d8f2b7d4d292b..1e11c08e88b13 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 0000000000000..c50a033d6df2b --- /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 9732919997f14..a78e50532272d 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 794a58788b352..554a7a454add2 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 0000000000000..729d0f15d5e9e --- /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 993f0f8771bf7..75c0041b81cb6 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 0000000000000..7bd9a393de87c --- /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 0000000000000..cdda49684aad8 --- /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 0000000000000..6330751c8a006 --- /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 0000000000000..0a63628659679 --- /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 0000000000000..f79b938f653c4 --- /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 4f91a90563d42..d0cf71ae41c37 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