From a1d3e029ca396cec56ed07f3c39496777e02f66e Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Mon, 26 Oct 2026 20:09:48 +0000 Subject: [PATCH] chore(vdev): only report files modified by cue fmt in check docs (#26725) Co-authored-by: Patrick Ribbsaeter <233187023+patrickswedish@users.noreply.github.com> --- ...6707_vdev_check_docs_unstaged_edits.fix.md | 3 + vdev/src/commands/check/docs.rs | 165 ++++++++++++++++-- 2 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 changelog.d/26707_vdev_check_docs_unstaged_edits.fix.md diff --git a/changelog.d/26707_vdev_check_docs_unstaged_edits.fix.md b/changelog.d/26707_vdev_check_docs_unstaged_edits.fix.md new file mode 100644 index 0000000..2864280 --- /dev/null +++ b/changelog.d/26707_vdev_check_docs_unstaged_edits.fix.md @@ -0,0 +1,3 @@ +Fixed `vdev check docs` reporting unrelated dirty working tree files after running `cue fmt`. The check now snapshots CUE files before and after formatting so only files actually modified by the formatter are reported. + +authors: patrickswedish diff --git a/vdev/src/commands/check/docs.rs b/vdev/src/commands/check/docs.rs index abff220..2b89501 100644 --- a/vdev/src/commands/check/docs.rs +++ b/vdev/src/commands/check/docs.rs @@ -1,8 +1,13 @@ -use crate::app; -use crate::app::CommandExt; -use crate::utils::git; -use anyhow::{Result, bail}; -use std::process::Command; +use std::{ + collections::BTreeMap, + fs, + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::{Context, Result, bail}; + +use crate::{app, app::CommandExt, utils::paths::find_repo_root}; #[derive(clap::Args, Debug)] #[command()] @@ -44,15 +49,151 @@ impl Cli { } fn get_unformatted_cue_files() -> Result> { - app::exec(CUE_SCRIPT, ["fmt"], true)?; + let repo_root = find_repo_root()?; + Self::find_unformatted_cue_files(&repo_root, || app::exec(CUE_SCRIPT, ["fmt"], true)) + } + + fn find_unformatted_cue_files(repo_root: &Path, run_fmt: F) -> Result> + where + F: FnOnce() -> Result<()>, + { + let source_dir = repo_root.join(CUE_DIR); + let before = snapshot_cue_files(repo_root, &source_dir)?; + + run_fmt()?; + + let after = snapshot_cue_files(repo_root, &source_dir)?; - let file_names = git::run_and_check_output(&["diff", "--name-only", CUE_DIR])? - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .map(String::from) + let mut modified: Vec = after + .into_iter() + .filter(|(path, content)| before.get(path) != Some(content)) + .map(|(path, _)| path) .collect(); + modified.sort(); + + Ok(modified) + } +} + +fn snapshot_cue_files(repo_root: &Path, source_dir: &Path) -> Result>> { + let files = collect_cue_files(source_dir)?; + let mut snapshot = BTreeMap::new(); + + for path in files { + let content = + fs::read(&path).with_context(|| format!("Failed to read {}", path.display()))?; + let normalized = normalize_crlf(&content); + let rel_path = relative_repo_path(repo_root, &path); + snapshot.insert(rel_path, normalized); + } + + Ok(snapshot) +} + +fn collect_cue_files(source_dir: &Path) -> Result> { + let mut files = Vec::new(); + walk_cue_files(source_dir, &mut files)?; + files.sort(); + Ok(files) +} + +fn walk_cue_files(dir: &Path, files: &mut Vec) -> Result<()> { + if !dir.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + walk_cue_files(&path, files)?; + } else if path.extension().and_then(|s| s.to_str()) == Some("cue") { + files.push(path); + } + } + Ok(()) +} + +fn normalize_crlf(bytes: &[u8]) -> Vec { + let mut out = Vec::with_capacity(bytes.len()); + let mut iter = bytes.iter().peekable(); + while let Some(&b) = iter.next() { + if b == b'\r' && iter.peek() == Some(&&b'\n') { + continue; + } + out.push(b); + } + out +} + +fn relative_repo_path(repo_root: &Path, path: &Path) -> String { + let rel: &Path = path.strip_prefix(repo_root).unwrap_or(path); + let mut normalized = PathBuf::new(); + for component in rel.components() { + normalized.push(component); + } + normalized.to_string_lossy().replace('\\', "/") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ignores_preexisting_formatted_edits_and_reports_only_formatter_changes() { + let temp = tempfile::tempdir().expect("create tempdir"); + let cue_dir = temp.path().join("website").join("cue"); + fs::create_dir_all(&cue_dir).expect("create cue dir"); + + let clean_edited = cue_dir.join("already_formatted.cue"); + let unformatted = cue_dir.join("needs_fmt.cue"); + + fs::write(&clean_edited, "package metadata\n\nfoo: \"bar\"\n").expect("write clean file"); + fs::write(&unformatted, "package metadata\nfoo: \"baz\"\n") + .expect("write unformatted file"); + + let modified = Cli::find_unformatted_cue_files(temp.path(), || { + // Simulate `cue fmt` leaving `already_formatted.cue` untouched while formatting `needs_fmt.cue` + fs::write(&unformatted, "package metadata\n\nfoo: \"baz\"\n")?; + Ok(()) + }) + .expect("check cue formatting"); + + assert_eq!(modified, vec!["website/cue/needs_fmt.cue".to_string()]); + } + + #[test] + fn ignores_crlf_formatting_differences() { + let temp = tempfile::tempdir().expect("create tempdir"); + let cue_dir = temp.path().join("website").join("cue"); + fs::create_dir_all(&cue_dir).expect("create cue dir"); + + let file = cue_dir.join("test.cue"); + fs::write(&file, "package metadata\r\n\r\nfoo: \"bar\"\r\n").expect("write CRLF file"); + + let modified = Cli::find_unformatted_cue_files(temp.path(), || { + // Simulate `cue fmt` writing LF + fs::write(&file, "package metadata\n\nfoo: \"bar\"\n")?; + Ok(()) + }) + .expect("check cue formatting"); + + assert!(modified.is_empty()); + } + + #[test] + fn handles_path_with_brackets() { + let temp = tempfile::Builder::new() + .prefix("vector[1]") + .tempdir() + .expect("create tempdir"); + let cue_dir = temp.path().join("website").join("cue"); + fs::create_dir_all(&cue_dir).expect("create cue dir"); + + let file = cue_dir.join("test.cue"); + fs::write(&file, "package metadata\n\nfoo: \"bar\"\n").expect("write file"); - Ok(file_names) + let modified = + Cli::find_unformatted_cue_files(temp.path(), || Ok(())).expect("check cue formatting"); + assert!(modified.is_empty()); } } -- 2.55.0.windows.3