blob: 684d1cae9e4874d3a29f9a53fa9f903dfda68b10 [file] [log] [blame]
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use gix_features::{interrupt, parallel::in_parallel, progress, progress::Progress};
use gix_hash::oid;
use crate::fs;
pub mod checkout;
pub(crate) mod entry;
/// Note that interruption still produce an `Ok(…)` value, so the caller should look at `should_interrupt` to communicate the outcome.
/// `dir` is the directory into which to checkout the `index`.
/// `git_dir` is the `.git` directory for reading additional per-repository configuration files.
#[allow(clippy::too_many_arguments)]
pub fn checkout<Find, E>(
index: &mut gix_index::State,
dir: impl Into<std::path::PathBuf>,
find: Find,
files: &mut impl Progress,
bytes: &mut impl Progress,
should_interrupt: &AtomicBool,
options: checkout::Options,
) -> Result<checkout::Outcome, checkout::Error<E>>
where
Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
E: std::error::Error + Send + Sync + 'static,
{
let paths = index.take_path_backing();
let res = checkout_inner(index, &paths, dir, find, files, bytes, should_interrupt, options);
index.return_path_backing(paths);
res
}
#[allow(clippy::too_many_arguments)]
fn checkout_inner<Find, E>(
index: &mut gix_index::State,
paths: &gix_index::PathStorage,
dir: impl Into<std::path::PathBuf>,
find: Find,
files: &mut impl Progress,
bytes: &mut impl Progress,
should_interrupt: &AtomicBool,
options: checkout::Options,
) -> Result<checkout::Outcome, checkout::Error<E>>
where
Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Send + Clone,
E: std::error::Error + Send + Sync + 'static,
{
let num_files = AtomicUsize::default();
let dir = dir.into();
let case = if options.fs.ignore_case {
gix_glob::pattern::Case::Fold
} else {
gix_glob::pattern::Case::Sensitive
};
let (chunk_size, thread_limit, num_threads) = gix_features::parallel::optimize_chunk_size_and_thread_limit(
100,
index.entries().len().into(),
options.thread_limit,
None,
);
let state = fs::cache::State::for_checkout(options.overwrite_existing, options.attribute_globals.clone().into());
let attribute_files = state.build_attribute_list(index, paths, case);
let mut ctx = chunk::Context {
buf: Vec::new(),
path_cache: fs::Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files),
find,
options,
num_files: &num_files,
};
let chunk::Outcome {
mut collisions,
mut errors,
mut bytes_written,
delayed,
} = if num_threads == 1 {
let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
chunk::process(entries_with_paths, files, bytes, &mut ctx)?
} else {
let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt);
in_parallel(
gix_features::iter::Chunks {
inner: entries_with_paths,
size: chunk_size,
},
thread_limit,
{
let ctx = ctx.clone();
move |_| (progress::Discard, progress::Discard, ctx.clone())
},
|chunk, (files, bytes, ctx)| chunk::process(chunk.into_iter(), files, bytes, ctx),
chunk::Reduce {
files,
bytes,
num_files: &num_files,
aggregate: Default::default(),
marker: Default::default(),
},
)?
};
for (entry, entry_path) in delayed {
bytes_written += chunk::checkout_entry_handle_result(
entry,
entry_path,
&mut errors,
&mut collisions,
files,
bytes,
&mut ctx,
)? as u64;
}
Ok(checkout::Outcome {
files_updated: num_files.load(Ordering::Relaxed),
collisions,
errors,
bytes_written,
})
}
mod chunk {
use std::sync::atomic::{AtomicUsize, Ordering};
use bstr::BStr;
use gix_features::progress::Progress;
use gix_hash::oid;
use crate::{
fs, index,
index::{checkout, entry},
os,
};
mod reduce {
use std::{
marker::PhantomData,
sync::atomic::{AtomicUsize, Ordering},
};
use gix_features::progress::Progress;
use crate::index::checkout;
pub struct Reduce<'a, 'entry, P1, P2, E> {
pub files: &'a mut P1,
pub bytes: &'a mut P2,
pub num_files: &'a AtomicUsize,
pub aggregate: super::Outcome<'entry>,
pub marker: PhantomData<E>,
}
impl<'a, 'entry, P1, P2, E> gix_features::parallel::Reduce for Reduce<'a, 'entry, P1, P2, E>
where
P1: Progress,
P2: Progress,
E: std::error::Error + Send + Sync + 'static,
{
type Input = Result<super::Outcome<'entry>, checkout::Error<E>>;
type FeedProduce = ();
type Output = super::Outcome<'entry>;
type Error = checkout::Error<E>;
fn feed(&mut self, item: Self::Input) -> Result<Self::FeedProduce, Self::Error> {
let item = item?;
let super::Outcome {
bytes_written,
delayed,
errors,
collisions,
} = item;
self.aggregate.bytes_written += bytes_written;
self.aggregate.delayed.extend(delayed);
self.aggregate.errors.extend(errors);
self.aggregate.collisions.extend(collisions);
self.bytes.set(self.aggregate.bytes_written as usize);
self.files.set(self.num_files.load(Ordering::Relaxed));
Ok(())
}
fn finalize(self) -> Result<Self::Output, Self::Error> {
Ok(self.aggregate)
}
}
}
pub use reduce::Reduce;
#[derive(Default)]
pub struct Outcome<'a> {
pub collisions: Vec<checkout::Collision>,
pub errors: Vec<checkout::ErrorRecord>,
pub delayed: Vec<(&'a mut gix_index::Entry, &'a BStr)>,
pub bytes_written: u64,
}
#[derive(Clone)]
pub struct Context<'a, Find: Clone> {
pub find: Find,
pub path_cache: fs::Cache,
pub buf: Vec<u8>,
pub options: checkout::Options,
/// We keep these shared so that there is the chance for printing numbers that aren't looking like
/// multiple of chunk sizes. Purely cosmetic. Otherwise it's the same as `files`.
pub num_files: &'a AtomicUsize,
}
pub fn process<'entry, Find, E>(
entries_with_paths: impl Iterator<Item = (&'entry mut gix_index::Entry, &'entry BStr)>,
files: &mut impl Progress,
bytes: &mut impl Progress,
ctx: &mut Context<'_, Find>,
) -> Result<Outcome<'entry>, checkout::Error<E>>
where
Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Clone,
E: std::error::Error + Send + Sync + 'static,
{
let mut delayed = Vec::new();
let mut collisions = Vec::new();
let mut errors = Vec::new();
let mut bytes_written = 0;
for (entry, entry_path) in entries_with_paths {
// TODO: write test for that
if entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) {
files.inc();
continue;
}
// Symlinks always have to be delayed on windows as they have to point to something that exists on creation.
// And even if not, there is a distinction between file and directory symlinks, hence we have to check what the target is
// before creating it.
// 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
// around writing through symlinks (even though we handle this).
// This also means that we prefer content in files over symlinks in case of collisions, which probably is for the better, too.
if entry.mode == gix_index::entry::Mode::SYMLINK {
delayed.push((entry, entry_path));
continue;
}
bytes_written +=
checkout_entry_handle_result(entry, entry_path, &mut errors, &mut collisions, files, bytes, ctx)?
as u64;
}
Ok(Outcome {
bytes_written,
errors,
collisions,
delayed,
})
}
pub fn checkout_entry_handle_result<Find, E>(
entry: &mut gix_index::Entry,
entry_path: &BStr,
errors: &mut Vec<checkout::ErrorRecord>,
collisions: &mut Vec<checkout::Collision>,
files: &mut impl Progress,
bytes: &mut impl Progress,
Context {
find,
path_cache,
buf,
options,
num_files,
}: &mut Context<'_, Find>,
) -> Result<usize, checkout::Error<E>>
where
Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E> + Clone,
E: std::error::Error + Send + Sync + 'static,
{
let res = entry::checkout(
entry,
entry_path,
entry::Context { find, path_cache, buf },
options.clone(),
);
files.inc();
num_files.fetch_add(1, Ordering::SeqCst);
match res {
Ok(object_size) => {
bytes.inc_by(object_size);
Ok(object_size)
}
Err(index::checkout::Error::Io(err)) if os::indicates_collision(&err) => {
// We are here because a file existed or was blocked by a directory which shouldn't be possible unless
// we are on a file insensitive file system.
files.fail(format!("{}: collided ({:?})", entry_path, err.kind()));
collisions.push(checkout::Collision {
path: entry_path.into(),
error_kind: err.kind(),
});
Ok(0)
}
Err(err) => {
if options.keep_going {
errors.push(checkout::ErrorRecord {
path: entry_path.into(),
error: Box::new(err),
});
Ok(0)
} else {
Err(err)
}
}
}
}
}