use super::Cli;
use crate::diff;
use anyhow::{Context, bail};
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};
pub(crate) fn collect_files(
dir: &Path,
exts: &[&str],
out: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
let walker = WalkBuilder::new(dir)
.hidden(false)
.git_ignore(true)
.git_global(false)
.git_exclude(false)
.parents(true)
.build();
for entry in walker {
let entry = entry?;
let path = entry.path();
if entry.file_type().is_some_and(|ft| ft.is_file())
&& ext_in(path.extension().and_then(|e| e.to_str()), exts)
{
out.push(path.to_path_buf());
}
}
Ok(())
}
pub(crate) fn resolve_inputs(cli: &Cli, exts: &[&str]) -> anyhow::Result<Vec<PathBuf>> {
if cli.paths.is_empty() {
diff::changed_files(exts)
} else {
resolve_all(&cli.paths, exts)
}
}
#[inline]
pub(crate) fn ext_in(ext: Option<&str>, exts: &[&str]) -> bool {
ext.is_some_and(|e| exts.iter().any(|x| e.eq_ignore_ascii_case(x)))
}
pub(crate) fn resolve_all(inputs: &[PathBuf], exts: &[&str]) -> anyhow::Result<Vec<PathBuf>> {
let mut paths: Vec<PathBuf> = Vec::new();
for input in inputs {
let resolved = resolve_paths(input, exts)
.with_context(|| format!("failed to resolve path {}", input.display()))?;
paths.extend(resolved);
}
paths.sort();
paths.dedup();
Ok(paths)
}
fn resolve_paths(path: &Path, exts: &[&str]) -> anyhow::Result<Vec<PathBuf>> {
if path.is_file() {
if ext_in(path.extension().and_then(|e| e.to_str()), exts) {
return Ok(vec![path.to_path_buf()]);
}
return Ok(Vec::new());
}
if !path.exists() {
bail!("path does not exist: {}", path.display());
}
if !path.is_dir() {
bail!("path is neither a file nor a directory: {}", path.display());
}
let mut files = Vec::new();
collect_files(path, exts, &mut files)
.with_context(|| format!("failed to read directory {}", path.display()))?;
files.sort();
Ok(files)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
struct TempDir(PathBuf);
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn scan_repo(git_is_file: bool, tag: &str) -> Vec<PathBuf> {
let dir =
std::env::temp_dir().join(format!("rlt-path-ignore-{}-{}", tag, std::process::id()));
let guard = TempDir(dir.clone());
let _guard = guard;
fs::create_dir_all(&dir).unwrap();
let git = dir.join(".git");
if git_is_file {
fs::write(&git, "gitdir: /nowhere\n").unwrap();
} else {
fs::create_dir_all(&git).unwrap();
}
fs::write(dir.join(".gitignore"), "target/\nignored.rs\n!kept.rs\n").unwrap();
fs::write(dir.join("top.rs"), "fn a() {}\n").unwrap();
fs::write(dir.join("ignored.rs"), "fn b() {}\n").unwrap();
fs::write(dir.join("kept.rs"), "fn c() {}\n").unwrap();
fs::create_dir_all(dir.join("target")).unwrap();
fs::write(dir.join("target").join("gen.rs"), "fn d() {}\n").unwrap();
let mut files = Vec::new();
collect_files(&dir, &["rs"], &mut files).unwrap();
files.sort();
files
}
#[test]
fn collect_files_follows_gitignore() {
let files = scan_repo(false, "dir");
assert_eq!(
files,
vec![
std::env::temp_dir()
.join(format!("rlt-path-ignore-dir-{}", std::process::id()))
.join("kept.rs"),
std::env::temp_dir()
.join(format!("rlt-path-ignore-dir-{}", std::process::id()))
.join("top.rs"),
],
"gitignore rules (ignore + negation) must shape collection"
);
}
#[test]
fn collect_files_follows_gitignore_worktree_git_file() {
let files = scan_repo(true, "file");
assert_eq!(
files.len(),
2,
"same ignore rules must apply for a .git file"
);
}
}