History

Merge branch 'improvements-for-cargo'

Sebastian Thiel committed 2 years ago

Commit 41cd53e2 parents 4ccf39b ab2108331 files · +821 −151Browse files

Unified · Split

31 files
  1. gitoxide-core/src/repository/clean.rs +1 −0
  2. gix-actor/src/signature/decode.rs +7 −2
  3. gix-actor/tests/signature/mod.rs +26 −1
  4. gix-dir/src/walk/classify.rs +11 −1
  5. gix-dir/src/walk/function.rs +13 −5
  6. gix-dir/src/walk/mod.rs +15 −0
  7. gix-dir/src/walk/readdir.rs +4 −0
  8. gix-dir/tests/fixtures/many-symlinks.sh +26 −0
  9. gix-dir/tests/walk/mod.rs +127 −0
  10. gix-dir/tests/walk_utils/mod.rs +10 −1
  11. gix-index/src/entry/mod.rs +0 −3
  12. gix-object/tests/commit/from_bytes.rs +27 −0
  13. gix-object/tests/fixtures/commit/invalid-timestamp.txt +6 −0
  14. gix-pathspec/src/search/matching.rs +4 −4
  15. gix-pathspec/tests/search/mod.rs +49 −2
  16. gix-status/src/index_as_worktree_with_renames/mod.rs +1 −0
  17. gix/Cargo.toml +1 −1
  18. gix/src/dirwalk/iter.rs +188 −0
  19. gix/src/dirwalk/mod.rs +91 −0
  20. gix/src/dirwalk/options.rs +23 −20
  21. gix/src/lib.rs +3 −1
  22. gix/src/repository/dirwalk.rs +33 −38
  23. gix/src/repository/mod.rs +1 −1
  24. gix/src/status/index_worktree.rs +9 −23
  25. gix/src/status/mod.rs +2 −34
  26. gix/src/status/platform.rs +3 −3
  27. gix/src/util.rs +81 −0
  28. gix/src/worktree/mod.rs +15 −1
  29. gix/tests/repository/mod.rs +35 −8
  30. gix/tests/repository/open.rs +8 −2
  31. justfile +1 −0
