diff --git a/Cargo.lock b/Cargo.lock index 51fab78..bbef74b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,6 +204,7 @@ version = "0.2.1" dependencies = [ "clap", "tempfile", + "walkdir", ] [[package]] @@ -219,6 +220,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "strsim" version = "0.11.1" @@ -261,6 +271,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -270,6 +290,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 0114426..d9d0b9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "GPL-3.0" [dependencies] clap = { version = "4.5", features = ["derive"] } +walkdir = "2.5.0" [dev-dependencies] tempfile = "3.10" diff --git a/src/main.rs b/src/main.rs index ac860d9..daa9ca6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; use std::fs; use std::io::{self, Write}; use std::path::{Path, PathBuf}; +use walkdir::WalkDir; /// Helper function to display paths without Windows UNC prefix (\\?\) fn display_path(path: &Path) -> String { @@ -98,75 +99,50 @@ fn collect_flatten_plan( top_level_dirs: std::collections::HashSet::new(), }; - collect_flatten_plan_recursive(dir, dir, max_depth, 0, include, exclude, &mut plan, None)?; - - Ok(plan) -} - -fn collect_flatten_plan_recursive( - root: &Path, - current: &Path, - max_depth: Option, - current_depth: usize, - include: &Option>, - exclude: &Option>, - plan: &mut FlattenPlan, - top_level_dir: Option, -) -> io::Result<()> { + let mut walker = WalkDir::new(dir); if let Some(max) = max_depth { - if current_depth > max { - return Ok(()); + // A file at user-facing depth N (N directories below the root) sits at + // walkdir depth N + 1, since walkdir counts the entry itself + walker = walker.max_depth(max.saturating_add(1)); + } + + let entries = walker.into_iter().filter_entry(|entry| { + // Apply include/exclude filters to top-level directories, skipping + // their entire subtree when filtered out + if entry.depth() == 1 && entry.file_type().is_dir() { + match entry.file_name().to_str() { + Some(dir_name) => should_include_top_level_dir(dir_name, include, exclude), + None => false, + } + } else { + true } - } + }); - for entry in fs::read_dir(current)? { + for entry in entries { let entry = entry?; - let path = entry.path(); - let file_type = entry.file_type()?; - - if file_type.is_dir() { - // Determine the top-level directory name - let new_top_level_dir = if current == root { - // We're at the root, so this subdirectory is a top-level directory - if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) { - // Check if we should include this top-level directory - if !should_include_top_level_dir(dir_name, include, exclude) { - continue; // Skip this entire subtree - } - Some(dir_name.to_string()) - } else { - continue; - } - } else { - // We're in a subdirectory, inherit the top-level directory - top_level_dir.clone() - }; - // Recursively traverse subdirectories - collect_flatten_plan_recursive( - root, - &path, - max_depth, - current_depth + 1, - include, - exclude, - plan, - new_top_level_dir, - )?; - } else if file_type.is_file() { - // Only collect files that are in subdirectories (not in root) - if path.parent() != Some(root) { - plan.files.push(path); - - // Track the top-level directory - if let Some(ref dir) = top_level_dir { - plan.top_level_dirs.insert(dir.clone()); - } - } + // Only collect files that are in subdirectories (not in root) + if !entry.file_type().is_file() || entry.depth() < 2 { + continue; + } + + let path = entry.into_path(); + + // Track the top-level directory the file lives in + if let Some(top_level_dir) = path + .strip_prefix(dir) + .ok() + .and_then(|relative| relative.components().next()) + .and_then(|component| component.as_os_str().to_str()) + { + plan.top_level_dirs.insert(top_level_dir.to_string()); } + + plan.files.push(path); } - Ok(()) + Ok(plan) } fn get_confirmation() -> io::Result {