Skip to content
Closed
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
35 changes: 35 additions & 0 deletions gix/src/dirwalk/for_each.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use gix_dir::EntryRef;

use crate::dirwalk;

/// The error returned by [`crate::Repository::dirwalk_for_each()`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
Dirwalk(#[from] dirwalk::Error),
#[error("The user-provided callback failed")]
ForEach(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
}

/// An entry provided to the `for_each` callback during a directory walk.
#[derive(Debug, Clone)]
pub struct Entry<'a> {
/// The directory entry itself.
pub entry: EntryRef<'a>,
/// `collapsed_directory_status` is `Some(dir_status)` if this entry was part of a directory with the given
/// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was
/// [gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch]. It will also be `Some(dir_status)` if that option
/// was [gix_dir::walk::CollapsedEntriesEmissionMode::All].
pub collapsed_directory_status: Option<gix_dir::entry::Status>,
}

impl<'a> Entry<'a> {
/// Create a new entry from the given components.
pub fn new(entry: EntryRef<'a>, collapsed_directory_status: Option<gix_dir::entry::Status>) -> Self {
Self {
entry,
collapsed_directory_status,
}
}
}
2 changes: 2 additions & 0 deletions gix/src/dirwalk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use crate::{config, AttributeStack, Pathspec};

mod options;

/// Closure-based API for directory walking.
pub mod for_each;
///
pub mod iter;

Expand Down
90 changes: 90 additions & 0 deletions gix/src/repository/dirwalk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,94 @@ impl Repository {
options,
)
}

/// Perform a directory walk configured with `options`, calling `for_each` for each entry. Use `patterns` to
/// further filter entries. `should_interrupt` is polled to see if an interrupt is requested, causing an
/// error to be returned instead.
///
/// This is a convenience method that wraps [`dirwalk()`](Self::dirwalk) and provides a simpler interface
/// for processing entries via a closure rather than implementing the [`gix_dir::walk::Delegate`] trait.
///
/// The `index` is used to determine if entries are tracked, and for excludes and attributes
/// lookup. Note that items will only count as tracked if they have the [`gix_index::entry::Flags::UPTODATE`]
/// flag set.
///
/// The closure receives a [`dirwalk::for_each::Entry`] for each entry in the walk and should return
/// a [`gix_dir::walk::Action`] to control traversal flow:
/// - `std::ops::ControlFlow::Continue(())` to continue the traversal
/// - `std::ops::ControlFlow::Break(())` to stop the traversal
///
/// # Example
///
/// ```no_run
/// use gix::dirwalk::for_each::Entry;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let repo = gix::discover(".")?;
/// let index = repo.index()?;
/// let options = repo.dirwalk_options()?;
///
/// repo.dirwalk_for_each(
/// &index,
/// std::iter::empty::<&str>(),
/// &Default::default(),
/// options,
/// |entry: Entry<'_>| -> Result<gix::dir::walk::Action, std::convert::Infallible> {
/// println!("{}", entry.entry.rela_path);
/// Ok(std::ops::ControlFlow::Continue(()))
/// },
/// )?;
/// # Ok(())
/// # }
/// ```
pub fn dirwalk_for_each<E>(
&self,
index: &gix_index::State,
patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
should_interrupt: &AtomicBool,
options: dirwalk::Options,
mut for_each: impl FnMut(dirwalk::for_each::Entry<'_>) -> Result<gix_dir::walk::Action, E>,
) -> Result<dirwalk::Outcome<'_>, dirwalk::for_each::Error>
where
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
struct ForEachDelegate<F, E> {
for_each: F,
error: Option<E>,
}

impl<F, E> gix_dir::walk::Delegate for ForEachDelegate<F, E>
where
F: FnMut(dirwalk::for_each::Entry<'_>) -> Result<gix_dir::walk::Action, E>,
{
fn emit(
&mut self,
entry: gix_dir::EntryRef<'_>,
collapsed_directory_status: Option<gix_dir::entry::Status>,
) -> gix_dir::walk::Action {
match (self.for_each)(dirwalk::for_each::Entry::new(entry, collapsed_directory_status)) {
Ok(action) => action,
Err(err) => {
self.error = Some(err);
std::ops::ControlFlow::Break(())
}
}
}
}

let mut delegate = ForEachDelegate {
for_each: &mut for_each,
error: None,
};

match self.dirwalk(index, patterns, should_interrupt, options, &mut delegate) {
Ok(outcome) => {
if let Some(err) = delegate.error {
Err(dirwalk::for_each::Error::ForEach(err.into()))
} else {
Ok(outcome)
}
}
Err(err) => Err(dirwalk::for_each::Error::Dirwalk(err)),
}
}
}
85 changes: 85 additions & 0 deletions gix/tests/gix/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,91 @@ mod dirwalk {
);
Ok(())
}

#[test]
fn for_each_closure() -> crate::Result {
let repo = crate::named_repo("make_basic_repo.sh")?;
let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory);
let index = repo.index()?;

let mut entries = Vec::new();
repo.dirwalk_for_each(
&index,
None::<&str>,
&AtomicBool::default(),
untracked_only,
|entry: gix::dirwalk::for_each::Entry<'_>| {
entries.push((
entry.entry.rela_path.to_string(),
entry.entry.disk_kind.expect("kind is known"),
));
Ok::<_, std::convert::Infallible>(std::ops::ControlFlow::Continue(()))
},
)?;

entries.sort_by(|a, b| a.0.cmp(&b.0));
let expected = [
("all-untracked".to_string(), Repository),
("bare-repo-with-index.git".to_string(), Directory),
("bare.git".into(), Directory),
("empty-core-excludes".into(), Repository),
("non-bare-repo-without-index".into(), Repository),
("non-bare-without-worktree".into(), Directory),
("some".into(), Directory),
("unborn".into(), Repository),
];
assert_eq!(entries, expected, "for_each works the same as delegate");
Ok(())
}

#[test]
fn for_each_with_early_break() -> crate::Result {
let repo = crate::named_repo("make_basic_repo.sh")?;
let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory);
let index = repo.index()?;

let mut count = 0;
repo.dirwalk_for_each(
&index,
None::<&str>,
&AtomicBool::default(),
untracked_only,
|_entry: gix::dirwalk::for_each::Entry<'_>| {
count += 1;
if count >= 3 {
Ok::<_, std::convert::Infallible>(std::ops::ControlFlow::Break(()))
} else {
Ok(std::ops::ControlFlow::Continue(()))
}
},
)?;

assert_eq!(count, 3, "walk stopped after 3 entries due to Break");
Ok(())
}

#[test]
fn for_each_with_error() -> crate::Result {
let repo = crate::named_repo("make_basic_repo.sh")?;
let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory);
let index = repo.index()?;

let result = repo.dirwalk_for_each(
&index,
None::<&str>,
&AtomicBool::default(),
untracked_only,
|_entry: gix::dirwalk::for_each::Entry<'_>| Err(std::io::Error::other("test error")),
);

assert!(result.is_err(), "should propagate callback errors");
let err = result.err().expect("error");
assert!(
matches!(err, gix::dirwalk::for_each::Error::ForEach(_)),
"error should be ForEach variant"
);
Ok(())
}
}

#[test]
Expand Down