From c123c6419ccff598e963b4de4aeefaf12d662e02 Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Thu, 19 Mar 2026 04:29:09 -0400 Subject: [PATCH] feat: allow user to pass multiple files to fix and check commands reimplement the iterator using ignore::Walk This implementation simplifies things a bit also it returns a lazy iterator which is memory efficient for large codebases. --- bin/src/config.rs | 10 ++-- bin/src/dirs.rs | 145 ++++++++++++++++------------------------------ 2 files changed, 55 insertions(+), 100 deletions(-) diff --git a/bin/src/config.rs b/bin/src/config.rs index 7764d603..5d59be46 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -39,7 +39,7 @@ pub enum SubCommand { pub struct Check { /// File or directory to run check on #[clap(default_value = ".", parse(from_os_str))] - target: PathBuf, + target: Vec, /// Globs of file patterns to skip #[clap(short, long)] @@ -77,8 +77,7 @@ impl Check { Ok(ReadOnlyVfs::singleton("", src.as_bytes())) } else { let all_ignores = [self.ignore.as_slice(), extra_ignores].concat(); - let ignore = dirs::build_ignore_set(&all_ignores, &self.target, self.unrestricted)?; - let files = dirs::walk_nix_files(ignore, &self.target)?; + let files = dirs::walk_nix_files(&all_ignores, &self.target, self.unrestricted)?; Ok(vfs(&files.collect::>())) } } @@ -88,7 +87,7 @@ impl Check { pub struct Fix { /// File or directory to run fix on #[clap(default_value = ".", parse(from_os_str))] - target: PathBuf, + target: Vec, /// Globs of file patterns to skip #[clap(short, long)] @@ -130,8 +129,7 @@ impl Fix { Ok(ReadOnlyVfs::singleton("", src.as_bytes())) } else { let all_ignores = [self.ignore.as_slice(), extra_ignores].concat(); - let ignore = dirs::build_ignore_set(&all_ignores, &self.target, self.unrestricted)?; - let files = dirs::walk_nix_files(ignore, &self.target)?; + let files = dirs::walk_nix_files(&all_ignores, &self.target, self.unrestricted)?; Ok(vfs(&files.collect::>())) } } diff --git a/bin/src/dirs.rs b/bin/src/dirs.rs index 13b66d7a..ed863484 100644 --- a/bin/src/dirs.rs +++ b/bin/src/dirs.rs @@ -1,112 +1,69 @@ use std::{ - fs, - io::{self, Error, ErrorKind}, + io, path::{Path, PathBuf}, }; -use crate::dirs; +use ignore::{overrides::OverrideBuilder, WalkBuilder}; -use ignore::{ - Error as IgnoreError, Match, - gitignore::{Gitignore, GitignoreBuilder}, -}; +/// Walks through target paths and returns an iterator of .nix files, respecting gitignore rules. +/// +/// # Arguments +/// * `ignore_patterns` - Globs of file patterns to skip (e.g., "*.tmp", "build/*") +/// * `targets` - File or directory paths to walk through +/// * `unrestricted` - If true, don't respect .gitignore files; if false, respect them +pub fn walk_nix_files>( + ignore_patterns: &[String], + targets: &[P], + unrestricted: bool, +) -> Result, io::Error> { + let mut targets_iter = targets.iter(); -#[derive(Debug)] -pub struct Walker { - dirs: Vec, - files: Vec, - ignore: Gitignore, -} + let first_target = targets_iter + .next() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "No targets provided"))?; -impl Walker { - pub fn new>(target: P, ignore: Gitignore) -> io::Result { - let target = target.as_ref().to_path_buf(); - if !target.exists() { - Err(Error::new( - ErrorKind::NotFound, - format!("file not found: {}", target.display()), - )) - } else if target.is_dir() { - Ok(Self { - dirs: vec![target], - files: vec![], - ignore, - }) - } else { - Ok(Self { - dirs: vec![], - files: vec![target], - ignore, - }) - } - } -} + let mut builder = WalkBuilder::new(first_target.as_ref()); -impl Iterator for Walker { - type Item = PathBuf; - fn next(&mut self) -> Option { - self.files.pop().or_else(|| { - while let Some(dir) = self.dirs.pop() { - if dir.is_dir() - && let Match::None | Match::Whitelist(_) = self.ignore.matched(&dir, true) - { - let mut found = false; - for entry in fs::read_dir(&dir).ok()? { - let entry = entry.ok()?; - let path = entry.path(); - if path.is_dir() { - self.dirs.push(path); - } else if path.is_file() - && let Match::None | Match::Whitelist(_) = - self.ignore.matched(&path, false) - { - found = true; - self.files.push(path); - } - } - if found { - break; - } - } - } - self.files.pop() - }) + for target in targets_iter { + builder.add(target.as_ref()); } -} -pub fn build_ignore_set>( - ignore: &[String], - target: P, - unrestricted: bool, -) -> Result { - let gitignore_path = target.as_ref().join(".gitignore"); + builder.git_ignore(!unrestricted); - // Looks like GitignoreBuilder::new does not source globs - // within gitignore_path by default, we have to enforce that - // using GitignoreBuilder::add. Probably a bug in the ignore - // crate? - let mut gitignore = GitignoreBuilder::new(&gitignore_path); + // Add files/directories to ignore set passed in --ignore + if !ignore_patterns.is_empty() { + let mut override_builder = OverrideBuilder::new(""); - // if we are to "restrict" aka "respect" .gitignore, then - // add globs from gitignore path as well - if !unrestricted { - gitignore.add(&gitignore_path); + for pattern in ignore_patterns { + // Note: The `!` prefix has inverted semantics in OverrideBuilder compared to gitignore. + // In OverrideBuilder: `!pattern` means "ignore files matching pattern" + // In gitignore: `!pattern` means "don't ignore files matching pattern" (whitelist) + // So we add `!` to make ignore_patterns actually ignore files. + override_builder + .add(&format!("!{pattern}")) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + } - // ignore .git by default, nobody cares about .git, i'm sure - gitignore.add_line(None, ".git")?; - } + let overrides = override_builder + .build() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; - for i in ignore { - gitignore.add_line(None, i.as_str())?; + builder.overrides(overrides); } - gitignore.build() -} + let walker = builder.build(); -pub fn walk_nix_files>( - ignore: Gitignore, - target: P, -) -> Result, io::Error> { - let walker = dirs::Walker::new(target, ignore)?; - Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) + Ok(walker + .filter_map(|result| match result { + Ok(entry) => Some(entry), + Err(err) => { + eprintln!("Warning: Error reading directory entry: {err}"); + None + } + }) + .filter_map(|entry| { + let path = entry.path(); + (path.is_file() && matches!(path.extension(), Some(ext) if ext == "nix")) + .then(|| path.to_path_buf()) + })) }