use super::common::{
fail, fixing_enabled, hl, ok, repo_root, restage, run as run_tool, staged_files, warn, which,
Restaged,
};
use crate::check::Outcome;
use crate::git;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub const RUST_PATHS: &[&str] = &[
".rs",
"Cargo.toml",
"Cargo.lock",
"rustfmt.toml",
"clippy.toml",
];
pub const EXTS: &[&str] = &[".rs"];
fn is_rust_path(f: &str) -> bool {
let name = f.rsplit('/').next().unwrap_or(f);
RUST_PATHS.iter().any(|pattern| {
if pattern.starts_with('.') {
name.ends_with(pattern)
} else {
name == *pattern
}
})
}
fn cargo_root_for(root: &str, file: &str) -> Option<PathBuf> {
let mut dir = Path::new(root).join(file);
dir.pop();
loop {
if dir.join("Cargo.toml").is_file() {
return Some(dir);
}
if dir == Path::new(root) || !dir.starts_with(root) {
return None;
}
if !dir.pop() {
return None;
}
}
}
fn cargo_roots<'a>(root: &str, files: impl Iterator<Item = &'a str>) -> Vec<PathBuf> {
let mut seen = BTreeSet::new();
for f in files.filter(|f| is_rust_path(f)) {
if let Some(d) = cargo_root_for(root, f) {
seen.insert(d);
}
}
seen.into_iter().collect()
}
fn component_available(dir: &Path, sub: &str) -> bool {
let Some(cargo) = which("cargo") else {
return false;
};
Command::new(cargo)
.arg(sub)
.arg("--version")
.current_dir(dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn cargo_argv() -> Option<Vec<String>> {
which("cargo").map(|c| vec![c])
}
fn cargo_for(roots: &[PathBuf], component: Option<&str>, missing: &str) -> Option<Vec<String>> {
let argv = cargo_argv().or_else(|| {
warn(missing);
None
})?;
if let Some(c) = component {
for dir in roots {
if !component_available(dir, c) {
warn(missing);
return None;
}
}
}
Some(argv)
}
fn run_in_roots(roots: &[PathBuf], argv: &[String], args: &[&str]) -> bool {
let extra: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
let mut all_ok = true;
for dir in roots {
let d = dir.to_string_lossy().into_owned();
if !run_tool(&d, argv, &extra) {
all_ok = false;
}
}
all_ok
}
fn each_root(
roots: &[PathBuf],
component: Option<&str>,
args: &[&str],
missing: &str,
) -> Option<bool> {
let argv = cargo_for(roots, component, missing)?;
Some(run_in_roots(roots, &argv, args))
}
pub fn fmt(_args: &[std::ffi::OsString]) -> Outcome {
let files = staged_files(EXTS);
if files.is_empty() {
return Outcome::Passed;
}
let root = repo_root();
let roots = cargo_roots(&root, files.iter().map(String::as_str));
if roots.is_empty() {
return Outcome::Passed;
}
const MISSING: &str =
"Rust staged but rustfmt is not installed. `rustup component add rustfmt`.";
let Some(argv) = cargo_for(&roots, Some("fmt"), MISSING) else {
return Outcome::Unavailable;
};
if run_in_roots(&roots, &argv, &["fmt", "--all", "--", "--check"]) {
ok("Rust formatting is clean");
return Outcome::Passed;
}
if fixing_enabled() && run_in_roots(&roots, &argv, &["fmt", "--all"]) {
match restage(&files) {
Restaged::Staged => {
ok("Rust reformatted and re-staged");
return Outcome::Fixed;
}
Restaged::Failed(stuck) => {
fail(&format!(
"cargo fmt rewrote these files but {} failed — the index still holds the \
UNFORMATTED content: {}",
hl("git add"),
stuck.join(", ")
));
return Outcome::Failed;
}
Restaged::Nothing => {}
}
}
fail(&format!("Unformatted Rust. Run {}.", hl("cargo fmt --all")));
Outcome::Failed
}
pub fn clippy(_args: &[std::ffi::OsString]) -> Outcome {
let files: Vec<String> = staged_files(&[])
.into_iter()
.filter(|f| is_rust_path(f))
.collect();
if files.is_empty() {
return Outcome::Passed;
}
let root = repo_root();
let roots = cargo_roots(&root, files.iter().map(String::as_str));
if roots.is_empty() {
return Outcome::Passed;
}
match each_root(
&roots,
Some("clippy"),
&[
"clippy",
"--workspace",
"--all-targets",
"--all-features",
"--",
"-D",
"warnings",
],
"Rust staged but clippy is not installed. `rustup component add clippy`.",
) {
None => Outcome::Unavailable,
Some(true) => {
ok("Clippy passed");
Outcome::Passed
}
Some(false) => {
fail(&format!(
"Clippy warnings. Fix them or run {}.",
hl("cargo clippy --fix")
));
Outcome::Failed
}
}
}
pub fn test(refs: &[crate::pushrefs::PushRef]) -> Outcome {
let Some(root) = git::stdout(&["rev-parse", "--show-toplevel"]) else {
return Outcome::Passed;
};
let zero = git::stdout(&["hash-object", "--stdin"])
.map(|h| "0".repeat(h.len()))
.unwrap_or_else(|| "0".repeat(40));
let mut ran_any = false;
for r in refs {
let changed = crate::pushrefs::changed_files_for(r, &zero);
let roots = cargo_roots(&root, changed.iter().map(String::as_str));
if roots.is_empty() {
continue;
}
let (where_, _guard) = crate::pushed_tree::where_to_run(&r.local_oid, &root);
let roots: Vec<PathBuf> = roots
.iter()
.map(|rt| {
rt.strip_prefix(&root)
.map(|rel| where_.join(rel))
.unwrap_or_else(|_| rt.clone())
})
.collect();
match each_root(
&roots,
None,
&["test", "--workspace", "--all-features"],
"Rust changed but cargo is not installed.",
) {
None => return Outcome::Unavailable,
Some(true) => ran_any = true,
Some(false) => {
fail("Rust tests failed. Push aborted.");
return Outcome::Failed;
}
}
}
if ran_any {
ok("Rust tests passed");
}
Outcome::Passed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognises_rust_paths() {
assert!(is_rust_path("src/main.rs"));
assert!(is_rust_path("Cargo.toml"));
assert!(is_rust_path("crates/a/Cargo.lock"));
assert!(is_rust_path("rustfmt.toml"));
assert!(!is_rust_path("README.md"));
assert!(!is_rust_path("src/main.rsx"));
assert!(!is_rust_path("docs/Cargo.toml.md"));
assert!(!is_rust_path("vendor/NotCargo.toml"));
}
#[test]
fn finds_the_nearest_manifest_not_the_repo_root() {
let tmp = std::env::temp_dir().join("amont-cargo-roots");
let _ = std::fs::remove_dir_all(&tmp);
let nested = tmp.join("services/engine");
std::fs::create_dir_all(nested.join("src")).unwrap();
std::fs::write(nested.join("Cargo.toml"), "[package]\n").unwrap();
let root = tmp.to_string_lossy().into_owned();
let got = cargo_roots(&root, ["services/engine/src/main.rs"].into_iter());
assert_eq!(got, vec![nested.clone()], "should find the nested manifest");
std::fs::create_dir_all(tmp.join("scripts")).unwrap();
let none = cargo_roots(&root, ["scripts/loose.rs"].into_iter());
assert!(none.is_empty(), "no manifest above it: {none:?}");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn several_files_in_one_crate_yield_one_root() {
let tmp = std::env::temp_dir().join("amont-cargo-dedupe");
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(tmp.join("src")).unwrap();
std::fs::write(tmp.join("Cargo.toml"), "[package]\n").unwrap();
let root = tmp.to_string_lossy().into_owned();
let got = cargo_roots(&root, ["src/a.rs", "src/b.rs", "Cargo.toml"].into_iter());
assert_eq!(got.len(), 1, "one cargo invocation, not three: {got:?}");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn non_rust_files_select_nothing() {
let got = cargo_roots("/tmp", ["README.md", "a.py"].into_iter());
assert!(got.is_empty());
}
}