Skip to content

Commit 54dfbe8

Browse files
authored
Add commit info to version display (#130)
1 parent 63a3159 commit 54dfbe8

File tree

6 files changed

+241
-5
lines changed

6 files changed

+241
-5
lines changed

Cargo.lock

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ anstream = "0.6.15"
2121
anyhow = "1.0.86"
2222
assert_cmd = { version = "2.0.16", features = ["color"] }
2323
axoupdater = { version = "0.8.1", default-features = false, features = [ "github_releases"] }
24-
clap = { version = "4.5.16", features = ["derive", "env"] }
24+
clap = { version = "4.5.16", features = ["derive", "env", "string", "wrap_help"] }
2525
clap_complete = "4.5.37"
2626
ctrlc = "3.4.5"
2727
dunce = "1.0.5"
@@ -67,6 +67,9 @@ insta-cmd = "0.6.0"
6767
predicates = "3.1.2"
6868
regex = "1.11.0"
6969

70+
[build-dependencies]
71+
fs-err = "2.11.0"
72+
7073
[lints.rust]
7174
dead_code = "allow"
7275

build.rs

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/* MIT License
2+
3+
Copyright (c) 2023 Astral Software Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+
*/
23+
24+
use std::{
25+
path::{Path, PathBuf},
26+
process::Command,
27+
};
28+
29+
use fs_err as fs;
30+
31+
fn main() {
32+
// The workspace root directory is not available without walking up the tree
33+
// https://github.com/rust-lang/cargo/issues/3946
34+
let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).to_path_buf();
35+
36+
commit_info(&workspace_root);
37+
}
38+
39+
fn commit_info(workspace_root: &Path) {
40+
// If not in a git repository, do not attempt to retrieve commit information
41+
let git_dir = workspace_root.join(".git");
42+
if !git_dir.exists() {
43+
return;
44+
}
45+
46+
if let Some(git_head_path) = git_head(&git_dir) {
47+
println!("cargo:rerun-if-changed={}", git_head_path.display());
48+
49+
let git_head_contents = fs::read_to_string(git_head_path);
50+
if let Ok(git_head_contents) = git_head_contents {
51+
// The contents are either a commit or a reference in the following formats
52+
// - "<commit>" when the head is detached
53+
// - "ref <ref>" when working on a branch
54+
// If a commit, checking if the HEAD file has changed is sufficient
55+
// If a ref, we need to add the head file for that ref to rebuild on commit
56+
let mut git_ref_parts = git_head_contents.split_whitespace();
57+
git_ref_parts.next();
58+
if let Some(git_ref) = git_ref_parts.next() {
59+
let git_ref_path = git_dir.join(git_ref);
60+
println!("cargo:rerun-if-changed={}", git_ref_path.display());
61+
}
62+
}
63+
}
64+
65+
let output = match Command::new("git")
66+
.arg("log")
67+
.arg("-1")
68+
.arg("--date=short")
69+
.arg("--abbrev=9")
70+
.arg("--format=%H %h %cd %(describe)")
71+
.output()
72+
{
73+
Ok(output) if output.status.success() => output,
74+
_ => return,
75+
};
76+
let stdout = String::from_utf8(output.stdout).unwrap();
77+
let mut parts = stdout.split_whitespace();
78+
let mut next = || parts.next().unwrap();
79+
println!("cargo:rustc-env=PREFLIGIT_COMMIT_HASH={}", next());
80+
println!("cargo:rustc-env=PREFLIGIT_COMMIT_SHORT_HASH={}", next());
81+
println!("cargo:rustc-env=PREFLIGIT_COMMIT_DATE={}", next());
82+
83+
// Describe can fail for some commits
84+
// https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem
85+
if let Some(describe) = parts.next() {
86+
let mut describe_parts = describe.split('-');
87+
println!(
88+
"cargo:rustc-env=PREFLIGIT_LAST_TAG={}",
89+
describe_parts.next().unwrap()
90+
);
91+
// If this is the tagged commit, this component will be missing
92+
println!(
93+
"cargo:rustc-env=PREFLIGIT_LAST_TAG_DISTANCE={}",
94+
describe_parts.next().unwrap_or("0")
95+
);
96+
}
97+
}
98+
99+
fn git_head(git_dir: &Path) -> Option<PathBuf> {
100+
// The typical case is a standard git repository.
101+
let git_head_path = git_dir.join("HEAD");
102+
if git_head_path.exists() {
103+
return Some(git_head_path);
104+
}
105+
if !git_dir.is_file() {
106+
return None;
107+
}
108+
// If `.git/HEAD` doesn't exist and `.git` is actually a file,
109+
// then let's try to attempt to read it as a worktree. If it's
110+
// a worktree, then its contents will look like this, e.g.:
111+
//
112+
// gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2
113+
//
114+
// And the HEAD file we want to watch will be at:
115+
//
116+
// /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD
117+
let contents = fs::read_to_string(git_dir).ok()?;
118+
let (label, worktree_path) = contents.split_once(':')?;
119+
if label != "gitdir" {
120+
return None;
121+
}
122+
let worktree_path = worktree_path.trim();
123+
Some(PathBuf::from(worktree_path))
124+
}

