Unified · Split
| @@ -86,6 +86,7 @@ pub(crate) mod function { · 85 unmodified lines | ||
| 86 | 86 | } else { |
| 87 | 87 | Vec::new() |
| 88 | 88 | }, |
| 89 | + &gix::interrupt::IS_INTERRUPTED, | |
| 89 | 90 | options, |
| 90 | 91 | &mut collect, |
| 91 | 92 | )?; |
| @@ -36,9 +36,14 @@ pub(crate) mod function { · 35 unmodified lines | ||
| 36 | 36 | take_while(1..=2, AsChar::is_dec_digit) |
| 37 | 37 | .verify_map(|v| to_signed::<OffsetInSeconds>(v).ok()) |
| 38 | 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 | − let offset = (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 }; | |
| 41 | + .map(|(time, sign, hours, minutes, trailing_digits)| { | |
| 42 | + let offset = if trailing_digits.is_empty() { | |
| 43 | + (hours * 3600 + minutes * 60) * if sign == Sign::Minus { -1 } else { 1 } | |
| 44 | + } else { | |
| 45 | + 0 | |
| 46 | + }; | |
| 42 | 47 | Time { |
| 43 | 48 | seconds: time, |
| 44 | 49 | offset, |
| @@ -53,7 +53,7 @@ mod write_to { · 52 unmodified lines | ||
| 53 | 53 | } |
| 54 | 54 | |
| 55 | 55 | use bstr::ByteSlice; |
| 56 | −use gix_actor::Signature; | |
| 56 | +use gix_actor::{Signature, SignatureRef}; | |
| 57 | 57 | |
| 58 | 58 | #[test] |
| 59 | 59 | fn trim() { |
| @@ -80,3 +80,28 @@ fn round_trip() -> Result<(), Box<dyn std::error::Error>> { · 20 unmodified lines | ||
| 80 | 80 | } |
| 81 | 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 | 134 | emit_ignored, |
| 135 | 135 | for_deletion, |
| 136 | 136 | classify_untracked_bare_repositories, |
| 137 | + symlinks_to_directories_are_ignored_like_directories, | |
| 137 | 138 | .. |
| 138 | 139 | }: Options, |
| 139 | 140 | ctx: &mut Context<'_>, |
| @@ -199,6 +200,15 @@ pub fn path( · 59 unmodified lines | ||
| 199 | 200 | .pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes) |
| 200 | 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 | 212 | let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| { |
| 203 | 213 | if recurse_repositories { |
| 204 | 214 | return current_kind; |
| @@ -245,7 +255,7 @@ pub fn path( · 40 unmodified lines | ||
| 245 | 255 | .as_mut() |
| 246 | 256 | .map_or(Ok(None), |stack| { |
| 247 | 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 | 259 | .map(|platform| platform.excluded_kind()) |
| 250 | 260 | }) |
| 251 | 261 | .map_err(Error::ExcludesAccess)? |
| @@ -69,9 +69,16 @@ pub fn walk( · 68 unmodified lines | ||
| 69 | 69 | )?; |
| 70 | 70 | if !can_recurse( |
| 71 | 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 | 80 | options.for_deletion, |
| 74 | − worktree_root_is_repository, /* is root */ | |
| 81 | + worktree_root_is_repository, | |
| 75 | 82 | delegate, |
| 76 | 83 | ) { |
| 77 | 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 | 154 | rela_path: &BStr, |
| 148 | 155 | info: classify::Outcome, |
| 149 | 156 | for_deletion: Option<ForDeletionMode>, |
| 150 | − is_root: bool, | |
| 157 | + worktree_root_is_repository: bool, | |
| 151 | 158 | delegate: &mut dyn Delegate, |
| 152 | 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 | 162 | return false; |
| 155 | 163 | } |
| 156 | 164 | delegate.can_recurse( |
| 157 | 165 | EntryRef::from_outcome(Cow::Borrowed(rela_path), info), |
| 158 | 166 | for_deletion, |
| 159 | − is_root, | |
| 167 | + worktree_root_is_repository, | |
| 160 | 168 | ) |
| 161 | 169 | } |
| 162 | 170 | |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | use crate::{entry, EntryRef}; |
| 2 | 2 | use bstr::BStr; |
| 3 | 3 | use std::path::PathBuf; |
| 4 | +use std::sync::atomic::AtomicBool; | |
| 4 | 5 | |
| 5 | 6 | /// A type returned by the [`Delegate::emit()`] as passed to [`walk()`](function::walk()). |
| 6 | 7 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] |
| @@ -183,10 +184,22 @@ pub struct Options { · 176 unmodified lines | ||
| 183 | 184 | pub emit_empty_directories: bool, |
| 184 | 185 | /// If `None`, no entries inside of collapsed directories are emitted. Otherwise, act as specified by `Some(mode)`. |
| 185 | 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 | 197 | /// All information that is required to perform a dirwalk, and classify paths properly. |
| 189 | 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 | 203 | /// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`]. |
| 191 | 204 | /// |
| 192 | 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 | 274 | #[derive(Debug, thiserror::Error)] |
| 262 | 275 | #[allow(missing_docs)] |
| 263 | 276 | pub enum Error { |
| 277 | + #[error("Interrupted")] | |
| 278 | + Interrupted, | |
| 264 | 279 | #[error("Worktree root at '{}' is not a directory", root.display())] |
| 265 | 280 | WorktreeRootIsFile { root: PathBuf }, |
| 266 | 281 | #[error("Traversal root '{}' contains relative path components and could not be normalized", root.display())] |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | use bstr::{BStr, BString, ByteSlice}; |
| 2 | 2 | use std::borrow::Cow; |
| 3 | 3 | use std::path::{Path, PathBuf}; |
| 4 | +use std::sync::atomic::Ordering; | |
| 4 | 5 | |
| 5 | 6 | use crate::entry::{PathspecMatch, Status}; |
| 6 | 7 | use crate::walk::function::{can_recurse, emit_entry}; |
| @@ -23,6 +24,9 @@ pub(super) fn recursive( · 16 unmodified lines | ||
| 23 | 24 | out: &mut Outcome, |
| 24 | 25 | state: &mut State, |
| 25 | 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 | 30 | out.read_dir_calls += 1; |
| 27 | 31 | let entries = gix_fs::read_dir(current, opts.precompose_unicode).map_err(|err| Error::ReadDir { |
| 28 | 32 | path: current.to_owned(), |
| @@ -17,3 +17,29 @@ git init immediate-breakout-symlink · 16 unmodified lines | ||
| 17 | 17 | (cd immediate-breakout-symlink |
| 18 | 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 | 1 | use gix_dir::{walk, EntryRef}; |
| 2 | 2 | use pretty_assertions::assert_eq; |
| 3 | +use std::sync::atomic::AtomicBool; | |
| 3 | 4 | |
| 4 | 5 | use crate::walk_utils::{ |
| 5 | 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 | 17 | use gix_dir::walk::ForDeletionMode; |
| 17 | 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 | 95 | #[test] |
| 20 | 96 | #[cfg_attr(windows, ignore = "symlinks the way they are organized don't yet work on windows")] |
| 21 | 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 | 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 | 173 | #[test] |
| 47 | 174 | fn empty_root() -> crate::Result { |
| 48 | 175 | let root = fixture("empty"); |
| @@ -2,6 +2,7 @@ use bstr::BStr; · 1 unmodified line | ||
| 2 | 2 | use gix_dir::{entry, walk, Entry}; |
| 3 | 3 | use gix_testtools::scripted_fixture_read_only; |
| 4 | 4 | use std::path::{Path, PathBuf}; |
| 5 | +use std::sync::atomic::AtomicBool; | |
| 5 | 6 | |
| 6 | 7 | pub fn fixture_in(filename: &str, name: &str) -> PathBuf { |
| 7 | 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 | 32 | emit_untracked: walk::EmissionMode::Matching, |
| 32 | 33 | emit_empty_directories: true, |
| 33 | 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 | 270 | cb: impl FnOnce(&mut dyn walk::Delegate, walk::Context) -> Result<(walk::Outcome, PathBuf), walk::Error>, |
| 269 | 271 | patterns: impl IntoIterator<Item = impl AsRef<BStr>>, |
| 270 | 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 | 278 | ) -> Result<(walk::Outcome, PathBuf), walk::Error> { |
| 273 | 279 | let git_dir = worktree_root.join(git_dir.unwrap_or(".git")); |
| 274 | 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 | 348 | excludes: Some(&mut stack), |
| 343 | 349 | objects: &gix_object::find::Never, |
| 344 | 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 | 356 | pub struct Options<'a> { |
| 350 | 357 | pub fresh_index: bool, |
| 351 | 358 | pub git_dir: Option<&'a str>, |
| 359 | + pub should_interrupt: Option<&'a AtomicBool>, | |
| 352 | 360 | } |
| 353 | 361 | |
| 354 | 362 | impl<'a> Options<'a> { |
| @@ -365,6 +373,7 @@ impl<'a> Default for Options<'a> { · 10 unmodified lines | ||
| 365 | 373 | Options { |
| 366 | 374 | fresh_index: true, |
| 367 | 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 | 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 | 23 | bitflags! { |
| 27 | 24 | /// The kind of file of an entry. |
| 28 | 25 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] |
| @@ -8,6 +8,33 @@ use crate::{ · 7 unmodified lines | ||
| 8 | 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 | 38 | #[test] |
| 12 | 39 | fn unsigned() -> crate::Result { |
| 13 | 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 | 176 | matches!(pattern.path.get(common_len), None | Some(&b'/')) |
| 177 | 177 | } else { |
| 178 | 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 | 184 | return !pattern.is_excluded(); |
| 185 | 185 | } |
| 186 | 186 | } |
| @@ -191,7 +191,7 @@ impl Search { · 4 unmodified lines | ||
| 191 | 191 | /// Returns `true` if `relative_path` matches the prefix of this pathspec. |
| 192 | 192 | /// |
| 193 | 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 | 195 | /// partially included in `pathspec`, otherwise it has to be fully included. |
| 196 | 196 | pub fn directory_matches_prefix(&self, relative_path: &BStr, leading: bool) -> bool { |
| 197 | 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 | 237 | return !pattern.is_excluded(); |
| 238 | 238 | } |
| 239 | 239 | } |
| @@ -135,6 +135,47 @@ fn no_pathspecs_match_everything() -> crate::Result { · 134 unmodified lines | ||
| 135 | 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 | 179 | #[test] |
| 139 | 180 | fn starts_with() -> crate::Result { |
| 140 | 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 | 302 | None, |
| 262 | 303 | Path::new(""), |
| 263 | 304 | )?; |
| 264 | − assert!(!search.can_match_relative_path("b".into(), None)); | |
| 265 | − assert!(!search.can_match_relative_path("a".into(), None)); | |
| 305 | + assert!( | |
| 306 | + search.can_match_relative_path("b".into(), None), | |
| 307 | + "non-trivial excludes are ignored in favor of false-positives" | |
| 308 | + ); | |
| 309 | + assert!( | |
| 310 | + search.can_match_relative_path("a".into(), None), | |
| 311 | + "non-trivial excludes are ignored in favor of false-positives" | |
| 312 | + ); | |
| 266 | 313 | assert!(search.can_match_relative_path("c".into(), None)); |
| 267 | 314 | assert!(search.can_match_relative_path("c/".into(), None)); |
| 268 | 315 | |
| @@ -87,6 +87,7 @@ pub(super) mod function { · 86 unmodified lines | ||
| 87 | 87 | gix_dir::walk( |
| 88 | 88 | worktree, |
| 89 | 89 | gix_dir::walk::Context { |
| 90 | + should_interrupt: Some(ctx.should_interrupt), | |
| 90 | 91 | git_dir_realpath: dirwalk_ctx.git_dir_realpath, |
| 91 | 92 | current_dir: dirwalk_ctx.current_dir, |
| 92 | 93 | index, |
| @@ -74,7 +74,7 @@ interrupt = ["dep:signal-hook", "gix-tempfile/signals"] · 73 unmodified lines | ||
| 74 | 74 | index = ["dep:gix-index"] |
| 75 | 75 | |
| 76 | 76 | ## Support directory walks with Git-style annoations. |
| 77 | −dirwalk = ["dep:gix-dir"] | |
| 77 | +dirwalk = ["dep:gix-dir", "attributes", "excludes"] | |
| 78 | 78 | |
| 79 | 79 | ## Access to credential helpers, which provide credentials for URLs. |
| 80 | 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 | 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 | 4 | /// Construction |
| 24 | 5 | impl Options { |
| 25 | 6 | pub(crate) fn from_fs_caps(caps: gix_fs::Capabilities) -> Self { |
| @@ -36,6 +17,7 @@ impl Options { · 10 unmodified lines | ||
| 36 | 17 | classify_untracked_bare_repositories: false, |
| 37 | 18 | emit_collapsed: None, |
| 38 | 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 | 36 | emit_empty_directories: v.emit_empty_directories, |
| 55 | 37 | classify_untracked_bare_repositories: v.classify_untracked_bare_repositories, |
| 56 | 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 | 164 | self.emit_collapsed = value; |
| 181 | 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 | 177 | #[allow(clippy::empty_docs)] |
| 178 | 178 | pub mod clone; |
| 179 | 179 | pub mod commit; |
| 180 | −#[cfg(feature = "dirwalk")] | |
| 181 | 180 | /// |
| 181 | +#[cfg(feature = "dirwalk")] | |
| 182 | 182 | #[allow(clippy::empty_docs)] |
| 183 | 183 | pub mod dirwalk; |
| 184 | 184 | pub mod head; |
| @@ -191,6 +191,8 @@ pub mod repository; · 6 unmodified lines | ||
| 191 | 191 | #[cfg(feature = "attributes")] |
| 192 | 192 | pub mod submodule; |
| 193 | 193 | pub mod tag; |
| 194 | +#[cfg(any(feature = "dirwalk", feature = "status"))] | |
| 195 | +pub(crate) mod util; | |
| 194 | 196 | |
| 195 | 197 | /// |
| 196 | 198 | #[allow(clippy::empty_docs)] |
| @@ -1,37 +1,8 @@ | ||
| 1 | −use crate::bstr::BStr; | |
| 2 | −use crate::{config, dirwalk, AttributeStack, Pathspec, Repository}; | |
| 3 | −use std::path::PathBuf; | |
| 4 | − | |
| 5 | −/// The error returned by [dirwalk()](Repository::dirwalk()). | |
| 6 | −#[derive(Debug, thiserror::Error)] | |
| 7 | −#[allow(missing_docs)] | |
| 8 | −pub enum Error { | |
| 9 | − #[error(transparent)] | |
| 10 | − Walk(#[from] gix_dir::walk::Error), | |
| 11 | − #[error("A working tree is required to perform a directory walk")] | |
| 12 | − MissingWorkDir, | |
| 13 | − #[error(transparent)] | |
| 14 | − Excludes(#[from] config::exclude_stack::Error), | |
| 15 | − #[error(transparent)] | |
| 16 | − Pathspec(#[from] crate::pathspec::init::Error), | |
| 17 | − #[error(transparent)] | |
| 18 | − Prefix(#[from] gix_path::realpath::Error), | |
| 19 | − #[error(transparent)] | |
| 20 | − FilesystemOptions(#[from] config::boolean::Error), | |
| 21 | −} | |
| 22 | − | |
| 23 | −/// The outcome of the [dirwalk()](Repository::dirwalk). | |
| 24 | −pub struct Outcome<'repo> { | |
| 25 | − /// The excludes stack used for the dirwalk, for access of `.gitignore` information. | |
| 26 | − pub excludes: AttributeStack<'repo>, | |
| 27 | − /// The pathspecs used to guide the operation, | |
| 28 | − pub pathspec: Pathspec<'repo>, | |
| 29 | − /// The root actually being used for the traversal, and useful to transform the paths returned for the user. | |
| 30 | − /// It's always within the [`work-dir`](Repository::work_dir). | |
| 31 | − pub traversal_root: PathBuf, | |
| 32 | − /// The actual result of the dirwalk. | |
| 33 | − pub dirwalk: gix_dir::walk::Outcome, | |
| 34 | −} | |
| 1 | +use crate::bstr::{BStr, BString}; | |
| 2 | +use crate::util::OwnedOrStaticAtomicBool; | |
| 3 | +use crate::worktree::IndexPersistedOrInMemory; | |
| 4 | +use crate::{config, dirwalk, Repository}; | |
| 5 | +use std::sync::atomic::AtomicBool; | |
| 35 | 6 | |
| 36 | 7 | impl Repository { |
| 37 | 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 | 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 | 19 | /// The `index` is used to determine if entries are tracked, and for excludes and attributes |
| 48 | 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 | 25 | &self, |
| 54 | 26 | index: &gix_index::State, |
| 55 | 27 | patterns: impl IntoIterator<Item = impl AsRef<BStr>>, |
| 28 | + should_interrupt: &AtomicBool, | |
| 56 | 29 | options: dirwalk::Options, |
| 57 | 30 | delegate: &mut dyn gix_dir::walk::Delegate, |
| 58 | − ) -> Result<Outcome<'_>, Error> { | |
| 31 | + ) -> Result<dirwalk::Outcome<'_>, dirwalk::Error> { | |
| 59 | 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 | 34 | let mut excludes = self.excludes( |
| 62 | 35 | index, |
| 63 | 36 | None, |
| @@ -78,6 +51,7 @@ impl Repository { · 14 unmodified lines | ||
| 78 | 51 | let (outcome, traversal_root) = gix_dir::walk( |
| 79 | 52 | workdir, |
| 80 | 53 | gix_dir::walk::Context { |
| 54 | + should_interrupt: Some(should_interrupt), | |
| 81 | 55 | git_dir_realpath: git_dir_realpath.as_ref(), |
| 82 | 56 | current_dir: self.current_dir(), |
| 83 | 57 | index, |
| @@ -101,11 +75,32 @@ impl Repository { · 17 unmodified lines | ||
| 101 | 75 | delegate, |
| 102 | 76 | )?; |
| 103 | 77 | |
| 104 | − Ok(Outcome { | |
| 78 | + Ok(dirwalk::Outcome { | |
| 105 | 79 | dirwalk: outcome, |
| 106 | 80 | traversal_root, |
| 107 | 81 | excludes, |
| 108 | 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 | 45 | pub mod diff; |
| 46 | 46 | /// |
| 47 | 47 | #[cfg(feature = "dirwalk")] |
| 48 | −pub mod dirwalk; | |
| 48 | +mod dirwalk; | |
| 49 | 49 | /// |
| 50 | 50 | #[cfg(feature = "attributes")] |
| 51 | 51 | pub mod filter; |
| @@ -297,7 +297,7 @@ pub struct Iter { · 296 unmodified lines | ||
| 297 | 297 | std::thread::JoinHandle<Result<iter::Outcome, crate::status::index_worktree::Error>>, |
| 298 | 298 | )>, |
| 299 | 299 | #[cfg(feature = "parallel")] |
| 300 | − should_interrupt: crate::status::OwnedOrStaticAtomic, | |
| 300 | + should_interrupt: crate::status::OwnedOrStaticAtomicBool, | |
| 301 | 301 | /// Without parallelization, the iterator has to buffer all changes in advance. |
| 302 | 302 | #[cfg(not(feature = "parallel"))] |
| 303 | 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 | 748 | impl super::Iter { |
| 748 | 749 | /// Return the outcome of the iteration, or `None` if the iterator isn't fully consumed. |
| 749 | 750 | pub fn outcome_mut(&mut self) -> Option<&mut Outcome> { |
| 750 | 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 | 761 | impl super::Iter { |
| @@ -781,28 +788,7 @@ pub mod iter { · 26 unmodified lines | ||
| 781 | 788 | #[cfg(feature = "parallel")] |
| 782 | 789 | impl Drop for super::Iter { |
| 783 | 790 | fn drop(&mut self) { |
| 784 | − use crate::status::OwnedOrStaticAtomic; | |
| 785 | − let Some((rx, handle)) = self.rx_and_join.take() else { | |
| 786 | − return; | |
| 787 | − }; | |
| 788 | − let prev = self.should_interrupt.swap(true, std::sync::atomic::Ordering::Relaxed); | |
| 789 | − let undo = match &self.should_interrupt { | |
| 790 | − OwnedOrStaticAtomic::Shared(flag) => *flag, | |
| 791 | − OwnedOrStaticAtomic::Owned { flag, private: false } => flag.as_ref(), | |
| 792 | − OwnedOrStaticAtomic::Owned { private: true, .. } => { | |
| 793 | − // Leak the handle to let it shut down in the background, so drop returns more quickly. | |
| 794 | − drop((rx, handle)); | |
| 795 | − return; | |
| 796 | − } | |
| 797 | − }; | |
| 798 | − // Wait until there is time to respond before we undo the change. | |
| 799 | − handle.join().ok(); | |
| 800 | − undo.fetch_update( | |
| 801 | − std::sync::atomic::Ordering::SeqCst, | |
| 802 | − std::sync::atomic::Ordering::SeqCst, | |
| 803 | − |current| current.then_some(prev), | |
| 804 | − ) | |
| 805 | − .ok(); | |
| 791 | + crate::util::parallel_iter_drop(self.rx_and_join.take(), &self.should_interrupt); | |
| 806 | 792 | } |
| 807 | 793 | } |
| 808 | 794 | |
| @@ -1,9 +1,7 @@ | ||
| 1 | 1 | use crate::config::cache::util::ApplyLeniencyDefault; |
| 2 | +use crate::util::OwnedOrStaticAtomicBool; | |
| 2 | 3 | use crate::{config, Repository}; |
| 3 | 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 | 6 | /// A structure to hold options configuring the status request, which can then be turned into an iterator. |
| 9 | 7 | pub struct Platform<'repo, Progress> |
| @@ -15,37 +13,7 @@ where · 5 unmodified lines | ||
| 15 | 13 | index: Option<crate::worktree::IndexPersistedOrInMemory>, |
| 16 | 14 | submodules: Submodule, |
| 17 | 15 | index_worktree_options: index_worktree::Options, |
| 18 | − should_interrupt: Option<OwnedOrStaticAtomic>, | |
| 19 | −} | |
| 20 | − | |
| 21 | −#[derive(Clone)] | |
| 22 | −enum OwnedOrStaticAtomic { | |
| 23 | − Owned { | |
| 24 | − flag: Arc<AtomicBool>, | |
| 25 | − #[cfg_attr(not(feature = "parallel"), allow(dead_code))] | |
| 26 | − private: bool, | |
| 27 | − }, | |
| 28 | − Shared(&'static AtomicBool), | |
| 29 | −} | |
| 30 | − | |
| 31 | −impl Default for OwnedOrStaticAtomic { | |
| 32 | − fn default() -> Self { | |
| 33 | − OwnedOrStaticAtomic::Owned { | |
| 34 | − flag: Arc::new(AtomicBool::default()), | |
| 35 | − private: true, | |
| 36 | − } | |
| 37 | − } | |
| 38 | −} | |
| 39 | − | |
| 40 | −impl Deref for OwnedOrStaticAtomic { | |
| 41 | − type Target = std::sync::atomic::AtomicBool; | |
| 42 | − | |
| 43 | − fn deref(&self) -> &Self::Target { | |
| 44 | − match self { | |
| 45 | − OwnedOrStaticAtomic::Owned { flag, .. } => flag, | |
| 46 | − OwnedOrStaticAtomic::Shared(flag) => flag, | |
| 47 | − } | |
| 48 | − } | |
| 16 | + should_interrupt: Option<OwnedOrStaticAtomicBool>, | |
| 49 | 17 | } |
| 50 | 18 | |
| 51 | 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 | 2 | use std::sync::atomic::AtomicBool; |
| 3 | 3 | |
| 4 | 4 | /// Builder |
| @@ -45,7 +45,7 @@ where · 40 unmodified lines | ||
| 45 | 45 | /// |
| 46 | 46 | /// If it is `true`, the iteration will stop immediately. |
| 47 | 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 | 49 | self |
| 50 | 50 | } |
| 51 | 51 | |
| @@ -53,7 +53,7 @@ where · 1 unmodified line | ||
| 53 | 53 | /// |
| 54 | 54 | /// If it is `true`, the iteration will stop immediately. |
| 55 | 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 | 57 | flag: should_interrupt, |
| 58 | 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 | 24 | #[cfg(feature = "index")] |
| 25 | 25 | pub enum IndexPersistedOrInMemory { |
| 26 | 26 | /// The index as loaded from disk, and shared across clones of the owning `Repository`. |
| 27 | − Persisted(crate::worktree::Index), | |
| 27 | + Persisted(Index), | |
| 28 | 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 | 30 | /// Note that unless saved explicitly, it will not persist. |
| 31 | 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 | 48 | /// A stand-in to a worktree as result of a worktree iteration. |
| 35 | 49 | /// |
| 36 | 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 | 19 | mod dirwalk { |
| 20 | 20 | use gix_dir::entry::Kind::*; |
| 21 | 21 | use gix_dir::walk::EmissionMode; |
| 22 | + use std::sync::atomic::AtomicBool; | |
| 22 | 23 | |
| 23 | 24 | #[test] |
| 24 | 25 | fn basics() -> crate::Result { |
| @@ -26,22 +27,48 @@ mod dirwalk { · 1 unmodified line | ||
| 26 | 27 | let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory); |
| 27 | 28 | let mut collect = gix::dir::walk::delegate::Collect::default(); |
| 28 | 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 | 44 | assert_eq!( |
| 31 | 45 | collect |
| 32 | 46 | .into_entries_by_path() |
| 33 | 47 | .into_iter() |
| 34 | 48 | .map(|e| (e.0.rela_path.to_string(), e.0.disk_kind.expect("kind is known"))) |
| 35 | 49 | .collect::<Vec<_>>(), |
| 36 | − [ | |
| 37 | − ("all-untracked".to_string(), Repository), | |
| 38 | − ("bare-repo-with-index.git".to_string(), Directory), | |
| 39 | − ("bare.git".into(), Directory), | |
| 40 | − ("non-bare-repo-without-index".into(), Repository), | |
| 41 | − ("some".into(), Directory) | |
| 42 | − ], | |
| 50 | + expected, | |
| 43 | 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 | 72 | Ok(()) |
| 46 | 73 | } |
| 47 | 74 | } |
| @@ -268,8 +268,14 @@ mod object_caches { · 267 unmodified lines | ||
| 268 | 268 | fn default_git_and_custom_caches() -> crate::Result { |
| 269 | 269 | let opts = gix::open::Options::isolated(); |
| 270 | 270 | let repo = named_subrepo_opts("make_config_repos.sh", "object-caches", opts)?; |
| 271 | − assert_eq!(repo.objects.has_object_cache(), cfg!(feature = "comfort")); | |
| 272 | − assert_eq!(repo.objects.has_pack_cache(), cfg!(feature = "comfort")); | |
| 271 | + assert_eq!( | |
| 272 | + repo.objects.has_object_cache(), | |
| 273 | + cfg!(all(feature = "parallel", feature = "comfort")) | |
| 274 | + ); | |
| 275 | + assert_eq!( | |
| 276 | + repo.objects.has_pack_cache(), | |
| 277 | + cfg!(all(feature = "parallel", feature = "comfort")) | |
| 278 | + ); | |
| 273 | 279 | Ok(()) |
| 274 | 280 | } |
| 275 | 281 | |
| @@ -181,6 +181,7 @@ unit-tests: · 180 unmodified lines | ||
| 181 | 181 | cargo test -p gix-protocol --features blocking-client |
| 182 | 182 | cargo test -p gix-protocol --features async-client |
| 183 | 183 | cargo test -p gix --no-default-features |
| 184 | + cargo test -p gix --no-default-features --features basic,extras,comfort | |
| 184 | 185 | cargo test -p gix --features async-network-client |
| 185 | 186 | cargo test -p gix --features blocking-network-client |
| 186 | 187 | cargo test -p gitoxide-core --lib |