diff --git a/src/aggregate.rs b/src/aggregate.rs index a6cf3a19..7d3c1773 100644 --- a/src/aggregate.rs +++ b/src/aggregate.rs @@ -712,6 +712,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, false, sort_by_size_in_bytes, @@ -790,6 +791,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, true, false, @@ -919,6 +921,7 @@ mod tests { skip_metadata: false, apfs_clone_metadata: true, }, + base_dir: None, }, true, true, @@ -960,6 +963,7 @@ mod tests { skip_metadata: false, apfs_clone_metadata: true, }, + base_dir: None, }, true, true, @@ -1061,6 +1065,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, true, true, @@ -1095,6 +1100,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, false, true, @@ -1133,6 +1139,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: crate::IgnorePatterns::from_files(ignore_from).unwrap(), metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, false, true, diff --git a/src/clean.rs b/src/clean.rs index 3b2a32b5..881c80df 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -321,6 +321,7 @@ mod tests { ignore_dirs: BTreeSet::new(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, } } diff --git a/src/common.rs b/src/common.rs index c5655c60..4b943b43 100644 --- a/src/common.rs +++ b/src/common.rs @@ -252,6 +252,12 @@ pub struct WalkOptions { /// Gitignore-style patterns whose matches are left out of the traversal entirely. /// `None` if no pattern was configured. pub ignore_patterns: Option, + /// The directory the traversal logically runs in, replacing the process working + /// directory as the base that ignore patterns and relative paths are matched + /// against. Set when a sole input directory was expanded into its entries instead + /// of being entered with `set_current_dir` - e.g. beyond `MAX_PATH` on Windows. + /// `None` uses the process working directory. + pub base_dir: Option, /// Platform-specific metadata requested during traversal. pub metadata_options: crate::TraversalOptions, } @@ -322,7 +328,15 @@ impl WalkOptions { }, ); let ignore_dirs = self.ignore_dirs.clone(); - let cwd = std::env::current_dir().unwrap_or_default(); + // `base_dir` stands in for the working directory when the input directory + // was expanded into its entries instead of being entered, so that ignore + // patterns see the same relative paths they would have seen after a + // successful `set_current_dir`. + let cwd = self + .base_dir + .clone() + .or_else(|| std::env::current_dir().ok()) + .unwrap_or_default(); let cross_filesystems = self.cross_filesystems; // Excluding an entry means pruning it from the walk *and* from the emitted events, so the @@ -470,6 +484,7 @@ mod tests { ignore_dirs: BTreeSet::new(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }; #[cfg(unix)] let mut parameters = vec![ @@ -532,6 +547,7 @@ mod tests { ignore_dirs: canonicalize_ignore_dirs(&[root.path().to_owned()]), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }; let paths = options @@ -713,6 +729,7 @@ mod tests { ignore_dirs: BTreeSet::default(), ignore_patterns: Some(patterns_from("nested/secret\n")), metadata_options: crate::TraversalOptions::default(), + base_dir: None, }; let paths = options @@ -738,6 +755,54 @@ mod tests { assert!(paths.iter().any(|path| path == "visible")); } + /// A directory that cannot be entered is expanded into absolute inputs, with + /// `base_dir` recording where those inputs came from. Entries found under them + /// must still match ignore patterns relative to that directory, exactly as they + /// would had changing into it succeeded - otherwise patterns like `sub/ignored` + /// stop excluding anything below the expanded roots. + #[test] + fn base_dir_anchors_ignore_patterns_of_expanded_roots() { + let root = tempfile::tempdir().unwrap(); + let sub = root.path().join("sub"); + std::fs::create_dir(&sub).unwrap(); + std::fs::write(sub.join("ignored.txt"), []).unwrap(); + std::fs::write(sub.join("kept.txt"), []).unwrap(); + let options = WalkOptions { + threads: 1, + count_hard_links: false, + apparent_size: false, + cross_filesystems: true, + ignore_dirs: BTreeSet::default(), + ignore_patterns: Some(patterns_from("sub/ignored.txt\n")), + base_dir: Some(root.path().to_owned()), + metadata_options: crate::TraversalOptions::default(), + }; + + let names = options + .iter_from_paths( + vec![WalkRoot { + index: 0, + // Walkers derive the pattern root from the input path itself, but + // `base_dir` takes precedence in matching. + pattern_root: Some(sub.clone()), + path: sub, + #[cfg(any(windows, target_os = "macos"))] + entry: None, + device_id: 0, + }], + false, + walk::Order::Completion, + ) + .filter_map(|(_, event)| match event { + walk::RootEvent::Entry(entry) => Some(entry.unwrap().file_name), + walk::RootEvent::Finished => None, + }) + .collect::>(); + + assert!(!names.iter().any(|name| name == "ignored.txt")); + assert!(names.iter().any(|name| name == "kept.txt")); + } + #[test] fn excluded_input_paths_are_dropped_before_the_walk() { // Relies on the crate directory being current, like `test_ignore_directories` above. @@ -769,6 +834,7 @@ mod tests { ignore_dirs: BTreeSet::default(), ignore_patterns: Some(patterns_from(contents)), metadata_options: crate::TraversalOptions::default(), + base_dir: None, }; let mut paths = options diff --git a/src/interactive/app/terminal.rs b/src/interactive/app/terminal.rs index 342592b2..baeabaf8 100644 --- a/src/interactive/app/terminal.rs +++ b/src/interactive/app/terminal.rs @@ -101,6 +101,11 @@ impl TerminalApp { let display = DisplayOptions::new(byte_format); let window = MainWindow::default(); + // Resolve paths from the expanded directory when changing into it failed. + if let Some(base_dir) = &walk_options.base_dir { + traversal.tree.rename(traversal.root_index, base_dir)?; + } + let read_only = snapshot_load_duration.is_some(); let mut state = AppState::new(walk_options, input, root_path, read_only); if config.gitignore == Some(false) { diff --git a/src/interactive/app/tests/journeys_with_writes.rs b/src/interactive/app/tests/journeys_with_writes.rs index cc64de55..4c233111 100644 --- a/src/interactive/app/tests/journeys_with_writes.rs +++ b/src/interactive/app/tests/journeys_with_writes.rs @@ -29,6 +29,93 @@ fn marked_file_names(app: &TerminalApp, message: &str) -> BTreeSet { .collect() } +#[test] +fn expanded_root_anchors_refresh_title_and_gitignore() -> Result<()> { + for initial_name in [None, Some("existing.ignored")] { + let fixture = TempDir::new()?; + let repo = fixture.path().canonicalize()?; + gix::init(&repo)?; + fs::write(repo.join(".gitignore"), b"*.ignored\n")?; + let root = repo.join("expanded"); + fs::create_dir(&root)?; + let input_paths = initial_name + .into_iter() + .map(|name| root.join(name)) + .collect::>(); + for path in &input_paths { + fs::write(path, b"existing")?; + } + let mut expected_paths = input_paths.iter().cloned().collect::>(); + let mut terminal = new_test_terminal()?; + let mut app = TerminalApp::initialize( + &mut terminal, + WalkOptions { + base_dir: Some(root.clone()), + ..crate::snapshot_walk_options() + }, + ByteFormat::Metric, + true, + input_paths, + Some(root.clone()), + Config::default(), + dua::traverse::Traversal::new(), + None, + )?; + + let tree = app.state.tree_view(&mut app.traversal); + assert_eq!( + app.state.display_path(&tree), + root, + "initial_name={initial_name:?}: the initial title must show the expanded root, even without entries" + ); + app.traverse()?; + app.process_events_once(&mut terminal, into_events([]))?; + assert_eq!( + app.state.entries.len(), + expected_paths.len(), + "initial_name={initial_name:?}: the initial scan must contain only the supplied expanded input paths" + ); + assert_eq!( + app.state + .gitignored_entries + .as_ref() + .map(|entries| entries.len()), + Some(expected_paths.len()), + "initial_name={initial_name:?}: gitignore detection must be enabled and match all initial entries using the parent repository's .gitignore" + ); + + let added = root.join("new.ignored"); + fs::write(&added, b"new")?; + expected_paths.insert(added); + app.process_events_once(&mut terminal, into_codes("R"))?; + + let tree = app.state.tree_view(&mut app.traversal); + assert_eq!( + app.state.display_path(&tree), + root, + "initial_name={initial_name:?}: refresh must keep the title anchored to the expanded root" + ); + assert_eq!( + app.state + .entries + .iter() + .map(|entry| tree.path_of(entry.index)) + .collect::>(), + expected_paths, + "initial_name={initial_name:?}: refresh must rescan the expanded root, preserving existing entries and discovering the new file" + ); + assert_eq!( + app.state + .gitignored_entries + .as_ref() + .map(|entries| entries.len()), + Some(expected_paths.len()), + "initial_name={initial_name:?}: gitignore detection must remain enabled and match all refreshed entries using the parent repository's .gitignore" + ); + } + Ok(()) +} + #[test] fn refresh_discards_marks_before_reusing_tree_indices() -> Result<()> { let fixture = TempDir::new()?; @@ -235,6 +322,7 @@ $precious.tmp ignore_dirs: BTreeSet::default(), ignore_patterns: None, metadata_options: dua::TraversalOptions::default(), + base_dir: None, }; let (_key_send, key_receive) = crossbeam::channel::bounded(0); let mut app = TerminalApp::initialize( @@ -377,6 +465,7 @@ fn cleanup_candidates_are_marked_with_one_key_after_entering_project_dir() -> Re ignore_dirs: BTreeSet::default(), ignore_patterns: None, metadata_options: dua::TraversalOptions::default(), + base_dir: None, }; let (_key_send, key_receive) = crossbeam::channel::bounded(0); let mut app = TerminalApp::initialize( diff --git a/src/interactive/app/tests/unit.rs b/src/interactive/app/tests/unit.rs index 65206365..ddc58af4 100644 --- a/src/interactive/app/tests/unit.rs +++ b/src/interactive/app/tests/unit.rs @@ -208,6 +208,7 @@ fn it_can_sort_directory_mtimes_by_recursive_entries() { ignore_dirs: BTreeSet::default(), ignore_patterns: None, metadata_options: dua::TraversalOptions::default(), + base_dir: None, }, Vec::new(), None, diff --git a/src/interactive/app/tests/utils.rs b/src/interactive/app/tests/utils.rs index dc2686aa..080994be 100644 --- a/src/interactive/app/tests/utils.rs +++ b/src/interactive/app/tests/utils.rs @@ -205,6 +205,7 @@ pub fn untraversed_app_and_terminal_with_closure( ignore_dirs: BTreeSet::default(), ignore_patterns: None, metadata_options: dua::TraversalOptions::default(), + base_dir: None, }; let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect(); diff --git a/src/main.rs b/src/main.rs index b6d84358..12ab1d60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -210,17 +210,19 @@ fn main() -> Result<()> { None, ) } else { - let walk_options = walk_options_from(&traversal.scan)?; + let mut walk_options = walk_options_from(&traversal.scan)?; let (input_paths, root_path) = if clean_depth.is_some() { (clean_input_paths(traversal.scan.input)?, None) } else { let has_complete_root = traversal.scan.input.is_empty() || traversal.scan.input.len() == 1 && traversal.scan.input[0].is_dir(); - let input_paths = - extract_paths_maybe_set_cwd(traversal.scan.input, &walk_options)?; + let (input_paths, expanded_root) = + extract_paths_maybe_set_cwd(traversal.scan.input, &mut walk_options)?; ( input_paths, - has_complete_root.then(std::env::current_dir).transpose()?, + has_complete_root + .then(|| expanded_root.map_or_else(std::env::current_dir, Ok)) + .transpose()?, ) }; ( @@ -458,12 +460,12 @@ fn main() -> Result<()> { )? } } else { - let walk_options = walk_options_from(&traversal.scan)?; + let mut walk_options = walk_options_from(&traversal.scan)?; if let Some(depth) = depth { let config = dua::Config::load()?; let byte_format = traversal.byte_format(&config); - let paths = - extract_paths_maybe_set_cwd(traversal.scan.input, &walk_options)?; + let (paths, _expanded_root) = + extract_paths_maybe_set_cwd(traversal.scan.input, &mut walk_options)?; let stdout = io::stdout(); let out_supports_colors = stdout.is_terminal(); dua::aggregate_tree( @@ -481,7 +483,7 @@ fn main() -> Result<()> { let byte_format = traversal.byte_format(&config); let inputs = extract_aggregate_inputs_maybe_set_cwd( traversal.scan.input, - &walk_options, + &mut walk_options, )?; run_aggregation( inputs, @@ -514,9 +516,11 @@ fn main() -> Result<()> { None => { let config = dua::Config::load()?; let byte_format = global_traversal.byte_format(&config); - let walk_options = walk_options_from(&global_traversal.scan)?; - let inputs = - extract_aggregate_inputs_maybe_set_cwd(global_traversal.scan.input, &walk_options)?; + let mut walk_options = walk_options_from(&global_traversal.scan)?; + let inputs = extract_aggregate_inputs_maybe_set_cwd( + global_traversal.scan.input, + &mut walk_options, + )?; run_aggregation(inputs, walk_options, true, true, byte_format, false)? } }; @@ -549,8 +553,9 @@ fn run_stacks( dua::stacks_from_replay(out, &mut replay, max_depth) } else { let traversal = merge_scan_args(global, &traversal); - let walk_options = walk_options_from(&traversal)?; - let inputs = extract_paths_maybe_set_cwd(traversal.input, &walk_options)?; + let mut walk_options = walk_options_from(&traversal)?; + let (inputs, _expanded_root) = + extract_paths_maybe_set_cwd(traversal.input, &mut walk_options)?; dua::stacks(out, stderr_if_tty(), walk_options, inputs, max_depth) } } @@ -666,6 +671,7 @@ fn snapshot_walk_options() -> dua::WalkOptions { ignore_dirs: std::collections::BTreeSet::new(), ignore_patterns: None, metadata_options: dua::TraversalOptions::default(), + base_dir: None, } } @@ -723,6 +729,7 @@ fn walk_options_from(traversal: &options::ScanArgs) -> Result #[cfg(target_os = "macos")] apfs_clone_metadata: traversal.deduplicate_apfs_clones && !traversal.apparent_size, }, + base_dir: None, }; if walk_options.threads == 0 { @@ -732,14 +739,37 @@ fn walk_options_from(traversal: &options::ScanArgs) -> Result Ok(walk_options) } +/// Change the working directory to `path`, returning `None` on success. +/// +/// On Windows, `ERROR_FILENAME_EXCED_RANGE` (os error 206) can prevent entering a +/// directory that is still traversable. In that case, leave the working directory +/// unchanged and return the absolute path for callers to expand into inputs instead. +/// All other errors are propagated. +fn set_current_dir(path: &Path) -> io::Result> { + #[cfg(windows)] + const ERROR_FILENAME_EXCED_RANGE: i32 = 206; + + match std::env::set_current_dir(path) { + Ok(()) => Ok(None), + #[cfg(windows)] + Err(err) if err.raw_os_error() == Some(ERROR_FILENAME_EXCED_RANGE) => { + std::path::absolute(path).map(Some) + } + Err(err) => Err(err), + } +} + fn extract_aggregate_inputs_maybe_set_cwd( paths: Vec, - walk_options: &dua::WalkOptions, + walk_options: &mut dua::WalkOptions, ) -> Result { #[cfg(any(windows, target_os = "macos"))] if paths.is_empty() || paths.len() == 1 && paths[0].is_dir() { - if let Some(path) = paths.first() { - std::env::set_current_dir(path)?; + if let Some(path) = paths.first() + && set_current_dir(path)?.is_some() + { + return extract_paths_maybe_set_cwd(paths, walk_options) + .map(|(paths, _root)| AggregateInputs::Paths(paths)); } let cwd = std::env::current_dir()?; @@ -792,49 +822,65 @@ fn extract_aggregate_inputs_maybe_set_cwd( return Ok(AggregateInputs::Entries(selected_entries)); } - extract_paths_maybe_set_cwd(paths, walk_options).map(AggregateInputs::Paths) + extract_paths_maybe_set_cwd(paths, walk_options) + .map(|(paths, _root)| AggregateInputs::Paths(paths)) } fn extract_paths_maybe_set_cwd( mut paths: Vec, - walk_options: &dua::WalkOptions, -) -> Result, io::Error> { + walk_options: &mut dua::WalkOptions, +) -> Result<(Vec, Option), io::Error> { let cross_filesystems = walk_options.cross_filesystems; // Paths were explicitly passed by the user on the command-line; per `--ignore-dirs`'s // documented behavior, these are never subject to `-i`/`--ignore-dirs` filtering, only // paths that we ourselves expand into roots below are. let paths_were_expanded = paths.is_empty() || (paths.len() == 1 && paths[0].is_dir()); - if paths.len() == 1 && paths[0].is_dir() { - std::env::set_current_dir(&paths[0])?; + // Changing into a sole directory input keeps the reported paths short. + let expanded_root = if paths.len() == 1 && paths[0].is_dir() { + let root = set_current_dir(&paths[0])?; paths.clear(); - } + root + } else { + None + }; + // Downstream traversals must resolve ignore patterns as if the expanded directory + // had been entered - record it as their working-directory stand-in. + walk_options.base_dir.clone_from(&expanded_root); let cwd = std::env::current_dir()?; - let cwd_device = device_id(&cwd).ok(); + // With an expanded root, the working directory is unchanged, so its entries are + // resolved against the root instead for the ignore checks below. + let base_dir = expanded_root.as_deref().unwrap_or(&cwd); + let cwd_device = device_id(base_dir).ok(); let paths = if paths.is_empty() { - cwd_dirlist().map(|paths| match cwd_device { + let paths = match &expanded_root { + Some(root) => dirlist(root)?, + None => cwd_dirlist()?, + }; + match cwd_device { Some(cwd_device) if !cross_filesystems => paths .into_iter() .filter(|path| device_id(path).map_or(true, |device| device == cwd_device)) .collect(), _ => paths, - })? + } } else { paths }; // Drop excluded top-level paths here rather than during the walk, so they are left out of the // report entirely instead of showing up as being empty. - Ok(paths + let paths: Vec = paths .into_iter() .filter(|path| { walk_options .ignore_patterns .as_ref() - .is_none_or(|patterns| !patterns.excludes_input_path(path, &cwd)) + .is_none_or(|patterns| !patterns.excludes_input_path(path, base_dir)) }) - .filter(|path| !paths_were_expanded || !walk_options.is_ignored_directory(path, &cwd)) - .collect()) + .filter(|path| !paths_were_expanded || !walk_options.is_ignored_directory(path, base_dir)) + .collect(); + Ok((paths, expanded_root)) } #[cfg(feature = "tui-crossplatform")] @@ -871,21 +917,25 @@ fn device_id(_path: &Path) -> io::Result { } fn cwd_dirlist() -> Result, io::Error> { - let mut entries: Vec<_> = fs::read_dir(".")? - .filter_map(|e| { - e.ok() - .and_then(|e| e.path().strip_prefix(".").ok().map(ToOwned::to_owned)) - }) - .filter(|p| { - if let Ok(meta) = p.symlink_metadata() - && meta.file_type().is_symlink() - { - return false; - } - true + Ok(dirlist(Path::new("."))? + .into_iter() + .filter_map(|path| path.strip_prefix(".").ok().map(ToOwned::to_owned)) + .collect()) +} + +/// List sorted, non-symlink entries of `dir`, keeping `dir` as the prefix of each +/// returned path so the entries remain usable without changing the working directory. +/// The directory's path is added to errors for context. +fn dirlist(dir: &Path) -> Result, io::Error> { + let mut entries: Vec<_> = fs::read_dir(dir) + .map_err(|err| io::Error::new(err.kind(), format!("{}: {err}", dir.display())))? + .filter_map(|entry| entry.ok().map(|entry| entry.path())) + .filter(|path| { + !path + .symlink_metadata() + .is_ok_and(|meta| meta.file_type().is_symlink()) }) .collect(); - entries.sort(); Ok(entries) } @@ -1249,4 +1299,97 @@ mod tests { assert_eq!(merged.scan.ignore_dirs, subcommand.scan.ignore_dirs); } + + #[cfg(unix)] + #[test] + fn directory_change_errors_are_propagated_on_unix() { + use std::os::unix::fs::PermissionsExt as _; + + let fixture = tempfile::tempdir().unwrap(); + let root = fixture.path(); + let file = root.join("file.txt"); + fs::write(&file, b"data").unwrap(); + let cwd = std::env::current_dir().unwrap(); + let mut options = super::walk_options_from(&scan_args()).unwrap(); + fs::set_permissions(root, fs::Permissions::from_mode(0o400)).unwrap(); + if file.metadata().is_ok() { + // Privileged users can bypass the missing search permission. + fs::set_permissions(root, fs::Permissions::from_mode(0o700)).unwrap(); + return; + } + + let results = [ + super::extract_paths_maybe_set_cwd(vec![root.to_owned()], &mut options).map(|_| ()), + super::extract_aggregate_inputs_maybe_set_cwd(vec![root.to_owned()], &mut options) + .map(|_| ()), + ]; + fs::set_permissions(root, fs::Permissions::from_mode(0o700)).unwrap(); + + assert_eq!(std::env::current_dir().unwrap(), cwd); + for result in results { + assert_eq!( + result.expect_err("directory change must fail").kind(), + std::io::ErrorKind::PermissionDenied + ); + } + } + + /// `set_current_dir` enforces `MAX_PATH` on Windows (os error 206) even for + /// directories that can still be traversed through verbatim paths. A sole directory + /// input like this must be expanded into absolute input paths, without changing + /// the working directory. + #[cfg(windows)] + #[test] + fn sole_directory_beyond_max_path_expands_into_absolute_inputs() { + use std::os::windows::ffi::OsStrExt as _; + + let fixture = tempfile::tempdir().unwrap(); + let mut deep = fixture.path().join("用户目录".repeat(20)); + while deep.as_os_str().encode_wide().count() < 270 { + deep = deep.join("a-long-directory-name-to-exceed-max-path"); + } + let deep = PathBuf::from(format!(r"\\?\{}", deep.display())); + fs::create_dir_all(&deep).unwrap(); + fs::write(deep.join("file.txt"), b"x").unwrap(); + + let cwd = std::env::current_dir().unwrap(); + let mut options = super::walk_options_from(&scan_args()).unwrap(); + let (paths, expanded_root) = + super::extract_paths_maybe_set_cwd(vec![deep.clone()], &mut options).unwrap(); + + // The directory could not be entered, so the working directory is unchanged. + assert_eq!(std::env::current_dir().unwrap(), cwd); + assert_eq!(expanded_root.as_deref(), Some(deep.as_path())); + assert_eq!(paths, vec![deep.join("file.txt")]); + // Traversals resolve ignore patterns against the expanded directory. + assert_eq!(options.base_dir.as_deref(), Some(deep.as_path())); + + let mut aggregate_options = super::walk_options_from(&scan_args()).unwrap(); + let super::AggregateInputs::Paths(aggregate_paths) = + super::extract_aggregate_inputs_maybe_set_cwd( + vec![deep.clone()], + &mut aggregate_options, + ) + .unwrap() + else { + panic!("aggregation must fall back to paths when the directory exceeds MAX_PATH"); + }; + assert_eq!( + aggregate_paths, paths, + "aggregation must use the same absolute inputs after error 206" + ); + assert_eq!( + std::env::current_dir().unwrap(), + cwd, + "aggregation's fallback must leave the working directory unchanged" + ); + assert_eq!( + aggregate_options.base_dir.as_deref(), + Some(deep.as_path()), + "aggregation must anchor ignore matching to the expanded root" + ); + + // Removing the deep tree only works through its verbatim path. + fs::remove_dir_all(&deep).ok(); + } } diff --git a/src/stacks.rs b/src/stacks.rs index 390206b3..e2a0f557 100644 --- a/src/stacks.rs +++ b/src/stacks.rs @@ -270,6 +270,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: TraversalOptions::default(), + base_dir: None, } } diff --git a/src/traverse.rs b/src/traverse.rs index 78815ce3..6058a7e6 100644 --- a/src/traverse.rs +++ b/src/traverse.rs @@ -1888,6 +1888,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::new(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, } } @@ -2269,6 +2270,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, vec![dir.path().to_owned()], None, @@ -2323,6 +2325,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, vec![dir.path().to_owned(), dir.path().to_owned()], None, @@ -2363,6 +2366,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, vec![dir.path().to_owned()], None, @@ -2408,6 +2412,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, vec![root_path.clone()], None, @@ -2460,6 +2465,7 @@ mod tests { skip_metadata: false, apfs_clone_metadata: deduplicate, }, + base_dir: None, }, vec![path.to_owned()], None, @@ -2510,6 +2516,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, }, vec![root.clone(), valid.clone()], None, diff --git a/src/tree.rs b/src/tree.rs index 3de18a1e..51bff30b 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -297,6 +297,7 @@ mod tests { ignore_dirs: std::collections::BTreeSet::default(), ignore_patterns: None, metadata_options: crate::TraversalOptions::default(), + base_dir: None, } }