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
2466dependencies = [2466dependencies = [
2467 "bstr",2467 "bstr",
2468 "document-features",2468 "document-features",
2469 "filetime",
2470 "gix-attributes 0.16.0",2469 "gix-attributes 0.16.0",
2471 "gix-features 0.32.1",2470 "gix-features 0.32.1",
2472 "gix-filter",
2473 "gix-fs 0.4.1",2471 "gix-fs 0.4.1",
2474 "gix-glob 0.10.2",2472 "gix-glob 0.10.2",
2475 "gix-hash 0.11.4",2473 "gix-hash 0.11.4",
@@ -2479,13 +2477,8 @@ dependencies = [ · 3 unmodified lines
2479 "gix-odb",2477 "gix-odb",
2480 "gix-path 0.8.4",2478 "gix-path 0.8.4",
2481 "gix-testtools",2479 "gix-testtools",
2482 "io-close",
2483 "once_cell",
2484 "serde",2480 "serde",
2485 "symlink",2481 "symlink",
2486 "tempfile",
2487 "thiserror",
2488 "walkdir",
2489]2482]
2490 2483
2491[[package]]2484[[package]]
crate-status.md+1−19
@@ -482,25 +482,7 @@ Make it the best-performing implementation and the most convenient one. · 481 unmodified lines
482 * [x] attributes482 * [x] attributes
483 483
484### gix-worktree484### gix-worktree
485* handle the working **tree/checkout**485* [x] A stack to to efficiently generate attribute lists for matching paths against.
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
504 486
505### gix-revision487### gix-revision
506* [x] `describe()` (similar to `git name-rev`)488* [x] `describe()` (similar to `git name-rev`)
gix-worktree/Cargo.toml+1−9
@@ -3,7 +3,7 @@ name = "gix-worktree" · 2 unmodified lines
3version = "0.24.0"3version = "0.24.0"
4repository = "https://github.com/Byron/gitoxide"4repository = "https://github.com/Byron/gitoxide"
5license = "MIT OR Apache-2.0"5license = "MIT OR Apache-2.0"
6description = "A crate of the gitoxide project dedicated implementing everything around working trees and git excludes"6description = "A crate of the gitoxide project for shared worktree related types and utilities."
7authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]7authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
8edition = "2021"8edition = "2021"
9include = ["src/**/*", "LICENSE-*", "CHANGELOG.md"]9include = ["src/**/*", "LICENSE-*", "CHANGELOG.md"]
@@ -33,25 +33,17 @@ gix-path = { version = "^0.8.4", path = "../gix-path" } · 23 unmodified lines
33gix-attributes = { version = "^0.16.0", path = "../gix-attributes" }33gix-attributes = { version = "^0.16.0", path = "../gix-attributes" }
34gix-ignore = { version = "^0.5.1", path = "../gix-ignore" }34gix-ignore = { version = "^0.5.1", path = "../gix-ignore" }
35gix-features = { version = "^0.32.1", path = "../gix-features" }35gix-features = { version = "^0.32.1", path = "../gix-features" }
36gix-filter = { version = "^0.3.0", path = "../gix-filter" }
37 36
38serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}37serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]}
39 38
40thiserror = "1.0.26"
41filetime = "0.2.15"
42bstr = { version = "1.3.0", default-features = false }39bstr = { version = "1.3.0", default-features = false }
43 40
44document-features = { version = "0.2.0", optional = true }41document-features = { version = "0.2.0", optional = true }
45io-close = "0.3.7"
46 42
47[dev-dependencies]43[dev-dependencies]
48gix-testtools = { path = "../tests/tools" }44gix-testtools = { path = "../tests/tools" }
49gix-odb = { path = "../gix-odb" }45gix-odb = { path = "../gix-odb" }
50symlink = "0.1.0"46symlink = "0.1.0"
51once_cell = "1.18.0"
52
53walkdir = "2.3.2"
54tempfile = "3.2.0"
55 47
56[package.metadata.docs.rs]48[package.metadata.docs.rs]
57features = ["document-features", "serde"]49features = ["document-features", "serde"]
gix-worktree/src/checkout/chunk.rs+0−402
@@ -1,402 +0,0 @@
1use std::{
2 collections::BTreeSet,
3 sync::atomic::{AtomicUsize, Ordering},
4};
5
6use bstr::{BStr, BString};
7use gix_hash::oid;
8
9use crate::{checkout, checkout::entry, Cache};
10
11mod 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}
72pub use reduce::Reduce;
73
74use crate::checkout::entry::DelayedFilteredStream;
75
76#[derive(Default)]
77pub 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)]
92pub 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)]
101pub 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
109impl 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
121pub 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>>
128where
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
179pub 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>>
186where
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
286pub struct WriteWithProgress<'a, T> {
287 pub inner: T,
288 pub progress: Option<&'a AtomicUsize>,
289}
290
291impl<'a, T> std::io::Write for WriteWithProgress<'a, T>
292where
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
308pub 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>>
323where
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
358fn 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>
365where
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
382fn 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 @@
1use std::{
2 fs::OpenOptions,
3 io::Write,
4 path::{Path, PathBuf},
5};
6
7use bstr::BStr;
8use gix_filter::{driver::apply::MaybeDelayed, pipeline::convert::ToWorktreeOutcome};
9use gix_hash::oid;
10use gix_index::{entry::Stat, Entry};
11use io_close::Close;
12
13use crate::Cache;
14
15pub 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.
23pub 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
36pub 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
46impl 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))]
57pub 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>>
78where
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.
189fn 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
208fn 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))]
219fn 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)]
223fn 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
233fn 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
245pub(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))]
273pub(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>>
278where
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 @@
1use std::sync::atomic::AtomicBool;
2
3use gix_features::{interrupt, parallel::in_parallel_with_finalize, progress::Progress};
4use gix_hash::oid;
5
6use 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)]
19pub 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>>
28where
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)]
39fn 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>>
49where
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 @@
1use bstr::BString;
2use 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)]
6pub 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.
14pub 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)]
23pub 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)]
40pub 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)]
73pub 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
99mod chunk;
100mod entry;
101pub(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.
2//!2//!
3//! Unless specified differently, all operations need an index file (e.g. `.git/index`) as driver.3//! Unless specified differently, all operations need an index file (e.g. `.git/index`) as driver.
4//!4//!
@@ -11,9 +11,6 @@ · 6 unmodified lines
11#![deny(missing_docs, rust_2018_idioms, unsafe_code)]11#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
12use bstr::BString;12use bstr::BString;
13 13
14///
15pub mod read;
16
17/// A cache for efficiently executing operations on directories and files which are encountered in sorted order.14/// A cache for efficiently executing operations on directories and files which are encountered in sorted order.
18/// That way, these operations can be re-used for subsequent invocations in the same directory.15/// That way, these operations can be re-used for subsequent invocations in the same directory.
19///16///
@@ -52,9 +49,3 @@ pub(crate) type PathIdMapping = (BString, gix_hash::ObjectId); · 32 unmodified lines
52 49
53///50///
54pub mod cache;51pub mod cache;
55///
56pub mod checkout;
57pub use checkout::function::checkout;
58
59pub mod status;
60pub 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
6use std::{
7 fs::{read_link, File},
8 io::{self, Read},
9 path::Path,
10};
11
12use gix_object::Blob;
13use 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.
20pub 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.
27pub 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.
34pub 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.
39pub 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 @@
1use gix_hash::ObjectId;
2use gix_index as index;
3use index::Entry;
4
5/// Compares the content of two blobs in some way.
6pub 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.
24pub 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)]
35pub struct FastEq;
36
37impl 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)]
64pub struct HashEq;
65
66impl 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 @@
1use std::{io, marker::PhantomData, path::Path};
2
3use bstr::BStr;
4use filetime::FileTime;
5use gix_features::parallel::{in_parallel_if, Reduce};
6
7use 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.
27pub 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>
35where
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
86struct 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
96type StatusResult<'index, T> = Result<(&'index gix_index::Entry, &'index BStr, Option<Change<T>>, bool), Error>;
97
98impl<'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
265struct ReduceChange<'a, 'index, T: VisitEntry<'index>> {
266 collector: &'a mut T,
267 phantom: PhantomData<fn(&'index ())>,
268}
269
270impl<'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
292struct WorktreeBlob<'a> {
293 buf: &'a mut Vec<u8>,
294 path: &'a Path,
295 entry: &'a gix_index::Entry,
296 options: &'a Options,
297}
298
299struct OdbBlob<'a, Find, E>
300where
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
309impl<'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
321impl<'a, Find, E> content::ReadDataOnce<'a, Error> for OdbBlob<'a, Find, E>
322where
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///
3mod types;
4pub use types::{Change, Error, Options, VisitEntry};
5
6mod recorder;
7pub use recorder::Recorder;
8
9///
10pub mod content;
11pub(crate) mod function;
gix-worktree/src/status/recorder.rs+0−27
@@ -1,27 +0,0 @@
1use bstr::BStr;
2use gix_index as index;
3
4use crate::status::{Change, VisitEntry};
5
6/// Convenience implementation of [`VisitEntry`] that collects all non-trivial changes into a `Vec`.
7#[derive(Debug, Default)]
8pub 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
13impl<'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 @@
1use bstr::BStr;
2
3/// The error returned by [`status()`][crate::status()].
4#[derive(Debug, thiserror::Error)]
5#[allow(missing_docs)]
6pub 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.
19pub 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)]
32pub 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.
56pub 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 @@
1make_ignore_and_attributes_setup.tar.xz1make_ignore_and_attributes_setup.tar.xz
2make_mixed_without_submodules.tar.xz
3make_mixed_without_submodules_and_symlinks.tar.xz
4make_attributes_baseline.tar.xz2make_attributes_baseline.tar.xz
5make_dangerous_symlink.tar.xz
6status_unchanged.tar.xz
7status_changed.tar.xz
gix-worktree/tests/fixtures/generated-archives/make_ignorecase_collisions.tar.xz+0−3
@@ -1,3 +0,0 @@
1version https://git-lfs.github.com/spec/v1
2oid sha256:2984e2e61b28635014165351cd872ea7e4f09c28b8b4bbe50692a465ef648033
3size 10616
gix-worktree/tests/fixtures/generated-archives/make_special_exclude_case.tar.xz+2−2
@@ -1,3 +1,3 @@
1version https://git-lfs.github.com/spec/v11version https://git-lfs.github.com/spec/v1
2oid sha256:24f605623efc49819d1b30c52fe22da8f94f2d267e8030ec9bc3b9b845801f762oid sha256:1804dc740055b8a5afe65a2db14f29c8ae4691896e67342a8dcb11530fd448c6
3size 92203size 9240
gix-worktree/tests/fixtures/generated-archives/racy_git.tar.xz+0−3
@@ -1,3 +0,0 @@
1version https://git-lfs.github.com/spec/v1
2oid sha256:35b728a127f3b6170bac44469ff36d5ad0be2a4247a8926f1aaffb97b5973efc
3size 1596
gix-worktree/tests/fixtures/generated-archives/status_conflict.tar.xz+0−3
@@ -1,3 +0,0 @@
1version https://git-lfs.github.com/spec/v1
2oid sha256:cd6d32ab7a1e372d80a617926cac2463f6620baedf74642d78fe7f8c956fd031
3size 11036
gix-worktree/tests/fixtures/generated-archives/status_intent_to_add.tar.xz+0−3
@@ -1,3 +0,0 @@
1version https://git-lfs.github.com/spec/v1
2oid sha256:876670d74c01249d361aa73d83ab20d846db7c922a3ca825f778b5f9d746c401
3size 9304
gix-worktree/tests/fixtures/generated-archives/status_removed.tar.xz+0−3
@@ -1,3 +0,0 @@
1version https://git-lfs.github.com/spec/v1
2oid sha256:e5fe85a65e3689e9e60598130be60761dc4ea129e04d7d5501320f7ebad1eb2b
3size 10520
gix-worktree/tests/fixtures/make_dangerous_symlink.sh+0−24
@@ -1,24 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git 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
10empty_oid=$(git hash-object -w --stdin </dev/null)
11fake_dir_target=$(echo -n 'A-dir' | git hash-object -w --stdin)
12fake_file_target=$(echo -n 'A-file' | git hash-object -w --stdin)
13
14git update-index --index-info <<-EOF
15100644 $empty_oid A-dir/a
16100644 $empty_oid A-file
17120000 $fake_dir_target FAKE-DIR
18120000 $fake_file_target FAKE-FILE
19100644 $empty_oid fake-dir/b
20100644 $empty_oid fake-file
21EOF
22
23git commit -m "init"
24git checkout -f HEAD;
gix-worktree/tests/fixtures/make_ignore_setup.sh+0−18
@@ -1,18 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6cat <<EOF >.git/info/exclude
7# a sample .git/info/exclude
8file-anywhere
9/file-from-top
10
11dir-anywhere/
12/dir-from-top
13
14subdir-anywhere/file
15subdir-anywhere/dir/
16EOF
17
18git commit --allow-empty -m "init"
gix-worktree/tests/fixtures/make_ignorecase_collisions.sh+0−27
@@ -1,27 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6empty_oid=$(git hash-object -w --stdin </dev/null)
7content_oid=$(echo "content" | git hash-object -w --stdin)
8symlink_target=$(echo -n 'X' | git hash-object -w --stdin)
9
10echo "FILE_? filter=arrow" > .gitattributes
11git add -A
12
13git update-index --index-info <<-EOF
14100644 $content_oid FILE_X
15100644 $content_oid FILE_x
16100644 $content_oid file_X
17100644 $content_oid file_x
18100644 $empty_oid D/B
19100644 $empty_oid D/C
20100644 $empty_oid d
21100644 $empty_oid X
22120000 $symlink_target x
23120000 $symlink_target link-to-X
24EOF
25
26git commit -m "init"
27git checkout -f HEAD;
gix-worktree/tests/fixtures/make_mixed_without_submodules.sh+0−20
@@ -1,20 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6touch empty
7echo -n "content" > executable
8chmod +x executable
9
10mkdir dir
11echo "other content" > dir/content
12echo "* filter=arrow" > .gitattributes
13echo "executable -filter" >> .gitattributes
14echo ".gitattributes -filter" >> .gitattributes
15
16mkdir dir/sub-dir
17(cd dir/sub-dir && ln -sf ../content symlink)
18
19git add -A
20git commit -m "Commit"
gix-worktree/tests/fixtures/make_mixed_without_submodules_and_symlinks.sh+0−20
@@ -1,20 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6touch empty
7echo -n "content" > executable
8chmod +x executable
9
10mkdir dir
11echo "other content" > dir/content
12echo "* filter=arrow" > .gitattributes
13echo "executable -filter" >> .gitattributes
14echo ".gitattributes -filter" >> .gitattributes
15
16mkdir dir/sub-dir
17echo "even other content" > dir/sub-dir/file
18
19git add -A
20git commit -m "Commit"
gix-worktree/tests/fixtures/racy_git.sh+0−12
@@ -1,12 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6echo -n "foo" > content
7
8git add -A
9git commit -m "Commit"
10
11# file size should not be changed by this
12echo -n "bar" > content
gix-worktree/tests/fixtures/status_changed.sh+0−26
@@ -1,26 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6touch empty
7echo -n "content" > executable
8chmod +x executable
9
10mkdir dir
11echo -n "other content" > dir/content
12echo -n "other content" > dir/content2
13mkdir dir/sub-dir
14(cd dir/sub-dir && ln -sf ../content symlink)
15
16git add -A
17git commit -m "Commit"
18
19chmod +x dir/content
20echo "new content" > dir/content2
21chmod -x executable
22echo -n "foo" > executable
23
24rm empty
25ln -sf dir/content empty
26git reset
gix-worktree/tests/fixtures/status_conflict.sh+0−18
@@ -1,18 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6echo base > content
7git add -A
8git commit -m "base"
9
10git checkout -b feat
11echo feat > content
12git commit -am "feat"
13
14git checkout main
15echo base-change > content
16git commit -am "new base"
17
18git merge feat || :
gix-worktree/tests/fixtures/status_intent_to_add.sh+0−9
@@ -1,9 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6touch content
7echo -n "content" > content
8
9git add --intent-to-add -A
gix-worktree/tests/fixtures/status_removed.sh+0−18
@@ -1,18 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6touch empty
7echo -n "content" > executable
8chmod +x executable
9
10mkdir dir
11echo -n "other content" > dir/content
12mkdir dir/sub-dir
13(cd dir/sub-dir && ln -sf ../content symlink)
14
15git add -A
16git commit -m "Commit"
17rm -rf ./empty ./executable ./dir/content ./dir/sub-dir/symlink
18git reset
gix-worktree/tests/fixtures/status_unchanged.sh+0−20
@@ -1,20 +0,0 @@
1#!/bin/bash
2set -eu -o pipefail
3
4git init -q
5
6touch empty
7echo -n "content" > executable
8chmod +x executable
9
10mkdir dir
11echo -n "other content" > dir/content
12mkdir dir/sub-dir
13(cd dir/sub-dir && ln -sf ../content symlink)
14
15git add -A
16git commit -m "Commit"
17
18touch ./empty ./executable ./dir/content ./dir/sub-dir/symlink
19
20git 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 @@
1use std::path::Path;1use std::path::Path;
2 2
3use gix_testtools::tempfile::{tempdir, TempDir};
3use gix_worktree::{cache, Cache};4use gix_worktree::{cache, Cache};
4use tempfile::{tempdir, TempDir};
5 5
6#[allow(clippy::ptr_arg)]6#[allow(clippy::ptr_arg)]
7fn panic_on_find<'buf>(_oid: &gix_hash::oid, _buf: &'buf mut Vec<u8>) -> std::io::Result<gix_object::BlobRef<'buf>> {7fn 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)]
2use std::os::unix::prelude::MetadataExt;
3use std::{
4 fs,
5 io::{ErrorKind, ErrorKind::AlreadyExists},
6 path::{Path, PathBuf},
7 sync::atomic::{AtomicBool, AtomicUsize, Ordering},
8};
9
10use gix_features::progress;
11use gix_object::bstr::ByteSlice;
12use gix_odb::FindExt;
13use gix_worktree::checkout::Collision;
14use once_cell::sync::Lazy;
15use tempfile::TempDir;
16
17use crate::fixture_path;
18
19static 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
38fn 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]
47fn 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]
88fn 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]
117fn 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)]
151fn 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]
225fn 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]
237fn 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]
251fn 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]
315fn 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]
338fn 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
411fn multi_threaded() -> bool {
412 gix_features::parallel::num_threads(None) > 1
413}
414
415fn 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
443pub 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
456fn 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
463fn 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
494fn 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
498fn 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
504fn 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
513fn paths<'a>(p: impl IntoIterator<Item = &'a str>) -> Vec<PathBuf> {
514 p.into_iter().map(PathBuf::from).collect()
515}
516
517fn 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 @@
1mod cache;1mod cache;
2mod checkout;
3mod status;
4
5use std::path::{Path, PathBuf};
6 2
7use gix_hash::ObjectId;3use gix_hash::ObjectId;
8pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;4pub 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
10pub fn hex_to_id(hex: &str) -> ObjectId {6pub fn hex_to_id(hex: &str) -> ObjectId {
11 ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")7 ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
12}8}
13
14pub 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 @@
1use std::sync::{
2 atomic::{AtomicUsize, Ordering},
3 Arc,
4};
5
6use bstr::BStr;
7use filetime::{set_file_mtime, FileTime};
8use gix_index as index;
9use gix_index::Entry;
10use gix_worktree::{
11 status,
12 status::{
13 content::{CompareBlobs, FastEq, ReadDataOnce},
14 Change, Options, Recorder,
15 },
16};
17
18use 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).
24const 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
31fn 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]
54fn 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]
67fn intent_to_add() {
68 fixture(
69 "status_intent_to_add",
70 &[(BStr::new(b"content"), Some(Change::IntentToAdd), false)],
71 );
72}
73
74#[test]
75fn 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]
90fn 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)]
99fn 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]
133fn 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
147 set -ex; \147 set -ex; \
148 cargo test; \148 cargo test; \
149 cargo test --features verbose-object-parsing-errors149 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"
154 cargo test -p gix-tempfile --features signals150 cargo test -p gix-tempfile --features signals
155 cargo test -p gix-tempfile151 cargo test -p gix-tempfile
156 cargo test -p gix-features152 cargo test -p gix-features