diff --git a/Cargo.lock b/Cargo.lock index 6a69123..59fd854 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -2167,6 +2168,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "terminal_size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termtree" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index bb00347..4efd64a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ anstream = "0.6.15" anyhow = "1.0.86" assert_cmd = { version = "2.0.16", features = ["color"] } axoupdater = { version = "0.8.1", default-features = false, features = [ "github_releases"] } -clap = { version = "4.5.16", features = ["derive", "env"] } +clap = { version = "4.5.16", features = ["derive", "env", "string", "wrap_help"] } clap_complete = "4.5.37" ctrlc = "3.4.5" dunce = "1.0.5" @@ -67,6 +67,9 @@ insta-cmd = "0.6.0" predicates = "3.1.2" regex = "1.11.0" +[build-dependencies] +fs-err = "2.11.0" + [lints.rust] dead_code = "allow" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..047892a --- /dev/null +++ b/build.rs @@ -0,0 +1,124 @@ +/* MIT License + +Copyright (c) 2023 Astral Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +use fs_err as fs; + +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()).to_path_buf(); + + commit_info(&workspace_root); +} + +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; + } + + if let Some(git_head_path) = git_head(&git_dir) { + println!("cargo:rerun-if-changed={}", git_head_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.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=PREFLIGIT_COMMIT_HASH={}", next()); + println!("cargo:rustc-env=PREFLIGIT_COMMIT_SHORT_HASH={}", next()); + println!("cargo:rustc-env=PREFLIGIT_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=PREFLIGIT_LAST_TAG={}", + describe_parts.next().unwrap() + ); + // If this is the tagged commit, this component will be missing + println!( + "cargo:rustc-env=PREFLIGIT_LAST_TAG_DISTANCE={}", + describe_parts.next().unwrap_or("0") + ); + } +} + +fn git_head(git_dir: &Path) -> Option { + // The typical case is a standard git repository. + let git_head_path = git_dir.join("HEAD"); + if git_head_path.exists() { + return Some(git_head_path); + } + if !git_dir.is_file() { + return None; + } + // If `.git/HEAD` doesn't exist and `.git` is actually a file, + // then let's try to attempt to read it as a worktree. If it's + // a worktree, then its contents will look like this, e.g.: + // + // gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2 + // + // And the HEAD file we want to watch will be at: + // + // /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD + let contents = fs::read_to_string(git_dir).ok()?; + let (label, worktree_path) = contents.split_once(':')?; + if label != "gitdir" { + return None; + } + let worktree_path = worktree_path.trim(); + Some(PathBuf::from(worktree_path)) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index de20325..3b7d3fb 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -79,7 +79,7 @@ impl From for anstream::ColorChoice { #[command( name = "prefligit", author, - version, + long_version = crate::version::version(), about = "pre-commit reimplemented in Rust" )] #[command(propagate_version = true)] diff --git a/src/main.rs b/src/main.rs index cc34539..a76ba95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ mod process; mod profiler; mod run; mod store; +mod version; mod warnings; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -128,7 +129,7 @@ async fn run(mut cli: Cli) -> Result { cli.command = Some(Command::Run(Box::new(cli.run_args.clone()))); } - debug!("prefligit: {}", env!("CARGO_PKG_VERSION")); + debug!("prefligit: {}", version::version()); match get_root().await { Ok(root) => { @@ -144,8 +145,6 @@ async fn run(mut cli: Cli) -> Result { } } - // TODO: read git commit info - macro_rules! show_settings { ($arg:expr) => { if cli.globals.show_settings { diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..e689919 --- /dev/null +++ b/src/version.rs @@ -0,0 +1,99 @@ +/* MIT License + +Copyright (c) 2023 Astral Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// See also +use std::fmt; + +use serde::Serialize; + +/// Information about the git repository where prefligit 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, +} + +/// prefligit's version. +#[derive(Serialize)] +pub struct VersionInfo { + /// prefligit's version, such as "0.0.6" + 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(()) + } +} + +impl From for clap::builder::Str { + fn from(val: VersionInfo) -> Self { + val.to_string().into() + } +} + +/// Returns information about prefligit's version. +pub 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 = env!("CARGO_PKG_VERSION").to_string(); + + // Commit info is pulled from git and set by `build.rs` + let commit_info = option_env_str!("PREFLIGIT_COMMIT_HASH").map(|commit_hash| CommitInfo { + short_commit_hash: option_env_str!("PREFLIGIT_COMMIT_SHORT_HASH").unwrap(), + commit_hash, + commit_date: option_env_str!("PREFLIGIT_COMMIT_DATE").unwrap(), + last_tag: option_env_str!("PREFLIGIT_LAST_TAG"), + commits_since_last_tag: option_env_str!("PREFLIGIT_LAST_TAG_DISTANCE") + .as_deref() + .map_or(0, |value| value.parse::().unwrap_or(0)), + }); + + VersionInfo { + version, + commit_info, + } +}