Skip to content

Commit

Permalink
feat!: allow directory walk to be interrupted with should_interrupt
Browse files Browse the repository at this point in the history
… flag.

That way, it can be much more responsive to interruption.
  • Loading branch information
Byron committed Mar 17, 2024
1 parent e7e91cf commit 35b74e7
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 1 deletion.
7 changes: 7 additions & 0 deletions gix-dir/src/walk/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{entry, EntryRef};
use bstr::BStr;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;

/// A type returned by the [`Delegate::emit()`] as passed to [`walk()`](function::walk()).
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
Expand Down Expand Up @@ -195,6 +196,10 @@ pub struct Options {

/// All information that is required to perform a dirwalk, and classify paths properly.
pub struct Context<'a> {
/// If not `None`, it will be checked before entering any directory to trigger early interruption.
///
/// If this flag is `true` at any point in the iteration, it will abort with an error.
pub should_interrupt: Option<&'a AtomicBool>,
/// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`].
///
/// It's used to help us differentiate our own `.git` directory from nested unrelated repositories,
Expand Down Expand Up @@ -269,6 +274,8 @@ pub struct Outcome {
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Interrupted")]
Interrupted,
#[error("Worktree root at '{}' is not a directory", root.display())]
WorktreeRootIsFile { root: PathBuf },
#[error("Traversal root '{}' contains relative path components and could not be normalized", root.display())]
Expand Down
4 changes: 4 additions & 0 deletions gix-dir/src/walk/readdir.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bstr::{BStr, BString, ByteSlice};
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;

use crate::entry::{PathspecMatch, Status};
use crate::walk::function::{can_recurse, emit_entry};
Expand All @@ -23,6 +24,9 @@ pub(super) fn recursive(
out: &mut Outcome,
state: &mut State,
) -> Result<(Action, bool), Error> {
if ctx.should_interrupt.map_or(false, |flag| flag.load(Ordering::Relaxed)) {
return Err(Error::Interrupted);
}
out.read_dir_calls += 1;
let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir {
path: current.to_owned(),
Expand Down
19 changes: 19 additions & 0 deletions gix-dir/tests/walk/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use gix_dir::{walk, EntryRef};
use pretty_assertions::assert_eq;
use std::sync::atomic::AtomicBool;

use crate::walk_utils::{
collect, collect_filtered, collect_filtered_with_cwd, entry, entry_dirstat, entry_nokind, entry_nomatch, entryps,
Expand Down Expand Up @@ -151,6 +152,24 @@ fn root_may_be_a_symlink_if_it_is_the_worktree() -> crate::Result {
Ok(())
}

#[test]
fn should_interrupt_works_even_in_empty_directories() {
let root = fixture("empty");
let should_interrupt = AtomicBool::new(true);
let err = try_collect_filtered_opts_collect(
&root,
None,
|keep, ctx| walk(&root, ctx, gix_dir::walk::Options { ..options() }, keep),
None::<&str>,
Options {
should_interrupt: Some(&should_interrupt),
..Default::default()
},
)
.unwrap_err();
assert!(matches!(err, gix_dir::walk::Error::Interrupted));
}

#[test]
fn empty_root() -> crate::Result {
let root = fixture("empty");
Expand Down
10 changes: 9 additions & 1 deletion gix-dir/tests/walk_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use bstr::BStr;
use gix_dir::{entry, walk, Entry};
use gix_testtools::scripted_fixture_read_only;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;

pub fn fixture_in(filename: &str, name: &str) -> PathBuf {
let root = scripted_fixture_read_only(format!("{filename}.sh")).expect("script works");
Expand Down Expand Up @@ -269,7 +270,11 @@ pub fn try_collect_filtered_opts(
cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
delegate: &mut dyn gix_dir::walk::Delegate,
Options { fresh_index, git_dir }: Options<'_>,
Options {
fresh_index,
git_dir,
should_interrupt,
}: Options<'_>,
) -> Result<(walk::Outcome, PathBuf), walk::Error> {
let git_dir = worktree_root.join(git_dir.unwrap_or(".git"));
let mut index = std::fs::read(git_dir.join("index")).ok().map_or_else(
Expand Down Expand Up @@ -343,13 +348,15 @@ pub fn try_collect_filtered_opts(
excludes: Some(&mut stack),
objects: &gix_object::find::Never,
explicit_traversal_root,
should_interrupt,
},
)
}

pub struct Options<'a> {
pub fresh_index: bool,
pub git_dir: Option<&'a str>,
pub should_interrupt: Option<&'a AtomicBool>,
}

impl<'a> Options<'a> {
Expand All @@ -366,6 +373,7 @@ impl<'a> Default for Options<'a> {
Options {
fresh_index: true,
git_dir: None,
should_interrupt: None,
}
}
}
Expand Down

0 comments on commit 35b74e7

Please sign in to comment.