xtask/src/fmt.rs
//! `cargo xtask fmt [--check|--write]` (lane OSSF-SILVER-CODE, 2026-10-31): rustfmt over
//! the WHOLE tree, every `.rs` file git tracks, with the repository's `rustfmt.toml`.
//!
//! `--check` (the default) lists the files rustfmt would change and exits 1 when there
//! is one; `--write` formats them. `cargo xtask ci --quick` runs the check on every
//! push, so the tree stays formatted (OpenSSF silver: coding_standards_enforced).
//!
//! Why not `cargo fmt --all`: it follows local path dependencies OUT of this repository
//! (its own `--help` says so), so in an estate checkout it would reformat the siblings,
//! and it misses the files of the detached workspaces (estate, holger-downstream, fuzz,
//! ...). `git ls-files` names exactly this repository's files, in every workspace.
//!
//! Generated code (`*/generated/*.rs`, written by `cargo xtask gen-proto` and pulled in
//! with `include!`) is left as its generator writes it: [`SKIP`].
use std::path::{Path, PathBuf};
use std::process::Command;
/// Path fragments of tracked `.rs` files that are not ours to format.
pub const SKIP: &[&str] = &["/generated/"];
/// Files per rustfmt run: keeps the argument list well under any `ARG_MAX`.
const BATCH: usize = 200;
/// Whether a repository-relative path is formatted by this verb.
pub fn keep(rel: &str) -> bool {
let rooted = format!("/{rel}");
rel.ends_with(".rs") && !SKIP.iter().any(|s| rooted.contains(s))
}
/// Every `.rs` file git tracks under `root`, minus [`SKIP`], as absolute paths.
pub fn tracked_rs(root: &Path) -> anyhow::Result<Vec<PathBuf>> {
let out = Command::new("git")
.args(["ls-files", "-z", "--", "*.rs"])
.current_dir(root)
.output()
.map_err(|e| anyhow::anyhow!("could not run git ls-files in {}: {e}", root.display()))?;
anyhow::ensure!(
out.status.success(),
"git ls-files in {} failed: {}",
root.display(),
String::from_utf8_lossy(&out.stderr)
);
Ok(out
.stdout
.split(|b| *b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.filter(|rel| keep(rel))
.map(|rel| root.join(rel))
.collect())
}
/// rustfmt over `files` with `config` (a `rustfmt.toml`). With `write` false it changes
/// nothing and answers the files rustfmt would change; with `write` true it formats
/// them and answers the files it changed. A file rustfmt cannot parse is an error.
pub fn rustfmt(config: &Path, files: &[PathBuf], write: bool) -> anyhow::Result<Vec<PathBuf>> {
let bin = std::env::var_os("RUSTFMT").unwrap_or_else(|| "rustfmt".into());
let mut changed = Vec::new();
for batch in files.chunks(BATCH) {
let mut cmd = Command::new(&bin);
cmd.arg("--config-path").arg(config).arg("-l");
if !write {
cmd.arg("--check");
}
let out = cmd.args(batch).output().map_err(|e| {
anyhow::anyhow!("could not run rustfmt ({}): {e}", bin.to_string_lossy())
})?;
let stderr = String::from_utf8_lossy(&out.stderr);
anyhow::ensure!(
!stderr.lines().any(|l| l.starts_with("error")),
"rustfmt could not format every file:\n{stderr}"
);
// `--check -l` exits 1 when a file differs; that is the answer, not a failure.
anyhow::ensure!(
out.status.success() || !write,
"rustfmt failed ({}):\n{stderr}",
out.status
);
for line in String::from_utf8_lossy(&out.stdout).lines() {
let p = PathBuf::from(line.trim());
if !p.as_os_str().is_empty() && !changed.contains(&p) {
changed.push(p);
}
}
}
Ok(changed)
}
/// The verb: `--write` formats, anything else checks.
pub fn run(root: &Path, args: &[String]) -> i32 {
let write = args.iter().any(|a| a == "--write");
let files = match tracked_rs(root) {
Ok(f) => f,
Err(e) => {
eprintln!("xtask fmt: {e:#}");
return 2;
}
};
match rustfmt(&root.join("rustfmt.toml"), &files, write) {
Ok(changed) if write => {
eprintln!(
"xtask fmt: {} of {} files formatted",
changed.len(),
files.len()
);
0
}
Ok(changed) if changed.is_empty() => {
eprintln!("xtask fmt: {} files, all rustfmt-clean", files.len());
0
}
Ok(changed) => {
for f in &changed {
println!("{}", f.strip_prefix(root).unwrap_or(f).display());
}
eprintln!(
"xtask fmt: {} of {} files are not rustfmt-clean; `cargo xtask fmt --write` formats them",
changed.len(),
files.len()
);
1
}
Err(e) => {
eprintln!("xtask fmt: {e:#}");
2
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask has a parent")
.to_path_buf()
}
#[test]
fn generated_code_is_left_as_its_generator_writes_it() {
assert!(!keep("server/lib/src/generated/holger.v1.rs"));
assert!(!keep("holger-cli/src/generated/holger.room.v1.rs"));
assert!(keep("server/lib/src/lib.rs"));
assert!(keep("generated.rs"), "a file NAMED generated is ours");
assert!(!keep("README.md"));
}
#[test]
fn the_walk_names_this_repositorys_files_in_every_workspace() {
let files = tracked_rs(&root()).expect("git ls-files");
let has = |rel: &str| files.iter().any(|f| f.ends_with(rel));
assert!(
has("xtask/src/fmt.rs"),
"the xtask itself (the estate workspace)"
);
assert!(has("server/lib/src/lib.rs"), "the root workspace");
assert!(!files
.iter()
.any(|f| f.to_string_lossy().contains("/generated/")));
}
/// Measure, never assert: the check must see an unformatted file, and must not touch
/// it, before a green answer over the tree means anything.
#[test]
fn the_check_sees_an_unformatted_file_and_write_formats_it() {
let dir = std::env::temp_dir().join(format!("holger-xtask-fmt-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let config = dir.join("rustfmt.toml");
std::fs::write(&config, "edition = \"2021\"\n").unwrap();
let ugly = dir.join("ugly.rs");
let text = "fn main( ){let x=1;println!(\"{}\",x);}\n";
std::fs::write(&ugly, text).unwrap();
let tidy = dir.join("tidy.rs");
std::fs::write(&tidy, "fn main() {}\n").unwrap();
let files = vec![ugly.clone(), tidy.clone()];
let seen = rustfmt(&config, &files, false).expect("rustfmt --check");
assert_eq!(
seen,
vec![ugly.clone()],
"the check names the unformatted file only"
);
assert_eq!(
std::fs::read_to_string(&ugly).unwrap(),
text,
"the check changes nothing"
);
rustfmt(&config, &files, true).expect("rustfmt");
assert!(
rustfmt(&config, &files, false)
.expect("rustfmt --check")
.is_empty(),
"formatted after --write"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_file_rustfmt_cannot_parse_is_an_error_not_a_pass() {
let dir = std::env::temp_dir().join(format!("holger-xtask-fmt-bad-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let config = dir.join("rustfmt.toml");
std::fs::write(&config, "edition = \"2021\"\n").unwrap();
let bad = dir.join("bad.rs");
std::fs::write(&bad, "fn main( {\n").unwrap();
assert!(rustfmt(&config, &[bad], false).is_err());
std::fs::remove_dir_all(&dir).ok();
}
/// The silver criterion itself: Holger's whole tree is rustfmt-clean.
#[test]
fn holgers_whole_tree_is_rustfmt_clean() {
let root = root();
let files = tracked_rs(&root).expect("git ls-files");
let changed = rustfmt(&root.join("rustfmt.toml"), &files, false).expect("rustfmt --check");
let shown: Vec<String> = changed
.iter()
.take(20)
.map(|f| f.strip_prefix(&root).unwrap_or(f).display().to_string())
.collect();
assert!(
changed.is_empty(),
"{} of {} files are not rustfmt-clean (`cargo xtask fmt --write`), first ones:\n{}",
changed.len(),
files.len(),
shown.join("\n")
);
}
}