Skip to content

Commit 6914d1a

Browse files
committed
feat: add Repository::dirwalk_with_delegate().
That way it's possible to perform arbitrary directory walks, useful for status, clean, and add.
1 parent d8bd45e commit 6914d1a

File tree

7 files changed

+247
-1
lines changed

7 files changed

+247
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ default = ["max-performance-safe", "comfort", "basic", "extras"]
5151
basic = ["blob-diff", "revision", "index"]
5252

5353
## Various additional features and capabilities that are not necessarily part of what most users would need.
54-
extras = ["worktree-stream", "worktree-archive", "revparse-regex", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status"]
54+
extras = ["worktree-stream", "worktree-archive", "revparse-regex", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"]
5555

5656
## Various progress-related features that improve the look of progress message units.
5757
comfort = ["gix-features/progress-unit-bytes", "gix-features/progress-unit-human-numbers"]
@@ -73,6 +73,9 @@ interrupt = ["dep:signal-hook", "gix-tempfile/signals"]
7373
## Access to `.git/index` files.
7474
index = ["dep:gix-index"]
7575

76+
## Support directory walks with Git-style annoations.
77+
dirwalk = ["dep:gix-dir"]
78+
7679
## Access to credential helpers, which provide credentials for URLs.
7780
# Note that `gix-negotiate` just piggibacks here, as 'credentials' is equivalent to 'fetch & push' right now.
7881
credentials = ["dep:gix-credentials", "dep:gix-prompt", "dep:gix-negotiate"]
@@ -251,6 +254,7 @@ gix-sec = { version = "^0.10.4", path = "../gix-sec" }
251254
gix-date = { version = "^0.8.3", path = "../gix-date" }
252255
gix-refspec = { version = "^0.22.0", path = "../gix-refspec" }
253256
gix-filter = { version = "^0.9.0", path = "../gix-filter", optional = true }
257+
gix-dir = { version = "^0.1.0", path = "../gix-dir", optional = true }
254258

255259
gix-config = { version = "^0.35.0", path = "../gix-config" }
256260
gix-odb = { version = "^0.58.0", path = "../gix-odb" }

gix/src/dirwalk.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use gix_dir::walk::{EmissionMode, ForDeletionMode};
2+
3+
/// Options for use in the [`Repository::dirwalk()`](crate::Repository::dirwalk()) function.
4+
///
5+
/// Note that all values start out disabled.
6+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
7+
pub struct Options {
8+
precompose_unicode: bool,
9+
ignore_case: bool,
10+
11+
recurse_repositories: bool,
12+
emit_pruned: bool,
13+
emit_ignored: Option<EmissionMode>,
14+
for_deletion: Option<ForDeletionMode>,
15+
emit_tracked: bool,
16+
emit_untracked: EmissionMode,
17+
emit_empty_directories: bool,
18+
classify_untracked_bare_repositories: bool,
19+
}
20+
21+
/// Construction
22+
impl Options {
23+
pub(crate) fn from_fs_caps(caps: gix_fs::Capabilities) -> Self {
24+
Self {
25+
precompose_unicode: caps.precompose_unicode,
26+
ignore_case: caps.ignore_case,
27+
recurse_repositories: false,
28+
emit_pruned: false,
29+
emit_ignored: None,
30+
for_deletion: None,
31+
emit_tracked: false,
32+
emit_untracked: Default::default(),
33+
emit_empty_directories: false,
34+
classify_untracked_bare_repositories: false,
35+
}
36+
}
37+
}
38+
39+
impl From<Options> for gix_dir::walk::Options {
40+
fn from(v: Options) -> Self {
41+
gix_dir::walk::Options {
42+
precompose_unicode: v.precompose_unicode,
43+
ignore_case: v.ignore_case,
44+
recurse_repositories: v.recurse_repositories,
45+
emit_pruned: v.emit_pruned,
46+
emit_ignored: v.emit_ignored,
47+
for_deletion: v.for_deletion,
48+
emit_tracked: v.emit_tracked,
49+
emit_untracked: v.emit_untracked,
50+
emit_empty_directories: v.emit_empty_directories,
51+
classify_untracked_bare_repositories: v.classify_untracked_bare_repositories,
52+
}
53+
}
54+
}
55+
56+
impl Options {
57+
/// If `toggle` is `true`, we will stop figuring out if any directory that is a candidate for recursion is also a nested repository,
58+
/// which saves time but leads to recurse into it. If `false`, nested repositories will not be traversed.
59+
pub fn recurse_repositories(mut self, toggle: bool) -> Self {
60+
self.recurse_repositories = toggle;
61+
self
62+
}
63+
/// If `toggle` is `true`, entries that are pruned and whose [Kind](gix_dir::entry::Kind) is known will be emitted.
64+
pub fn emit_pruned(mut self, toggle: bool) -> Self {
65+
self.emit_pruned = toggle;
66+
self
67+
}
68+
/// If `value` is `Some(mode)`, entries that are ignored will be emitted according to the given `mode`.
69+
/// If `None`, ignored entries will not be emitted at all.
70+
pub fn emit_ignored(mut self, value: Option<EmissionMode>) -> Self {
71+
self.emit_ignored = value;
72+
self
73+
}
74+
/// When the walk is for deletion, `value` must be `Some(_)` to assure we don't collapse directories that have precious files in
75+
/// them, and otherwise assure that no entries are observable that shouldn't be deleted.
76+
/// If `None`, precious files are treated like expendable files, which is usually what you want when displaying them
77+
/// for addition to the repository, and the collapse of folders can be more generous in relation to ignored files.
78+
pub fn for_deletion(mut self, value: Option<ForDeletionMode>) -> Self {
79+
self.for_deletion = value;
80+
self
81+
}
82+
/// If `toggle` is `true`, we will also emit entries for tracked items. Otherwise these will remain 'hidden',
83+
/// even if a pathspec directly refers to it.
84+
pub fn emit_tracked(mut self, toggle: bool) -> Self {
85+
self.emit_tracked = toggle;
86+
self
87+
}
88+
/// Controls the way untracked files are emitted. By default, this is happening immediately and without any simplification.
89+
pub fn emit_untracked(mut self, toggle: EmissionMode) -> Self {
90+
self.emit_untracked = toggle;
91+
self
92+
}
93+
/// If `toggle` is `true`, emit empty directories as well. Note that a directory also counts as empty if it has any
94+
/// amount or depth of nested subdirectories, as long as none of them includes a file.
95+
/// Thus, this makes leaf-level empty directories visible, as those don't have any content.
96+
pub fn emit_empty_directories(mut self, toggle: bool) -> Self {
97+
self.emit_empty_directories = toggle;
98+
self
99+
}
100+
101+
/// If `toggle` is `true`, we will not only find non-bare repositories in untracked directories, but also bare ones.
102+
///
103+
/// Note that this is very costly, but without it, bare repositories will appear like untracked directories when collapsed,
104+
/// and they will be recursed into.
105+
pub fn classify_untracked_bare_repositories(mut self, toggle: bool) -> Self {
106+
self.classify_untracked_bare_repositories = toggle;
107+
self
108+
}
109+
}

gix/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ pub use gix_commitgraph as commitgraph;
101101
#[cfg(feature = "credentials")]
102102
pub use gix_credentials as credentials;
103103
pub use gix_date as date;
104+
#[cfg(feature = "dirwalk")]
105+
pub use gix_dir as dir;
104106
pub use gix_features as features;
105107
use gix_features::threading::OwnShared;
106108
pub use gix_features::{
@@ -174,6 +176,9 @@ pub use types::{Pathspec, PathspecDetached, Submodule};
174176
///
175177
pub mod clone;
176178
pub mod commit;
179+
#[cfg(feature = "dirwalk")]
180+
///
181+
pub mod dirwalk;
177182
pub mod head;
178183
pub mod id;
179184
pub mod object;

gix/src/repository/dirwalk.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use crate::bstr::BStr;
2+
use crate::{config, dirwalk, Repository};
3+
use std::path::Path;
4+
5+
/// The error returned by [dirwalk()](Repository::dirwalk()).
6+
#[derive(Debug, thiserror::Error)]
7+
#[allow(missing_docs)]
8+
pub enum Error {
9+
#[error(transparent)]
10+
Walk(#[from] gix_dir::walk::Error),
11+
#[error("A working tree is required to perform a directory walk")]
12+
MissinWorkDir,
13+
#[error(transparent)]
14+
Excludes(#[from] config::exclude_stack::Error),
15+
#[error(transparent)]
16+
Pathspec(#[from] crate::pathspec::init::Error),
17+
#[error(transparent)]
18+
Prefix(#[from] gix_path::realpath::Error),
19+
#[error(transparent)]
20+
FilesystemOptions(#[from] config::boolean::Error),
21+
}
22+
23+
impl Repository {
24+
/// Return default options suitable for performing a directory walk on this repository.
25+
///
26+
/// Used in conjunction with [`dirwalk()`](Self::dirwalk())
27+
pub fn dirwalk_options(&self) -> Result<dirwalk::Options, config::boolean::Error> {
28+
Ok(dirwalk::Options::from_fs_caps(self.filesystem_options()?))
29+
}
30+
31+
/// Perform a directory walk configured with `options` under control of the `delegate`. Use `patterns` to
32+
/// further filter entries.
33+
///
34+
/// The `index` is used to determine if entries are tracked, and for excludes and attributes
35+
/// lookup. Note that items will only count as tracked if they have the [`gix_index::entry::Flags::UPTODATE`]
36+
/// flag set.
37+
///
38+
/// See [`gix_dir::walk::delegate::Collect`] for a delegate that collects all seen entries.
39+
pub fn dirwalk(
40+
&self,
41+
index: &gix_index::State,
42+
patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
43+
options: dirwalk::Options,
44+
delegate: &mut dyn gix_dir::walk::Delegate,
45+
) -> Result<gix_dir::walk::Outcome, Error> {
46+
let workdir = self.work_dir().ok_or(Error::MissinWorkDir)?;
47+
let mut excludes = self
48+
.excludes(
49+
index,
50+
None,
51+
crate::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
52+
)?
53+
.detach();
54+
let (mut pathspec, mut maybe_attributes) = self
55+
.pathspec(
56+
patterns,
57+
true, /* inherit ignore case */
58+
index,
59+
crate::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
60+
)?
61+
.into_parts();
62+
63+
let prefix = self.prefix()?.unwrap_or(Path::new(""));
64+
let git_dir_realpath =
65+
crate::path::realpath_opts(self.git_dir(), self.current_dir(), crate::path::realpath::MAX_SYMLINKS)?;
66+
let fs_caps = self.filesystem_options()?;
67+
let accelerate_lookup = fs_caps.ignore_case.then(|| index.prepare_icase_backing());
68+
gix_dir::walk(
69+
&workdir.join(prefix),
70+
workdir,
71+
gix_dir::walk::Context {
72+
git_dir_realpath: git_dir_realpath.as_ref(),
73+
current_dir: self.current_dir(),
74+
index,
75+
ignore_case_index_lookup: accelerate_lookup.as_ref(),
76+
pathspec: &mut pathspec,
77+
pathspec_attributes: &mut |relative_path, case, is_dir, out| {
78+
let stack = maybe_attributes
79+
.as_mut()
80+
.expect("can only be called if attributes are used in patterns");
81+
stack
82+
.set_case(case)
83+
.at_entry(relative_path, Some(is_dir), &self.objects)
84+
.map_or(false, |platform| platform.matching_attributes(out))
85+
},
86+
excludes: Some(&mut excludes),
87+
objects: &self.objects,
88+
},
89+
options.into(),
90+
delegate,
91+
)
92+
.map_err(Into::into)
93+
}
94+
}

gix/src/repository/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ mod config;
4343
#[cfg(feature = "blob-diff")]
4444
pub mod diff;
4545
///
46+
#[cfg(feature = "dirwalk")]
47+
pub mod dirwalk;
48+
///
4649
#[cfg(feature = "attributes")]
4750
pub mod filter;
4851
mod graph;

gix/tests/repository/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,36 @@ mod state;
1515
mod submodule;
1616
mod worktree;
1717

18+
#[cfg(feature = "dirwalk")]
19+
mod dirwalk {
20+
use gix_dir::entry::Kind::*;
21+
use gix_dir::walk::EmissionMode;
22+
23+
#[test]
24+
fn basics() -> crate::Result {
25+
let repo = crate::named_repo("make_basic_repo.sh")?;
26+
let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory);
27+
let mut collect = gix::dir::walk::delegate::Collect::default();
28+
let index = repo.index()?;
29+
repo.dirwalk(&index, None::<&str>, untracked_only, &mut collect)?;
30+
assert_eq!(
31+
collect
32+
.into_entries_by_path()
33+
.into_iter()
34+
.map(|e| (e.0.rela_path.to_string(), e.0.disk_kind.expect("kind is known")))
35+
.collect::<Vec<_>>(),
36+
[
37+
("bare-repo-with-index.git".to_string(), Directory),
38+
("bare.git".into(), Directory),
39+
("non-bare-repo-without-index".into(), Repository),
40+
("some".into(), Directory)
41+
],
42+
"note how bare repos are just directories by default"
43+
);
44+
Ok(())
45+
}
46+
}
47+
1848
#[test]
1949
fn size_in_memory() {
2050
let actual_size = std::mem::size_of::<Repository>();

0 commit comments

Comments
 (0)