-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
refactor(walk): extract entry filtering and normalization logic #2043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
5022954
2849149
6b68d13
6ead00f
03fd037
cd161f4
7d778c7
a79e718
a191c8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -302,6 +302,238 @@ impl<'a, W: Write> ReceiverBuffer<'a, W> { | |
| } | ||
| } | ||
|
|
||
| struct EntryFilter<'a> { | ||
| config: &'a Config, | ||
| patterns: &'a [Regex], | ||
| } | ||
|
|
||
| impl<'a> EntryFilter<'a> { | ||
| pub fn new(config: &'a Config, patterns: &'a [Regex]) -> Self { | ||
| Self { config, patterns } | ||
| } | ||
|
|
||
| /// Fast-path checks that operate on the raw ignore::DirEntry | ||
| fn evaluate_before_normalization(&self, entry: &ignore::DirEntry) -> Option<WalkState> { | ||
| if let Some(state) = self.check_ignore_file(entry) { | ||
| return Some(state); | ||
| } | ||
| if let Some(state) = self.check_root_dir(entry) { | ||
| return Some(state); | ||
| } | ||
| None | ||
| } | ||
|
|
||
| /// Evaluates a normalized entry against all configured constraints to determine its walk state. | ||
| fn evaluate(&self, entry: &DirEntry) -> Option<WalkState> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it would be worth using a macro like macro_rules! entry_filter {
($e:expr) => {
let result = $e;
if result.is_some() {
return result;
}
}
}Then each of these checks becomes Or alternatively, have this return self.check_min_depth(entry)?;
self.check_patterns(entry)?;
//...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Internal functions now return a if self.is_below_min_depth(entry)
|| self.fails_pattern_filter(entry)
|| self.fails_extension_filter(entry)
|| self.matches_ignored_file_type(entry)
|| fails_owner_constraints
|| self.fails_size_constraints(entry)
|| self.fails_modification_time_constraints(entry)
{
return Err(WalkState::Continue);
} |
||
| if let Some(state) = self.check_min_depth(entry) { | ||
| return Some(state); | ||
| } | ||
| if let Some(state) = self.check_patterns(entry) { | ||
| return Some(state); | ||
| } | ||
| if let Some(state) = self.check_extensions(entry) { | ||
| return Some(state); | ||
| } | ||
| if let Some(state) = self.check_file_types(entry) { | ||
| return Some(state); | ||
| } | ||
| if let Some(state) = self.check_owner(entry) { | ||
| return Some(state); | ||
| } | ||
| if let Some(state) = self.check_size(entry) { | ||
| return Some(state); | ||
| } | ||
| if let Some(state) = self.check_modification_time(entry) { | ||
| return Some(state); | ||
| } | ||
| None | ||
| } | ||
|
|
||
| fn check_ignore_file(&self, entry: &ignore::DirEntry) -> Option<WalkState> { | ||
|
parneetsingh022 marked this conversation as resolved.
Outdated
|
||
| // If the entry is a directory that contains a | ||
| // "ignore contain" file, we want to skip this | ||
| // directory. | ||
| // Check the filetype first to avoid unnecessary | ||
| // syscalls. | ||
| if entry.file_type().is_some_and(|t| t.is_dir()) { | ||
| let entry_path = entry.path(); | ||
| if self | ||
| .config | ||
| .ignore_contain | ||
| .iter() | ||
| .any(|ic| entry_path.join(ic).exists()) | ||
| { | ||
| return Some(WalkState::Skip); | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn check_root_dir(&self, entry: &ignore::DirEntry) -> Option<WalkState> { | ||
|
parneetsingh022 marked this conversation as resolved.
Outdated
|
||
| if entry.depth() == 0 { | ||
| // Skip the root directory entry. | ||
| return Some(WalkState::Continue); | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn check_min_depth(&self, entry: &DirEntry) -> Option<WalkState> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of these functions are actually just checking a condition, and return WalkState::Continue. I wonder if i instead of these returning Option, it woudl be simpler to just have them return
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you are right. There is no point in returning |
||
| let min_depth = self.config.min_depth?; | ||
|
|
||
| if entry.depth().is_none_or(|depth| depth < min_depth) { | ||
| return Some(WalkState::Continue); | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn check_patterns(&self, entry: &DirEntry) -> Option<WalkState> { | ||
| let entry_path = entry.path(); | ||
|
|
||
| let search_str = search_str_for_entry(entry_path, self.config.full_path_base.as_deref()); | ||
|
|
||
| if !self | ||
| .patterns | ||
| .iter() | ||
| .all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref()))) | ||
| { | ||
| return Some(WalkState::Continue); | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn check_extensions(&self, entry: &DirEntry) -> Option<WalkState> { | ||
|
parneetsingh022 marked this conversation as resolved.
Outdated
|
||
| let entry_path = entry.path(); | ||
| if let Some(ref exts_regex) = self.config.extensions { | ||
| if let Some(path_str) = entry_path.file_name() { | ||
| if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } else { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } | ||
| None | ||
| } | ||
|
|
||
| fn check_file_types(&self, entry: &DirEntry) -> Option<WalkState> { | ||
| if let Some(ref file_types) = self.config.file_types | ||
| && file_types.should_ignore(entry) | ||
| { | ||
| return Some(WalkState::Continue); | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn check_size(&self, entry: &DirEntry) -> Option<WalkState> { | ||
| let entry_path = entry.path(); | ||
| if !self.config.size_constraints.is_empty() { | ||
| if entry_path.is_file() { | ||
| if let Some(metadata) = entry.metadata() { | ||
| let file_size = metadata.len(); | ||
| if self | ||
| .config | ||
| .size_constraints | ||
| .iter() | ||
| .any(|sc| !sc.is_within(file_size)) | ||
| { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } else { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } else { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn check_modification_time(&self, entry: &DirEntry) -> Option<WalkState> { | ||
| if !self.config.time_constraints.is_empty() { | ||
| let mut matched = false; | ||
| if let Some(metadata) = entry.metadata() | ||
| && let Ok(modified) = metadata.modified() | ||
| { | ||
| matched = self | ||
| .config | ||
| .time_constraints | ||
| .iter() | ||
| .all(|tf| tf.applies_to(&modified)); | ||
| } | ||
| if !matched { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| fn check_owner(&self, entry: &DirEntry) -> Option<WalkState> { | ||
|
parneetsingh022 marked this conversation as resolved.
Outdated
|
||
| if let Some(ref owner_constraint) = self.config.owner_constraint { | ||
| if let Some(metadata) = entry.metadata() { | ||
| if !owner_constraint.matches(metadata) { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } else { | ||
| return Some(WalkState::Continue); | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| #[cfg(not(unix))] | ||
| #[inline] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would hope this inline annotation isn't necessary
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but it might be better to just not have this defined for non-unix, and have a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, that makes sense. I removed the extra non-Unix helper implementation. Instead, the owner constraint check is now only called on #[cfg(unix)]
let fails_owner_constraints = self.fails_owner_constraints(entry);
#[cfg(not(unix))]
let fails_owner_constraints = false; |
||
| fn check_owner(&self, _entry: &DirEntry) -> Option<WalkState> { | ||
| None | ||
| } | ||
| } | ||
|
|
||
| /// Converts an `ignore` walker entry into a `DirEntry`. | ||
| /// | ||
| /// Normal entries are wrapped directly. Broken symlinks are recovered from | ||
| /// `NotFound` errors and returned as `DirEntry::broken_symlink`. All other | ||
| /// errors are forwarded to the worker result channel, returning `Continue` if | ||
| /// the error was sent and `Quit` if the channel is closed. | ||
| fn normalize_walk_entry( | ||
| entry: Result<ignore::DirEntry, ignore::Error>, | ||
| tx: &mut BatchSender, | ||
| ) -> Result<DirEntry, WalkState> { | ||
| match entry { | ||
| Ok(e) => Ok(DirEntry::normal(e)), | ||
|
|
||
| Err(ignore::Error::WithPath { | ||
| path, | ||
| err: inner_err, | ||
| }) if inner_err | ||
| .io_error() | ||
| .is_some_and(|io_error| io_error.kind() == io::ErrorKind::NotFound) | ||
| && path | ||
| .symlink_metadata() | ||
| .ok() | ||
| .is_some_and(|m| m.file_type().is_symlink()) => | ||
| { | ||
| Ok(DirEntry::broken_symlink(path)) | ||
| } | ||
|
|
||
| Err(err) => { | ||
| let result = tx.send(WorkerResult::Error(err)); | ||
|
|
||
| match result { | ||
| Ok(_) => Err(WalkState::Continue), | ||
| Err(_) => Err(WalkState::Quit), | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// State shared by the sender and receiver threads. | ||
| struct WorkerState { | ||
| /// The search patterns. | ||
|
|
@@ -445,6 +677,7 @@ impl WorkerState { | |
| let patterns = &self.patterns; | ||
| let config = &self.config; | ||
| let quit_flag = self.quit_flag.as_ref(); | ||
| let filter = EntryFilter::new(config, patterns); | ||
|
|
||
| let mut limit = 0x100; | ||
| if let Some(cmd) = &config.command | ||
|
|
@@ -461,133 +694,19 @@ impl WorkerState { | |
| return WalkState::Quit; | ||
| } | ||
|
|
||
| if let Ok(e) = &entry { | ||
| // If the entry is a directory that contains a | ||
| // "ignore contain" file", we want to skip this | ||
| // directory. | ||
| // Check the filetype first to avoid unnecessary | ||
| // syscalls. | ||
| if e.file_type().is_some_and(|t| t.is_dir()) { | ||
| let entry_path = e.path(); | ||
| if config | ||
| .ignore_contain | ||
| .iter() | ||
| .any(|ic| entry_path.join(ic).exists()) | ||
| { | ||
| return WalkState::Skip; | ||
| } | ||
| } | ||
| if e.depth() == 0 { | ||
| // Skip the root directory entry. | ||
| return WalkState::Continue; | ||
| } | ||
| } | ||
| let entry = match entry { | ||
| Ok(e) => DirEntry::normal(e), | ||
| Err(ignore::Error::WithPath { | ||
| path, | ||
| err: inner_err, | ||
| }) if inner_err | ||
| .io_error() | ||
| .is_some_and(|io_error| io_error.kind() == io::ErrorKind::NotFound) | ||
| && path | ||
| .symlink_metadata() | ||
| .ok() | ||
| .is_some_and(|m| m.file_type().is_symlink()) => | ||
| { | ||
| DirEntry::broken_symlink(path) | ||
| } | ||
| Err(err) => { | ||
| return match tx.send(WorkerResult::Error(err)) { | ||
| Ok(_) => WalkState::Continue, | ||
| Err(_) => WalkState::Quit, | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| if let Some(min_depth) = config.min_depth | ||
| && entry.depth().is_none_or(|d| d < min_depth) | ||
| { | ||
| return WalkState::Continue; | ||
| } | ||
|
|
||
| // Check the name first, since it doesn't require metadata | ||
| let entry_path = entry.path(); | ||
|
|
||
| let search_str = search_str_for_entry(entry_path, config.full_path_base.as_deref()); | ||
|
|
||
| if !patterns | ||
| .iter() | ||
| .all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref()))) | ||
| { | ||
| return WalkState::Continue; | ||
| } | ||
|
|
||
| // Filter out unwanted extensions. | ||
| if let Some(ref exts_regex) = config.extensions { | ||
| if let Some(path_str) = entry_path.file_name() { | ||
| if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) { | ||
| return WalkState::Continue; | ||
| } | ||
| } else { | ||
| return WalkState::Continue; | ||
| } | ||
| } | ||
|
|
||
| // Filter out unwanted file types. | ||
| if let Some(ref file_types) = config.file_types | ||
| && file_types.should_ignore(&entry) | ||
| if let Ok(e) = &entry | ||
| && let Some(state) = filter.evaluate_before_normalization(e) | ||
| { | ||
| return WalkState::Continue; | ||
| return state; | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| { | ||
| if let Some(ref owner_constraint) = config.owner_constraint { | ||
| if let Some(metadata) = entry.metadata() { | ||
| if !owner_constraint.matches(metadata) { | ||
| return WalkState::Continue; | ||
| } | ||
| } else { | ||
| return WalkState::Continue; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Filter out unwanted sizes if it is a file and we have been given size constraints. | ||
| if !config.size_constraints.is_empty() { | ||
| if entry_path.is_file() { | ||
| if let Some(metadata) = entry.metadata() { | ||
| let file_size = metadata.len(); | ||
| if config | ||
| .size_constraints | ||
| .iter() | ||
| .any(|sc| !sc.is_within(file_size)) | ||
| { | ||
| return WalkState::Continue; | ||
| } | ||
| } else { | ||
| return WalkState::Continue; | ||
| } | ||
| } else { | ||
| return WalkState::Continue; | ||
| } | ||
| } | ||
| let entry = match normalize_walk_entry(entry, &mut tx) { | ||
| Ok(entry) => entry, | ||
| Err(state) => return state, | ||
| }; | ||
|
|
||
| // Filter out unwanted modification times | ||
| if !config.time_constraints.is_empty() { | ||
| let mut matched = false; | ||
| if let Some(metadata) = entry.metadata() | ||
| && let Ok(modified) = metadata.modified() | ||
| { | ||
| matched = config | ||
| .time_constraints | ||
| .iter() | ||
| .all(|tf| tf.applies_to(&modified)); | ||
| } | ||
| if !matched { | ||
| return WalkState::Continue; | ||
| } | ||
| if let Some(state) = filter.evaluate(&entry) { | ||
| return state; | ||
| } | ||
|
|
||
| if config.is_printing() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.