History

chore!: remove `checkout` and `status` functionality so only `Cache` remains.

`gix-worktree` is now a base-crate for use by derived crates that provide additional functionality. Shared types or utilities go here.

Sebastian Thiel committed 3 years ago

Commit 1958dff1 parent bd961b338 files · +6 −2593Browse files

Unified · Split

38 files
  1. Cargo.lock +0 −7
  2. crate-status.md +1 −19
  3. gix-worktree/Cargo.toml +1 −9
  4. gix-worktree/src/checkout/chunk.rs +0 −402
  5. gix-worktree/src/checkout/entry.rs +0 −294
  6. gix-worktree/src/checkout/function.rs +0 −168
  7. gix-worktree/src/checkout/mod.rs +0 −101
  8. gix-worktree/src/lib.rs +1 −10
  9. gix-worktree/src/read.rs +0 −64
  10. gix-worktree/src/status/content.rs +0 −80
  11. gix-worktree/src/status/function.rs +0 −331
  12. gix-worktree/src/status/mod.rs +0 −11
  13. gix-worktree/src/status/recorder.rs +0 −27
  14. gix-worktree/src/status/types.rs +0 −69
  15. gix-worktree/src/untracked.rs +0 −1
  16. gix-worktree/tests/fixtures/generated-archives/.gitignore +0 −5
  17. gix-worktree/tests/fixtures/generated-archives/make_ignorecase_collisions.tar.xz +0 −3
  18. gix-worktree/tests/fixtures/generated-archives/make_special_exclude_case.tar.xz +2 −2
  19. gix-worktree/tests/fixtures/generated-archives/racy_git.tar.xz +0 −3
  20. gix-worktree/tests/fixtures/generated-archives/status_conflict.tar.xz +0 −3
  21. gix-worktree/tests/fixtures/generated-archives/status_intent_to_add.tar.xz +0 −3
  22. gix-worktree/tests/fixtures/generated-archives/status_removed.tar.xz +0 −3
  23. gix-worktree/tests/fixtures/make_dangerous_symlink.sh +0 −24
  24. gix-worktree/tests/fixtures/make_ignore_setup.sh +0 −18
  25. gix-worktree/tests/fixtures/make_ignorecase_collisions.sh +0 −27
  26. gix-worktree/tests/fixtures/make_mixed_without_submodules.sh +0 −20
  27. gix-worktree/tests/fixtures/make_mixed_without_submodules_and_symlinks.sh +0 −20
  28. gix-worktree/tests/fixtures/racy_git.sh +0 −12
  29. gix-worktree/tests/fixtures/status_changed.sh +0 −26
  30. gix-worktree/tests/fixtures/status_conflict.sh +0 −18
  31. gix-worktree/tests/fixtures/status_intent_to_add.sh +0 −9
  32. gix-worktree/tests/fixtures/status_removed.sh +0 −18
  33. gix-worktree/tests/fixtures/status_unchanged.sh +0 −20
  34. gix-worktree/tests/worktree/cache/create_directory.rs +1 −1
  35. gix-worktree/tests/worktree/checkout.rs +0 −526
  36. gix-worktree/tests/worktree/mod.rs +0 −9
  37. gix-worktree/tests/worktree/status.rs +0 −226
  38. justfile +0 −4
