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
86 } else {86 } else {
87 Vec::new()87 Vec::new()
88 },88 },
89 &gix::interrupt::IS_INTERRUPTED,
89 options,90 options,
90 &mut collect,91 &mut collect,
91 )?;92 )?;
gix-actor/src/signature/decode.rs+7−2
@@ -36,9 +36,14 @@ pub(crate) mod function { · 35 unmodified lines
36 take_while(1..=2, AsChar::is_dec_digit)36 take_while(1..=2, AsChar::is_dec_digit)
37 .verify_map(|v| to_signed::<OffsetInSeconds>(v).ok())37 .verify_map(|v| to_signed::<OffsetInSeconds>(v).ok())
38 .context(StrContext::Expected("MM".into())),38 .context(StrContext::Expected("MM".into())),
39 take_while(0.., AsChar::is_dec_digit).map(|v: &[u8]| v),
39 )40 )
40 .map(|(time, sign, hours, minutes)| {41 .map(|(time, sign, hours, minutes, trailing_digits)| {
41 let offset = (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 };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 };
42 Time {47 Time {
43 seconds: time,48 seconds: time,
44 offset,49 offset,
gix-actor/tests/signature/mod.rs+26−1
@@ -53,7 +53,7 @@ mod write_to { · 52 unmodified lines
53}53}
54 54
55use bstr::ByteSlice;55use bstr::ByteSlice;
56use gix_actor::Signature;56use gix_actor::{Signature, SignatureRef};
57 57
58#[test]58#[test]
59fn trim() {59fn trim() {
@@ -80,3 +80,28 @@ fn round_trip() -> Result<(), Box<dyn std::error::Error>> { · 20 unmodified lines
80 }80 }
81 Ok(())81 Ok(())
82}82}
83
84#[test]
85fn 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
134 emit_ignored,134 emit_ignored,
135 for_deletion,135 for_deletion,
136 classify_untracked_bare_repositories,136 classify_untracked_bare_repositories,
137 symlinks_to_directories_are_ignored_like_directories,
137 ..138 ..
138 }: Options,139 }: Options,
139 ctx: &mut Context<'_>,140 ctx: &mut Context<'_>,
@@ -199,6 +200,15 @@ pub fn path( · 59 unmodified lines
199 .pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)200 .pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
200 .map(Into::into);201 .map(Into::into);
201 202
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
202 let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {212 let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {
203 if recurse_repositories {213 if recurse_repositories {
204 return current_kind;214 return current_kind;
@@ -245,7 +255,7 @@ pub fn path( · 40 unmodified lines
245 .as_mut()255 .as_mut()
246 .map_or(Ok(None), |stack| {256 .map_or(Ok(None), |stack| {
247 stack257 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)
249 .map(|platform| platform.excluded_kind())259 .map(|platform| platform.excluded_kind())
250 })260 })
251 .map_err(Error::ExcludesAccess)?261 .map_err(Error::ExcludesAccess)?
gix-dir/src/walk/function.rs+13−5
@@ -69,9 +69,16 @@ pub fn walk( · 68 unmodified lines
69 )?;69 )?;
70 if !can_recurse(70 if !can_recurse(
71 buf.as_bstr(),71 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 },
73 options.for_deletion,80 options.for_deletion,
74 worktree_root_is_repository, /* is root */81 worktree_root_is_repository,
75 delegate,82 delegate,
76 ) {83 ) {
77 if buf.is_empty() && !root_info.disk_kind.map_or(false, |kind| kind.is_dir()) {84 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
147 rela_path: &BStr,154 rela_path: &BStr,
148 info: classify::Outcome,155 info: classify::Outcome,
149 for_deletion: Option<ForDeletionMode>,156 for_deletion: Option<ForDeletionMode>,
150 is_root: bool,157 worktree_root_is_repository: bool,
151 delegate: &mut dyn Delegate,158 delegate: &mut dyn Delegate,
152) -> bool {159) -> 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 {
154 return false;162 return false;
155 }163 }
156 delegate.can_recurse(164 delegate.can_recurse(
157 EntryRef::from_outcome(Cow::Borrowed(rela_path), info),165 EntryRef::from_outcome(Cow::Borrowed(rela_path), info),
158 for_deletion,166 for_deletion,
159 is_root,167 worktree_root_is_repository,
160 )168 )
161}169}
162 170
gix-dir/src/walk/mod.rs+15−0
@@ -1,6 +1,7 @@
1use crate::{entry, EntryRef};1use crate::{entry, EntryRef};
2use bstr::BStr;2use bstr::BStr;
3use std::path::PathBuf;3use std::path::PathBuf;
4use std::sync::atomic::AtomicBool;
4 5
5/// A type returned by the [`Delegate::emit()`] as passed to [`walk()`](function::walk()).6/// A type returned by the [`Delegate::emit()`] as passed to [`walk()`](function::walk()).
6#[derive(Debug, Copy, Clone, Eq, PartialEq)]7#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -183,10 +184,22 @@ pub struct Options { · 176 unmodified lines
183 pub emit_empty_directories: bool,184 pub emit_empty_directories: bool,
184 /// If `None`, no entries inside of collapsed directories are emitted. Otherwise, act as specified by `Some(mode)`.185 /// If `None`, no entries inside of collapsed directories are emitted. Otherwise, act as specified by `Some(mode)`.
185 pub emit_collapsed: Option<CollapsedEntriesEmissionMode>,186 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,
186}195}
187 196
188/// All information that is required to perform a dirwalk, and classify paths properly.197/// All information that is required to perform a dirwalk, and classify paths properly.
189pub struct Context<'a> {198pub 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>,
190 /// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`].203 /// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`].
191 ///204 ///
192 /// It's used to help us differentiate our own `.git` directory from nested unrelated repositories,205 /// 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
261#[derive(Debug, thiserror::Error)]274#[derive(Debug, thiserror::Error)]
262#[allow(missing_docs)]275#[allow(missing_docs)]
263pub enum Error {276pub enum Error {
277 #[error("Interrupted")]
278 Interrupted,
264 #[error("Worktree root at '{}' is not a directory", root.display())]279 #[error("Worktree root at '{}' is not a directory", root.display())]
265 WorktreeRootIsFile { root: PathBuf },280 WorktreeRootIsFile { root: PathBuf },
266 #[error("Traversal root '{}' contains relative path components and could not be normalized", root.display())]281 #[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 @@
1use bstr::{BStr, BString, ByteSlice};1use bstr::{BStr, BString, ByteSlice};
2use std::borrow::Cow;2use std::borrow::Cow;
3use std::path::{Path, PathBuf};3use std::path::{Path, PathBuf};
4use std::sync::atomic::Ordering;
4 5
5use crate::entry::{PathspecMatch, Status};6use crate::entry::{PathspecMatch, Status};
6use crate::walk::function::{can_recurse, emit_entry};7use crate::walk::function::{can_recurse, emit_entry};
@@ -23,6 +24,9 @@ pub(super) fn recursive( · 16 unmodified lines
23 out: &mut Outcome,24 out: &mut Outcome,
24 state: &mut State,25 state: &mut State,
25) -> Result<(Action, bool), Error> {26) -> Result<(Action, bool), Error> {
27 if ctx.should_interrupt.map_or(false, |flag| flag.load(Ordering::Relaxed)) {
28 return Err(Error::Interrupted);
29 }
26 out.read_dir_calls += 1;30 out.read_dir_calls += 1;
27 let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir {31 let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir {
28 path: current.to_owned(),32 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
17(cd immediate-breakout-symlink17(cd immediate-breakout-symlink
18 ln -s .. breakout18 ln -s .. breakout
19)19)
20
21git init excluded-symlinks-to-dir
22(cd excluded-symlinks-to-dir
23 cat <<EOF >.gitignore
24src1
25src2/
26file1
27file2/
28ignored
29ignored-must-be-dir/
30EOF
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
45ln -s excluded-symlinks-to-dir worktree-root-is-symlink
gix-dir/tests/walk/mod.rs+127−0
@@ -1,5 +1,6 @@
1use gix_dir::{walk, EntryRef};1use gix_dir::{walk, EntryRef};
2use pretty_assertions::assert_eq;2use pretty_assertions::assert_eq;
3use std::sync::atomic::AtomicBool;
3 4
4use crate::walk_utils::{5use crate::walk_utils::{
5 collect, collect_filtered, collect_filtered_with_cwd, entry, entry_dirstat, entry_nokind, entry_nomatch, entryps,6 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
16use gix_dir::walk::ForDeletionMode;17use gix_dir::walk::ForDeletionMode;
17use gix_ignore::Kind::*;18use gix_ignore::Kind::*;
18 19
20#[test]
21#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
22fn 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
19#[test]95#[test]
20#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]96#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
21fn root_may_not_lead_through_symlinks() -> crate::Result {97fn root_may_not_lead_through_symlinks() -> crate::Result {
@@ -43,6 +119,57 @@ fn root_may_not_lead_through_symlinks() -> crate::Result { · 21 unmodified lines
43 Ok(())119 Ok(())
44}120}
45 121
122#[test]
123#[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")]
124fn 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]
156fn 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
46#[test]173#[test]
47fn empty_root() -> crate::Result {174fn empty_root() -> crate::Result {
48 let root = fixture("empty");175 let root = fixture("empty");
gix-dir/tests/walk_utils/mod.rs+10−1
@@ -2,6 +2,7 @@ use bstr::BStr; · 1 unmodified line
2use gix_dir::{entry, walk, Entry};2use gix_dir::{entry, walk, Entry};
3use gix_testtools::scripted_fixture_read_only;3use gix_testtools::scripted_fixture_read_only;
4use std::path::{Path, PathBuf};4use std::path::{Path, PathBuf};
5use std::sync::atomic::AtomicBool;
5 6
6pub fn fixture_in(filename: &str, name: &str) -> PathBuf {7pub fn fixture_in(filename: &str, name: &str) -> PathBuf {
7 let root = scripted_fixture_read_only(format!("{filename}.sh")).expect("script works");8 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
31 emit_untracked: walk::EmissionMode::Matching,32 emit_untracked: walk::EmissionMode::Matching,
32 emit_empty_directories: true,33 emit_empty_directories: true,
33 emit_collapsed: None,34 emit_collapsed: None,
35 symlinks_to_directories_are_ignored_like_directories: false,
34 }36 }
35}37}
36 38
@@ -268,7 +270,11 @@ pub fn try_collect_filtered_opts( · 231 unmodified lines
268 cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,270 cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>,
269 patterns: impl IntoIterator<Item = impl AsRef<BStr>>,271 patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
270 delegate: &mut dyn gix_dir::walk::Delegate,272 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<'_>,
272) -> Result<(walk::Outcome, PathBuf), walk::Error> {278) -> Result<(walk::Outcome, PathBuf), walk::Error> {
273 let git_dir = worktree_root.join(git_dir.unwrap_or(".git"));279 let git_dir = worktree_root.join(git_dir.unwrap_or(".git"));
274 let mut index = std::fs::read(git_dir.join("index")).ok().map_or_else(280 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
342 excludes: Some(&mut stack),348 excludes: Some(&mut stack),
343 objects: &gix_object::find::Never,349 objects: &gix_object::find::Never,
344 explicit_traversal_root,350 explicit_traversal_root,
351 should_interrupt,
345 },352 },
346 )353 )
347}354}
@@ -349,6 +356,7 @@ pub fn try_collect_filtered_opts( · 1 unmodified line
349pub struct Options<'a> {356pub struct Options<'a> {
350 pub fresh_index: bool,357 pub fresh_index: bool,
351 pub git_dir: Option<&'a str>,358 pub git_dir: Option<&'a str>,
359 pub should_interrupt: Option<&'a AtomicBool>,
352}360}
353 361
354impl<'a> Options<'a> {362impl<'a> Options<'a> {
@@ -365,6 +373,7 @@ impl<'a> Default for Options<'a> { · 10 unmodified lines
365 Options {373 Options {
366 fresh_index: true,374 fresh_index: true,
367 git_dir: None,375 git_dir: None,
376 should_interrupt: None,
368 }377 }
369 }378 }
370}379}
gix-index/src/entry/mod.rs+0−3
@@ -20,9 +20,6 @@ mod write; · 19 unmodified lines
20 20
21use bitflags::bitflags;21use bitflags::bitflags;
22 22
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
26bitflags! {23bitflags! {
27 /// The kind of file of an entry.24 /// The kind of file of an entry.
28 #[derive(Copy, Clone, Debug, PartialEq, Eq)]25 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
gix-object/tests/commit/from_bytes.rs+27−0
@@ -8,6 +8,33 @@ use crate::{ · 7 unmodified lines
8 fixture_name, linus_signature, signature,8 fixture_name, linus_signature, signature,
9};9};
10 10
11#[test]
12fn 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
11#[test]38#[test]
12fn unsigned() -> crate::Result {39fn unsigned() -> crate::Result {
13 assert_eq!(40 assert_eq!(
gix-object/tests/fixtures/commit/invalid-timestamp.txt+6−0
@@ -0,0 +1,6 @@
1tree 7989dfb2ec2f41914611a22fb30bbc2b3849df9a
2parent 8845ae683e2688bc619baade49510c17e978518f
3author Name <name@example.com> 1312735823 +051800
4committer Name <name@example.com> 1312735823 +051800
5
6edit 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
176 matches!(pattern.path.get(common_len), None | Some(&b'/'))176 matches!(pattern.path.get(common_len), None | Some(&b'/'))
177 } else {177 } else {
178 relative_path.get(common_len) == Some(&b'/')178 relative_path.get(common_len) == Some(&b'/')
179 }179 };
180 }180 }
181 }181 }
182 }182 }
183 if is_match {183 if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
184 return !pattern.is_excluded();184 return !pattern.is_excluded();
185 }185 }
186 }186 }
@@ -191,7 +191,7 @@ impl Search { · 4 unmodified lines
191 /// Returns `true` if `relative_path` matches the prefix of this pathspec.191 /// Returns `true` if `relative_path` matches the prefix of this pathspec.
192 ///192 ///
193 /// For example, the relative path `d` matches `d/`, `d*/`, `d/` and `d/*`, but not `d/d/*` or `dir`.193 /// 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 be194 /// When `leading` is `true`, then `d` matches `d/d` as well. Thus, `relative_path` must may be
195 /// partially included in `pathspec`, otherwise it has to be fully included.195 /// partially included in `pathspec`, otherwise it has to be fully included.
196 pub fn directory_matches_prefix(&self, relative_path: &BStr, leading: bool) -> bool {196 pub fn directory_matches_prefix(&self, relative_path: &BStr, leading: bool) -> bool {
197 if self.patterns.is_empty() {197 if self.patterns.is_empty() {
@@ -233,7 +233,7 @@ impl Search { · 35 unmodified lines
233 };233 };
234 }234 }
235 }235 }
236 if is_match {236 if is_match && (!pattern.is_excluded() || pattern.always_matches()) {
237 return !pattern.is_excluded();237 return !pattern.is_excluded();
238 }238 }
239 }239 }
gix-pathspec/tests/search/mod.rs+49−2
@@ -135,6 +135,47 @@ fn no_pathspecs_match_everything() -> crate::Result { · 134 unmodified lines
135 Ok(())135 Ok(())
136}136}
137 137
138#[test]
139fn 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
138#[test]179#[test]
139fn starts_with() -> crate::Result {180fn starts_with() -> crate::Result {
140 let mut search = gix_pathspec::Search::from_specs(pathspecs(&["a/*"]), None, Path::new(""))?;181 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
261 None,302 None,
262 Path::new(""),303 Path::new(""),
263 )?;304 )?;
264 assert!(!search.can_match_relative_path("b".into(), None));305 assert!(
265 assert!(!search.can_match_relative_path("a".into(), None));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 );
266 assert!(search.can_match_relative_path("c".into(), None));313 assert!(search.can_match_relative_path("c".into(), None));
267 assert!(search.can_match_relative_path("c/".into(), None));314 assert!(search.can_match_relative_path("c/".into(), None));
268 315
gix-status/src/index_as_worktree_with_renames/mod.rs+1−0
@@ -87,6 +87,7 @@ pub(super) mod function { · 86 unmodified lines
87 gix_dir::walk(87 gix_dir::walk(
88 worktree,88 worktree,
89 gix_dir::walk::Context {89 gix_dir::walk::Context {
90 should_interrupt: Some(ctx.should_interrupt),
90 git_dir_realpath: dirwalk_ctx.git_dir_realpath,91 git_dir_realpath: dirwalk_ctx.git_dir_realpath,
91 current_dir: dirwalk_ctx.current_dir,92 current_dir: dirwalk_ctx.current_dir,
92 index,93 index,
gix/Cargo.toml+1−1
@@ -74,7 +74,7 @@ interrupt = ["dep:signal-hook", "gix-tempfile/signals"] · 73 unmodified lines
74index = ["dep:gix-index"]74index = ["dep:gix-index"]
75 75
76## Support directory walks with Git-style annoations.76## Support directory walks with Git-style annoations.
77dirwalk = ["dep:gix-dir"]77dirwalk = ["dep:gix-dir", "attributes", "excludes"]
78 78
79## Access to credential helpers, which provide credentials for URLs.79## Access to credential helpers, which provide credentials for URLs.
80# Note that `gix-negotiate` just piggibacks here, as 'credentials' is equivalent to 'fetch & push' right now.80# 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 @@
1use super::Iter;
2use crate::bstr::BString;
3use crate::util::OwnedOrStaticAtomicBool;
4use crate::worktree::IndexPersistedOrInMemory;
5use crate::{dirwalk, PathspecDetached, Repository};
6use std::path::PathBuf;
7
8/// An entry of the directory walk as returned by the [iterator](Iter).
9pub 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
19impl 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).
29pub 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)]
46pub 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
59impl 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
121impl 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
134impl 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")]
161impl 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
167struct Collect {
168 #[cfg(feature = "parallel")]
169 tx: std::sync::mpsc::Sender<Item>,
170 #[cfg(not(feature = "parallel"))]
171 items: Vec<Item>,
172}
173
174impl 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 @@
1use gix_dir::walk::{CollapsedEntriesEmissionMode, EmissionMode, ForDeletionMode};
2
3use crate::{config, AttributeStack, Pathspec};
4use std::path::PathBuf;
5
6mod options;
7
8///
9#[allow(clippy::empty_docs)]
10pub 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.
25pub 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)]
44pub 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).
60pub 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)]
76pub 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 @@
1use crate::dirwalk::Options;
1use gix_dir::walk::{CollapsedEntriesEmissionMode, EmissionMode, ForDeletionMode};2use gix_dir::walk::{CollapsedEntriesEmissionMode, EmissionMode, ForDeletionMode};
2 3
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)]
7pub 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
23/// Construction4/// Construction
24impl Options {5impl Options {
25 pub(crate) fn from_fs_caps(caps: gix_fs::Capabilities) -> Self {6 pub(crate) fn from_fs_caps(caps: gix_fs::Capabilities) -> Self {
@@ -36,6 +17,7 @@ impl Options { · 10 unmodified lines
36 classify_untracked_bare_repositories: false,17 classify_untracked_bare_repositories: false,
37 emit_collapsed: None,18 emit_collapsed: None,
38 empty_patterns_match_prefix: false,19 empty_patterns_match_prefix: false,
20 symlinks_to_directories_are_ignored_like_directories: false,
39 }21 }
40 }22 }
41}23}
@@ -54,6 +36,8 @@ impl From<Options> for gix_dir::walk::Options { · 12 unmodified lines
54 emit_empty_directories: v.emit_empty_directories,36 emit_empty_directories: v.emit_empty_directories,
55 classify_untracked_bare_repositories: v.classify_untracked_bare_repositories,37 classify_untracked_bare_repositories: v.classify_untracked_bare_repositories,
56 emit_collapsed: v.emit_collapsed,38 emit_collapsed: v.emit_collapsed,
39 symlinks_to_directories_are_ignored_like_directories: v
40 .symlinks_to_directories_are_ignored_like_directories,
57 }41 }
58 }42 }
59}43}
@@ -180,4 +164,23 @@ impl Options { · 120 unmodified lines
180 self.emit_collapsed = value;164 self.emit_collapsed = value;
181 self165 self
182 }166 }
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 }
183}186}
gix/src/lib.rs+3−1
@@ -177,8 +177,8 @@ pub use types::{Pathspec, PathspecDetached, Submodule}; · 176 unmodified lines
177#[allow(clippy::empty_docs)]177#[allow(clippy::empty_docs)]
178pub mod clone;178pub mod clone;
179pub mod commit;179pub mod commit;
180#[cfg(feature = "dirwalk")]
181///180///
181#[cfg(feature = "dirwalk")]
182#[allow(clippy::empty_docs)]182#[allow(clippy::empty_docs)]
183pub mod dirwalk;183pub mod dirwalk;
184pub mod head;184pub mod head;
@@ -191,6 +191,8 @@ pub mod repository; · 6 unmodified lines
191#[cfg(feature = "attributes")]191#[cfg(feature = "attributes")]
192pub mod submodule;192pub mod submodule;
193pub mod tag;193pub mod tag;
194#[cfg(any(feature = "dirwalk", feature = "status"))]
195pub(crate) mod util;
194 196
195///197///
196#[allow(clippy::empty_docs)]198#[allow(clippy::empty_docs)]
gix/src/repository/dirwalk.rs+33−38
@@ -1,37 +1,8 @@
1use crate::bstr::BStr;1use crate::bstr::{BStr, BString};
2use crate::{config, dirwalk, AttributeStack, Pathspec, Repository};2use crate::util::OwnedOrStaticAtomicBool;
3use std::path::PathBuf;3use crate::worktree::IndexPersistedOrInMemory;
4 4use crate::{config, dirwalk, Repository};
5/// The error returned by [dirwalk()](Repository::dirwalk()).5use std::sync::atomic::AtomicBool;
6#[derive(Debug, thiserror::Error)]
7#[allow(missing_docs)]
8pub 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).
24pub 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}
35 6
36impl Repository {7impl Repository {
37 /// Return default options suitable for performing a directory walk on this repository.8 /// Return default options suitable for performing a directory walk on this repository.
@@ -42,7 +13,8 @@ impl Repository { · 4 unmodified lines
42 }13 }
43 14
44 /// Perform a directory walk configured with `options` under control of the `delegate`. Use `patterns` to15 /// 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.
46 ///18 ///
47 /// The `index` is used to determine if entries are tracked, and for excludes and attributes19 /// The `index` is used to determine if entries are tracked, and for excludes and attributes
48 /// lookup. Note that items will only count as tracked if they have the [`gix_index::entry::Flags::UPTODATE`]20 /// 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
53 &self,25 &self,
54 index: &gix_index::State,26 index: &gix_index::State,
55 patterns: impl IntoIterator<Item = impl AsRef<BStr>>,27 patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
28 should_interrupt: &AtomicBool,
56 options: dirwalk::Options,29 options: dirwalk::Options,
57 delegate: &mut dyn gix_dir::walk::Delegate,30 delegate: &mut dyn gix_dir::walk::Delegate,
58 ) -> Result<Outcome<'_>, Error> {31 ) -> Result<dirwalk::Outcome<'_>, dirwalk::Error> {
59 let _span = gix_trace::coarse!("gix::dirwalk");32 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)?;
61 let mut excludes = self.excludes(34 let mut excludes = self.excludes(
62 index,35 index,
63 None,36 None,
@@ -78,6 +51,7 @@ impl Repository { · 14 unmodified lines
78 let (outcome, traversal_root) = gix_dir::walk(51 let (outcome, traversal_root) = gix_dir::walk(
79 workdir,52 workdir,
80 gix_dir::walk::Context {53 gix_dir::walk::Context {
54 should_interrupt: Some(should_interrupt),
81 git_dir_realpath: git_dir_realpath.as_ref(),55 git_dir_realpath: git_dir_realpath.as_ref(),
82 current_dir: self.current_dir(),56 current_dir: self.current_dir(),
83 index,57 index,
@@ -101,11 +75,32 @@ impl Repository { · 17 unmodified lines
101 delegate,75 delegate,
102 )?;76 )?;
103 77
104 Ok(Outcome {78 Ok(dirwalk::Outcome {
105 dirwalk: outcome,79 dirwalk: outcome,
106 traversal_root,80 traversal_root,
107 excludes,81 excludes,
108 pathspec,82 pathspec,
109 })83 })
110 }84 }
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 }
111}106}
gix/src/repository/mod.rs+1−1
@@ -45,7 +45,7 @@ mod config; · 44 unmodified lines
45pub mod diff;45pub mod diff;
46///46///
47#[cfg(feature = "dirwalk")]47#[cfg(feature = "dirwalk")]
48pub mod dirwalk;48mod dirwalk;
49///49///
50#[cfg(feature = "attributes")]50#[cfg(feature = "attributes")]
51pub mod filter;51pub mod filter;
gix/src/status/index_worktree.rs+9−23
@@ -297,7 +297,7 @@ pub struct Iter { · 296 unmodified lines
297 std::thread::JoinHandle<Result<iter::Outcome, crate::status::index_worktree::Error>>,297 std::thread::JoinHandle<Result<iter::Outcome, crate::status::index_worktree::Error>>,
298 )>,298 )>,
299 #[cfg(feature = "parallel")]299 #[cfg(feature = "parallel")]
300 should_interrupt: crate::status::OwnedOrStaticAtomic,300 should_interrupt: crate::status::OwnedOrStaticAtomicBool,
301 /// Without parallelization, the iterator has to buffer all changes in advance.301 /// Without parallelization, the iterator has to buffer all changes in advance.
302 #[cfg(not(feature = "parallel"))]302 #[cfg(not(feature = "parallel"))]
303 items: std::vec::IntoIter<iter::Item>,303 items: std::vec::IntoIter<iter::Item>,
@@ -744,11 +744,18 @@ pub mod iter { · 440 unmodified lines
744 }744 }
745 }745 }
746 746
747 /// Access
747 impl super::Iter {748 impl super::Iter {
748 /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed.749 /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed.
749 pub fn outcome_mut(&mut self) -> Option<&mut Outcome> {750 pub fn outcome_mut(&mut self) -> Option<&mut Outcome> {
750 self.out.as_mut()751 self.out.as_mut()
751 }752 }
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 }
752 }759 }
753 760
754 impl super::Iter {761 impl super::Iter {
@@ -781,28 +788,7 @@ pub mod iter { · 26 unmodified lines
781 #[cfg(feature = "parallel")]788 #[cfg(feature = "parallel")]
782 impl Drop for super::Iter {789 impl Drop for super::Iter {
783 fn drop(&mut self) {790 fn drop(&mut self) {
784 use crate::status::OwnedOrStaticAtomic;791 crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt);
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();
806 }792 }
807 }793 }
808 794
gix/src/status/mod.rs+2−34
@@ -1,9 +1,7 @@
1use crate::config::cache::util::ApplyLeniencyDefault;1use crate::config::cache::util::ApplyLeniencyDefault;
2use crate::util::OwnedOrStaticAtomicBool;
2use crate::{config, Repository};3use crate::{config, Repository};
3pub use gix_status as plumbing;4pub use gix_status as plumbing;
4use std::ops::Deref;
5use std::sync::atomic::AtomicBool;
6use std::sync::Arc;
7 5
8/// A structure to hold options configuring the status request, which can then be turned into an iterator.6/// A structure to hold options configuring the status request, which can then be turned into an iterator.
9pub struct Platform<'repo, Progress>7pub struct Platform<'repo, Progress>
@@ -15,37 +13,7 @@ where · 5 unmodified lines
15 index: Option<crate::worktree::IndexPersistedOrInMemory>,13 index: Option<crate::worktree::IndexPersistedOrInMemory>,
16 submodules: Submodule,14 submodules: Submodule,
17 index_worktree_options: index_worktree::Options,15 index_worktree_options: index_worktree::Options,
18 should_interrupt: Option<OwnedOrStaticAtomic>,16 should_interrupt: Option<OwnedOrStaticAtomicBool>,
19}
20
21#[derive(Clone)]
22enum 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
31impl Default for OwnedOrStaticAtomic {
32 fn default() -> Self {
33 OwnedOrStaticAtomic::Owned {
34 flag: Arc::new(AtomicBool::default()),
35 private: true,
36 }
37 }
38}
39
40impl 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 }
49}17}
50 18
51/// How to obtain a submodule's status.19/// How to obtain a submodule's status.
gix/src/status/platform.rs+3−3
@@ -1,4 +1,4 @@
1use crate::status::{index_worktree, OwnedOrStaticAtomic, Platform, Submodule, UntrackedFiles};1use crate::status::{index_worktree, OwnedOrStaticAtomicBool, Platform, Submodule, UntrackedFiles};
2use std::sync::atomic::AtomicBool;2use std::sync::atomic::AtomicBool;
3 3
4/// Builder4/// Builder
@@ -45,7 +45,7 @@ where · 40 unmodified lines
45 ///45 ///
46 /// If it is `true`, the iteration will stop immediately.46 /// If it is `true`, the iteration will stop immediately.
47 pub fn should_interrupt_shared(mut self, should_interrupt: &'static AtomicBool) -> Self {47 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));
49 self49 self
50 }50 }
51 51
@@ -53,7 +53,7 @@ where · 1 unmodified line
53 ///53 ///
54 /// If it is `true`, the iteration will stop immediately.54 /// If it is `true`, the iteration will stop immediately.
55 pub fn should_interrupt_owned(mut self, should_interrupt: std::sync::Arc<AtomicBool>) -> Self {55 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 {
57 flag: should_interrupt,57 flag: should_interrupt,
58 private: false,58 private: false,
59 });59 });
gix/src/util.rs+81−0
@@ -0,0 +1,81 @@
1use std::ops::Deref;
2use std::sync::atomic::AtomicBool;
3use std::sync::Arc;
4
5#[derive(Clone)]
6pub 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
15impl Default for OwnedOrStaticAtomicBool {
16 fn default() -> Self {
17 OwnedOrStaticAtomicBool::Owned {
18 flag: Arc::new(AtomicBool::default()),
19 private: true,
20 }
21 }
22}
23
24impl 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
35impl From<&'static AtomicBool> for OwnedOrStaticAtomicBool {
36 fn from(value: &'static AtomicBool) -> Self {
37 OwnedOrStaticAtomicBool::Shared(value)
38 }
39}
40
41impl<'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
50impl From<Arc<AtomicBool>> for OwnedOrStaticAtomicBool {
51 fn from(flag: Arc<AtomicBool>) -> Self {
52 OwnedOrStaticAtomicBool::Owned { flag, private: false }
53 }
54}
55#[cfg(feature = "parallel")]
56pub 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
24#[cfg(feature = "index")]24#[cfg(feature = "index")]
25pub enum IndexPersistedOrInMemory {25pub enum IndexPersistedOrInMemory {
26 /// The index as loaded from disk, and shared across clones of the owning `Repository`.26 /// The index as loaded from disk, and shared across clones of the owning `Repository`.
27 Persisted(crate::worktree::Index),27 Persisted(Index),
28 /// A temporary index as created from the `HEAD^{tree}`, with the file path set to the place where it would be stored naturally.28 /// A temporary index as created from the `HEAD^{tree}`, with the file path set to the place where it would be stored naturally.
29 ///29 ///
30 /// Note that unless saved explicitly, it will not persist.30 /// Note that unless saved explicitly, it will not persist.
31 InMemory(gix_index::File),31 InMemory(gix_index::File),
32}32}
33 33
34#[cfg(feature = "index")]
35impl From<Index> for IndexPersistedOrInMemory {
36 fn from(value: Index) -> Self {
37 IndexPersistedOrInMemory::Persisted(value)
38 }
39}
40
41#[cfg(feature = "index")]
42impl From<gix_index::File> for IndexPersistedOrInMemory {
43 fn from(value: gix_index::File) -> Self {
44 IndexPersistedOrInMemory::InMemory(value)
45 }
46}
47
34/// A stand-in to a worktree as result of a worktree iteration.48/// A stand-in to a worktree as result of a worktree iteration.
35///49///
36/// It provides access to typical worktree state, but may not actually point to a valid checkout as the latter has been moved or50/// 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
19mod dirwalk {19mod dirwalk {
20 use gix_dir::entry::Kind::*;20 use gix_dir::entry::Kind::*;
21 use gix_dir::walk::EmissionMode;21 use gix_dir::walk::EmissionMode;
22 use std::sync::atomic::AtomicBool;
22 23
23 #[test]24 #[test]
24 fn basics() -> crate::Result {25 fn basics() -> crate::Result {
@@ -26,22 +27,48 @@ mod dirwalk { · 1 unmodified line
26 let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory);27 let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory);
27 let mut collect = gix::dir::walk::delegate::Collect::default();28 let mut collect = gix::dir::walk::delegate::Collect::default();
28 let index = repo.index()?;29 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 ];
30 assert_eq!(44 assert_eq!(
31 collect45 collect
32 .into_entries_by_path()46 .into_entries_by_path()
33 .into_iter()47 .into_iter()
34 .map(|e| (e.0.rela_path.to_string(), e.0.disk_kind.expect("kind is known")))48 .map(|e| (e.0.rela_path.to_string(), e.0.disk_kind.expect("kind is known")))
35 .collect::<Vec<_>>(),49 .collect::<Vec<_>>(),
36 [50 expected,
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 ],
43 "note how bare repos are just directories by default"51 "note how bare repos are just directories by default"
44 );52 );
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 );
45 Ok(())72 Ok(())
46 }73 }
47}74}
gix/tests/repository/open.rs+8−2
@@ -268,8 +268,14 @@ mod object_caches { · 267 unmodified lines
268 fn default_git_and_custom_caches() -> crate::Result {268 fn default_git_and_custom_caches() -> crate::Result {
269 let opts = gix::open::Options::isolated();269 let opts = gix::open::Options::isolated();
270 let repo = named_subrepo_opts("make_config_repos.sh", "object-caches", opts)?;270 let repo = named_subrepo_opts("make_config_repos.sh", "object-caches", opts)?;
271 assert_eq!(repo.objects.has_object_cache(), cfg!(feature = "comfort"));271 assert_eq!(
272 assert_eq!(repo.objects.has_pack_cache(), cfg!(feature = "comfort"));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 );
273 Ok(())279 Ok(())
274 }280 }
275 281
justfile+1−0
@@ -181,6 +181,7 @@ unit-tests: · 180 unmodified lines
181 cargo test -p gix-protocol --features blocking-client181 cargo test -p gix-protocol --features blocking-client
182 cargo test -p gix-protocol --features async-client182 cargo test -p gix-protocol --features async-client
183 cargo test -p gix --no-default-features183 cargo test -p gix --no-default-features
184 cargo test -p gix --no-default-features --features basic,extras,comfort
184 cargo test -p gix --features async-network-client185 cargo test -p gix --features async-network-client
185 cargo test -p gix --features blocking-network-client186 cargo test -p gix --features blocking-network-client
186 cargo test -p gitoxide-core --lib187 cargo test -p gitoxide-core --lib