src/cli/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ impl From<ColorChoice> for anstream::ColorChoice {
7979
#[command(
8080
name = "prefligit",
8181
author,
82-
version,
82+
long_version = crate::version::version(),
8383
about = "pre-commit reimplemented in Rust"
8484
)]
8585
#[command(propagate_version = true)]

src/main.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ mod process;
3030
mod profiler;
3131
mod run;
3232
mod store;
33+
mod version;
3334
mod warnings;
3435

3536
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@@ -128,7 +129,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
128129
cli.command = Some(Command::Run(Box::new(cli.run_args.clone())));
129130
}
130131

131-
debug!("prefligit: {}", env!("CARGO_PKG_VERSION"));
132+
debug!("prefligit: {}", version::version());
132133

133134
match get_root().await {
134135
Ok(root) => {
@@ -144,8 +145,6 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
144145
}
145146
}
146147

147-
// TODO: read git commit info
148-
149148
macro_rules! show_settings {
150149
($arg:expr) => {
151150
if cli.globals.show_settings {

src/version.rs

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* MIT License
2+
3+
Copyright (c) 2023 Astral Software Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+
*/
23+
24+
// See also <https://github.com/astral-sh/ruff/blob/8118d29419055b779719cc96cdf3dacb29ac47c9/crates/ruff/src/version.rs>
25+
use std::fmt;
26+
27+
use serde::Serialize;
28+
29+
/// Information about the git repository where prefligit was built from.
30+
#[derive(Serialize)]
31+
pub(crate) struct CommitInfo {
32+
short_commit_hash: String,
33+
commit_hash: String,
34+
commit_date: String,
35+
last_tag: Option<String>,
36+
commits_since_last_tag: u32,
37+
}
38+
39+
/// prefligit's version.
40+
#[derive(Serialize)]
41+
pub struct VersionInfo {
42+
/// prefligit's version, such as "0.0.6"
43+
version: String,
44+
/// Information about the git commit we may have been built from.
45+
///
46+
/// `None` if not built from a git repo or if retrieval failed.
47+
commit_info: Option<CommitInfo>,
48+
}
49+
50+
impl fmt::Display for VersionInfo {
51+
/// Formatted version information: "<version>[+<commits>] (<commit> <date>)"
52+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53+
write!(f, "{}", self.version)?;
54+
55+
if let Some(ref ci) = self.commit_info {
56+
if ci.commits_since_last_tag > 0 {
57+
write!(f, "+{}", ci.commits_since_last_tag)?;
58+
}
59+
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
60+
}
61+
62+
Ok(())
63+
}
64+
}
65+
66+
impl From<VersionInfo> for clap::builder::Str {
67+
fn from(val: VersionInfo) -> Self {
68+
val.to_string().into()
69+
}
70+
}
71+
72+
/// Returns information about prefligit's version.
73+
pub fn version() -> VersionInfo {
74+
// Environment variables are only read at compile-time
75+
macro_rules! option_env_str {
76+
($name:expr) => {
77+
option_env!($name).map(|s| s.to_string())
78+
};
79+
}
80+
81+
// This version is pulled from Cargo.toml and set by Cargo
82+
let version = env!("CARGO_PKG_VERSION").to_string();
83+
84+
// Commit info is pulled from git and set by `build.rs`
85+
let commit_info = option_env_str!("PREFLIGIT_COMMIT_HASH").map(|commit_hash| CommitInfo {
86+
short_commit_hash: option_env_str!("PREFLIGIT_COMMIT_SHORT_HASH").unwrap(),
87+
commit_hash,
88+
commit_date: option_env_str!("PREFLIGIT_COMMIT_DATE").unwrap(),
89+
last_tag: option_env_str!("PREFLIGIT_LAST_TAG"),
90+
commits_since_last_tag: option_env_str!("PREFLIGIT_LAST_TAG_DISTANCE")
91+
.as_deref()
92+
.map_or(0, |value| value.parse::<u32>().unwrap_or(0)),
93+
});
94+
95+
VersionInfo {
96+
version,
97+
commit_info,
98+
}
99+
}

0 commit comments

Comments
 (0)