Cargo.lock+0−7
@@ -2466,10 +2466,8 @@ version = "0.24.0" · 2465 unmodified lines
24662466 dependencies = [
24672467 "bstr",
24682468 "document-features",
2469− "filetime",
24702469 "gix-attributes 0.16.0",
24712470 "gix-features 0.32.1",
2472− "gix-filter",
24732471 "gix-fs 0.4.1",
24742472 "gix-glob 0.10.2",
24752473 "gix-hash 0.11.4",
@@ -2479,13 +2477,8 @@ dependencies = [ · 3 unmodified lines
24792477 "gix-odb",
24802478 "gix-path 0.8.4",
24812479 "gix-testtools",
2482− "io-close",
2483− "once_cell",
24842480 "serde",
24852481 "symlink",
2486− "tempfile",
2487− "thiserror",
2488− "walkdir",
24892482 ]
24902483
24912484 [[package]]
crate-status.md+1−19
@@ -482,25 +482,7 @@ Make it the best-performing implementation and the most convenient one. · 481 unmodified lines
482482 * [x] attributes
483483
484484 ### gix-worktree
485−* handle the working **tree/checkout**
486− - [x] checkout an index of files, executables and symlinks just as fast as git
487− - [x] forbid symlinks in directories
488− - [ ] handle submodules
489− - [ ] handle sparse directories
490− - [ ] handle sparse index
491− - [x] linear scaling with multi-threading up to IO saturation
492− - supported attributes to affect working tree and index contents
493− - [x] eol
494− - [x] working-tree-encoding
495− - …more
496− - **filtering**
497− - [x] `text`
498− - [x] `ident`
499− - [x] filter processes
500− - [x] single-invocation clean/smudge filters
501−* access to per-path information, like `.gitignore` and `.gitattributes` in a manner well suited for efficient lookups
502− * [x] _exclude_ information
503− * [x] attributes
485+* [x] A stack to to efficiently generate attribute lists for matching paths against.
504486
505487 ### gix-revision
506488 * [x] `describe()` (similar to `git name-rev`)
gix-worktree/Cargo.toml+1−9
@@ -3,7 +3,7 @@ name = "gix-worktree" · 2 unmodified lines
33 version = "0.24.0"
44 repository = "https://github.com/Byron/gitoxide"
55 license = "MIT OR Apache-2.0"
6−description = "A crate of the gitoxide project dedicated implementing everything around working trees and git excludes"
6+description = "A crate of the gitoxide project for shared worktree related types and utilities."
77 authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
88 edition = "2021"
99 include = ["src/**/*", "LICENSE-*", "CHANGELOG.md"]
@@ -33,25 +33,17 @@ gix-path = { version = "^0.8.4", path = "../gix-path" } · 23 unmodified lines
3333 gix-attributes = { version = "^0.16.0", path = "../gix-attributes" }
3434 gix-ignore = { version = "^0.5.1", path = "../gix-ignore" }
3535 gix-features = { version = "^0.32.1", path = "../gix-features" }
36−gix-filter = { version = "^0.3.0", path = "../gix-filter" }
3736
3837 serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
3938
40−thiserror = "1.0.26"
41−filetime = "0.2.15"
4239 bstr = { version = "1.3.0", default-features = false }
4340
4441 document-features = { version = "0.2.0", optional = true }
45−io-close = "0.3.7"
4642
4743 [dev-dependencies]
4844 gix-testtools = { path = "../tests/tools" }
4945 gix-odb = { path = "../gix-odb" }
5046 symlink = "0.1.0"
51−once_cell = "1.18.0"
52−
53−walkdir = "2.3.2"
54−tempfile = "3.2.0"
5547
5648 [package.metadata.docs.rs]
5749 features = ["document-features", "serde"]
gix-worktree/src/checkout/chunk.rs+0−402
@@ -1,402 +0,0 @@
1−use std::{
2− collections::BTreeSet,
3− sync::atomic::{AtomicUsize, Ordering},
4−};
5−
6−use bstr::{BStr, BString};
7−use gix_hash::oid;
8−
9−use crate::{checkout, checkout::entry, Cache};
10−
11−mod reduce {
12− use std::marker::PhantomData;
13−
14− use gix_features::progress::Progress;
15−
16− use crate::checkout;
17−
18− pub struct Reduce<'a, 'entry, P1, P2, E> {
19− pub files: Option<&'a mut P1>,
20− pub bytes: Option<&'a mut P2>,
21− pub aggregate: super::Outcome<'entry>,
22− pub marker: PhantomData<E>,
23− }
24−
25− impl<'a, 'entry, P1, P2, E> gix_features::parallel::Reduce for Reduce<'a, 'entry, P1, P2, E>
26− where
27− P1: Progress,
28− P2: Progress,
29− E: std::error::Error + Send + Sync + 'static,
30− {
31− type Input = Result<super::Outcome<'entry>, checkout::Error<E>>;
32− type FeedProduce = ();
33− type Output = super::Outcome<'entry>;
34− type Error = checkout::Error<E>;
35−
36− fn feed(&mut self, item: Self::Input) -> Result<Self::FeedProduce, Self::Error> {
37− let item = item?;
38− let super::Outcome {
39− bytes_written,
40− files,
41− delayed_symlinks,
42− errors,
43− collisions,
44− delayed_paths_unknown,
45− delayed_paths_unprocessed,
46− } = item;
47− self.aggregate.bytes_written += bytes_written;
48− self.aggregate.files += files;
49− self.aggregate.delayed_symlinks.extend(delayed_symlinks);
50− self.aggregate.errors.extend(errors);
51− self.aggregate.collisions.extend(collisions);
52− self.aggregate.delayed_paths_unknown.extend(delayed_paths_unknown);
53− self.aggregate
54− .delayed_paths_unprocessed
55− .extend(delayed_paths_unprocessed);
56−
57− if let Some(progress) = self.bytes.as_deref_mut() {
58− progress.set(self.aggregate.bytes_written as gix_features::progress::Step);
59− }
60− if let Some(progress) = self.files.as_deref_mut() {
61− progress.set(self.aggregate.files);
62− }
63−
64− Ok(())
65− }
66−
67− fn finalize(self) -> Result<Self::Output, Self::Error> {
68− Ok(self.aggregate)
69− }
70− }
71−}
72−pub use reduce::Reduce;
73−
74−use crate::checkout::entry::DelayedFilteredStream;
75−
76−#[derive(Default)]
77−pub struct Outcome<'a> {
78− pub collisions: Vec<checkout::Collision>,
79− pub errors: Vec<checkout::ErrorRecord>,
80− pub delayed_symlinks: Vec<(&'a mut gix_index::Entry, &'a BStr)>,
81− // all (immediately) written bytes
82− pub bytes_written: u64,
83− // the amount of files we processed
84− pub files: usize,
85− /// Relative paths that the process listed as 'delayed' even though we never passed them.
86− pub delayed_paths_unknown: Vec<BString>,
87− /// All paths that were left unprocessed, because they were never listed by the process even though we passed them.
88− pub delayed_paths_unprocessed: Vec<BString>,
89−}
90−
91−#[derive(Clone)]
92−pub struct Context<Find: Clone> {
93− pub find: Find,
94− pub path_cache: Cache,
95− pub filters: gix_filter::Pipeline,
96− pub buf: Vec<u8>,
97− pub options: Options,
98−}
99−
100−#[derive(Clone, Copy)]
101−pub struct Options {
102− pub fs: gix_fs::Capabilities,
103− pub destination_is_initially_empty: bool,
104− pub overwrite_existing: bool,
105− pub keep_going: bool,
106− pub filter_process_delay: gix_filter::driver::apply::Delay,
107−}
108−
109−impl From<&checkout::Options> for Options {
110− fn from(opts: &checkout::Options) -> Self {
111− Options {
112− fs: opts.fs,
113− destination_is_initially_empty: opts.destination_is_initially_empty,
114− overwrite_existing: opts.overwrite_existing,
115− keep_going: opts.keep_going,
116− filter_process_delay: opts.filter_process_delay,
117− }
118− }
119−}
120−
121−pub fn process<'entry, Find, E>(
122− entries_with_paths: impl Iterator<Item = (&'entry mut gix_index::Entry, &'entry BStr)>,
123− files: Option<&AtomicUsize>,
124− bytes: Option<&AtomicUsize>,
125− delayed_filter_results: &mut Vec<DelayedFilteredStream<'entry>>,
126− ctx: &mut Context<Find>,
127−) -> Result<Outcome<'entry>, checkout::Error<E>>
128−where
129− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Clone,
130− E: std::error::Error + Send + Sync + 'static,
131−{
132− let mut delayed_symlinks = Vec::new();
133− let mut collisions = Vec::new();
134− let mut errors = Vec::new();
135− let mut bytes_written = 0;
136− let mut files_in_chunk = 0;
137−
138− for (entry, entry_path) in entries_with_paths {
139− // TODO: write test for that
140− if entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
141− if let Some(files) = files {
142− files.fetch_add(1, Ordering::SeqCst);
143− }
144− files_in_chunk += 1;
145− continue;
146− }
147−
148− // Symlinks always have to be delayed on windows as they have to point to something that exists on creation.
149− // And even if not, there is a distinction between file and directory symlinks, hence we have to check what the target is
150− // before creating it.
151− // And to keep things sane, we just do the same on non-windows as well which is similar to what git does and adds some safety
152− // around writing through symlinks (even though we handle this).
153− // This also means that we prefer content in files over symlinks in case of collisions, which probably is for the better, too.
154− if entry.mode == gix_index::entry::Mode::SYMLINK {
155− delayed_symlinks.push((entry, entry_path));
156− continue;
157− }
158−
159− match checkout_entry_handle_result(entry, entry_path, &mut errors, &mut collisions, files, bytes, ctx)? {
160− entry::Outcome::Written { bytes } => {
161− bytes_written += bytes as u64;
162− files_in_chunk += 1
163− }
164− entry::Outcome::Delayed(delayed) => delayed_filter_results.push(delayed),
165− }
166− }
167−
168− Ok(Outcome {
169− bytes_written,
170− files: files_in_chunk,
171− errors,
172− collisions,
173− delayed_symlinks,
174− delayed_paths_unknown: Vec::new(),
175− delayed_paths_unprocessed: Vec::new(),
176− })
177−}
178−
179−pub fn process_delayed_filter_results<Find, E>(
180− mut delayed_filter_results: Vec<DelayedFilteredStream<'_>>,
181− files: Option<&AtomicUsize>,
182− bytes: Option<&AtomicUsize>,
183− out: &mut Outcome<'_>,
184− ctx: &mut Context<Find>,
185−) -> Result<(), checkout::Error<E>>
186−where
187− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Clone,
188− E: std::error::Error + Send + Sync + 'static,
189−{
190− let Options {
191− destination_is_initially_empty,
192− overwrite_existing,
193− keep_going,
194− ..
195− } = ctx.options;
196− let mut bytes_written = 0;
197− let mut delayed_files = 0;
198− // Sort by path for fast lookups
199− delayed_filter_results.sort_by(|a, b| a.entry_path.cmp(b.entry_path));
200− // We process each key and do as the filter process tells us, while collecting data about the overall progress.
201− let keys: BTreeSet<_> = delayed_filter_results.iter().map(|d| d.key.clone()).collect();
202− let mut unknown_paths = Vec::new();
203− let mut rela_path_as_path = Default::default();
204− for key in keys {
205− loop {
206− let rela_paths = ctx.filters.driver_state_mut().list_delayed_paths(&key)?;
207− if rela_paths.is_empty() {
208− break;
209− }
210−
211− for rela_path in rela_paths {
212− let delayed = match delayed_filter_results.binary_search_by(|d| d.entry_path.cmp(rela_path.as_ref())) {
213− Ok(idx) => &mut delayed_filter_results[idx],
214− Err(_) => {
215− if keep_going {
216− unknown_paths.push(rela_path);
217− continue;
218− } else {
219− return Err(checkout::Error::FilterPathUnknown { rela_path });
220− }
221− }
222− };
223− let mut read = std::io::BufReader::with_capacity(
224− 512 * 1024,
225− ctx.filters.driver_state_mut().fetch_delayed(
226− &key,
227− rela_path.as_ref(),
228− gix_filter::driver::Operation::Smudge,
229− )?,
230− );
231− let (file, set_executable_after_creation) = match entry::open_file(
232− &std::mem::take(&mut delayed.validated_file_path), // mark it as seen, relevant for `unprocessed_paths`
233− destination_is_initially_empty,
234− overwrite_existing,
235− delayed.needs_executable_bit,
236− delayed.entry.mode,
237− ) {
238− Ok(res) => res,
239− Err(err) => {
240− if !is_collision(&err, delayed.entry_path, &mut out.collisions, files) {
241− handle_error(err, delayed.entry_path, files, &mut out.errors, ctx.options.keep_going)?;
242− }
243− std::io::copy(&mut read, &mut std::io::sink())?;
244− continue;
245− }
246− };
247− let mut write = WriteWithProgress {
248− inner: std::io::BufWriter::with_capacity(512 * 1024, file),
249− progress: bytes,
250− };
251− bytes_written += std::io::copy(&mut read, &mut write)?;
252− entry::finalize_entry(
253− delayed.entry,
254− write.inner.into_inner().map_err(std::io::IntoInnerError::into_error)?,
255− set_executable_after_creation.then(|| {
256− rela_path_as_path = gix_path::from_bstr(delayed.entry_path);
257− rela_path_as_path.as_ref()
258− }),
259− )?;
260− delayed_files += 1;
261− if let Some(files) = files {
262− files.fetch_add(1, Ordering::SeqCst);
263− }
264− }
265− }
266− }
267−
268− let unprocessed_paths = delayed_filter_results
269− .into_iter()
270− .filter_map(|d| (!d.validated_file_path.as_os_str().is_empty()).then(|| d.entry_path.to_owned()))
271− .collect();
272−
273− if !keep_going && !unknown_paths.is_empty() {
274− return Err(checkout::Error::FilterPathsUnprocessed {
275− rela_paths: unprocessed_paths,
276− });
277− }
278−
279− out.delayed_paths_unknown = unknown_paths;
280− out.delayed_paths_unprocessed = unprocessed_paths;
281− out.bytes_written += bytes_written;
282− out.files += delayed_files;
283− Ok(())
284−}
285−
286−pub struct WriteWithProgress<'a, T> {
287− pub inner: T,
288− pub progress: Option<&'a AtomicUsize>,
289−}
290−
291−impl<'a, T> std::io::Write for WriteWithProgress<'a, T>
292−where
293− T: std::io::Write,
294−{
295− fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
296− let written = self.inner.write(buf)?;
297− if let Some(progress) = self.progress {
298− progress.fetch_add(written as gix_features::progress::Step, Ordering::SeqCst);
299− }
300− Ok(written)
301− }
302−
303− fn flush(&mut self) -> std::io::Result<()> {
304− self.inner.flush()
305− }
306−}
307−
308−pub fn checkout_entry_handle_result<'entry, Find, E>(
309− entry: &'entry mut gix_index::Entry,
310− entry_path: &'entry BStr,
311− errors: &mut Vec<checkout::ErrorRecord>,
312− collisions: &mut Vec<checkout::Collision>,
313− files: Option<&AtomicUsize>,
314− bytes: Option<&AtomicUsize>,
315− Context {
316− find,
317− path_cache,
318− filters,
319− buf,
320− options,
321− }: &mut Context<Find>,
322−) -> Result<entry::Outcome<'entry>, checkout::Error<E>>
323−where
324− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Clone,
325− E: std::error::Error + Send + Sync + 'static,
326−{
327− let res = entry::checkout(
328− entry,
329− entry_path,
330− entry::Context {
331− find,
332− path_cache,
333− filters,
334− buf,
335− },
336− *options,
337− );
338− match res {
339− Ok(out) => {
340− if let Some(num) = out.as_bytes() {
341− if let Some(bytes) = bytes {
342− bytes.fetch_add(num, Ordering::SeqCst);
343− }
344− if let Some(files) = files {
345− files.fetch_add(1, Ordering::SeqCst);
346− }
347− }
348− Ok(out)
349− }
350− Err(checkout::Error::Io(err)) if is_collision(&err, entry_path, collisions, files) => {
351− Ok(entry::Outcome::Written { bytes: 0 })
352− }
353− Err(err) => handle_error(err, entry_path, files, errors, options.keep_going)
354− .map(|()| entry::Outcome::Written { bytes: 0 }),
355− }
356−}
357−
358−fn handle_error<E>(
359− err: E,
360− entry_path: &BStr,
361− files: Option<&AtomicUsize>,
362− errors: &mut Vec<checkout::ErrorRecord>,
363− keep_going: bool,
364−) -> Result<(), E>
365−where
366− E: std::error::Error + Send + Sync + 'static,
367−{
368− if keep_going {
369− errors.push(checkout::ErrorRecord {
370− path: entry_path.into(),
371− error: Box::new(err),
372− });
373− if let Some(files) = files {
374− files.fetch_add(1, Ordering::SeqCst);
375− }
376− Ok(())
377− } else {
378− Err(err)
379− }
380−}
381−
382−fn is_collision(
383− err: &std::io::Error,
384− entry_path: &BStr,
385− collisions: &mut Vec<checkout::Collision>,
386− files: Option<&AtomicUsize>,
387−) -> bool {
388− if !gix_fs::symlink::is_collision_error(err) {
389− return false;
390− }
391− // We are here because a file existed or was blocked by a directory which shouldn't be possible unless
392− // we are on a file insensitive file system.
393− gix_features::trace::error!("{entry_path}: collided ({:?})", err.kind());
394− collisions.push(checkout::Collision {
395− path: entry_path.into(),
396− error_kind: err.kind(),
397− });
398− if let Some(files) = files {
399− files.fetch_add(1, Ordering::SeqCst);
400− }
401− true
402−}
gix-worktree/src/checkout/entry.rs+0−294
@@ -1,294 +0,0 @@
1−use std::{
2− fs::OpenOptions,
3− io::Write,
4− path::{Path, PathBuf},
5−};
6−
7−use bstr::BStr;
8−use gix_filter::{driver::apply::MaybeDelayed, pipeline::convert::ToWorktreeOutcome};
9−use gix_hash::oid;
10−use gix_index::{entry::Stat, Entry};
11−use io_close::Close;
12−
13−use crate::Cache;
14−
15−pub struct Context<'a, Find> {
16− pub find: &'a mut Find,
17− pub path_cache: &'a mut Cache,
18− pub filters: &'a mut gix_filter::Pipeline,
19− pub buf: &'a mut Vec<u8>,
20−}
21−
22−/// A delayed result of a long-running filter process, which is made available as stream.
23−pub struct DelayedFilteredStream<'a> {
24− /// The key identifying the driver program
25− pub key: gix_filter::driver::Key,
26− /// If the file is going to be an executable.
27− pub needs_executable_bit: bool,
28− /// The validated path on disk at which the file should be placed.
29− pub validated_file_path: PathBuf,
30− /// The entry to adjust with the file we will write.
31− pub entry: &'a mut gix_index::Entry,
32− /// The relative path at which the entry resides (for use when querying the delayed entry).
33− pub entry_path: &'a BStr,
34−}
35−
36−pub enum Outcome<'a> {
37− /// The file was written.
38− Written {
39− /// The amount of written bytes.
40− bytes: usize,
41− },
42− /// The will be ready later.
43− Delayed(DelayedFilteredStream<'a>),
44−}
45−
46−impl Outcome<'_> {
47− /// Return ourselves as (in-memory) bytes if possible.
48− pub fn as_bytes(&self) -> Option<usize> {
49− match self {
50− Outcome::Written { bytes } => Some(*bytes),
51− Outcome::Delayed { .. } => None,
52− }
53− }
54−}
55−
56−#[cfg_attr(not(unix), allow(unused_variables))]
57−pub fn checkout<'entry, Find, E>(
58− entry: &'entry mut Entry,
59− entry_path: &'entry BStr,
60− Context {
61− find,
62− filters,
63− path_cache,
64− buf,
65− }: Context<'_, Find>,
66− crate::checkout::chunk::Options {
67− fs: gix_fs::Capabilities {
68− symlink,
69− executable_bit,
70− ..
71− },
72− destination_is_initially_empty,
73− overwrite_existing,
74− filter_process_delay,
75− ..
76− }: crate::checkout::chunk::Options,
77−) -> Result<Outcome<'entry>, crate::checkout::Error<E>>
78−where
79− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
80− E: std::error::Error + Send + Sync + 'static,
81−{
82− let dest_relative = gix_path::try_from_bstr(entry_path).map_err(|_| crate::checkout::Error::IllformedUtf8 {
83− path: entry_path.to_owned(),
84− })?;
85− let is_dir = Some(entry.mode == gix_index::entry::Mode::COMMIT || entry.mode == gix_index::entry::Mode::DIR);
86− let path_cache = path_cache.at_path(dest_relative, is_dir, &mut *find)?;
87− let dest = path_cache.path();
88−
89− let object_size = match entry.mode {
90− gix_index::entry::Mode::FILE | gix_index::entry::Mode::FILE_EXECUTABLE => {
91− let obj = find(&entry.id, buf).map_err(|err| crate::checkout::Error::Find {
92− err,
93− oid: entry.id,
94− path: dest.to_path_buf(),
95− })?;
96−
97− let filtered = filters.convert_to_worktree(
98− obj.data,
99− entry_path,
100− |_, attrs| {
101− path_cache.matching_attributes(attrs);
102− },
103− filter_process_delay,
104− )?;
105− let (num_bytes, file, set_executable_after_creation) = match filtered {
106− ToWorktreeOutcome::Unchanged(buf) | ToWorktreeOutcome::Buffer(buf) => {
107− let (mut file, flag) = open_file(
108− dest,
109− destination_is_initially_empty,
110− overwrite_existing,
111− executable_bit,
112− entry.mode,
113− )?;
114− file.write_all(buf)?;
115− (buf.len(), file, flag)
116− }
117− ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut filtered)) => {
118− let (mut file, flag) = open_file(
119− dest,
120− destination_is_initially_empty,
121− overwrite_existing,
122− executable_bit,
123− entry.mode,
124− )?;
125− let num_bytes = std::io::copy(&mut filtered, &mut file)? as usize;
126− (num_bytes, file, flag)
127− }
128− ToWorktreeOutcome::Process(MaybeDelayed::Delayed(key)) => {
129− return Ok(Outcome::Delayed(DelayedFilteredStream {
130− key,
131− needs_executable_bit: false,
132− validated_file_path: dest.to_owned(),
133− entry,
134− entry_path,
135− }))
136− }
137− };
138−
139− // For possibly existing, overwritten files, we must change the file mode explicitly.
140− finalize_entry(entry, file, set_executable_after_creation.then_some(dest))?;
141− num_bytes
142− }
143− gix_index::entry::Mode::SYMLINK => {
144− let obj = find(&entry.id, buf).map_err(|err| crate::checkout::Error::Find {
145− err,
146− oid: entry.id,
147− path: dest.to_path_buf(),
148− })?;
149− let symlink_destination = gix_path::try_from_byte_slice(obj.data)
150− .map_err(|_| crate::checkout::Error::IllformedUtf8 { path: obj.data.into() })?;
151−
152− if symlink {
153− try_op_or_unlink(dest, overwrite_existing, |p| {
154− gix_fs::symlink::create(symlink_destination, p)
155− })?;
156− } else {
157− let mut file = try_op_or_unlink(dest, overwrite_existing, |p| {
158− open_options(p, destination_is_initially_empty, overwrite_existing).open(dest)
159− })?;
160− file.write_all(obj.data)?;
161− file.close()?;
162− }
163−
164− entry.stat = Stat::from_fs(&std::fs::symlink_metadata(dest)?)?;
165− obj.data.len()
166− }
167− gix_index::entry::Mode::DIR => {
168− gix_features::trace::warn!(
169− "Skipped sparse directory at '{entry_path}' ({id}) as it cannot yet be handled",
170− id = entry.id
171− );
172− 0
173− }
174− gix_index::entry::Mode::COMMIT => {
175− gix_features::trace::warn!(
176− "Skipped submodule at '{entry_path}' ({id}) as it cannot yet be handled",
177− id = entry.id
178− );
179− 0
180− }
181− _ => unreachable!(),
182− };
183− Ok(Outcome::Written { bytes: object_size })
184−}
185−
186−/// Note that this works only because we assume to not race ourselves when symlinks are involved, and we do this by
187−/// delaying symlink creation to the end and will always do that sequentially.
188−/// It's still possible to fall for a race if other actors create symlinks in our path, but that's nothing to defend against.
189−fn try_op_or_unlink<T>(
190− path: &Path,
191− overwrite_existing: bool,
192− op: impl Fn(&Path) -> std::io::Result<T>,
193−) -> std::io::Result<T> {
194− if overwrite_existing {
195− match op(path) {
196− Ok(res) => Ok(res),
197− Err(err) if gix_fs::symlink::is_collision_error(&err) => {
198− try_unlink_path_recursively(path, &std::fs::symlink_metadata(path)?)?;
199− op(path)
200− }
201− Err(err) => Err(err),
202− }
203− } else {
204− op(path)
205− }
206−}
207−
208−fn try_unlink_path_recursively(path: &Path, path_meta: &std::fs::Metadata) -> std::io::Result<()> {
209− if path_meta.is_dir() {
210− std::fs::remove_dir_all(path)
211− } else if path_meta.file_type().is_symlink() {
212− gix_fs::symlink::remove(path)
213− } else {
214− std::fs::remove_file(path)
215− }
216−}
217−
218−#[cfg(not(debug_assertions))]
219−fn debug_assert_dest_is_no_symlink(_path: &Path) {}
220−
221−/// This is a debug assertion as we expect the machinery calling this to prevent this possibility in the first place
222−#[cfg(debug_assertions)]
223−fn debug_assert_dest_is_no_symlink(path: &Path) {
224− if let Ok(meta) = path.metadata() {
225− debug_assert!(
226− !meta.file_type().is_symlink(),
227− "BUG: should not ever allow to overwrite/write-into the target of a symbolic link: {}",
228− path.display()
229− );
230− }
231−}
232−
233−fn open_options(path: &Path, destination_is_initially_empty: bool, overwrite_existing: bool) -> OpenOptions {
234− if overwrite_existing || !destination_is_initially_empty {
235− debug_assert_dest_is_no_symlink(path);
236− }
237− let mut options = gix_features::fs::open_options_no_follow();
238− options
239− .create_new(destination_is_initially_empty && !overwrite_existing)
240− .create(!destination_is_initially_empty || overwrite_existing)
241− .write(true);
242− options
243−}
244−
245−pub(crate) fn open_file(
246− path: &Path,
247− destination_is_initially_empty: bool,
248− overwrite_existing: bool,
249− fs_supports_executable_bit: bool,
250− entry_mode: gix_index::entry::Mode,
251−) -> std::io::Result<(std::fs::File, bool)> {
252− #[cfg_attr(windows, allow(unused_mut))]
253− let mut options = open_options(path, destination_is_initially_empty, overwrite_existing);
254− let needs_executable_bit = fs_supports_executable_bit && entry_mode == gix_index::entry::Mode::FILE_EXECUTABLE;
255− #[cfg(unix)]
256− let set_executable_after_creation = if needs_executable_bit && destination_is_initially_empty {
257− use std::os::unix::fs::OpenOptionsExt;
258− // Note that these only work if the file was newly created, but won't if it's already
259− // existing, possibly without the executable bit set. Thus we do this only if the file is new.
260− options.mode(0o777);
261− false
262− } else {
263− needs_executable_bit
264− };
265− // not supported on windows
266− #[cfg(windows)]
267− let set_executable_after_creation = needs_executable_bit;
268− try_op_or_unlink(path, overwrite_existing, |p| options.open(p)).map(|f| (f, set_executable_after_creation))
269−}
270−
271−/// Close `file` and store its stats in `entry`, possibly setting `file` executable depending on `set_executable_after_creation`.
272−#[cfg_attr(windows, allow(unused_variables))]
273−pub(crate) fn finalize_entry<E>(
274− entry: &mut gix_index::Entry,
275− file: std::fs::File,
276− set_executable_after_creation: Option<&Path>,
277−) -> Result<(), crate::checkout::Error<E>>
278−where
279− E: std::error::Error + Send + Sync + 'static,
280−{
281− // For possibly existing, overwritten files, we must change the file mode explicitly.
282− #[cfg(unix)]
283− if let Some(path) = set_executable_after_creation {
284− use std::os::unix::fs::PermissionsExt;
285− let mut perm = std::fs::symlink_metadata(path)?.permissions();
286− perm.set_mode(0o777);
287− std::fs::set_permissions(path, perm)?;
288− }
289− // NOTE: we don't call `file.sync_all()` here knowing that some filesystems don't handle this well.
290− // revisit this once there is a bug to fix.
291− entry.stat = Stat::from_fs(&file.metadata()?)?;
292− file.close()?;
293− Ok(())
294−}
gix-worktree/src/checkout/function.rs+0−168
@@ -1,168 +0,0 @@
1−use std::sync::atomic::AtomicBool;
2−
3−use gix_features::{interrupt, parallel::in_parallel_with_finalize, progress::Progress};
4−use gix_hash::oid;
5−
6−use crate::{cache, checkout::chunk, Cache};
7−
8−/// Checkout the entire `index` into `dir`, and resolve objects found in index entries with `find` to write their content to their
9−/// respective path in `dir`.
10−/// Use `files` to count each fully checked out file, and count the amount written `bytes`. If `should_interrupt` is `true`, the
11−/// operation will abort.
12−/// `options` provide a lot of context on how to perform the operation.
13−///
14−/// ### Handling the return value
15−///
16−/// Note that interruption still produce an `Ok(…)` value, so the caller should look at `should_interrupt` to communicate the outcome.
17−///
18−#[allow(clippy::too_many_arguments)]
19−pub fn checkout<Find, E>(
20− index: &mut gix_index::State,
21− dir: impl Into<std::path::PathBuf>,
22− find: Find,
23− files: &mut impl Progress,
24− bytes: &mut impl Progress,
25− should_interrupt: &AtomicBool,
26− options: crate::checkout::Options,
27−) -> Result<crate::checkout::Outcome, crate::checkout::Error<E>>
28−where
29− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
30− E: std::error::Error + Send + Sync + 'static,
31−{
32− let paths = index.take_path_backing();
33− let res = checkout_inner(index, &paths, dir, find, files, bytes, should_interrupt, options);
34− index.return_path_backing(paths);
35− res
36−}
37−
38−#[allow(clippy::too_many_arguments)]
39−fn checkout_inner<Find, E>(
40− index: &mut gix_index::State,
41− paths: &gix_index::PathStorage,
42− dir: impl Into<std::path::PathBuf>,
43− find: Find,
44− files: &mut impl Progress,
45− bytes: &mut impl Progress,
46− should_interrupt: &AtomicBool,
47− mut options: crate::checkout::Options,
48−) -> Result<crate::checkout::Outcome, crate::checkout::Error<E>>
49−where
50− Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
51− E: std::error::Error + Send + Sync + 'static,
52−{
53− let num_files = files.counter();
54− let num_bytes = bytes.counter();
55− let dir = dir.into();
56− let case = if options.fs.ignore_case {
57− gix_glob::pattern::Case::Fold
58− } else {
59− gix_glob::pattern::Case::Sensitive
60− };
61− let (chunk_size, thread_limit, num_threads) = gix_features::parallel::optimize_chunk_size_and_thread_limit(
62− 100,
63− index.entries().len().into(),
64− options.thread_limit,
65− None,
66− );
67−
68− let state = cache::State::for_checkout(options.overwrite_existing, std::mem::take(&mut options.attributes));
69− let attribute_files = state.id_mappings_from_index(index, paths, case);
70− let mut ctx = chunk::Context {
71− buf: Vec::new(),
72− options: (&options).into(),
73− path_cache: Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files),
74− filters: options.filters,
75− find,
76− };
77−
78− let chunk::Outcome {
79− mut collisions,
80− mut errors,
81− mut bytes_written,
82− files: files_updated,
83− delayed_symlinks,
84− delayed_paths_unknown,
85− delayed_paths_unprocessed,
86− } = if num_threads == 1 {
87− let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
88− let mut delayed_filter_results = Vec::new();
89− let mut out = chunk::process(
90− entries_with_paths,
91− num_files.as_deref(),
92− num_bytes.as_deref(),
93− &mut delayed_filter_results,
94− &mut ctx,
95− )?;
96− chunk::process_delayed_filter_results(
97− delayed_filter_results,
98− num_files.as_deref(),
99− num_bytes.as_deref(),
100− &mut out,
101− &mut ctx,
102− )?;
103− out
104− } else {
105− let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
106− in_parallel_with_finalize(
107− gix_features::iter::Chunks {
108− inner: entries_with_paths,
109− size: chunk_size,
110− },
111− thread_limit,
112− {
113− let ctx = ctx.clone();
114− move |_| (Vec::new(), ctx)
115− },
116− |chunk, (delayed_filter_results, ctx)| {
117− chunk::process(
118− chunk.into_iter(),
119− num_files.as_deref(),
120− num_bytes.as_deref(),
121− delayed_filter_results,
122− ctx,
123− )
124− },
125− |(delayed_filter_results, mut ctx)| {
126− let mut out = chunk::Outcome::default();
127− chunk::process_delayed_filter_results(
128− delayed_filter_results,
129− num_files.as_deref(),
130− num_bytes.as_deref(),
131− &mut out,
132− &mut ctx,
133− )?;
134− Ok(out)
135− },
136− chunk::Reduce {
137− files: num_files.is_none().then_some(files),
138− bytes: num_bytes.is_none().then_some(bytes),
139− aggregate: Default::default(),
140− marker: Default::default(),
141− },
142− )?
143− };
144−
145− for (entry, entry_path) in delayed_symlinks {
146− bytes_written += chunk::checkout_entry_handle_result(
147− entry,
148− entry_path,
149− &mut errors,
150− &mut collisions,
151− num_files.as_deref(),
152− num_bytes.as_deref(),
153− &mut ctx,
154− )?
155− .as_bytes()
156− .expect("only symlinks are delayed here, they are never filtered (or delayed again)")
157− as u64;
158− }
159−
160− Ok(crate::checkout::Outcome {
161− files_updated,
162− collisions,
163− errors,
164− bytes_written,
165− delayed_paths_unknown,
166− delayed_paths_unprocessed,
167− })
168−}
gix-worktree/src/checkout/mod.rs+0−101
@@ -1,101 +0,0 @@
1−use bstr::BString;
2−use gix_index::entry::stat;
3−
4−/// Information about a path that failed to checkout as something else was already present.
5−#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
6−pub struct Collision {
7− /// the path that collided with something already present on disk.
8− pub path: BString,
9− /// The io error we encountered when checking out `path`.
10− pub error_kind: std::io::ErrorKind,
11−}
12−
13−/// A path that encountered an IO error.
14−pub struct ErrorRecord {
15− /// the path that encountered the error.
16− pub path: BString,
17− /// The error
18− pub error: Box<dyn std::error::Error + Send + Sync + 'static>,
19−}
20−
21−/// The outcome of checking out an entire index.
22−#[derive(Default)]
23−pub struct Outcome {
24− /// The amount of files updated, or created.
25− pub files_updated: usize,
26− /// The amount of bytes written to disk,
27− pub bytes_written: u64,
28− /// The encountered collisions, which can happen on a case-insensitive filesystem.
29− pub collisions: Vec<Collision>,
30− /// Other errors that happened during checkout.
31− pub errors: Vec<ErrorRecord>,
32− /// Relative paths that the process listed as 'delayed' even though we never passed them.
33− pub delayed_paths_unknown: Vec<BString>,
34− /// All paths that were left unprocessed, because they were never listed by the process even though we passed them.
35− pub delayed_paths_unprocessed: Vec<BString>,
36−}
37−
38−/// Options to further configure the checkout operation.
39−#[derive(Clone, Default)]
40−pub struct Options {
41− /// capabilities of the file system
42− pub fs: gix_fs::Capabilities,
43− /// If set, don't use more than this amount of threads.
44− /// Otherwise, usually use as many threads as there are logical cores.
45− /// A value of 0 is interpreted as no-limit
46− pub thread_limit: Option<usize>,
47− /// If true, we assume no file to exist in the target directory, and want exclusive access to it.
48− /// This should be enabled when cloning to avoid checks for freshness of files. This also enables
49− /// detection of collisions based on whether or not exclusive file creation succeeds or fails.
50− pub destination_is_initially_empty: bool,
51− /// If true, default false, worktree entries on disk will be overwritten with content from the index
52− /// even if they appear to be changed. When creating directories that clash with existing worktree entries,
53− /// these will try to delete the existing entry.
54− /// This is similar in behaviour as `git checkout --force`.
55− pub overwrite_existing: bool,
56− /// If true, default false, try to checkout as much as possible and don't abort on first error which isn't
57− /// due to a conflict.
58− /// The checkout operation will never fail, but count the encountered errors instead along with their paths.
59− pub keep_going: bool,
60− /// Control how stat comparisons are made when checking if a file is fresh.
61− pub stat_options: stat::Options,
62− /// A stack of attributes to use with the filesystem cache to use as driver for filters.
63− pub attributes: crate::cache::state::Attributes,
64− /// The filter pipeline to use for applying mandatory filters before writing to the worktree.
65− pub filters: gix_filter::Pipeline,
66− /// Control how long-running processes may use the 'delay' capability.
67− pub filter_process_delay: gix_filter::driver::apply::Delay,
68−}
69−
70−/// The error returned by the [checkout()][crate::checkout()] function.
71−#[derive(Debug, thiserror::Error)]
72−#[allow(missing_docs)]
73−pub enum Error<E: std::error::Error + Send + Sync + 'static> {
74− #[error("Could not convert path to UTF8: {}", .path)]
75− IllformedUtf8 { path: BString },
76− #[error("The clock was off when reading file related metadata after updating a file on disk")]
77− Time(#[from] std::time::SystemTimeError),
78− #[error("IO error while writing blob or reading file metadata or changing filetype")]
79− Io(#[from] std::io::Error),
80− #[error("object {} for checkout at {} could not be retrieved from object database", .oid.to_hex(), .path.display())]
81− Find {
82− #[source]
83− err: E,
84− oid: gix_hash::ObjectId,
85− path: std::path::PathBuf,
86− },
87− #[error(transparent)]
88− Filter(#[from] gix_filter::pipeline::convert::to_worktree::Error),
89− #[error(transparent)]
90− FilterListDelayed(#[from] gix_filter::driver::delayed::list::Error),
91− #[error(transparent)]
92− FilterFetchDelayed(#[from] gix_filter::driver::delayed::fetch::Error),
93− #[error("The entry at path '{rela_path}' was listed as delayed by the filter process, but we never passed it")]
94− FilterPathUnknown { rela_path: BString },
95− #[error("The following paths were delayed and apparently forgotten to be processed by the filter driver: ")]
96− FilterPathsUnprocessed { rela_paths: Vec<BString> },
97−}
98−
99−mod chunk;
100−mod entry;
101−pub(crate) mod function;
gix-worktree/src/lib.rs+1−10
@@ -1,4 +1,4 @@
1−//! A crate with all index-centric functionality that is interacting with a worktree.
1+//! A crate with utility types for use by other crates that implement specifics.
22 //!
33 //! Unless specified differently, all operations need an index file (e.g. `.git/index`) as driver.
44 //!
@@ -11,9 +11,6 @@ · 6 unmodified lines
1111 #![deny(missing_docs, rust_2018_idioms, unsafe_code)]
1212 use bstr::BString;
1313
14−///
15−pub mod read;
16−
1714 /// A cache for efficiently executing operations on directories and files which are encountered in sorted order.
1815 /// That way, these operations can be re-used for subsequent invocations in the same directory.
1916 ///
@@ -52,9 +49,3 @@ pub(crate) type PathIdMapping = (BString, gix_hash::ObjectId); · 32 unmodified lines
5249
5350 ///
5451 pub mod cache;
55−///
56−pub mod checkout;
57−pub use checkout::function::checkout;
58−
59−pub mod status;
60−pub use status::function::status;
gix-worktree/src/read.rs+0−64
@@ -1,64 +0,0 @@
1−//! This module allows creating git blobs from worktree files.
2−//!
3−//! For the most part a blob just contains the raw on-disk data. However symlinks need to be considered properly
4−//! and attributes/config options need to be considered.
5−
6−use std::{
7− fs::{read_link, File},
8− io::{self, Read},
9− path::Path,
10−};
11−
12−use gix_object::Blob;
13−use gix_path as path;
14−
15−// TODO: tests
16−
17−// TODO: what to do about precompose unicode and ignore_case for symlinks
18−
19−/// Create a blob from a file or symlink.
20−pub fn blob(path: &Path, capabilities: &gix_fs::Capabilities) -> io::Result<Blob> {
21− let mut data = Vec::new();
22− data_to_buf(path, &mut data, capabilities)?;
23− Ok(Blob { data })
24−}
25−
26−/// Create a blob from a file or symlink.
27−pub fn blob_with_meta(path: &Path, is_symlink: bool, capabilities: &gix_fs::Capabilities) -> io::Result<Blob> {
28− let mut data = Vec::new();
29− data_to_buf_with_meta(path, &mut data, is_symlink, capabilities)?;
30− Ok(Blob { data })
31−}
32−
33−/// Create blob data from a file or symlink.
34−pub fn data_to_buf<'a>(path: &Path, buf: &'a mut Vec<u8>, capabilities: &gix_fs::Capabilities) -> io::Result<&'a [u8]> {
35− data_to_buf_with_meta(path, buf, path.symlink_metadata()?.is_symlink(), capabilities)
36−}
37−
38−/// Create a blob from a file or symlink.
39−pub fn data_to_buf_with_meta<'a>(
40− path: &Path,
41− buf: &'a mut Vec<u8>,
42− is_symlink: bool,
43− capabilities: &gix_fs::Capabilities,
44−) -> io::Result<&'a [u8]> {
45− buf.clear();
46− // symlinks are only stored as actual symlinks if the FS supports it otherwise they are just
47− // normal files with their content equal to the linked path (so can be read normally)
48− //
49− if is_symlink && capabilities.symlink {
50− // conversion to bstr can never fail because symlinks are only used
51− // on unix (by git) so no reason to use the try version here
52− let symlink_path = path::into_bstr(read_link(path)?);
53− buf.extend_from_slice(&symlink_path);
54− // TODO: there is no reason this should be a clone
55− // std isn't great about allowing users to avoid allocations but we could
56− // simply write our own wrapper around libc::readlink which reuses the
57− // buffer. This would require unsafe code tough (obviously)
58− } else {
59− buf.clear();
60− File::open(path)?.read_to_end(buf)?;
61− // TODO apply filters
62− }
63− Ok(buf.as_slice())
64−}
gix-worktree/src/status/content.rs+0−80
@@ -1,80 +0,0 @@
1−use gix_hash::ObjectId;
2−use gix_index as index;
3−use index::Entry;
4−
5−/// Compares the content of two blobs in some way.
6−pub trait CompareBlobs {
7− /// Output data produced by [`compare_blobs()`][CompareBlobs::compare_blobs()].
8− type Output;
9−
10− /// Providing the underlying index `entry`, allow comparing a file in the worktree of size `worktree_blob_size`
11− /// and allow reading its bytes using `worktree_blob`.
12− /// If this function returns `None` the `entry` and the `worktree_blob` are assumed to be identical.
13− /// Use `entry_blob` to obtain the data for the blob referred to by `entry`, allowing comparisons of the data itself.
14− fn compare_blobs<'a, E>(
15− &mut self,
16− entry: &'a gix_index::Entry,
17− worktree_blob_size: usize,
18− worktree_blob: impl ReadDataOnce<'a, E>,
19− entry_blob: impl ReadDataOnce<'a, E>,
20− ) -> Result<Option<Self::Output>, E>;
21−}
22−
23−/// Lazy borrowed access to blob data.
24−pub trait ReadDataOnce<'a, E> {
25− /// Returns the contents of this blob.
26− ///
27− /// This potentially performs IO and other expensive operations
28− /// and should only be called when necessary.
29− fn read_data(self) -> Result<&'a [u8], E>;
30−}
31−
32−/// Compares to blobs by comparing their size and oid, and only looks at the file if
33−/// the size matches, therefore it's very fast.
34−#[derive(Clone)]
35−pub struct FastEq;
36−
37−impl CompareBlobs for FastEq {
38− type Output = ();
39−
40− fn compare_blobs<'a, E>(
41− &mut self,
42− entry: &'a Entry,
43− worktree_blob_size: usize,
44− worktree_blob: impl ReadDataOnce<'a, E>,
45− _entry_blob: impl ReadDataOnce<'a, E>,
46− ) -> Result<Option<Self::Output>, E> {
47− // make sure to account for racily smudged entries here so that they don't always keep
48− // showing up as modified even after their contents have changed again, to a potentially
49− // unmodified state. That means that we want to ignore stat.size == 0 for non_empty_blobs.
50− if entry.stat.size as usize != worktree_blob_size && (entry.id.is_empty_blob() || entry.stat.size != 0) {
51− return Ok(Some(()));
52− }
53− let blob = worktree_blob.read_data()?;
54− let file_hash = gix_object::compute_hash(entry.id.kind(), gix_object::Kind::Blob, blob);
55− Ok((entry.id != file_hash).then_some(()))
56− }
57−}
58−
59−/// Compares files to blobs by *always* comparing their hashes.
60−///
61−/// Same as [`FastEq`] but does not contain a fast path for files with mismatched files and
62−/// therefore always returns an OID that can be reused later.
63−#[derive(Clone)]
64−pub struct HashEq;
65−
66−impl CompareBlobs for HashEq {
67− type Output = ObjectId;
68−
69− fn compare_blobs<'a, E>(
70− &mut self,
71− entry: &'a Entry,
72− _worktree_blob_size: usize,
73− worktree_blob: impl ReadDataOnce<'a, E>,
74− _entry_blob: impl ReadDataOnce<'a, E>,
75− ) -> Result<Option<Self::Output>, E> {
76− let blob = worktree_blob.read_data()?;
77− let file_hash = gix_object::compute_hash(entry.id.kind(), gix_object::Kind::Blob, blob);
78− Ok((entry.id != file_hash).then_some(file_hash))
79− }
80−}
gix-worktree/src/status/function.rs+0−331
@@ -1,331 +0,0 @@
1−use std::{io, marker::PhantomData, path::Path};
2−
3−use bstr::BStr;
4−use filetime::FileTime;
5−use gix_features::parallel::{in_parallel_if, Reduce};
6−
7−use crate::{
8− read,
9− status::{
10− content,
11− content::CompareBlobs,
12− types::{Error, Options},
13− Change, VisitEntry,
14− },
15−};
16−
17−/// Calculates the changes that need to be applied to an `index` to match the state of the `worktree` and makes them
18−/// observable in `collector`, along with information produced by `compare` which gets to see blobs that may have changes.
19−/// `options` are used to configure the operation.
20−///
21−/// Note that `index` is updated with the latest seen stat information from the worktree, and its timestamp is adjusted to
22−/// the current time for which it will be considered fresh.
23−///
24−/// Note that this isn't technically quite what this function does as this also provides some additional information,
25−/// like whether a file has conflicts, and files that were added with `git add` are shown as a special
26−/// changes despite not technically requiring a change to the index since `git add` already added the file to the index.
27−pub fn status<'index, T, Find, E>(
28− index: &'index mut gix_index::State,
29− worktree: &Path,
30− collector: &mut impl VisitEntry<'index, ContentChange = T>,
31− compare: impl CompareBlobs<Output = T> + Send + Clone,
32− find: Find,
33− options: Options,
34−) -> Result<(), Error>
35−where
36− T: Send,
37− E: std::error::Error + Send + Sync + 'static,
38− Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
39−{
40− // the order is absolutely critical here we use the old timestamp to detect racy index entries
41− // (modified at or after the last index update) during the index update we then set those
42− // entries size to 0 (see below) to ensure they keep showing up as racy and reset the timestamp.
43− let timestamp = index.timestamp();
44− index.set_timestamp(FileTime::now());
45− let (chunk_size, thread_limit, _) = gix_features::parallel::optimize_chunk_size_and_thread_limit(
46− 100,
47− index.entries().len().into(),
48− options.thread_limit,
49− None,
50− );
51− let (entries, path_backing) = index.entries_mut_and_pathbacking();
52− in_parallel_if(
53− || true, // TODO: heuristic: when is parallelization not worth it?
54− entries.chunks_mut(chunk_size),
55− thread_limit,
56− {
57− let options = &options;
58− move |_| {
59− (
60− State {
61− buf: Vec::new(),
62− odb_buf: Vec::new(),
63− timestamp,
64− path_backing,
65− worktree,
66− options,
67− },
68− compare.clone(),
69− find.clone(),
70− )
71− }
72− },
73− |entries, (state, diff, find)| {
74− entries
75− .iter_mut()
76− .filter_map(|entry| state.process(entry, diff, find))
77− .collect()
78− },
79− ReduceChange {
80− collector,
81− phantom: PhantomData,
82− },
83− )
84−}
85−
86−struct State<'a, 'b> {
87− buf: Vec<u8>,
88− odb_buf: Vec<u8>,
89− timestamp: FileTime,
90− // path_cache: fs::Cache TODO path cache
91− path_backing: &'b [u8],
92− worktree: &'a Path,
93− options: &'a Options,
94−}
95−
96−type StatusResult<'index, T> = Result<(&'index gix_index::Entry, &'index BStr, Option<Change<T>>, bool), Error>;
97−
98−impl<'index> State<'_, 'index> {
99− fn process<T, Find, E>(
100− &mut self,
101− entry: &'index mut gix_index::Entry,
102− diff: &mut impl CompareBlobs<Output = T>,
103− find: &mut Find,
104− ) -> Option<StatusResult<'index, T>>
105− where
106− E: std::error::Error + Send + Sync + 'static,
107− Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
108− {
109− let conflict = match entry.stage() {
110− 0 => false,
111− 1 => true,
112− _ => return None,
113− };
114− if entry.flags.intersects(
115− gix_index::entry::Flags::UPTODATE
116− | gix_index::entry::Flags::SKIP_WORKTREE
117− | gix_index::entry::Flags::ASSUME_VALID
118− | gix_index::entry::Flags::FSMONITOR_VALID,
119− ) {
120− return None;
121− }
122− let path = entry.path_in(self.path_backing);
123− let status = self.compute_status(&mut *entry, path, diff, find);
124− Some(status.map(move |status| (&*entry, path, status, conflict)))
125− }
126−
127− /// # On how racy-git is handled here
128− ///
129− /// Basically the racy detection is a safety mechanism that ensures we can always just compare the stat
130− /// information between index and worktree and if they match we don't need to look at the content.
131− /// This usually just works but if a file updates quickly we could run into the following situation:
132− ///
133− /// * save file version `A` from disk into worktree (git add)
134− /// * file is changed so fast that the mtime doesn't change - *we only looks at seconds by default*
135− /// * file contents change but file-size stays the same, so `"foo" -> "bar"` has the same size but different content
136− ///
137− /// Now both `mtime` and `size`, and all other stat information, is the same but the file has actually changed.
138− /// This case is called *racily clean*. *The file should show up as changed but due to a data race it doesn't.*
139− /// This is the racy git problem.
140− ///
141− /// To solve this we do the following trick: Whenever we modify the index, which includes `git status`, we save the
142− /// current timestamp before the modification starts. This timestamp fundamentally represents a checkpoint of sorts.
143− /// We "promise" ourselves that after the modification finishes all entries modified before this timestamp have the
144− /// racy git problem resolved.
145− ///
146− /// So now when we modify the index we must resolve the racy git problem somehow. To do that we only need to look at
147− /// unchanged entries. Changed entries are not interesting since they are already showing up as changed anyway so there
148− /// isn't really a race-condition to worry about. This also explains why removing the `return` here doesn't have an apparent effect.
149− /// This entire branch here is just the optimization of "don't even look at index entries where the stat hasn't changed".
150− /// If we don't have this optimization the result shouldn't change, our status implementation will just be super slow :D
151−
152− /// We calculate whether this change is `racy_clean`, so if the last `timestamp` is before or the same as the `mtime` of the entry
153− /// which is what `new_stat.is_racy(..)` does in the branch, and only if we are sure that there is no race condition
154− /// do we `return` early. Since we don't `return` early we just do a full content comparison below,
155− /// which always yields the correct result, there is no race condition there.
156− ///
157− /// If a file showed up as racily clean and didn't change then we don't need to do anything. After this status check is
158− /// complete and the file won't show up as racily clean anymore, since it's mtime is now before the new timestamp.
159− /// However if the file did actually change then we really ran into one of those rare race conditions in that case we,
160− /// and git does the same, set the size of the file in the index to 0. This will always make the file show up as changed.
161− /// This adds the need to treat all files of size 0 in the index as changed. This is not quite right of course because 0 sized files
162− /// could be entirely valid and unchanged. Therefore this only applies if the oid doesn't match the oid of an empty file,
163− /// which is a constant.
164− ///
165− /// Adapted from [here](https://github.com/Byron/gitoxide/pull/805#discussion_r1164676777).
166− fn compute_status<T, Find, E>(
167− &mut self,
168− entry: &mut gix_index::Entry,
169− git_path: &BStr,
170− diff: &mut impl CompareBlobs<Output = T>,
171− find: &mut Find,
172− ) -> Result<Option<Change<T>>, Error>
173− where
174− E: std::error::Error + Send + Sync + 'static,
175− Find: for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
176− {
177− // TODO fs cache
178− let worktree_path = gix_path::try_from_bstr(git_path).map_err(|_| Error::IllformedUtf8)?;
179− let worktree_path = self.worktree.join(worktree_path);
180− let metadata = match worktree_path.symlink_metadata() {
181− // TODO: check if any parent directory is a symlink
182− // we need to use fs::Cache for that
183− Ok(metadata) if metadata.is_dir() => {
184− // index entries are normally only for files/symlinks
185− // if a file turned into a directory it was removed
186− // the only exception here are submodules which are
187− // part of the index despite being directories
188− //
189− // TODO: submodules:
190− // if entry.mode.contains(Mode::COMMIT) &&
191− // resolve_gitlink_ref(ce->name, "HEAD", &sub))
192− return Ok(Some(Change::Removed));
193− }
194− Ok(metadata) => metadata,
195− Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Some(Change::Removed)),
196− Err(err) => {
197− return Err(err.into());
198− }
199− };
200− if entry.flags.contains(gix_index::entry::Flags::INTENT_TO_ADD) {
201− return Ok(Some(Change::IntentToAdd));
202− }
203− let new_stat = gix_index::entry::Stat::from_fs(&metadata)?;
204− let executable_bit_changed =
205− match entry
206− .mode
207− .change_to_match_fs(&metadata, self.options.fs.symlink, self.options.fs.executable_bit)
208− {
209− Some(gix_index::entry::mode::Change::Type { .. }) => return Ok(Some(Change::Type)),
210− Some(gix_index::entry::mode::Change::ExecutableBit) => true,
211− None => false,
212− };
213−
214− // Here we implement racy-git. See racy-git.txt in the git documentation for a detailed documentation.
215− //
216− // A file is racy if:
217− // 1. its `mtime` is at or after the last index timestamp and its entry stat information
218− // matches the on-disk file but the file contents are actually modified
219− // 2. it's size is 0 (set after detecting a file was racy previously)
220− //
221− // The first case is detected below by checking the timestamp if the file is marked unmodified.
222− // The second case is usually detected either because the on-disk file is not empty, hence
223− // the basic stat match fails, or by checking whether the size doesn't fit the oid.
224− let mut racy_clean = false;
225− if !executable_bit_changed
226− && new_stat.matches(&entry.stat, self.options.stat)
227− // TODO: find a test for the following line or remove it. Is this more often hit with smudge/clean filters?
228− && (!entry.id.is_empty_blob() || entry.stat.size == 0)
229− {
230− racy_clean = new_stat.is_racy(self.timestamp, self.options.stat);
231− if !racy_clean {
232− return Ok(None);
233− }
234− }
235−
236− let read_file = WorktreeBlob {
237− buf: &mut self.buf,
238− path: &worktree_path,
239− entry,
240− options: self.options,
241− };
242− let read_blob = OdbBlob {
243− buf: &mut self.odb_buf,
244− id: &entry.id,
245− find,
246− };
247− let content_change = diff.compare_blobs::<Error>(entry, metadata.len() as usize, read_file, read_blob)?;
248− // This file is racy clean! Set the size to 0 so we keep detecting this as the file is updated.
249− if content_change.is_some() && racy_clean {
250− entry.stat.size = 0;
251− }
252− if content_change.is_some() || executable_bit_changed {
253− Ok(Some(Change::Modification {
254− executable_bit_changed,
255− content_change,
256− }))
257− } else {
258− // don't diff against this file next time since we know the file is unchanged.
259− entry.stat = new_stat;
260− Ok(None)
261− }
262− }
263−}
264−
265−struct ReduceChange<'a, 'index, T: VisitEntry<'index>> {
266− collector: &'a mut T,
267− phantom: PhantomData<fn(&'index ())>,
268−}
269−
270−impl<'index, T, C: VisitEntry<'index, ContentChange = T>> Reduce for ReduceChange<'_, 'index, C> {
271− type Input = Vec<StatusResult<'index, T>>;
272−
273− type FeedProduce = ();
274−
275− type Output = ();
276−
277− type Error = Error;
278−
279− fn feed(&mut self, items: Self::Input) -> Result<Self::FeedProduce, Self::Error> {
280− for item in items {
281− let (entry, path, change, conflict) = item?;
282− self.collector.visit_entry(entry, path, change, conflict);
283− }
284− Ok(())
285− }
286−
287− fn finalize(self) -> Result<Self::Output, Self::Error> {
288− Ok(())
289− }
290−}
291−
292−struct WorktreeBlob<'a> {
293− buf: &'a mut Vec<u8>,
294− path: &'a Path,
295− entry: &'a gix_index::Entry,
296− options: &'a Options,
297−}
298−
299−struct OdbBlob<'a, Find, E>
300−where
301− E: std::error::Error + Send + Sync + 'static,
302− Find: FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
303−{
304− buf: &'a mut Vec<u8>,
305− id: &'a gix_hash::oid,
306− find: Find,
307−}
308−
309−impl<'a> content::ReadDataOnce<'a, Error> for WorktreeBlob<'a> {
310− fn read_data(self) -> Result<&'a [u8], Error> {
311− let res = read::data_to_buf_with_meta(
312− self.path,
313− self.buf,
314− self.entry.mode == gix_index::entry::Mode::SYMLINK,
315− &self.options.fs,
316− )?;
317− Ok(res)
318− }
319−}
320−
321−impl<'a, Find, E> content::ReadDataOnce<'a, Error> for OdbBlob<'a, Find, E>
322−where
323− E: std::error::Error + Send + Sync + 'static,
324− Find: FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>,
325−{
326− fn read_data(mut self) -> Result<&'a [u8], Error> {
327− (self.find)(self.id, self.buf)
328− .map(|b| b.data)
329− .map_err(move |err| Error::Find(Box::new(err)))
330− }
331−}
gix-worktree/src/status/mod.rs+0−11
@@ -1,11 +0,0 @@
1−//! Changes between an index and a worktree.
2−///
3−mod types;
4−pub use types::{Change, Error, Options, VisitEntry};
5−
6−mod recorder;
7−pub use recorder::Recorder;
8−
9−///
10−pub mod content;
11−pub(crate) mod function;
gix-worktree/src/status/recorder.rs+0−27
@@ -1,27 +0,0 @@
1−use bstr::BStr;
2−use gix_index as index;
3−
4−use crate::status::{Change, VisitEntry};
5−
6−/// Convenience implementation of [`VisitEntry`] that collects all non-trivial changes into a `Vec`.
7−#[derive(Debug, Default)]
8−pub struct Recorder<'index, T = ()> {
9− /// collected changes, index entries without conflicts or changes are excluded.
10− pub records: Vec<(&'index BStr, Option<Change<T>>, bool)>,
11−}
12−
13−impl<'index, T: Send> VisitEntry<'index> for Recorder<'index, T> {
14− type ContentChange = T;
15−
16− fn visit_entry(
17− &mut self,
18− _entry: &'index index::Entry,
19− rela_path: &'index BStr,
20− status: Option<Change<Self::ContentChange>>,
21− conflict: bool,
22− ) {
23− if conflict || status.is_some() {
24− self.records.push((rela_path, status, conflict))
25− }
26− }
27−}
gix-worktree/src/status/types.rs+0−69
@@ -1,69 +0,0 @@
1−use bstr::BStr;
2−
3−/// The error returned by [`status()`][crate::status()].
4−#[derive(Debug, thiserror::Error)]
5−#[allow(missing_docs)]
6−pub enum Error {
7− #[error("Could not convert path to UTF8")]
8− IllformedUtf8,
9− #[error("The clock was off when reading file related metadata after updating a file on disk")]
10− Time(#[from] std::time::SystemTimeError),
11− #[error("IO error while writing blob or reading file metadata or changing filetype")]
12− Io(#[from] std::io::Error),
13− #[error("Failed to obtain blob from object database")]
14− Find(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
15−}
16−
17−#[derive(Clone, Default)]
18−/// Options that control how the index status with a worktree is computed.
19−pub struct Options {
20− /// Capabilities of the file system which affect the status computation.
21− pub fs: gix_fs::Capabilities,
22− /// If set, don't use more than this amount of threads.
23− /// Otherwise, usually use as many threads as there are logical cores.
24− /// A value of 0 is interpreted as no-limit
25− pub thread_limit: Option<usize>,
26− /// Options that control how stat comparisons are made when checking if a file is fresh.
27− pub stat: gix_index::entry::stat::Options,
28−}
29−
30−/// How an index entry needs to be changed to obtain the destination worktree state, i.e. `entry.apply(this_change) == worktree-entry`.
31−#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
32−pub enum Change<T = ()> {
33− /// This corresponding file does not exist in the worktree anymore.
34− Removed,
35− /// The type of file changed compared to the worktree, i.e. a symlink s now a file.
36− Type,
37− /// This worktree file was modified in some form, like a permission change or content change or both,
38− /// as compared to this entry.
39− Modification {
40− /// Indicates that one of the stat changes was an executable bit change
41− /// which is a significant change itself.
42− executable_bit_changed: bool,
43− /// The output of the [`CompareBlobs`][crate::status::content::CompareBlobs] run on this entry.
44− /// If there is no content change and only the executable bit
45− /// changed than this is `None`.
46− content_change: Option<T>,
47− },
48− /// An index entry that correspond to an untracked worktree file marked with `git add --intent-to-add`.
49− ///
50− /// This means it's not available in the object database yet or the index was created from,
51− /// even though now an entry exists that represents the worktree file.
52− IntentToAdd,
53−}
54−
55−/// Observe changes by comparing an index entry to the worktree or another index.
56−pub trait VisitEntry<'index> {
57− /// Data generated by comparing an entry with a file.
58− type ContentChange;
59− /// Observe the `change` of `entry` at the repository-relative `rela_path`, indicating whether
60− /// or not it has a `conflict`.
61− /// If `change` is `None`, there is no change.
62− fn visit_entry(
63− &mut self,
64− entry: &'index gix_index::Entry,
65− rela_path: &'index BStr,
66− change: Option<Change<Self::ContentChange>>,
67− conflict: bool,
68− );
69−}
gix-worktree/src/untracked.rs+0−1
@@ -1 +0,0 @@
1−// TODO: untracked file detection, needs fs::Cache
gix-worktree/tests/fixtures/generated-archives/.gitignore+0−5
@@ -1,7 +1,2 @@
11 make_ignore_and_attributes_setup.tar.xz
2−make_mixed_without_submodules.tar.xz
3−make_mixed_without_submodules_and_symlinks.tar.xz
42 make_attributes_baseline.tar.xz
5−make_dangerous_symlink.tar.xz
6−status_unchanged.tar.xz
7−status_changed.tar.xz
gix-worktree/tests/fixtures/generated-archives/make_ignorecase_collisions.tar.xz+0−3
@@ -1,3 +0,0 @@
1−version https://git-lfs.github.com/spec/v1
2−oid sha256:2984e2e61b28635014165351cd872ea7e4f09c28b8b4bbe50692a465ef648033
3−size 10616
gix-worktree/tests/fixtures/generated-archives/make_special_exclude_case.tar.xz+2−2
@@ -1,3 +1,3 @@
11 version https://git-lfs.github.com/spec/v1
2−oid sha256:24f605623efc49819d1b30c52fe22da8f94f2d267e8030ec9bc3b9b845801f76
3−size 9220
2+oid sha256:1804dc740055b8a5afe65a2db14f29c8ae4691896e67342a8dcb11530fd448c6
3+size 9240
gix-worktree/tests/fixtures/generated-archives/racy_git.tar.xz+0−3
@@ -1,3 +0,0 @@
1−version https://git-lfs.github.com/spec/v1
2−oid sha256:35b728a127f3b6170bac44469ff36d5ad0be2a4247a8926f1aaffb97b5973efc
3−size 1596
gix-worktree/tests/fixtures/generated-archives/status_conflict.tar.xz+0−3
@@ -1,3 +0,0 @@
1−version https://git-lfs.github.com/spec/v1
2−oid sha256:cd6d32ab7a1e372d80a617926cac2463f6620baedf74642d78fe7f8c956fd031
3−size 11036
gix-worktree/tests/fixtures/generated-archives/status_intent_to_add.tar.xz+0−3
@@ -1,3 +0,0 @@
1−version https://git-lfs.github.com/spec/v1
2−oid sha256:876670d74c01249d361aa73d83ab20d846db7c922a3ca825f778b5f9d746c401
3−size 9304
gix-worktree/tests/fixtures/generated-archives/status_removed.tar.xz+0−3
@@ -1,3 +0,0 @@
1−version https://git-lfs.github.com/spec/v1
2−oid sha256:e5fe85a65e3689e9e60598130be60761dc4ea129e04d7d5501320f7ebad1eb2b
3−size 10520
gix-worktree/tests/fixtures/make_dangerous_symlink.sh+0−24
@@ -1,24 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−# Every symlink is dangerous as it might either link to another directory and thus redirect
7−# all writes in the path, or it might point to a file and opening the symlink actually opens
8−# the target.
9−# We handle this by either validating symlinks specifically or create symlinks
10−empty_oid=$(git hash-object -w --stdin </dev/null)
11−fake_dir_target=$(echo -n 'A-dir' | git hash-object -w --stdin)
12−fake_file_target=$(echo -n 'A-file' | git hash-object -w --stdin)
13−
14−git update-index --index-info <<-EOF
15−100644 $empty_oid A-dir/a
16−100644 $empty_oid A-file
17−120000 $fake_dir_target FAKE-DIR
18−120000 $fake_file_target FAKE-FILE
19−100644 $empty_oid fake-dir/b
20−100644 $empty_oid fake-file
21−EOF
22−
23−git commit -m "init"
24−git checkout -f HEAD;
gix-worktree/tests/fixtures/make_ignore_setup.sh+0−18
@@ -1,18 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−cat <<EOF >.git/info/exclude
7−# a sample .git/info/exclude
8−file-anywhere
9−/file-from-top
10−
11−dir-anywhere/
12−/dir-from-top
13−
14−subdir-anywhere/file
15−subdir-anywhere/dir/
16−EOF
17−
18−git commit --allow-empty -m "init"
gix-worktree/tests/fixtures/make_ignorecase_collisions.sh+0−27
@@ -1,27 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−empty_oid=$(git hash-object -w --stdin </dev/null)
7−content_oid=$(echo "content" | git hash-object -w --stdin)
8−symlink_target=$(echo -n 'X' | git hash-object -w --stdin)
9−
10−echo "FILE_? filter=arrow" > .gitattributes
11−git add -A
12−
13−git update-index --index-info <<-EOF
14−100644 $content_oid FILE_X
15−100644 $content_oid FILE_x
16−100644 $content_oid file_X
17−100644 $content_oid file_x
18−100644 $empty_oid D/B
19−100644 $empty_oid D/C
20−100644 $empty_oid d
21−100644 $empty_oid X
22−120000 $symlink_target x
23−120000 $symlink_target link-to-X
24−EOF
25−
26−git commit -m "init"
27−git checkout -f HEAD;
gix-worktree/tests/fixtures/make_mixed_without_submodules.sh+0−20
@@ -1,20 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−touch empty
7−echo -n "content" > executable
8−chmod +x executable
9−
10−mkdir dir
11−echo "other content" > dir/content
12−echo "* filter=arrow" > .gitattributes
13−echo "executable -filter" >> .gitattributes
14−echo ".gitattributes -filter" >> .gitattributes
15−
16−mkdir dir/sub-dir
17−(cd dir/sub-dir && ln -sf ../content symlink)
18−
19−git add -A
20−git commit -m "Commit"
gix-worktree/tests/fixtures/make_mixed_without_submodules_and_symlinks.sh+0−20
@@ -1,20 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−touch empty
7−echo -n "content" > executable
8−chmod +x executable
9−
10−mkdir dir
11−echo "other content" > dir/content
12−echo "* filter=arrow" > .gitattributes
13−echo "executable -filter" >> .gitattributes
14−echo ".gitattributes -filter" >> .gitattributes
15−
16−mkdir dir/sub-dir
17−echo "even other content" > dir/sub-dir/file
18−
19−git add -A
20−git commit -m "Commit"
gix-worktree/tests/fixtures/racy_git.sh+0−12
@@ -1,12 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−echo -n "foo" > content
7−
8−git add -A
9−git commit -m "Commit"
10−
11−# file size should not be changed by this
12−echo -n "bar" > content
gix-worktree/tests/fixtures/status_changed.sh+0−26
@@ -1,26 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−touch empty
7−echo -n "content" > executable
8−chmod +x executable
9−
10−mkdir dir
11−echo -n "other content" > dir/content
12−echo -n "other content" > dir/content2
13−mkdir dir/sub-dir
14−(cd dir/sub-dir && ln -sf ../content symlink)
15−
16−git add -A
17−git commit -m "Commit"
18−
19−chmod +x dir/content
20−echo "new content" > dir/content2
21−chmod -x executable
22−echo -n "foo" > executable
23−
24−rm empty
25−ln -sf dir/content empty
26−git reset
gix-worktree/tests/fixtures/status_conflict.sh+0−18
@@ -1,18 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−echo base > content
7−git add -A
8−git commit -m "base"
9−
10−git checkout -b feat
11−echo feat > content
12−git commit -am "feat"
13−
14−git checkout main
15−echo base-change > content
16−git commit -am "new base"
17−
18−git merge feat || :
gix-worktree/tests/fixtures/status_intent_to_add.sh+0−9
@@ -1,9 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−touch content
7−echo -n "content" > content
8−
9−git add --intent-to-add -A
gix-worktree/tests/fixtures/status_removed.sh+0−18
@@ -1,18 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−touch empty
7−echo -n "content" > executable
8−chmod +x executable
9−
10−mkdir dir
11−echo -n "other content" > dir/content
12−mkdir dir/sub-dir
13−(cd dir/sub-dir && ln -sf ../content symlink)
14−
15−git add -A
16−git commit -m "Commit"
17−rm -rf ./empty ./executable ./dir/content ./dir/sub-dir/symlink
18−git reset
gix-worktree/tests/fixtures/status_unchanged.sh+0−20
@@ -1,20 +0,0 @@
1−#!/bin/bash
2−set -eu -o pipefail
3−
4−git init -q
5−
6−touch empty
7−echo -n "content" > executable
8−chmod +x executable
9−
10−mkdir dir
11−echo -n "other content" > dir/content
12−mkdir dir/sub-dir
13−(cd dir/sub-dir && ln -sf ../content symlink)
14−
15−git add -A
16−git commit -m "Commit"
17−
18−touch ./empty ./executable ./dir/content ./dir/sub-dir/symlink
19−
20−git reset # ensure index timestamp is large enough to not mark everything racy
gix-worktree/tests/worktree/cache/create_directory.rs+1−1
@@ -1,7 +1,7 @@
11 use std::path::Path;
22
3+use gix_testtools::tempfile::{tempdir, TempDir};
34 use gix_worktree::{cache, Cache};
4−use tempfile::{tempdir, TempDir};
55
66 #[allow(clippy::ptr_arg)]
77 fn panic_on_find<'buf>(_oid: &gix_hash::oid, _buf: &'buf mut Vec<u8>) -> std::io::Result<gix_object::BlobRef<'buf>> {
gix-worktree/tests/worktree/checkout.rs+0−526
@@ -1,526 +0,0 @@
1−#[cfg(unix)]
2−use std::os::unix::prelude::MetadataExt;
3−use std::{
4− fs,
5− io::{ErrorKind, ErrorKind::AlreadyExists},
6− path::{Path, PathBuf},
7− sync::atomic::{AtomicBool, AtomicUsize, Ordering},
8−};
9−
10−use gix_features::progress;
11−use gix_object::bstr::ByteSlice;
12−use gix_odb::FindExt;
13−use gix_worktree::checkout::Collision;
14−use once_cell::sync::Lazy;
15−use tempfile::TempDir;
16−
17−use crate::fixture_path;
18−
19−static DRIVER: Lazy<PathBuf> = Lazy::new(|| {
20− let mut cargo = std::process::Command::new(env!("CARGO"));
21− let res = cargo
22− .args(["build", "-p=gix-filter", "--example", "arrow"])
23− .status()
24− .expect("cargo should run fine");
25− assert!(res.success(), "cargo invocation should be successful");
26−
27− let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
28− .ancestors()
29− .nth(1)
30− .expect("first parent in target dir")
31− .join("debug")
32− .join("examples")
33− .join(if cfg!(windows) { "arrow.exe" } else { "arrow" });
34− assert!(path.is_file(), "Expecting driver to be located at {path:?}");
35− path
36−});
37−
38−fn driver_exe() -> String {
39− let mut exe = DRIVER.to_string_lossy().into_owned();
40− if cfg!(windows) {
41− exe = exe.replace('\\', "/");
42− }
43− exe
44−}
45−
46−#[test]
47−fn accidental_writes_through_symlinks_are_prevented_if_overwriting_is_forbidden() {
48− let mut opts = opts_from_probe();
49− // without overwrite mode, everything is safe.
50− opts.overwrite_existing = false;
51− let (source_tree, destination, _index, outcome) =
52− checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap();
53−
54− let source_files = dir_structure(&source_tree);
55− let worktree_files = dir_structure(&destination);
56−
57− if opts.fs.ignore_case {
58− assert_eq!(
59− stripped_prefix(&source_tree, &source_files),
60− stripped_prefix(&destination, &worktree_files),
61− );
62− if multi_threaded() {
63− assert_eq!(outcome.collisions.len(), 2);
64− } else {
65− assert_eq!(
66− outcome.collisions,
67− vec![
68− Collision {
69− path: "FAKE-DIR".into(),
70− error_kind: AlreadyExists
71− },
72− Collision {
73− path: "FAKE-FILE".into(),
74− error_kind: AlreadyExists
75− }
76− ]
77− );
78− }
79− } else {
80− let expected = ["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE", "fake-dir/b", "fake-file"];
81− assert_eq!(stripped_prefix(&source_tree, &source_files), paths(expected));
82− assert_eq!(stripped_prefix(&destination, &worktree_files), paths(expected));
83− assert!(outcome.collisions.is_empty());
84− };
85−}
86−
87−#[test]
88−fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() {
89− let mut opts = opts_from_probe();
90− // with overwrite mode
91− opts.overwrite_existing = true;
92− let (source_tree, destination, _index, outcome) =
93− checkout_index_in_tmp_dir(opts.clone(), "make_dangerous_symlink").unwrap();
94−
95− let source_files = dir_structure(&source_tree);
96− let worktree_files = dir_structure(&destination);
97−
98− if opts.fs.ignore_case {
99− assert_eq!(
100− stripped_prefix(&source_tree, &source_files),
101− paths(["A-dir/a", "A-file", "fake-dir/b", "fake-file"]),
102− );
103− assert_eq!(
104− stripped_prefix(&destination, &worktree_files),
105− paths(["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE"]),
106− );
107− assert!(outcome.collisions.is_empty());
108− } else {
109− let expected = ["A-dir/a", "A-file", "FAKE-DIR", "FAKE-FILE", "fake-dir/b", "fake-file"];
110− assert_eq!(stripped_prefix(&source_tree, &source_files), paths(expected));
111− assert_eq!(stripped_prefix(&destination, &worktree_files), paths(expected));
112− assert!(outcome.collisions.is_empty());
113− };
114−}
115−
116−#[test]
117−fn delayed_driver_process() -> crate::Result {
118− let mut opts = opts_from_probe();
119− opts.overwrite_existing = true;
120− opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow;
121− opts.destination_is_initially_empty = false;
122− setup_filter_pipeline(opts.filters.options_mut());
123− let (_source, destination, _index, outcome) =
124− checkout_index_in_tmp_dir_opts(opts, "make_mixed_without_submodules_and_symlinks", |_| true, |_| Ok(()))?;
125− assert_eq!(outcome.collisions.len(), 0);
126− assert_eq!(outcome.errors.len(), 0);
127− assert_eq!(outcome.files_updated, 5);
128−
129− let dest = destination.path();
130− assert_eq!(
131− std::fs::read(dest.join("executable"))?.as_bstr(),
132− "content",
133− "unfiltered"
134− );
135− assert_eq!(
136− std::fs::read(dest.join("dir").join("content"))?.as_bstr(),
137− "➡other content\r\n"
138− );
139− assert_eq!(
140− std::fs::read(dest.join("dir").join("sub-dir").join("file"))?.as_bstr(),
141− "➡even other content\r\n"
142− );
143− Ok(())
144−}
145−
146−#[test]
147−#[cfg_attr(
148− windows,
149− ignore = "on windows, the symlink to a directory doesn't seem to work and we really want to test with symlinks"
150−)]
151−fn overwriting_files_and_lone_directories_works() -> crate::Result {
152− for delay in [
153− gix_filter::driver::apply::Delay::Allow,
154− gix_filter::driver::apply::Delay::Forbid,
155− ] {
156− let mut opts = opts_from_probe();
157− opts.overwrite_existing = true;
158− opts.filter_process_delay = delay;
159− opts.destination_is_initially_empty = false;
160− setup_filter_pipeline(opts.filters.options_mut());
161− let (source, destination, _index, outcome) = checkout_index_in_tmp_dir_opts(
162− opts.clone(),
163− "make_mixed_without_submodules",
164− |_| true,
165− |d| {
166− let empty = d.join("empty");
167− symlink::symlink_dir(d.join(".."), &empty)?; // empty is symlink to the directory above
168− std::fs::write(d.join("executable"), b"foo")?; // executable is regular file and has different content
169− let dir = d.join("dir");
170− std::fs::create_dir(&dir)?;
171− std::fs::create_dir(dir.join("content"))?; // 'content' is a directory now
172−
173− let dir = dir.join("sub-dir");
174− std::fs::create_dir(&dir)?;
175−
176− symlink::symlink_dir(empty, dir.join("symlink"))?; // 'symlink' is a symlink to another file
177− Ok(())
178− },
179− )?;
180−
181− assert!(outcome.collisions.is_empty());
182−
183− assert_eq!(
184− stripped_prefix(&destination, &dir_structure(&destination)),
185− paths(["dir/content", "dir/sub-dir/symlink", "empty", "executable"])
186− );
187− let meta = std::fs::symlink_metadata(destination.path().join("empty"))?;
188− assert!(meta.is_file(), "'empty' is now a file");
189− assert_eq!(meta.len(), 0, "'empty' is indeed empty");
190−
191− let exe = destination.path().join("executable");
192− assert_eq!(std::fs::read(&exe)?, b"content", "'exe' has the correct content");
193−
194− let meta = std::fs::symlink_metadata(exe)?;
195− assert!(meta.is_file());
196− if opts.fs.executable_bit {
197− #[cfg(unix)]
198− assert_eq!(meta.mode() & 0o700, 0o700, "the executable bit is set where supported");
199− }
200−
201− assert_eq!(
202− std::fs::read(source.join("dir/content"))?.as_bstr(),
203− "other content\n",
204− "in the worktree, we have LF"
205− );
206− assert_eq!(
207− std::fs::read(destination.path().join("dir/content"))?.as_bstr(),
208− "➡other content\r\n",
209− "autocrlf is enabled, so we get CRLF when checking out as the pipeline is active, and we have a filter"
210− );
211−
212− let symlink = destination.path().join("dir/sub-dir/symlink");
213− // on windows, git won't create symlinks as its probe won't detect the capability, even though we do.
214− assert_eq!(std::fs::symlink_metadata(&symlink)?.is_symlink(), cfg!(unix));
215− assert_eq!(
216− std::fs::read(symlink)?.as_bstr(),
217− "➡other content\r\n",
218− "autocrlf is enabled"
219− );
220− }
221− Ok(())
222−}
223−
224−#[test]
225−fn symlinks_become_files_if_disabled() -> crate::Result {
226− let mut opts = opts_from_probe();
227− opts.fs.symlink = false;
228− let (source_tree, destination, _index, outcome) =
229− checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?;
230−
231− assert_equality(&source_tree, &destination, opts.fs.symlink)?;
232− assert!(outcome.collisions.is_empty());
233− Ok(())
234−}
235−
236−#[test]
237−fn allow_or_disallow_symlinks() -> crate::Result {
238− let mut opts = opts_from_probe();
239− for allowed in &[false, true] {
240− opts.fs.symlink = *allowed;
241− let (source_tree, destination, _index, outcome) =
242− checkout_index_in_tmp_dir(opts.clone(), "make_mixed_without_submodules")?;
243−
244− assert_equality(&source_tree, &destination, opts.fs.symlink)?;
245− assert!(outcome.collisions.is_empty());
246− }
247− Ok(())
248−}
249−
250−#[test]
251−fn keep_going_collects_results() {
252− let mut opts = opts_from_probe();
253− opts.keep_going = true;
254− let count = AtomicUsize::default();
255− let (_source_tree, destination, _index, outcome) = checkout_index_in_tmp_dir_opts(
256− opts,
257− "make_mixed_without_submodules",
258− |_id| {
259− !matches!(
260− count.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
261− (current < 2).then_some(current + 1)
262− }),
263− Ok(_)
264− )
265− },
266− |_| Ok(()),
267− )
268− .unwrap();
269−
270− if multi_threaded() {
271− assert_eq!(
272− outcome.errors.len(),
273− 2,
274− "content changes due to non-deterministic nature of racy threads"
275− )
276− } else {
277− assert_eq!(
278− outcome
279− .errors
280− .iter()
281− .map(|r| r.path.to_path_lossy().into_owned())
282− .collect::<Vec<_>>(),
283− paths(if cfg!(unix) {
284− [".gitattributes", "dir/content"]
285− } else {
286− // not actually a symlink anymore, even though symlinks are supported but git think differently.
287− ["dir/content", "dir/sub-dir/symlink"]
288− })
289− );
290− }
291−
292− if multi_threaded() {
293− let actual = dir_structure(&destination);
294− assert!(
295− (2..=3).contains(&actual.len()),
296− "it's 3 most of the time, but can be 2 of the 'empty' file is missing as the object couldn't be accessed.\
297− It's unclear why there isn't more, as it would keep going"
298− );
299− } else {
300− assert_eq!(
301− stripped_prefix(&destination, &dir_structure(&destination)),
302− paths(if cfg!(unix) {
303− Box::new(["dir/sub-dir/symlink", "empty", "executable"].into_iter()) as Box<dyn Iterator<Item = &str>>
304− } else {
305− Box::new(["empty", "executable"].into_iter())
306− }),
307− "some files could not be created"
308− );
309− }
310−
311− assert!(outcome.collisions.is_empty());
312−}
313−
314−#[test]
315−fn no_case_related_collisions_on_case_sensitive_filesystem() {
316− let opts = opts_from_probe();
317− if opts.fs.ignore_case {
318− eprintln!("Skipping case-sensitive testing on what would be a case-insensitive file system");
319− return;
320− }
321− let (source_tree, destination, index, outcome) =
322− checkout_index_in_tmp_dir(opts.clone(), "make_ignorecase_collisions").unwrap();
323−
324− assert!(outcome.collisions.is_empty());
325− let num_files = assert_equality(&source_tree, &destination, opts.fs.symlink).unwrap();
326− assert_eq!(
327− num_files,
328− index.entries().len() - 1,
329− "it checks out all files (minus 1 to account for .gitattributes which is skipped in the worktree in our tests)"
330− );
331− assert!(
332− destination.path().join(".gitattributes").is_file(),
333− "we do have attributes even though, dot files are ignored in `assert_equality`"
334− );
335−}
336−
337−#[test]
338−fn collisions_are_detected_on_a_case_insensitive_filesystem_even_with_delayed_filters() {
339− let mut opts = opts_from_probe();
340− if !opts.fs.ignore_case {
341− eprintln!("Skipping case-insensitive testing on what would be a case-sensitive file system");
342− return;
343− }
344− setup_filter_pipeline(opts.filters.options_mut());
345− opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow;
346− let (source_tree, destination, _index, outcome) =
347− checkout_index_in_tmp_dir(opts, "make_ignorecase_collisions").unwrap();
348−
349− let source_files = dir_structure(&source_tree);
350− assert_eq!(
351− stripped_prefix(&source_tree, &source_files),
352− paths(["d", "file_x", "link-to-X", "x"]),
353− "plenty of collisions prevent a checkout"
354− );
355−
356− let dest_files = dir_structure(&destination);
357− if multi_threaded() {
358− assert!(
359− (4..=6).contains(&dest_files.len()),
360− "due to the clash happening at nearly any time, and keep-going is false, we get a variance of files"
361− );
362− } else {
363− assert_eq!(
364− stripped_prefix(&destination, &dest_files),
365− paths(["D/B", "D/C", "FILE_X", "X", "link-to-X"]),
366− "we checkout files in order and generally handle collision detection differently, hence the difference"
367− );
368− }
369−
370− let error_kind = ErrorKind::AlreadyExists;
371− #[cfg(windows)]
372− let error_kind_dir = ErrorKind::PermissionDenied;
373− #[cfg(not(windows))]
374− let error_kind_dir = error_kind;
375−
376− if multi_threaded() {
377− assert!(
378− (5..=6).contains(&outcome.collisions.len()),
379− "can only assert on number as it's racily creating files so unclear which one clashes, and due to keep-going = false there is variance"
380− );
381− } else {
382− assert_eq!(
383− outcome.collisions,
384− vec![
385− Collision {
386− path: "d".into(),
387− error_kind: error_kind_dir,
388− },
389− Collision {
390− path: "FILE_x".into(),
391− error_kind,
392− },
393− Collision {
394− path: "file_X".into(),
395− error_kind,
396− },
397− Collision {
398− path: "file_x".into(),
399− error_kind,
400− },
401− Collision {
402− path: "x".into(),
403− error_kind,
404− },
405− ],
406− "these files couldn't be checked out"
407− );
408− }
409−}
410−
411−fn multi_threaded() -> bool {
412− gix_features::parallel::num_threads(None) > 1
413−}
414−
415−fn assert_equality(source_tree: &Path, destination: &TempDir, allow_symlinks: bool) -> crate::Result<usize> {
416− let source_files = dir_structure(source_tree);
417− let worktree_files = dir_structure(destination);
418−
419− assert_eq!(
420− stripped_prefix(source_tree, &source_files),
421− stripped_prefix(destination, &worktree_files),
422− );
423−
424− let mut count = 0;
425− for (source_file, worktree_file) in source_files.iter().zip(worktree_files.iter()) {
426− count += 1;
427− if !allow_symlinks && source_file.is_symlink() {
428− assert!(!worktree_file.is_symlink());
429− assert_eq!(fs::read(worktree_file)?.to_path()?, fs::read_link(source_file)?);
430− } else {
431− assert_eq!(fs::read(source_file)?, fs::read(worktree_file)?);
432− #[cfg(unix)]
433− assert_eq!(
434− fs::symlink_metadata(source_file)?.mode() & 0o700,
435− fs::symlink_metadata(worktree_file)?.mode() & 0o700,
436− "permissions of source and checked out file are comparable"
437− );
438− }
439− }
440− Ok(count)
441−}
442−
443−pub fn dir_structure<P: AsRef<std::path::Path>>(path: P) -> Vec<std::path::PathBuf> {
444− let path = path.as_ref();
445− let mut files: Vec<_> = walkdir::WalkDir::new(path)
446− .follow_links(false)
447− .into_iter()
448− .filter_entry(|e| e.path() == path || !e.file_name().to_string_lossy().starts_with('.'))
449− .flatten()
450− .filter_map(|e| (!e.path().symlink_metadata().map_or(true, |m| m.is_dir())).then(|| e.path().to_path_buf()))
451− .collect();
452− files.sort();
453− files
454−}
455−
456−fn checkout_index_in_tmp_dir(
457− opts: gix_worktree::checkout::Options,
458− name: &str,
459−) -> crate::Result<(PathBuf, TempDir, gix_index::File, gix_worktree::checkout::Outcome)> {
460− checkout_index_in_tmp_dir_opts(opts, name, |_d| true, |_| Ok(()))
461−}
462−
463−fn checkout_index_in_tmp_dir_opts(
464− opts: gix_worktree::checkout::Options,
465− name: &str,
466− mut allow_return_object: impl FnMut(&gix_hash::oid) -> bool + Send + Clone,
467− prep_dest: impl Fn(&Path) -> std::io::Result<()>,
468−) -> crate::Result<(PathBuf, TempDir, gix_index::File, gix_worktree::checkout::Outcome)> {
469− let source_tree = fixture_path(name);
470− let git_dir = source_tree.join(".git");
471− let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, Default::default())?;
472− let odb = gix_odb::at(git_dir.join("objects"))?.into_inner().into_arc()?;
473− let destination = tempfile::tempdir_in(std::env::current_dir()?)?;
474− prep_dest(destination.path()).expect("preparation must succeed");
475−
476− let outcome = gix_worktree::checkout(
477− &mut index,
478− destination.path(),
479− move |oid, buf| {
480− if allow_return_object(oid) {
481− odb.find_blob(oid, buf)
482− } else {
483− Err(gix_odb::find::existing_object::Error::NotFound { oid: oid.to_owned() })
484− }
485− },
486− &mut progress::Discard,
487− &mut progress::Discard,
488− &AtomicBool::default(),
489− opts,
490− )?;
491− Ok((source_tree, destination, index, outcome))
492−}
493−
494−fn stripped_prefix(prefix: impl AsRef<Path>, source_files: &[PathBuf]) -> Vec<&Path> {
495− source_files.iter().flat_map(|p| p.strip_prefix(&prefix)).collect()
496−}
497−
498−fn probe_gitoxide_dir() -> crate::Result<gix_fs::Capabilities> {
499− Ok(gix_fs::Capabilities::probe(
500− std::env::current_dir()?.join("..").join(".git"),
501− ))
502−}
503−
504−fn opts_from_probe() -> gix_worktree::checkout::Options {
505− gix_worktree::checkout::Options {
506− fs: probe_gitoxide_dir().unwrap(),
507− destination_is_initially_empty: true,
508− thread_limit: gix_features::parallel::num_threads(None).into(),
509− ..Default::default()
510− }
511−}
512−
513−fn paths<'a>(p: impl IntoIterator<Item = &'a str>) -> Vec<PathBuf> {
514− p.into_iter().map(PathBuf::from).collect()
515−}
516−
517−fn setup_filter_pipeline(opts: &mut gix_filter::pipeline::Options) {
518− opts.eol_config.auto_crlf = gix_filter::eol::AutoCrlf::Enabled;
519− opts.drivers = vec![gix_filter::Driver {
520− name: "arrow".into(),
521− clean: None,
522− smudge: None,
523− process: Some((driver_exe() + " process").into()),
524− required: true,
525− }];
526−}
gix-worktree/tests/worktree/mod.rs+0−9
@@ -1,8 +1,4 @@
11 mod cache;
2−mod checkout;
3−mod status;
4−
5−use std::path::{Path, PathBuf};
62
73 use gix_hash::ObjectId;
84 pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
@@ -10,8 +6,3 @@ pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; · 1 unmodified line
106 pub fn hex_to_id(hex: &str) -> ObjectId {
117 ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
128 }
13−
14−pub fn fixture_path(name: &str) -> PathBuf {
15− let dir = gix_testtools::scripted_fixture_read_only(Path::new(name).with_extension("sh")).expect("script works");
16− dir
17−}
gix-worktree/tests/worktree/status.rs+0−226
@@ -1,226 +0,0 @@
1−use std::sync::{
2− atomic::{AtomicUsize, Ordering},
3− Arc,
4−};
5−
6−use bstr::BStr;
7−use filetime::{set_file_mtime, FileTime};
8−use gix_index as index;
9−use gix_index::Entry;
10−use gix_worktree::{
11− status,
12− status::{
13− content::{CompareBlobs, FastEq, ReadDataOnce},
14− Change, Options, Recorder,
15− },
16−};
17−
18−use crate::fixture_path;
19−
20−// since tests are fixtures a bunch of stat information (like inode number)
21−// changes when extracting the data so we need to disable all advanced stat
22−// changes and only look at mtime seconds and file size to properly
23−// test all code paths (and to trigger racy git).
24−const TEST_OPTIONS: index::entry::stat::Options = index::entry::stat::Options {
25− trust_ctime: false,
26− check_stat: false,
27− use_nsec: false,
28− use_stdev: false,
29−};
30−
31−fn fixture(name: &str, expected_status: &[(&BStr, Option<Change>, bool)]) {
32− let worktree = fixture_path(name);
33− let git_dir = worktree.join(".git");
34− let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, Default::default()).unwrap();
35− let mut recorder = Recorder::default();
36− status(
37− &mut index,
38− &worktree,
39− &mut recorder,
40− FastEq,
41− |_, _| Ok::<_, std::convert::Infallible>(gix_object::BlobRef { data: &[] }),
42− Options {
43− fs: gix_fs::Capabilities::probe(git_dir),
44− stat: TEST_OPTIONS,
45− ..Options::default()
46− },
47− )
48− .unwrap();
49− recorder.records.sort_unstable_by_key(|(name, _, _)| *name);
50− assert_eq!(recorder.records, expected_status)
51−}
52−
53−#[test]
54−fn removed() {
55− fixture(
56− "status_removed",
57− &[
58− (BStr::new(b"dir/content"), Some(Change::Removed), false),
59− (BStr::new(b"dir/sub-dir/symlink"), Some(Change::Removed), false),
60− (BStr::new(b"empty"), Some(Change::Removed), false),
61− (BStr::new(b"executable"), Some(Change::Removed), false),
62− ],
63− );
64−}
65−
66−#[test]
67−fn intent_to_add() {
68− fixture(
69− "status_intent_to_add",
70− &[(BStr::new(b"content"), Some(Change::IntentToAdd), false)],
71− );
72−}
73−
74−#[test]
75−fn conflict() {
76− fixture(
77− "status_conflict",
78− &[(
79− BStr::new(b"content"),
80− Some(Change::Modification {
81− executable_bit_changed: false,
82− content_change: Some(()),
83− }),
84− true,
85− )],
86− );
87−}
88−
89−#[test]
90−fn unchanged() {
91− fixture("status_unchanged", &[]);
92−}
93−
94−#[test]
95−#[cfg_attr(
96− windows,
97− ignore = "needs work, on windows plenty of additional files are considered modified for some reason"
98−)]
99−fn modified() {
100− fixture(
101− "status_changed",
102− &[
103− (
104− BStr::new(b"dir/content"),
105− Some(Change::Modification {
106− executable_bit_changed: true,
107− content_change: None,
108− }),
109− false,
110− ),
111− (
112− BStr::new(b"dir/content2"),
113− Some(Change::Modification {
114− executable_bit_changed: false,
115− content_change: Some(()),
116− }),
117− false,
118− ),
119− (BStr::new(b"empty"), Some(Change::Type), false),
120− (
121− BStr::new(b"executable"),
122− Some(Change::Modification {
123− executable_bit_changed: true,
124− content_change: Some(()),
125− }),
126− false,
127− ),
128− ],
129− );
130−}
131−
132−#[test]
133−fn racy_git() {
134− let timestamp = 940040400;
135− // we need a writable fixture because we have to mess with `mtimes` manually, because touch -d
136− // respects the locale so the test wouldn't work depending on the timezone you
137− // run your test in.
138− let dir = gix_testtools::scripted_fixture_writable("racy_git.sh").expect("script works");
139− let worktree = dir.path();
140− let git_dir = worktree.join(".git");
141− let fs = gix_fs::Capabilities::probe(&git_dir);
142− let mut index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, Default::default()).unwrap();
143−
144− #[derive(Clone)]
145− struct CountCalls(Arc<AtomicUsize>, FastEq);
146− impl CompareBlobs for CountCalls {
147− type Output = ();
148−
149− fn compare_blobs<'a, E>(
150− &mut self,
151− entry: &'a Entry,
152− worktree_blob_size: usize,
153− worktree_blob: impl ReadDataOnce<'a, E>,
154− entry_blob: impl ReadDataOnce<'a, E>,
155− ) -> Result<Option<Self::Output>, E> {
156− self.0.fetch_add(1, Ordering::Relaxed);
157− self.1
158− .compare_blobs(entry, worktree_blob_size, worktree_blob, entry_blob)
159− }
160− }
161−
162− // We artificially mess with the entry's `mtime` so that it's before the timestamp saved by git.
163− // This would usually mean an invalid fs/invalid index file and as a result the racy git
164− // mitigation doesn't work and the worktree shows up as unchanged even tough the file did
165− // change.
166− // This case doesn't happen in the realworld (except for file corruption) but
167− // makes sure we are actually hitting the right codepath.
168− index.entries_mut()[0].stat.mtime.secs = timestamp;
169− set_file_mtime(worktree.join("content"), FileTime::from_unix_time(timestamp as i64, 0))
170− .expect("changing filetime works");
171− let mut recorder = Recorder::default();
172−
173− let count = Arc::new(AtomicUsize::new(0));
174− let counter = CountCalls(count.clone(), FastEq);
175− status(
176− &mut index,
177− worktree,
178− &mut recorder,
179− counter.clone(),
180− |_, _| Err(std::io::Error::new(std::io::ErrorKind::Other, "no odb access expected")),
181− Options {
182− fs,
183− stat: TEST_OPTIONS,
184− ..Options::default()
185− },
186− )
187− .unwrap();
188− assert_eq!(count.load(Ordering::Relaxed), 0, "no blob content is accessed");
189− assert_eq!(recorder.records, &[], "the testcase triggers racy git");
190−
191− // Now we also backdate the index timestamp to match the artificially created
192− // mtime above this is now a realistic realworld race-condition which should trigger racy git
193− // and cause proper output.
194− index.set_timestamp(FileTime::from_unix_time(timestamp as i64, 0));
195− let mut recorder = Recorder::default();
196− status(
197− &mut index,
198− worktree,
199− &mut recorder,
200− counter,
201− |_, _| Err(std::io::Error::new(std::io::ErrorKind::Other, "no odb access expected")),
202− Options {
203− fs,
204− stat: TEST_OPTIONS,
205− ..Options::default()
206− },
207− )
208− .unwrap();
209− assert_eq!(
210− count.load(Ordering::Relaxed),
211− 1,
212− "no we needed to access the blob content"
213− );
214− assert_eq!(
215− recorder.records,
216− &[(
217− BStr::new(b"content"),
218− Some(Change::Modification {
219− executable_bit_changed: false,
220− content_change: Some(()),
221− }),
222− false
223− )],
224− "racy change is correctly detected"
225− );
226−}
justfile+0−4
@@ -147,10 +147,6 @@ unit-tests: · 146 unmodified lines
147147 set -ex; \
148148 cargo test; \
149149 cargo test --features verbose-object-parsing-errors
150− cd gix-worktree; \
151− set -ex; \
152− cargo test; \
153− cargo test --features "internal-testing-gix-features-parallel"
154150 cargo test -p gix-tempfile --features signals
155151 cargo test -p gix-tempfile
156152 cargo test -p gix-features