gitoxide-core/src/repository/clean.rs+1−0
@@ -86,6 +86,7 @@ pub(crate) mod function { · 85 unmodified lines
8686 } else {
8787 Vec::new()
8888 },
89+ &gix::interrupt::IS_INTERRUPTED,
8990 options,
9091 &mut collect,
9192 )?;
gix-actor/src/signature/decode.rs+7−2
@@ -36,9 +36,14 @@ pub(crate) mod function { · 35 unmodified lines
3636 take_while(1..=2, AsChar::is_dec_digit)
3737 .verify_map(|v| to_signed::<OffsetInSeconds>(v).ok())
3838 .context(StrContext::Expected("MM".into())),
39+ take_while(0.., AsChar::is_dec_digit).map(|v: &[u8]| v),
3940 )
40− .map(|(time, sign, hours, minutes)| {
41− let offset = (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 };
41+ .map(|(time, sign, hours, minutes, trailing_digits)| {
42+ let offset = if trailing_digits.is_empty() {
43+ (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 }
44+ } else {
45+ 0
46+ };
4247 Time {
4348 seconds: time,
4449 offset,
gix-actor/tests/signature/mod.rs+26−1
@@ -53,7 +53,7 @@ mod write_to { · 52 unmodified lines
5353 }
5454
5555 use bstr::ByteSlice;
56−use gix_actor::Signature;
56+use gix_actor::{Signature, SignatureRef};
5757
5858 #[test]
5959 fn trim() {
@@ -80,3 +80,28 @@ fn round_trip() -> Result<(), Box<dyn std::error::Error>> { · 20 unmodified lines
8080 }
8181 Ok(())
8282 }
83+
84+#[test]
85+fn parse_timestamp_with_trailing_digits() {
86+ let signature = gix_actor::SignatureRef::from_bytes::<()>(b"first last <name@example.com> 1312735823 +051800")
87+ .expect("deal with trailing zeroes in timestamp by discarding it");
88+ assert_eq!(
89+ signature,
90+ SignatureRef {
91+ name: "first last".into(),
92+ email: "name@example.com".into(),
93+ time: gix_actor::date::Time::new(1312735823, 0),
94+ }
95+ );
96+
97+ let signature = gix_actor::SignatureRef::from_bytes::<()>(b"first last <name@example.com> 1312735823 +0518")
98+ .expect("this naturally works as the timestamp does not have trailing zeroes");
99+ assert_eq!(
100+ signature,
101+ SignatureRef {
102+ name: "first last".into(),
103+ email: "name@example.com".into(),
104+ time: gix_actor::date::Time::new(1312735823, 19080),
105+ }
106+ );
107+}
gix-dir/src/walk/classify.rs+11−1
@@ -134,6 +134,7 @@ pub fn path( · 133 unmodified lines
134134 emit_ignored,
135135 for_deletion,
136136 classify_untracked_bare_repositories,
137+ symlinks_to_directories_are_ignored_like_directories,
137138 ..
138139 }: Options,
139140 ctx: &mut Context<'_>,
@@ -199,6 +200,15 @@ pub fn path( · 59 unmodified lines
199200 .pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
200201 .map(Into::into);
201202
203+ let is_dir = if symlinks_to_directories_are_ignored_like_directories
204+ && ctx.excludes.is_some()
205+ && kind.map_or(false, |ft| ft == entry::Kind::Symlink)
206+ {
207+ path.metadata().ok().map(|md| md.is_dir()).or(Some(false))
208+ } else {
209+ kind.map(|ft| ft.is_dir())
210+ };
211+
202212 let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {
203213 if recurse_repositories {
204214 return current_kind;
@@ -245,7 +255,7 @@ pub fn path( · 40 unmodified lines
245255 .as_mut()
246256 .map_or(Ok(None), |stack| {
247257 stack
248− .at_entry(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.objects)
258+ .at_entry(rela_path.as_bstr(), is_dir, ctx.objects)
249259 .map(|platform| platform.excluded_kind())
250260 })
251261 .map_err(Error::ExcludesAccess)?
gix-dir/src/walk/function.rs+13−5
@@ -69,9 +69,16 @@ pub fn walk( · 68 unmodified lines
6969 )?;
7070 if !can_recurse(
7171 buf.as_bstr(),
72− root_info,
72+ if root == worktree_root && root_info.disk_kind == Some(entry::Kind::Symlink) && current.is_dir() {
73+ classify::Outcome {
74+ disk_kind: Some(entry::Kind::Directory),
75+ ..root_info
76+ }
77+ } else {
78+ root_info
79+ },
7380 options.for_deletion,
74− worktree_root_is_repository, /* is root */
81+ worktree_root_is_repository,
7582 delegate,
7683 ) {
7784 if buf.is_empty() && !root_info.disk_kind.map_or(false, |kind| kind.is_dir()) {
@@ -147,16 +154,17 @@ pub(super) fn can_recurse( · 69 unmodified lines
147154 rela_path: &BStr,
148155 info: classify::Outcome,
149156 for_deletion: Option<ForDeletionMode>,
150− is_root: bool,
157+ worktree_root_is_repository: bool,
151158 delegate: &mut dyn Delegate,
152159 ) -> bool {
153− if info.disk_kind.map_or(true, |k| !k.is_dir()) {
160+ let is_dir = info.disk_kind.map_or(false, |k| k.is_dir());
161+ if !is_dir {
154162 return false;
155163 }
156164 delegate.can_recurse(
157165 EntryRef::from_outcome(Cow::Borrowed(rela_path), info),
158166 for_deletion,
159− is_root,
167+ worktree_root_is_repository,
160168 )
161169 }
162170
gix-dir/src/walk/mod.rs+15−0
@@ -1,6 +1,7 @@
11 use crate::{entry, EntryRef};
22 use bstr::BStr;
33 use std::path::PathBuf;
4+use std::sync::atomic::AtomicBool;
45
56 /// A type returned by the [`Delegate::emit()`] as passed to [`walk()`](function::walk()).
67 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -183,10 +184,22 @@ pub struct Options { · 176 unmodified lines
183184 pub emit_empty_directories: bool,
184185 /// If `None`, no entries inside of collapsed directories are emitted. Otherwise, act as specified by `Some(mode)`.
185186 pub emit_collapsed: Option<CollapsedEntriesEmissionMode>,
187+ /// This is a `libgit2` compatibility flag, and if enabled, symlinks that point to directories will be considered a directory
188+ /// when checking for exclusion.
189+ ///
190+ /// This is relevant if `src2` points to `src`, and is excluded with `src2/`. If `false`, `src2` will not be excluded,
191+ /// if `true` it will be excluded as the symlink is considered a directory.
192+ ///
193+ /// In other words, for Git compatibility this flag should be `false`, the default, for `git2` compatibility it should be `true`.
194+ pub symlinks_to_directories_are_ignored_like_directories: bool,
186195 }
187196
188197 /// All information that is required to perform a dirwalk, and classify paths properly.
189198 pub struct Context<'a> {
199+ /// If not `None`, it will be checked before entering any directory to trigger early interruption.
200+ ///
201+ /// If this flag is `true` at any point in the iteration, it will abort with an error.
202+ pub should_interrupt: Option<&'a AtomicBool>,
190203 /// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`].
191204 ///
192205 /// It's used to help us differentiate our own `.git` directory from nested unrelated repositories,
@@ -261,6 +274,8 @@ pub struct Outcome { · 68 unmodified lines
261274 #[derive(Debug, thiserror::Error)]
262275 #[allow(missing_docs)]
263276 pub enum Error {
277+ #[error("Interrupted")]
278+ Interrupted,
264279 #[error("Worktree root at '{}' is not a directory", root.display())]
265280 WorktreeRootIsFile { root: PathBuf },
266281 #[error("Traversal root '{}' contains relative path components and could not be normalized", root.display())]
gix-dir/src/walk/readdir.rs+4−0
@@ -1,6 +1,7 @@
11 use bstr::{BStr, BString, ByteSlice};
22 use std::borrow::Cow;
33 use std::path::{Path, PathBuf};
4+use std::sync::atomic::Ordering;
45
56 use crate::entry::{PathspecMatch, Status};
67 use crate::walk::function::{can_recurse, emit_entry};
@@ -23,6 +24,9 @@ pub(super) fn recursive( · 16 unmodified lines
2324 out: &mut Outcome,
2425 state: &mut State,
2526 ) -> Result<(Action, bool), Error> {
27+ if ctx.should_interrupt.map_or(false, |flag| flag.load(Ordering::Relaxed)) {
28+ return Err(Error::Interrupted);
29+ }
2630 out.read_dir_calls += 1;
2731 let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir {
2832 path: current.to_owned(),
gix-dir/tests/fixtures/many-symlinks.sh+26−0
@@ -17,3 +17,29 @@ git init immediate-breakout-symlink · 16 unmodified lines
1717 (cd immediate-breakout-symlink
1818 ln -s .. breakout
1919 )
20+
21+git init excluded-symlinks-to-dir
22+(cd excluded-symlinks-to-dir
23+ cat <<EOF >.gitignore
24+src1
25+src2/
26+file1
27+file2/
28+ignored
29+ignored-must-be-dir/
30+EOF
31+ git add .gitignore && git commit -m "init"
32+
33+ mkdir src
34+ >src/file
35+
36+ mkdir ignored-must-be-dir ignored
37+ touch ignored-must-be-dir/file ignored/file
38+
39+ ln -s src src1
40+ ln -s src src2
41+ ln -s src/file file1
42+ ln -s src/file file2
43+)
44+
45+ln -s excluded-symlinks-to-dir worktree-root-is-symlink
gix-dir/tests/walk/mod.rs+127−0
@@ -1,5 +1,6 @@
11 use gix_dir::{walk, EntryRef};
22 use pretty_assertions::assert_eq;
3+use std::sync::atomic::AtomicBool;
34
45 use crate::walk_utils::{
56 collect, collect_filtered, collect_filtered_with_cwd, entry, entry_dirstat, entry_nokind, entry_nomatch, entryps,
@@ -16,6 +17,81 @@ use gix_dir::walk::EmissionMode::*; · 10 unmodified lines
1617 use gix_dir::walk::ForDeletionMode;
1718 use gix_ignore::Kind::*;
1819
20+#[test]
21+#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
22+fn symlink_to_dir_can_be_excluded() -> crate::Result {
23+ let root = fixture_in("many-symlinks", "excluded-symlinks-to-dir");
24+ let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
25+ walk(
26+ &root,
27+ ctx,
28+ gix_dir::walk::Options {
29+ emit_ignored: Some(Matching),
30+ ..options()
31+ },
32+ keep,
33+ )
34+ });
35+ assert_eq!(
36+ out,
37+ walk::Outcome {
38+ read_dir_calls: 2,
39+ returned_entries: entries.len(),
40+ seen_entries: 9,
41+ }
42+ );
43+
44+ assert_eq!(
45+ entries,
46+ &[
47+ entry("file1", Ignored(Expendable), Symlink),
48+ entry("file2", Untracked, Symlink),
49+ entry("ignored", Ignored(Expendable), Directory),
50+ entry("ignored-must-be-dir", Ignored(Expendable), Directory),
51+ entry("src/file", Untracked, File),
52+ entry("src1", Ignored(Expendable), Symlink),
53+ entry("src2", Untracked, Symlink), /* marked as src2/ in .gitignore */
54+ ],
55+ "by default, symlinks are counted as files only, even if they point to a directory, when handled by the exclude machinery"
56+ );
57+
58+ let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
59+ walk(
60+ &root,
61+ ctx,
62+ gix_dir::walk::Options {
63+ emit_ignored: Some(Matching),
64+ symlinks_to_directories_are_ignored_like_directories: true,
65+ ..options()
66+ },
67+ keep,
68+ )
69+ });
70+ assert_eq!(
71+ out,
72+ walk::Outcome {
73+ read_dir_calls: 2,
74+ returned_entries: entries.len(),
75+ seen_entries: 9,
76+ }
77+ );
78+
79+ assert_eq!(
80+ entries,
81+ &[
82+ entry("file1", Ignored(Expendable), Symlink),
83+ entry("file2", Untracked, Symlink),
84+ entry("ignored", Ignored(Expendable), Directory),
85+ entry("ignored-must-be-dir", Ignored(Expendable), Directory),
86+ entry("src/file", Untracked, File),
87+ entry("src1", Ignored(Expendable), Symlink),
88+ entry("src2", Ignored(Expendable), Symlink), /* marked as src2/ in .gitignore */
89+ ],
90+ "with libgit2 compatibility enabled, symlinks to directories are treated like a directory, not symlink"
91+ );
92+ Ok(())
93+}
94+
1995 #[test]
2096 #[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
2197 fn root_may_not_lead_through_symlinks() -> crate::Result {
@@ -43,6 +119,57 @@ fn root_may_not_lead_through_symlinks() -> crate::Result { · 21 unmodified lines
43119 Ok(())
44120 }
45121
122+#[test]
123+#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
124+fn root_may_be_a_symlink_if_it_is_the_worktree() -> crate::Result {
125+ let root = fixture_in("many-symlinks", "worktree-root-is-symlink");
126+ let ((_out, _root), entries) = collect(&root, None, |keep, ctx| {
127+ walk(
128+ &root,
129+ ctx,
130+ gix_dir::walk::Options {
131+ emit_ignored: Some(Matching),
132+ symlinks_to_directories_are_ignored_like_directories: true,
133+ ..options()
134+ },
135+ keep,
136+ )
137+ });
138+
139+ assert_eq!(
140+ entries,
141+ &[
142+ entry("file1", Ignored(Expendable), Symlink),
143+ entry("file2", Untracked, Symlink),
144+ entry("ignored", Ignored(Expendable), Directory),
145+ entry("ignored-must-be-dir", Ignored(Expendable), Directory),
146+ entry("src/file", Untracked, File),
147+ entry("src1", Ignored(Expendable), Symlink),
148+ entry("src2", Ignored(Expendable), Symlink), /* marked as src2/ in .gitignore */
149+ ],
150+ "it traversed the directory normally - without this capability, symlinked repos can't be traversed"
151+ );
152+ Ok(())
153+}
154+
155+#[test]
156+fn should_interrupt_works_even_in_empty_directories() {
157+ let root = fixture("empty");
158+ let should_interrupt = AtomicBool::new(true);
159+ let err = try_collect_filtered_opts_collect(
160+ &root,
161+ None,
162+ |keep, ctx| walk(&root, ctx, gix_dir::walk::Options { ..options() }, keep),
163+ None::<&str>,
164+ Options {
165+ should_interrupt: Some(&should_interrupt),
166+ ..Default::default()
167+ },
168+ )
169+ .unwrap_err();
170+ assert!(matches!(err, gix_dir::walk::Error::Interrupted));
171+}
172+
46173 #[test]
47174 fn empty_root() -> crate::Result {
48175 let root = fixture("empty");
gix-dir/tests/walk_utils/mod.rs+10−1
@@ -2,6 +2,7 @@ use bstr::BStr; · 1 unmodified line
22 use gix_dir::{entry, walk, Entry};
33 use gix_testtools::scripted_fixture_read_only;
44 use std::path::{Path, PathBuf};
5+use std::sync::atomic::AtomicBool;
56
67 pub fn fixture_in(filename: &str, name: &str) -> PathBuf {
78 let root = scripted_fixture_read_only(format!("{filename}.sh")).expect("script works");
@@ -31,6 +32,7 @@ pub fn options_emit_all() -> walk::Options { · 23 unmodified lines
3132 emit_untracked: walk::EmissionMode::Matching,
3233 emit_empty_directories: true,
3334 emit_collapsed: None,
35+ symlinks_to_directories_are_ignored_like_directories: false,
3436 }
3537 }
3638
@@ -268,7 +270,11 @@ pub fn try_collect_filtered_opts( · 231 unmodified lines
268270 cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
269271 patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
270272 delegate: &mut dyn gix_dir::walk::Delegate,
271− Options { fresh_index, git_dir }: Options<'_>,
273+ Options {
274+ fresh_index,
275+ git_dir,
276+ should_interrupt,
277+ }: Options<'_>,
272278 ) -> Result<(walk::Outcome, PathBuf), walk::Error> {
273279 let git_dir = worktree_root.join(git_dir.unwrap_or(".git"));
274280 let mut index = std::fs::read(git_dir.join("index")).ok().map_or_else(
@@ -342,6 +348,7 @@ pub fn try_collect_filtered_opts( · 67 unmodified lines
342348 excludes: Some(&mut stack),
343349 objects: &gix_object::find::Never,
344350 explicit_traversal_root,
351+ should_interrupt,
345352 },
346353 )
347354 }
@@ -349,6 +356,7 @@ pub fn try_collect_filtered_opts( · 1 unmodified line
349356 pub struct Options<'a> {
350357 pub fresh_index: bool,
351358 pub git_dir: Option<&'a str>,
359+ pub should_interrupt: Option<&'a AtomicBool>,
352360 }
353361
354362 impl<'a> Options<'a> {
@@ -365,6 +373,7 @@ impl<'a> Default for Options<'a> { · 10 unmodified lines
365373 Options {
366374 fresh_index: true,
367375 git_dir: None,
376+ should_interrupt: None,
368377 }
369378 }
370379 }
gix-index/src/entry/mod.rs+0−3
@@ -20,9 +20,6 @@ mod write; · 19 unmodified lines
2020
2121 use bitflags::bitflags;
2222
23−// TODO: we essentially treat this as an enum with the only exception being
24−// that `FILE_EXECUTABLE.contains(FILE)` works might want to turn this into an
25−// enum proper
2623 bitflags! {
2724 /// The kind of file of an entry.
2825 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
gix-object/tests/commit/from_bytes.rs+27−0
@@ -8,6 +8,33 @@ use crate::{ · 7 unmodified lines
88 fixture_name, linus_signature, signature,
99 };
1010
11+#[test]
12+fn invalid_timestsamp() {
13+ let actor = gix_actor::SignatureRef {
14+ name: b"Name".as_bstr(),
15+ email: b"name@example.com".as_bstr(),
16+ time: Time {
17+ seconds: 1312735823,
18+ offset: 0,
19+ sign: Sign::Plus,
20+ },
21+ };
22+ assert_eq!(
23+ CommitRef::from_bytes(&fixture_name("commit", "invalid-timestamp.txt"))
24+ .expect("auto-correct invalid timestamp by discarding it (time is still valid UTC)"),
25+ CommitRef {
26+ tree: b"7989dfb2ec2f41914611a22fb30bbc2b3849df9a".as_bstr(),
27+ parents: [b"8845ae683e2688bc619baade49510c17e978518f".as_bstr()].into(),
28+ author: actor,
29+ committer: actor,
30+ encoding: None,
31+ message: b"edit changelog to mention about x_sendfile_header default change".as_bstr(),
32+ extra_headers: vec![]
33+ },
34+ "the offset of the actor is null, leaving the UTC time"
35+ );
36+}
37+
1138 #[test]
1239 fn unsigned() -> crate::Result {
1340 assert_eq!(
gix-object/tests/fixtures/commit/invalid-timestamp.txt+6−0
@@ -0,0 +1,6 @@
1+tree 7989dfb2ec2f41914611a22fb30bbc2b3849df9a
2+parent 8845ae683e2688bc619baade49510c17e978518f
3+author Name <name@example.com> 1312735823 +051800
4+committer Name <name@example.com> 1312735823 +051800
5+
6+edit changelog to mention about x_sendfile_header default change
gix-pathspec/src/search/matching.rs+4−4
@@ -176,11 +176,11 @@ impl Search { · 175 unmodified lines
176176 matches!(pattern.path.get(common_len), None | Some(&b'/'))
177177 } else {
178178 relative_path.get(common_len) == Some(&b'/')
179− }
179+ };
180180 }
181181 }
182182 }
183− if is_match {
183+ if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
184184 return !pattern.is_excluded();
185185 }
186186 }
@@ -191,7 +191,7 @@ impl Search { · 4 unmodified lines
191191 /// Returns `true` if `relative_path` matches the prefix of this pathspec.
192192 ///
193193 /// For example, the relative path `d` matches `d/`, `d*/`, `d/` and `d/*`, but not `d/d/*` or `dir`.
194− /// When `leading` is `true`, then `d` matches `d/d` as well. Thus `relative_path` must may be
194+ /// When `leading` is `true`, then `d` matches `d/d` as well. Thus, `relative_path` must may be
195195 /// partially included in `pathspec`, otherwise it has to be fully included.
196196 pub fn directory_matches_prefix(&self, relative_path: &BStr, leading: bool) -> bool {
197197 if self.patterns.is_empty() {
@@ -233,7 +233,7 @@ impl Search { · 35 unmodified lines
233233 };
234234 }
235235 }
236− if is_match {
236+ if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
237237 return !pattern.is_excluded();
238238 }
239239 }
gix-pathspec/tests/search/mod.rs+49−2
@@ -135,6 +135,47 @@ fn no_pathspecs_match_everything() -> crate::Result { · 134 unmodified lines
135135 Ok(())
136136 }
137137
138+#[test]
139+fn included_directory_and_excluded_subdir_top_level_with_prefix() -> crate::Result {
140+ let mut search = gix_pathspec::Search::from_specs(pathspecs(&[":/foo", ":!/foo/target/"]), None, Path::new("foo"))?;
141+ let m = search
142+ .pattern_matching_relative_path("foo".into(), Some(true), &mut no_attrs)
143+ .expect("matches");
144+ assert_eq!(m.kind, Verbatim);
145+
146+ let m = search
147+ .pattern_matching_relative_path("foo/bar".into(), Some(false), &mut no_attrs)
148+ .expect("matches");
149+ assert_eq!(m.kind, Prefix);
150+
151+ let m = search
152+ .pattern_matching_relative_path("foo/target".into(), Some(false), &mut no_attrs)
153+ .expect("matches");
154+ assert_eq!(m.kind, Prefix, "files named `target` are allowed");
155+
156+ let m = search
157+ .pattern_matching_relative_path("foo/target".into(), Some(true), &mut no_attrs)
158+ .expect("matches");
159+ assert!(m.is_excluded(), "directories named `target` are excluded");
160+ assert_eq!(m.kind, Verbatim);
161+
162+ let m = search
163+ .pattern_matching_relative_path("foo/target/file".into(), Some(false), &mut no_attrs)
164+ .expect("matches");
165+ assert!(m.is_excluded(), "everything below `target/` is also excluded");
166+ assert_eq!(m.kind, Prefix);
167+
168+ assert!(search.directory_matches_prefix("foo/bar".into(), false));
169+ assert!(search.directory_matches_prefix("foo/bar".into(), true));
170+ assert!(search.directory_matches_prefix("foo".into(), false));
171+ assert!(search.directory_matches_prefix("foo".into(), true));
172+ assert!(search.can_match_relative_path("foo".into(), Some(true)));
173+ assert!(search.can_match_relative_path("foo".into(), Some(false)));
174+ assert!(search.can_match_relative_path("foo/hi".into(), Some(true)));
175+ assert!(search.can_match_relative_path("foo/hi".into(), Some(false)));
176+ Ok(())
177+}
178+
138179 #[test]
139180 fn starts_with() -> crate::Result {
140181 let mut search = gix_pathspec::Search::from_specs(pathspecs(&["a/*"]), None, Path::new(""))?;
@@ -261,8 +302,14 @@ fn simplified_search_respects_all_excluded() -> crate::Result { · 120 unmodified lines
261302 None,
262303 Path::new(""),
263304 )?;
264− assert!(!search.can_match_relative_path("b".into(), None));
265− assert!(!search.can_match_relative_path("a".into(), None));
305+ assert!(
306+ search.can_match_relative_path("b".into(), None),
307+ "non-trivial excludes are ignored in favor of false-positives"
308+ );
309+ assert!(
310+ search.can_match_relative_path("a".into(), None),
311+ "non-trivial excludes are ignored in favor of false-positives"
312+ );
266313 assert!(search.can_match_relative_path("c".into(), None));
267314 assert!(search.can_match_relative_path("c/".into(), None));
268315
gix-status/src/index_as_worktree_with_renames/mod.rs+1−0
@@ -87,6 +87,7 @@ pub(super) mod function { · 86 unmodified lines
8787 gix_dir::walk(
8888 worktree,
8989 gix_dir::walk::Context {
90+ should_interrupt: Some(ctx.should_interrupt),
9091 git_dir_realpath: dirwalk_ctx.git_dir_realpath,
9192 current_dir: dirwalk_ctx.current_dir,
9293 index,
gix/Cargo.toml+1−1
@@ -74,7 +74,7 @@ interrupt = ["dep:signal-hook", "gix-tempfile/signals"] · 73 unmodified lines
7474 index = ["dep:gix-index"]
7575
7676 ## Support directory walks with Git-style annoations.
77−dirwalk = ["dep:gix-dir"]
77+dirwalk = ["dep:gix-dir", "attributes", "excludes"]
7878
7979 ## Access to credential helpers, which provide credentials for URLs.
8080 # Note that `gix-negotiate` just piggibacks here, as 'credentials' is equivalent to 'fetch & push' right now.
gix/src/dirwalk/iter.rs+188−0
@@ -0,0 +1,188 @@
1+use super::Iter;
2+use crate::bstr::BString;
3+use crate::util::OwnedOrStaticAtomicBool;
4+use crate::worktree::IndexPersistedOrInMemory;
5+use crate::{dirwalk, PathspecDetached, Repository};
6+use std::path::PathBuf;
7+
8+/// An entry of the directory walk as returned by the [iterator](Iter).
9+pub struct Item {
10+ /// The directory entry.
11+ pub entry: gix_dir::Entry,
12+ /// `collapsed_directory_status` is `Some(dir_status)` if this entry was part of a directory with the given
13+ /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was
14+ /// [gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch]. It will also be `Some(dir_status)` if that option
15+ /// was [gix_dir::walk::CollapsedEntriesEmissionMode::All].
16+ pub collapsed_directory_status: Option<gix_dir::entry::Status>,
17+}
18+
19+impl Item {
20+ fn new(entry: gix_dir::EntryRef<'_>, collapsed_directory_status: Option<gix_dir::entry::Status>) -> Self {
21+ Item {
22+ entry: entry.to_owned(),
23+ collapsed_directory_status,
24+ }
25+ }
26+}
27+
28+/// The outcome of fully consumed [dirwalk iterator](Iter).
29+pub struct Outcome {
30+ /// The index originally passed in to create the iterator.
31+ pub index: IndexPersistedOrInMemory,
32+ /// The excludes stack used for the dirwalk, for access of `.gitignore` information.
33+ pub excludes: gix_worktree::Stack,
34+ /// The pathspecs used to guide the operation,
35+ pub pathspec: PathspecDetached,
36+ /// The root actually being used for the traversal, and useful to transform the paths returned for the user.
37+ /// It's always within the [`work-dir`](Repository::work_dir).
38+ pub traversal_root: PathBuf,
39+ /// The actual result of the dirwalk.
40+ pub dirwalk: gix_dir::walk::Outcome,
41+}
42+
43+/// The error returned by [Repository::dirwalk_iter()].
44+#[derive(Debug, thiserror::Error)]
45+#[allow(missing_docs)]
46+pub enum Error {
47+ #[error("Failed to spawn producer thread")]
48+ #[cfg(feature = "parallel")]
49+ SpawnThread(#[from] std::io::Error),
50+ #[error(transparent)]
51+ #[cfg(not(feature = "parallel"))]
52+ Dirwalk(#[from] dirwalk::Error),
53+ #[error(transparent)]
54+ #[cfg(not(feature = "parallel"))]
55+ DetachPathSpec(#[from] std::io::Error),
56+}
57+
58+/// Lifecycle
59+impl Iter {
60+ pub(crate) fn new(
61+ repo: &Repository,
62+ index: IndexPersistedOrInMemory,
63+ patterns: Vec<BString>,
64+ should_interrupt: OwnedOrStaticAtomicBool,
65+ options: dirwalk::Options,
66+ ) -> Result<Iter, Error> {
67+ #[cfg(feature = "parallel")]
68+ {
69+ let repo = repo.clone().into_sync();
70+ let (tx, rx) = std::sync::mpsc::channel();
71+ let handle = std::thread::Builder::new()
72+ .name("gix::dirwalk::iter::producer".into())
73+ .spawn({
74+ let should_interrupt = should_interrupt.clone();
75+ move || -> Result<Outcome, dirwalk::Error> {
76+ let repo: Repository = repo.into();
77+ let mut collect = Collect { tx };
78+ let out = repo.dirwalk(&index, patterns, &should_interrupt, options, &mut collect)?;
79+ Ok(Outcome {
80+ index,
81+ excludes: out.excludes.detach(),
82+ pathspec: out.pathspec.detach().map_err(|err| {
83+ dirwalk::Error::Walk(gix_dir::walk::Error::ReadDir {
84+ path: repo.git_dir().to_owned(),
85+ source: err,
86+ })
87+ })?,
88+ traversal_root: out.traversal_root,
89+ dirwalk: out.dirwalk,
90+ })
91+ }
92+ })?;
93+
94+ Ok(Iter {
95+ rx_and_join: Some((rx, handle)),
96+ should_interrupt,
97+ out: None,
98+ })
99+ }
100+ #[cfg(not(feature = "parallel"))]
101+ {
102+ let mut collect = Collect { items: Vec::new() };
103+ let out = repo.dirwalk(&index, patterns, &should_interrupt, options, &mut collect)?;
104+ let out = Outcome {
105+ index,
106+ excludes: out.excludes.detach(),
107+ pathspec: out.pathspec.detach()?,
108+ traversal_root: out.traversal_root,
109+ dirwalk: out.dirwalk,
110+ };
111+
112+ Ok(Iter {
113+ items: collect.items.into_iter(),
114+ out: Some(out),
115+ })
116+ }
117+ }
118+}
119+
120+/// Access
121+impl Iter {
122+ /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed.
123+ pub fn outcome_mut(&mut self) -> Option<&mut Outcome> {
124+ self.out.as_mut()
125+ }
126+
127+ /// Turn the iterator into the iteration outcome, which is `None` on error or if the iteration
128+ /// isn't complete.
129+ pub fn into_outcome(mut self) -> Option<Outcome> {
130+ self.out.take()
131+ }
132+}
133+
134+impl Iterator for Iter {
135+ type Item = Result<Item, dirwalk::Error>;
136+
137+ fn next(&mut self) -> Option<Self::Item> {
138+ #[cfg(feature = "parallel")]
139+ {
140+ let (rx, _join) = self.rx_and_join.as_ref()?;
141+ match rx.recv().ok() {
142+ Some(item) => Some(Ok(item)),
143+ None => {
144+ let (_rx, handle) = self.rx_and_join.take()?;
145+ match handle.join().expect("no panic") {
146+ Ok(out) => {
147+ self.out = Some(out);
148+ None
149+ }
150+ Err(err) => Some(Err(err)),
151+ }
152+ }
153+ }
154+ }
155+ #[cfg(not(feature = "parallel"))]
156+ self.items.next().map(Ok)
157+ }
158+}
159+
160+#[cfg(feature = "parallel")]
161+impl Drop for Iter {
162+ fn drop(&mut self) {
163+ crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt);
164+ }
165+}
166+
167+struct Collect {
168+ #[cfg(feature = "parallel")]
169+ tx: std::sync::mpsc::Sender<Item>,
170+ #[cfg(not(feature = "parallel"))]
171+ items: Vec<Item>,
172+}
173+
174+impl gix_dir::walk::Delegate for Collect {
175+ fn emit(
176+ &mut self,
177+ entry: gix_dir::EntryRef<'_>,
178+ collapsed_directory_status: Option<gix_dir::entry::Status>,
179+ ) -> gix_dir::walk::Action {
180+ // NOTE: we assume that the receiver triggers interruption so the operation will stop if the receiver is down.
181+ let item = Item::new(entry, collapsed_directory_status);
182+ #[cfg(feature = "parallel")]
183+ self.tx.send(item).ok();
184+ #[cfg(not(feature = "parallel"))]
185+ self.items.push(item);
186+ gix_dir::walk::Action::Continue
187+ }
188+}
gix/src/dirwalk/mod.rs+91−0
@@ -0,0 +1,91 @@
1+use gix_dir::walk::{CollapsedEntriesEmissionMode, EmissionMode, ForDeletionMode};
2+
3+use crate::{config, AttributeStack, Pathspec};
4+use std::path::PathBuf;
5+
6+mod options;
7+
8+///
9+#[allow(clippy::empty_docs)]
10+pub mod iter;
11+
12+/// An iterator for entries in a directory walk.
13+///
14+/// ### Parallel Operation
15+///
16+/// Note that without the `parallel` feature, the iterator becomes 'serial', which means that all entries will be traversed
17+/// in advance and it cannot be interrupted unless the interrupt flag is set from another thread.
18+///
19+/// It's a crutch that is just there to make single-threaded applications possible at all, as it's not really an iterator
20+/// anymore. If this matters, better run [Repository::dirwalk()](crate::Repository::dirwalk) by hand as it provides all
21+/// control one would need, just not as an iterator.
22+///
23+/// Also, even with `parallel` set, the first call to `next()` will block until there is an item available, without a chance
24+/// to interrupt unless the interrupt flag is set from another thread.
25+pub struct Iter {
26+ #[cfg(feature = "parallel")]
27+ #[allow(clippy::type_complexity)]
28+ rx_and_join: Option<(
29+ std::sync::mpsc::Receiver<iter::Item>,
30+ std::thread::JoinHandle<Result<iter::Outcome, Error>>,
31+ )>,
32+ #[cfg(feature = "parallel")]
33+ should_interrupt: crate::util::OwnedOrStaticAtomicBool,
34+ /// Without parallelization, the iterator has to buffer all changes in advance.
35+ #[cfg(not(feature = "parallel"))]
36+ items: std::vec::IntoIter<iter::Item>,
37+ /// The outcome of the operation, only available once the operation has ended.
38+ out: Option<iter::Outcome>,
39+}
40+
41+/// The error returned by [dirwalk()](crate::Repository::dirwalk()).
42+#[derive(Debug, thiserror::Error)]
43+#[allow(missing_docs)]
44+pub enum Error {
45+ #[error(transparent)]
46+ Walk(#[from] gix_dir::walk::Error),
47+ #[error("A working tree is required to perform a directory walk")]
48+ MissingWorkDir,
49+ #[error(transparent)]
50+ Excludes(#[from] config::exclude_stack::Error),
51+ #[error(transparent)]
52+ Pathspec(#[from] crate::pathspec::init::Error),
53+ #[error(transparent)]
54+ Prefix(#[from] gix_path::realpath::Error),
55+ #[error(transparent)]
56+ FilesystemOptions(#[from] config::boolean::Error),
57+}
58+
59+/// The outcome of the [dirwalk()](crate::Repository::dirwalk).
60+pub struct Outcome<'repo> {
61+ /// The excludes stack used for the dirwalk, for access of `.gitignore` information.
62+ pub excludes: AttributeStack<'repo>,
63+ /// The pathspecs used to guide the operation,
64+ pub pathspec: Pathspec<'repo>,
65+ /// The root actually being used for the traversal, and useful to transform the paths returned for the user.
66+ /// It's always within the [`work-dir`](crate::Repository::work_dir).
67+ pub traversal_root: PathBuf,
68+ /// The actual result of the dirwalk.
69+ pub dirwalk: gix_dir::walk::Outcome,
70+}
71+
72+/// Options for use in the [`Repository::dirwalk()`](crate::Repository::dirwalk()) function.
73+///
74+/// Note that all values start out disabled.
75+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
76+pub struct Options {
77+ precompose_unicode: bool,
78+ ignore_case: bool,
79+
80+ recurse_repositories: bool,
81+ emit_pruned: bool,
82+ emit_ignored: Option<EmissionMode>,
83+ for_deletion: Option<ForDeletionMode>,
84+ emit_tracked: bool,
85+ emit_untracked: EmissionMode,
86+ emit_empty_directories: bool,
87+ classify_untracked_bare_repositories: bool,
88+ emit_collapsed: Option<CollapsedEntriesEmissionMode>,
89+ symlinks_to_directories_are_ignored_like_directories: bool,
90+ pub(crate) empty_patterns_match_prefix: bool,
91+}
gix/src/dirwalk/options.rsrenamed from gix/src/dirwalk.rs+23−20
@@ -1,25 +1,6 @@
1+use crate::dirwalk::Options;
12 use gix_dir::walk::{CollapsedEntriesEmissionMode, EmissionMode, ForDeletionMode};
23
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− emit_collapsed: Option<CollapsedEntriesEmissionMode>,
20− pub(crate) empty_patterns_match_prefix: bool,
21−}
22−
234 /// Construction
245 impl Options {
256 pub(crate) fn from_fs_caps(caps: gix_fs::Capabilities) -> Self {
@@ -36,6 +17,7 @@ impl Options { · 10 unmodified lines
3617 classify_untracked_bare_repositories: false,
3718 emit_collapsed: None,
3819 empty_patterns_match_prefix: false,
20+ symlinks_to_directories_are_ignored_like_directories: false,
3921 }
4022 }
4123 }
@@ -54,6 +36,8 @@ impl From<Options> for gix_dir::walk::Options { · 12 unmodified lines
5436 emit_empty_directories: v.emit_empty_directories,
5537 classify_untracked_bare_repositories: v.classify_untracked_bare_repositories,
5638 emit_collapsed: v.emit_collapsed,
39+ symlinks_to_directories_are_ignored_like_directories: v
40+ .symlinks_to_directories_are_ignored_like_directories,
5741 }
5842 }
5943 }
@@ -180,4 +164,23 @@ impl Options { · 120 unmodified lines
180164 self.emit_collapsed = value;
181165 self
182166 }
167+
168+ /// This is a `libgit2` compatibility flag, and if enabled, symlinks that point to directories will be considered a directory
169+ /// when checking for exclusion.
170+ ///
171+ /// This is relevant if `src2` points to `src`, and is excluded with `src2/`. If `false`, `src2` will not be excluded,
172+ /// if `true` it will be excluded as the symlink is considered a directory.
173+ ///
174+ /// In other words, for Git compatibility this flag should be `false`, the default, for `git2` compatibility it should be `true`.
175+ pub fn symlinks_to_directories_are_ignored_like_directories(&mut self, toggle: bool) -> &mut Self {
176+ self.symlinks_to_directories_are_ignored_like_directories = toggle;
177+ self
178+ }
179+
180+ /// Like [`symlinks_to_directories_are_ignored_like_directories()`](Self::symlinks_to_directories_are_ignored_like_directories),
181+ /// but only requires a mutably borrowed instance.
182+ pub fn set_symlinks_to_directories_are_ignored_like_directories(&mut self, value: bool) -> &mut Self {
183+ self.symlinks_to_directories_are_ignored_like_directories = value;
184+ self
185+ }
183186 }
gix/src/lib.rs+3−1
@@ -177,8 +177,8 @@ pub use types::{Pathspec, PathspecDetached, Submodule}; · 176 unmodified lines
177177 #[allow(clippy::empty_docs)]
178178 pub mod clone;
179179 pub mod commit;
180−#[cfg(feature = "dirwalk")]
181180 ///
181+#[cfg(feature = "dirwalk")]
182182 #[allow(clippy::empty_docs)]
183183 pub mod dirwalk;
184184 pub mod head;
@@ -191,6 +191,8 @@ pub mod repository; · 6 unmodified lines
191191 #[cfg(feature = "attributes")]
192192 pub mod submodule;
193193 pub mod tag;
194+#[cfg(any(feature = "dirwalk", feature = "status"))]
195+pub(crate) mod util;
194196
195197 ///
196198 #[allow(clippy::empty_docs)]
gix/src/repository/dirwalk.rs+33−38
@@ -1,37 +1,8 @@
1−use crate::bstr::BStr;
2−use crate::{config, dirwalk, AttributeStack, Pathspec, Repository};
3−use std::path::PathBuf;
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− MissingWorkDir,
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−/// The outcome of the [dirwalk()](Repository::dirwalk).
24−pub struct Outcome<'repo> {
25− /// The excludes stack used for the dirwalk, for access of `.gitignore` information.
26− pub excludes: AttributeStack<'repo>,
27− /// The pathspecs used to guide the operation,
28− pub pathspec: Pathspec<'repo>,
29− /// The root actually being used for the traversal, and useful to transform the paths returned for the user.
30− /// It's always within the [`work-dir`](Repository::work_dir).
31− pub traversal_root: PathBuf,
32− /// The actual result of the dirwalk.
33− pub dirwalk: gix_dir::walk::Outcome,
34−}
1+use crate::bstr::{BStr, BString};
2+use crate::util::OwnedOrStaticAtomicBool;
3+use crate::worktree::IndexPersistedOrInMemory;
4+use crate::{config, dirwalk, Repository};
5+use std::sync::atomic::AtomicBool;
356
367 impl Repository {
378 /// Return default options suitable for performing a directory walk on this repository.
@@ -42,7 +13,8 @@ impl Repository { · 4 unmodified lines
4213 }
4314
4415 /// Perform a directory walk configured with `options` under control of the `delegate`. Use `patterns` to
45− /// further filter entries.
16+ /// further filter entries. `should_interrupt` is polled to see if an interrupt is requested, causing an
17+ /// error to be returned instead.
4618 ///
4719 /// The `index` is used to determine if entries are tracked, and for excludes and attributes
4820 /// lookup. Note that items will only count as tracked if they have the [`gix_index::entry::Flags::UPTODATE`]
@@ -53,11 +25,12 @@ impl Repository { · 4 unmodified lines
5325 &self,
5426 index: &gix_index::State,
5527 patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
28+ should_interrupt: &AtomicBool,
5629 options: dirwalk::Options,
5730 delegate: &mut dyn gix_dir::walk::Delegate,
58− ) -> Result<Outcome<'_>, Error> {
31+ ) -> Result<dirwalk::Outcome<'_>, dirwalk::Error> {
5932 let _span = gix_trace::coarse!("gix::dirwalk");
60− let workdir = self.work_dir().ok_or(Error::MissingWorkDir)?;
33+ let workdir = self.work_dir().ok_or(dirwalk::Error::MissingWorkDir)?;
6134 let mut excludes = self.excludes(
6235 index,
6336 None,
@@ -78,6 +51,7 @@ impl Repository { · 14 unmodified lines
7851 let (outcome, traversal_root) = gix_dir::walk(
7952 workdir,
8053 gix_dir::walk::Context {
54+ should_interrupt: Some(should_interrupt),
8155 git_dir_realpath: git_dir_realpath.as_ref(),
8256 current_dir: self.current_dir(),
8357 index,
@@ -101,11 +75,32 @@ impl Repository { · 17 unmodified lines
10175 delegate,
10276 )?;
10377
104− Ok(Outcome {
78+ Ok(dirwalk::Outcome {
10579 dirwalk: outcome,
10680 traversal_root,
10781 excludes,
10882 pathspec,
10983 })
11084 }
85+
86+ /// Create an iterator over a running traversal, which stops if the iterator is dropped. All arguments
87+ /// are the same as in [`dirwalk()`](Self::dirwalk).
88+ ///
89+ /// `should_interrupt` should be set to `Default::default()` if it is supposed to be unused.
90+ /// Otherwise, it can be created by passing a `&'static AtomicBool`, `&Arc<AtomicBool>` or `Arc<AtomicBool>`.
91+ pub fn dirwalk_iter(
92+ &self,
93+ index: impl Into<IndexPersistedOrInMemory>,
94+ patterns: impl IntoIterator<Item = impl Into<BString>>,
95+ should_interrupt: OwnedOrStaticAtomicBool,
96+ options: dirwalk::Options,
97+ ) -> Result<dirwalk::Iter, dirwalk::iter::Error> {
98+ dirwalk::Iter::new(
99+ self,
100+ index.into(),
101+ patterns.into_iter().map(Into::into).collect(),
102+ should_interrupt,
103+ options,
104+ )
105+ }
111106 }
gix/src/repository/mod.rs+1−1
@@ -45,7 +45,7 @@ mod config; · 44 unmodified lines
4545 pub mod diff;
4646 ///
4747 #[cfg(feature = "dirwalk")]
48−pub mod dirwalk;
48+mod dirwalk;
4949 ///
5050 #[cfg(feature = "attributes")]
5151 pub mod filter;
gix/src/status/index_worktree.rs+9−23
@@ -297,7 +297,7 @@ pub struct Iter { · 296 unmodified lines
297297 std::thread::JoinHandle<Result<iter::Outcome, crate::status::index_worktree::Error>>,
298298 )>,
299299 #[cfg(feature = "parallel")]
300− should_interrupt: crate::status::OwnedOrStaticAtomic,
300+ should_interrupt: crate::status::OwnedOrStaticAtomicBool,
301301 /// Without parallelization, the iterator has to buffer all changes in advance.
302302 #[cfg(not(feature = "parallel"))]
303303 items: std::vec::IntoIter<iter::Item>,
@@ -744,11 +744,18 @@ pub mod iter { · 440 unmodified lines
744744 }
745745 }
746746
747+ /// Access
747748 impl super::Iter {
748749 /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed.
749750 pub fn outcome_mut(&mut self) -> Option<&mut Outcome> {
750751 self.out.as_mut()
751752 }
753+
754+ /// Turn the iterator into the iteration outcome, which is `None` on error or if the iteration
755+ /// isn't complete.
756+ pub fn into_outcome(mut self) -> Option<Outcome> {
757+ self.out.take()
758+ }
752759 }
753760
754761 impl super::Iter {
@@ -781,28 +788,7 @@ pub mod iter { · 26 unmodified lines
781788 #[cfg(feature = "parallel")]
782789 impl Drop for super::Iter {
783790 fn drop(&mut self) {
784− use crate::status::OwnedOrStaticAtomic;
785− let Some((rx, handle)) = self.rx_and_join.take() else {
786− return;
787− };
788− let prev = self.should_interrupt.swap(true, std::sync::atomic::Ordering::Relaxed);
789− let undo = match &self.should_interrupt {
790− OwnedOrStaticAtomic::Shared(flag) => *flag,
791− OwnedOrStaticAtomic::Owned { flag, private: false } => flag.as_ref(),
792− OwnedOrStaticAtomic::Owned { private: true, .. } => {
793− // Leak the handle to let it shut down in the background, so drop returns more quickly.
794− drop((rx, handle));
795− return;
796− }
797− };
798− // Wait until there is time to respond before we undo the change.
799− handle.join().ok();
800− undo.fetch_update(
801− std::sync::atomic::Ordering::SeqCst,
802− std::sync::atomic::Ordering::SeqCst,
803− |current| current.then_some(prev),
804− )
805− .ok();
791+ crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt);
806792 }
807793 }
808794
gix/src/status/mod.rs+2−34
@@ -1,9 +1,7 @@
11 use crate::config::cache::util::ApplyLeniencyDefault;
2+use crate::util::OwnedOrStaticAtomicBool;
23 use crate::{config, Repository};
34 pub use gix_status as plumbing;
4−use std::ops::Deref;
5−use std::sync::atomic::AtomicBool;
6−use std::sync::Arc;
75
86 /// A structure to hold options configuring the status request, which can then be turned into an iterator.
97 pub struct Platform<'repo, Progress>
@@ -15,37 +13,7 @@ where · 5 unmodified lines
1513 index: Option<crate::worktree::IndexPersistedOrInMemory>,
1614 submodules: Submodule,
1715 index_worktree_options: index_worktree::Options,
18− should_interrupt: Option<OwnedOrStaticAtomic>,
19−}
20−
21−#[derive(Clone)]
22−enum OwnedOrStaticAtomic {
23− Owned {
24− flag: Arc<AtomicBool>,
25− #[cfg_attr(not(feature = "parallel"), allow(dead_code))]
26− private: bool,
27− },
28− Shared(&'static AtomicBool),
29−}
30−
31−impl Default for OwnedOrStaticAtomic {
32− fn default() -> Self {
33− OwnedOrStaticAtomic::Owned {
34− flag: Arc::new(AtomicBool::default()),
35− private: true,
36− }
37− }
38−}
39−
40−impl Deref for OwnedOrStaticAtomic {
41− type Target = std::sync::atomic::AtomicBool;
42−
43− fn deref(&self) -> &Self::Target {
44− match self {
45− OwnedOrStaticAtomic::Owned { flag, .. } => flag,
46− OwnedOrStaticAtomic::Shared(flag) => flag,
47− }
48− }
16+ should_interrupt: Option<OwnedOrStaticAtomicBool>,
4917 }
5018
5119 /// How to obtain a submodule's status.
gix/src/status/platform.rs+3−3
@@ -1,4 +1,4 @@
1−use crate::status::{index_worktree, OwnedOrStaticAtomic, Platform, Submodule, UntrackedFiles};
1+use crate::status::{index_worktree, OwnedOrStaticAtomicBool, Platform, Submodule, UntrackedFiles};
22 use std::sync::atomic::AtomicBool;
33
44 /// Builder
@@ -45,7 +45,7 @@ where · 40 unmodified lines
4545 ///
4646 /// If it is `true`, the iteration will stop immediately.
4747 pub fn should_interrupt_shared(mut self, should_interrupt: &'static AtomicBool) -> Self {
48− self.should_interrupt = Some(OwnedOrStaticAtomic::Shared(should_interrupt));
48+ self.should_interrupt = Some(OwnedOrStaticAtomicBool::Shared(should_interrupt));
4949 self
5050 }
5151
@@ -53,7 +53,7 @@ where · 1 unmodified line
5353 ///
5454 /// If it is `true`, the iteration will stop immediately.
5555 pub fn should_interrupt_owned(mut self, should_interrupt: std::sync::Arc<AtomicBool>) -> Self {
56− self.should_interrupt = Some(OwnedOrStaticAtomic::Owned {
56+ self.should_interrupt = Some(OwnedOrStaticAtomicBool::Owned {
5757 flag: should_interrupt,
5858 private: false,
5959 });
gix/src/util.rs+81−0
@@ -0,0 +1,81 @@
1+use std::ops::Deref;
2+use std::sync::atomic::AtomicBool;
3+use std::sync::Arc;
4+
5+#[derive(Clone)]
6+pub enum OwnedOrStaticAtomicBool {
7+ Owned {
8+ flag: Arc<AtomicBool>,
9+ #[cfg_attr(not(feature = "parallel"), allow(dead_code))]
10+ private: bool,
11+ },
12+ Shared(&'static AtomicBool),
13+}
14+
15+impl Default for OwnedOrStaticAtomicBool {
16+ fn default() -> Self {
17+ OwnedOrStaticAtomicBool::Owned {
18+ flag: Arc::new(AtomicBool::default()),
19+ private: true,
20+ }
21+ }
22+}
23+
24+impl Deref for OwnedOrStaticAtomicBool {
25+ type Target = std::sync::atomic::AtomicBool;
26+
27+ fn deref(&self) -> &Self::Target {
28+ match self {
29+ OwnedOrStaticAtomicBool::Owned { flag, .. } => flag,
30+ OwnedOrStaticAtomicBool::Shared(flag) => flag,
31+ }
32+ }
33+}
34+
35+impl From<&'static AtomicBool> for OwnedOrStaticAtomicBool {
36+ fn from(value: &'static AtomicBool) -> Self {
37+ OwnedOrStaticAtomicBool::Shared(value)
38+ }
39+}
40+
41+impl<'a> From<&'a Arc<AtomicBool>> for OwnedOrStaticAtomicBool {
42+ fn from(value: &'a Arc<AtomicBool>) -> Self {
43+ OwnedOrStaticAtomicBool::Owned {
44+ flag: value.clone(),
45+ private: false,
46+ }
47+ }
48+}
49+
50+impl From<Arc<AtomicBool>> for OwnedOrStaticAtomicBool {
51+ fn from(flag: Arc<AtomicBool>) -> Self {
52+ OwnedOrStaticAtomicBool::Owned { flag, private: false }
53+ }
54+}
55+#[cfg(feature = "parallel")]
56+pub fn parallel_iter_drop<T, U>(
57+ mut rx_and_join: Option<(std::sync::mpsc::Receiver<T>, std::thread::JoinHandle<U>)>,
58+ should_interrupt: &OwnedOrStaticAtomicBool,
59+) {
60+ let Some((rx, handle)) = rx_and_join.take() else {
61+ return;
62+ };
63+ let prev = should_interrupt.swap(true, std::sync::atomic::Ordering::Relaxed);
64+ let undo = match &should_interrupt {
65+ OwnedOrStaticAtomicBool::Shared(flag) => *flag,
66+ OwnedOrStaticAtomicBool::Owned { flag, private: false } => flag.as_ref(),
67+ OwnedOrStaticAtomicBool::Owned { private: true, .. } => {
68+ // Leak the handle to let it shut down in the background, so drop returns more quickly.
69+ drop((rx, handle));
70+ return;
71+ }
72+ };
73+ // Wait until there is time to respond before we undo the change.
74+ handle.join().ok();
75+ undo.fetch_update(
76+ std::sync::atomic::Ordering::SeqCst,
77+ std::sync::atomic::Ordering::SeqCst,
78+ |current| current.then_some(prev),
79+ )
80+ .ok();
81+}
gix/src/worktree/mod.rs+15−1
@@ -24,13 +24,27 @@ pub type Index = gix_fs::SharedFileSnapshot<gix_index::File>; · 23 unmodified lines
2424 #[cfg(feature = "index")]
2525 pub enum IndexPersistedOrInMemory {
2626 /// The index as loaded from disk, and shared across clones of the owning `Repository`.
27− Persisted(crate::worktree::Index),
27+ Persisted(Index),
2828 /// A temporary index as created from the `HEAD^{tree}`, with the file path set to the place where it would be stored naturally.
2929 ///
3030 /// Note that unless saved explicitly, it will not persist.
3131 InMemory(gix_index::File),
3232 }
3333
34+#[cfg(feature = "index")]
35+impl From<Index> for IndexPersistedOrInMemory {
36+ fn from(value: Index) -> Self {
37+ IndexPersistedOrInMemory::Persisted(value)
38+ }
39+}
40+
41+#[cfg(feature = "index")]
42+impl From<gix_index::File> for IndexPersistedOrInMemory {
43+ fn from(value: gix_index::File) -> Self {
44+ IndexPersistedOrInMemory::InMemory(value)
45+ }
46+}
47+
3448 /// A stand-in to a worktree as result of a worktree iteration.
3549 ///
3650 /// It provides access to typical worktree state, but may not actually point to a valid checkout as the latter has been moved or
gix/tests/repository/mod.rs+35−8
@@ -19,6 +19,7 @@ mod worktree; · 18 unmodified lines
1919 mod dirwalk {
2020 use gix_dir::entry::Kind::*;
2121 use gix_dir::walk::EmissionMode;
22+ use std::sync::atomic::AtomicBool;
2223
2324 #[test]
2425 fn basics() -> crate::Result {
@@ -26,22 +27,48 @@ mod dirwalk { · 1 unmodified line
2627 let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory);
2728 let mut collect = gix::dir::walk::delegate::Collect::default();
2829 let index = repo.index()?;
29− repo.dirwalk(&index, None::<&str>, untracked_only, &mut collect)?;
30+ repo.dirwalk(
31+ &index,
32+ None::<&str>,
33+ &AtomicBool::default(),
34+ untracked_only,
35+ &mut collect,
36+ )?;
37+ let expected = [
38+ ("all-untracked".to_string(), Repository),
39+ ("bare-repo-with-index.git".to_string(), Directory),
40+ ("bare.git".into(), Directory),
41+ ("non-bare-repo-without-index".into(), Repository),
42+ ("some".into(), Directory),
43+ ];
3044 assert_eq!(
3145 collect
3246 .into_entries_by_path()
3347 .into_iter()
3448 .map(|e| (e.0.rela_path.to_string(), e.0.disk_kind.expect("kind is known")))
3549 .collect::<Vec<_>>(),
36− [
37− ("all-untracked".to_string(), Repository),
38− ("bare-repo-with-index.git".to_string(), Directory),
39− ("bare.git".into(), Directory),
40− ("non-bare-repo-without-index".into(), Repository),
41− ("some".into(), Directory)
42− ],
50+ expected,
4351 "note how bare repos are just directories by default"
4452 );
53+ let mut iter = repo.dirwalk_iter(index, None::<&str>, Default::default(), untracked_only)?;
54+ let mut actual: Vec<_> = iter
55+ .by_ref()
56+ .map(Result::unwrap)
57+ .map(|item| {
58+ (
59+ item.entry.rela_path.to_string(),
60+ item.entry.disk_kind.expect("kind is known"),
61+ )
62+ })
63+ .collect();
64+ actual.sort_by(|a, b| a.0.cmp(&b.0));
65+ assert_eq!(actual, expected, "the iterator works the same");
66+ let out = iter.into_outcome().expect("iteration done and no error");
67+ assert_eq!(
68+ out.dirwalk.returned_entries,
69+ expected.len(),
70+ "just a minor sanity check, assuming everything else works as well"
71+ );
4572 Ok(())
4673 }
4774 }
gix/tests/repository/open.rs+8−2
@@ -268,8 +268,14 @@ mod object_caches { · 267 unmodified lines
268268 fn default_git_and_custom_caches() -> crate::Result {
269269 let opts = gix::open::Options::isolated();
270270 let repo = named_subrepo_opts("make_config_repos.sh", "object-caches", opts)?;
271− assert_eq!(repo.objects.has_object_cache(), cfg!(feature = "comfort"));
272− assert_eq!(repo.objects.has_pack_cache(), cfg!(feature = "comfort"));
271+ assert_eq!(
272+ repo.objects.has_object_cache(),
273+ cfg!(all(feature = "parallel", feature = "comfort"))
274+ );
275+ assert_eq!(
276+ repo.objects.has_pack_cache(),
277+ cfg!(all(feature = "parallel", feature = "comfort"))
278+ );
273279 Ok(())
274280 }
275281
justfile+1−0
@@ -181,6 +181,7 @@ unit-tests: · 180 unmodified lines
181181 cargo test -p gix-protocol --features blocking-client
182182 cargo test -p gix-protocol --features async-client
183183 cargo test -p gix --no-default-features
184+ cargo test -p gix --no-default-features --features basic,extras,comfort
184185 cargo test -p gix --features async-network-client
185186 cargo test -p gix --features blocking-network-client
186187 cargo test -p gitoxide-core --lib