Unified · Split
| @@ -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 | )?; |
| @@ -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, |
| @@ -53,7 +53,7 @@ mod write_to { · 52 unmodified lines | |||
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | use bstr::ByteSlice; | 55 | use bstr::ByteSlice; |
| 56 | use gix_actor::Signature; | 56 | use gix_actor::{Signature, SignatureRef}; |
| 57 | 57 | ||
| 58 | #[test] | 58 | #[test] |
| 59 | fn trim() { | 59 | fn 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] | ||
| 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 | } | ||
| @@ -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 | stack | 257 | 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)? |
| @@ -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 | ||
| @@ -1,6 +1,7 @@ | |||
| 1 | use crate::{entry, EntryRef}; | 1 | use crate::{entry, EntryRef}; |
| 2 | use bstr::BStr; | 2 | use bstr::BStr; |
| 3 | use std::path::PathBuf; | 3 | use std::path::PathBuf; |
| 4 | use 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. |
| 189 | pub struct Context<'a> { | 198 | 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>, | ||
| 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)] |
| 263 | pub enum Error { | 276 | pub 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())] |
| @@ -1,6 +1,7 @@ | |||
| 1 | use bstr::{BStr, BString, ByteSlice}; | 1 | use bstr::{BStr, BString, ByteSlice}; |
| 2 | use std::borrow::Cow; | 2 | use std::borrow::Cow; |
| 3 | use std::path::{Path, PathBuf}; | 3 | use std::path::{Path, PathBuf}; |
| 4 | use std::sync::atomic::Ordering; | ||
| 4 | 5 | ||
| 5 | use crate::entry::{PathspecMatch, Status}; | 6 | use crate::entry::{PathspecMatch, Status}; |
| 6 | use crate::walk::function::{can_recurse, emit_entry}; | 7 | use 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(), |
| @@ -17,3 +17,29 @@ git init immediate-breakout-symlink · 16 unmodified lines | |||
| 17 | (cd immediate-breakout-symlink | 17 | (cd immediate-breakout-symlink |
| 18 | ln -s .. breakout | 18 | ln -s .. breakout |
| 19 | ) | 19 | ) |
| 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 | ||
| @@ -1,5 +1,6 @@ | |||
| 1 | use gix_dir::{walk, EntryRef}; | 1 | use gix_dir::{walk, EntryRef}; |
| 2 | use pretty_assertions::assert_eq; | 2 | use pretty_assertions::assert_eq; |
| 3 | use std::sync::atomic::AtomicBool; | ||
| 3 | 4 | ||
| 4 | use crate::walk_utils::{ | 5 | use 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 | |||
| 16 | use gix_dir::walk::ForDeletionMode; | 17 | use gix_dir::walk::ForDeletionMode; |
| 17 | use gix_ignore::Kind::*; | 18 | use gix_ignore::Kind::*; |
| 18 | 19 | ||
| 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 | |||
| 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")] |
| 21 | fn root_may_not_lead_through_symlinks() -> crate::Result { | 97 | 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 | |||
| 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")] | ||
| 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 | |||
| 46 | #[test] | 173 | #[test] |
| 47 | fn empty_root() -> crate::Result { | 174 | fn empty_root() -> crate::Result { |
| 48 | let root = fixture("empty"); | 175 | let root = fixture("empty"); |
| @@ -2,6 +2,7 @@ use bstr::BStr; · 1 unmodified line | |||
| 2 | use gix_dir::{entry, walk, Entry}; | 2 | use gix_dir::{entry, walk, Entry}; |
| 3 | use gix_testtools::scripted_fixture_read_only; | 3 | use gix_testtools::scripted_fixture_read_only; |
| 4 | use std::path::{Path, PathBuf}; | 4 | use std::path::{Path, PathBuf}; |
| 5 | use std::sync::atomic::AtomicBool; | ||
| 5 | 6 | ||
| 6 | pub fn fixture_in(filename: &str, name: &str) -> PathBuf { | 7 | pub 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 | |||
| 349 | pub struct Options<'a> { | 356 | pub 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 | ||
| 354 | impl<'a> Options<'a> { | 362 | impl<'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 | } |
| @@ -20,9 +20,6 @@ mod write; · 19 unmodified lines | |||
| 20 | 20 | ||
| 21 | use bitflags::bitflags; | 21 | use 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 | ||
| 26 | bitflags! { | 23 | bitflags! { |
| 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)] |
| @@ -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] | ||
| 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 | |||
| 11 | #[test] | 38 | #[test] |
| 12 | fn unsigned() -> crate::Result { | 39 | fn unsigned() -> crate::Result { |
| 13 | assert_eq!( | 40 | assert_eq!( |
| @@ -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 | ||
| @@ -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 be | 194 | /// 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 | } |
| @@ -135,6 +135,47 @@ fn no_pathspecs_match_everything() -> crate::Result { · 134 unmodified lines | |||
| 135 | Ok(()) | 135 | Ok(()) |
| 136 | } | 136 | } |
| 137 | 137 | ||
| 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 | |||
| 138 | #[test] | 179 | #[test] |
| 139 | fn starts_with() -> crate::Result { | 180 | fn 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 | ||
| @@ -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, |
| @@ -74,7 +74,7 @@ interrupt = ["dep:signal-hook", "gix-tempfile/signals"] · 73 unmodified lines | |||
| 74 | index = ["dep:gix-index"] | 74 | index = ["dep:gix-index"] |
| 75 | 75 | ||
| 76 | ## Support directory walks with Git-style annoations. | 76 | ## Support directory walks with Git-style annoations. |
| 77 | dirwalk = ["dep:gix-dir"] | 77 | dirwalk = ["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. |
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -1,25 +1,6 @@ | |||
| 1 | use crate::dirwalk::Options; | ||
| 1 | use gix_dir::walk::{CollapsedEntriesEmissionMode, EmissionMode, ForDeletionMode}; | 2 | use 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)] | ||
| 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 | |||
| 23 | /// Construction | 4 | /// Construction |
| 24 | impl Options { | 5 | impl 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 | self | 165 | 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 | } |
| @@ -177,8 +177,8 @@ pub use types::{Pathspec, PathspecDetached, Submodule}; · 176 unmodified lines | |||
| 177 | #[allow(clippy::empty_docs)] | 177 | #[allow(clippy::empty_docs)] |
| 178 | pub mod clone; | 178 | pub mod clone; |
| 179 | pub mod commit; | 179 | pub mod commit; |
| 180 | #[cfg(feature = "dirwalk")] | ||
| 181 | /// | 180 | /// |
| 181 | #[cfg(feature = "dirwalk")] | ||
| 182 | #[allow(clippy::empty_docs)] | 182 | #[allow(clippy::empty_docs)] |
| 183 | pub mod dirwalk; | 183 | pub mod dirwalk; |
| 184 | pub mod head; | 184 | pub mod head; |
| @@ -191,6 +191,8 @@ pub mod repository; · 6 unmodified lines | |||
| 191 | #[cfg(feature = "attributes")] | 191 | #[cfg(feature = "attributes")] |
| 192 | pub mod submodule; | 192 | pub mod submodule; |
| 193 | pub mod tag; | 193 | pub mod tag; |
| 194 | #[cfg(any(feature = "dirwalk", feature = "status"))] | ||
| 195 | pub(crate) mod util; | ||
| 194 | 196 | ||
| 195 | /// | 197 | /// |
| 196 | #[allow(clippy::empty_docs)] | 198 | #[allow(clippy::empty_docs)] |
| @@ -1,37 +1,8 @@ | |||
| 1 | use crate::bstr::BStr; | 1 | use crate::bstr::{BStr, BString}; |
| 2 | use crate::{config, dirwalk, AttributeStack, Pathspec, Repository}; | 2 | use crate::util::OwnedOrStaticAtomicBool; |
| 3 | use std::path::PathBuf; | 3 | use crate::worktree::IndexPersistedOrInMemory; |
| 4 | 4 | use crate::{config, dirwalk, Repository}; | |
| 5 | /// The error returned by [dirwalk()](Repository::dirwalk()). | 5 | use std::sync::atomic::AtomicBool; |
| 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 | } | ||
| 35 | 6 | ||
| 36 | impl Repository { | 7 | impl 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` to | 15 | /// 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 attributes | 19 | /// 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 | } |
| @@ -45,7 +45,7 @@ mod config; · 44 unmodified lines | |||
| 45 | pub mod diff; | 45 | pub mod diff; |
| 46 | /// | 46 | /// |
| 47 | #[cfg(feature = "dirwalk")] | 47 | #[cfg(feature = "dirwalk")] |
| 48 | pub mod dirwalk; | 48 | mod dirwalk; |
| 49 | /// | 49 | /// |
| 50 | #[cfg(feature = "attributes")] | 50 | #[cfg(feature = "attributes")] |
| 51 | pub mod filter; | 51 | pub mod filter; |
| @@ -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 | ||
| @@ -1,9 +1,7 @@ | |||
| 1 | use crate::config::cache::util::ApplyLeniencyDefault; | 1 | use crate::config::cache::util::ApplyLeniencyDefault; |
| 2 | use crate::util::OwnedOrStaticAtomicBool; | ||
| 2 | use crate::{config, Repository}; | 3 | use crate::{config, Repository}; |
| 3 | pub use gix_status as plumbing; | 4 | pub use gix_status as plumbing; |
| 4 | use std::ops::Deref; | ||
| 5 | use std::sync::atomic::AtomicBool; | ||
| 6 | use 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. |
| 9 | pub struct Platform<'repo, Progress> | 7 | pub 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)] | ||
| 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 | } | ||
| 49 | } | 17 | } |
| 50 | 18 | ||
| 51 | /// How to obtain a submodule's status. | 19 | /// How to obtain a submodule's status. |
| @@ -1,4 +1,4 @@ | |||
| 1 | use crate::status::{index_worktree, OwnedOrStaticAtomic, Platform, Submodule, UntrackedFiles}; | 1 | use crate::status::{index_worktree, OwnedOrStaticAtomicBool, Platform, Submodule, UntrackedFiles}; |
| 2 | use std::sync::atomic::AtomicBool; | 2 | use std::sync::atomic::AtomicBool; |
| 3 | 3 | ||
| 4 | /// Builder | 4 | /// 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 | self | 49 | 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 | }); |
| @@ -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 | } | ||
| @@ -24,13 +24,27 @@ pub type Index = gix_fs::SharedFileSnapshot<gix_index::File>; · 23 unmodified lines | |||
| 24 | #[cfg(feature = "index")] | 24 | #[cfg(feature = "index")] |
| 25 | pub enum IndexPersistedOrInMemory { | 25 | pub 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")] | ||
| 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 | |||
| 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 or | 50 | /// It provides access to typical worktree state, but may not actually point to a valid checkout as the latter has been moved or |
| @@ -19,6 +19,7 @@ mod worktree; · 18 unmodified lines | |||
| 19 | mod dirwalk { | 19 | mod 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 | collect | 45 | 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 | } |
| @@ -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 | ||
| @@ -181,6 +181,7 @@ unit-tests: · 180 unmodified lines | |||
| 181 | cargo test -p gix-protocol --features blocking-client | 181 | cargo test -p gix-protocol --features blocking-client |
| 182 | cargo test -p gix-protocol --features async-client | 182 | cargo test -p gix-protocol --features async-client |
| 183 | cargo test -p gix --no-default-features | 183 | 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-client | 185 | cargo test -p gix --features async-network-client |
| 185 | cargo test -p gix --features blocking-network-client | 186 | cargo test -p gix --features blocking-network-client |
| 186 | cargo test -p gitoxide-core --lib | 187 | cargo test -p gitoxide-core --lib |