Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -919,6 +921,7 @@ mod tests {
skip_metadata: false,
apfs_clone_metadata: true,
},
base_dir: None,
},
true,
true,
Expand Down Expand Up @@ -960,6 +963,7 @@ mod tests {
skip_metadata: false,
apfs_clone_metadata: true,
},
base_dir: None,
},
true,
true,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ mod tests {
ignore_dirs: BTreeSet::new(),
ignore_patterns: None,
metadata_options: crate::TraversalOptions::default(),
base_dir: None,
}
}

Expand Down
68 changes: 67 additions & 1 deletion src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IgnorePatterns>,
/// 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<PathBuf>,
/// Platform-specific metadata requested during traversal.
pub metadata_options: crate::TraversalOptions,
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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![
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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::<Vec<_>>();

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.
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/interactive/app/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
89 changes: 89 additions & 0 deletions src/interactive/app/tests/journeys_with_writes.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::interactive::app::tests::utils::{
WritableFixture, initialized_app_and_terminal_from_paths, into_codes, into_events,

Check warning on line 2 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `WritableFixture`

Check warning on line 2 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `WritableFixture`
new_test_terminal,
};
use crate::interactive::terminal::TerminalApp;
use crate::interactive::widgets::Language;

Check warning on line 6 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `crate::interactive::widgets::Language`

Check warning on line 6 in src/interactive/app/tests/journeys_with_writes.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

unused import: `crate::interactive::widgets::Language`
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use dua::{ByteFormat, Config, WalkOptions};
Expand All @@ -29,6 +29,93 @@
.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::<Vec<_>>();
for path in &input_paths {
fs::write(path, b"existing")?;
}
let mut expected_paths = input_paths.iter().cloned().collect::<BTreeSet<_>>();
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::<BTreeSet<_>>(),
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()?;
Expand Down Expand Up @@ -235,6 +322,7 @@
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(
Expand Down Expand Up @@ -377,6 +465,7 @@
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(
Expand Down
1 change: 1 addition & 0 deletions src/interactive/app/tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/interactive/app/tests/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
index_by_name_and_size(app, name, None)
}

pub struct WritableFixture {

Check warning on line 77 in src/interactive/app/tests/utils.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

struct `WritableFixture` is never constructed
pub root: PathBuf,
}

Expand All @@ -84,7 +84,7 @@
}
}

fn delete_recursive(path: impl AsRef<Path>) -> Result<()> {

Check warning on line 87 in src/interactive/app/tests/utils.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

function `delete_recursive` is never used

Check warning on line 87 in src/interactive/app/tests/utils.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

function `delete_recursive` is never used
let mut files: Vec<_> = Vec::new();
let mut dirs: Vec<_> = Vec::new();

Expand Down Expand Up @@ -118,7 +118,7 @@
.collect::<Result<_, _>>()
}

fn copy_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> {

Check warning on line 121 in src/interactive/app/tests/utils.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

function `copy_recursive` is never used

Check warning on line 121 in src/interactive/app/tests/utils.rs

View workflow job for this annotation

GitHub Actions / Windows (nightly)

function `copy_recursive` is never used
for entry in dua_core::walk(
src.as_ref(),
1,
Expand Down Expand Up @@ -205,6 +205,7 @@
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();
Expand Down
Loading
Loading