diff --git a/src/files.rs b/src/files.rs index 8e837759..d4bb63b8 100644 --- a/src/files.rs +++ b/src/files.rs @@ -104,15 +104,15 @@ fn collect_changed_names( let mut names: std::collections::BTreeSet = Default::default(); // Committed changes in the range. - for line in git_diff_names(project_root, &["--diff-filter=d", &range])? { + for line in git_diff_names(project_root, &[&range])? { names.insert(line); } // Unstaged changes. - for line in git_diff_names(project_root, &["--diff-filter=d"])? { + for line in git_diff_names(project_root, &[])? { names.insert(line); } // Staged changes. - for line in git_diff_names(project_root, &["--cached", "--diff-filter=d"])? { + for line in git_diff_names(project_root, &["--cached"])? { names.insert(line); } diff --git a/src/linters/renovate_deps.rs b/src/linters/renovate_deps.rs index ca95e58f..acf29621 100644 --- a/src/linters/renovate_deps.rs +++ b/src/linters/renovate_deps.rs @@ -127,15 +127,7 @@ pub(crate) fn is_relevant(file_list: &FileList, project_root: &Path) -> bool { return true; } - let changed: HashSet = file_list - .files - .iter() - .filter_map(|path| { - path.strip_prefix(project_root) - .ok() - .map(|rel| rel.to_string_lossy().into_owned()) - }) - .collect(); + let changed = changed_rel_paths(file_list, project_root); if changed.is_empty() { return false; @@ -173,6 +165,34 @@ pub(crate) fn is_relevant(file_list: &FileList, project_root: &Path) -> bool { committed.keys().any(|path| changed.contains(path)) } +fn changed_rel_paths(file_list: &FileList, project_root: &Path) -> HashSet { + if !file_list.changed_paths.is_empty() { + return file_list + .changed_paths + .iter() + .map(|path| { + let path = Path::new(path); + path.strip_prefix(project_root).unwrap_or(path) + }) + .map(normalize_path) + .collect(); + } + + file_list + .files + .iter() + .filter_map(|path| path.strip_prefix(project_root).ok()) + .map(normalize_path) + .collect() +} + +fn normalize_path(path: &Path) -> String { + path.components() + .map(|component| component.as_os_str().to_string_lossy()) + .collect::>() + .join("/") +} + pub(crate) fn adaptive_relevance(ctx: &dyn AdaptiveRelevanceContext) -> bool { is_relevant(ctx.file_list(), ctx.project_root()) } @@ -514,10 +534,7 @@ fn committed_path_for_config(config_path: &Path) -> PathBuf { } fn display_path(project_root: &Path, path: &Path) -> String { - path.strip_prefix(project_root) - .unwrap_or(path) - .to_string_lossy() - .into_owned() + normalize_path(path.strip_prefix(project_root).unwrap_or(path)) } /// Parses Renovate's NDJSON log and returns the dep map. @@ -967,6 +984,19 @@ mod tests { assert!(diff.contains("renovate-tracked-deps.json")); } + #[test] + fn display_path_normalizes_separators() { + let dir = tempfile::tempdir().unwrap(); + let path = dir + .path() + .join(".github") + .join("renovate-tracked-deps.json"); + assert_eq!( + display_path(dir.path(), &path), + ".github/renovate-tracked-deps.json" + ); + } + #[test] fn resolves_supported_renovate_config_file() { let dir = tempfile::tempdir().unwrap(); @@ -1071,6 +1101,26 @@ mod tests { )); } + #[test] + fn relevant_when_tracked_manifest_was_deleted() { + let dir = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(dir.path().join(".github")).unwrap(); + write_snapshot( + &dir.path().join(".github/renovate-tracked-deps.json"), + &dep_map(&[("package.json", &[("npm", &["express"])])]), + ) + .unwrap(); + + let file_list = FileList { + files: vec![], + changed_paths: vec!["package.json".to_string()], + merge_base: Some("base".to_string()), + full: false, + }; + + assert!(is_relevant(&file_list, dir.path())); + } + #[test] fn not_relevant_for_untracked_change() { let dir = tempfile::tempdir().unwrap(); diff --git a/tests/e2e.rs b/tests/e2e.rs index e7cf233a..1c36d531 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -164,6 +164,115 @@ fn cases() { } } +// Unix-only: this e2e test creates a fake linter as a POSIX shell script and +// marks it executable with Unix permissions. +#[cfg(unix)] +#[test] +fn renovate_deps_fast_only_runs_for_deleted_tracked_file() { + let repo = git_repo(); + + std::fs::create_dir_all(repo.path().join(".github")).unwrap(); + std::fs::write( + repo.path().join("mise.toml"), + r#"[tools] +node = "22.0.0" + +# Linters +"npm:renovate" = "43.136.3" +"#, + ) + .unwrap(); + std::fs::write(repo.path().join(".github/renovate.json5"), "{}\n").unwrap(); + std::fs::write(repo.path().join("package.json"), "{}\n").unwrap(); + std::fs::write( + repo.path().join(".github/renovate-tracked-deps.json"), + r#"{ + "package.json": { + "npm": [ + "express" + ] + } +} +"#, + ) + .unwrap(); + + let out = Command::new("git") + .args(["add", "-A"]) + .current_dir(repo.path()) + .output() + .expect("failed to spawn git add"); + assert!( + out.status.success(), + "git add failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + let out = Command::new("git") + .args(["commit", "-q", "-m", "init"]) + .current_dir(repo.path()) + .output() + .expect("failed to spawn git commit"); + assert!( + out.status.success(), + "git commit failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + + let out = Command::new("git") + .args(["rm", "-q", "package.json"]) + .current_dir(repo.path()) + .output() + .expect("failed to spawn git rm"); + assert!( + out.status.success(), + "git rm failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + + let fake_bin_dir = tempfile::tempdir().expect("fake_bin tempdir"); + let renovate = fake_bin_dir.path().join("renovate"); + std::fs::write( + &renovate, + r#"#!/bin/sh +set -eu + +touch .renovate-ran +printf '%s\n' '{"msg":"Extracted dependencies","packageFiles":{"mise":[{"packageFile":"mise.toml","deps":[{"depName":"npm:renovate"}]}]}}' +"#, + ) + .unwrap(); + + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&renovate, std::fs::Permissions::from_mode(0o755)).unwrap(); + + let fake_path = format!( + "{}:{}", + fake_bin_dir.path().display(), + std::env::var("PATH").unwrap_or_default() + ); + let out = flint_with_env( + &["run", "--fast-only"], + repo.path(), + &[("PATH", &fake_path)], + ); + + assert!( + repo.path().join(".renovate-ran").exists(), + "renovate-deps was skipped; stdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + assert_eq!( + out.status.code(), + Some(1), + "expected stale renovate snapshot failure; stdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); +} + +// Unix-only: this e2e test creates fake linters as POSIX shell scripts and +// marks them executable with Unix permissions. #[cfg(unix)] #[test] fn markdown_tool_ignores_biome_owned_jsonc() { @@ -336,6 +445,8 @@ exit 1 ); } +// Unix-only: this e2e test creates a fake linter as a POSIX shell script and +// marks it executable with Unix permissions. #[cfg(unix)] #[test] fn rumdl_fix_hides_success_noise_when_another_file_fails() {