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