use std::{ path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}, }; use anyhow::bail; use gix::{NestedProgress, Progress, objs::find::Error, worktree::state::checkout}; use crate::{ index, index::{Options, parse_file}, }; pub fn checkout_exclusive( index_path: impl AsRef, dest_directory: impl AsRef, repo: Option, mut err: impl std::io::Write, mut progress: impl NestedProgress, should_interrupt: &AtomicBool, index::checkout_exclusive::Options { index: Options { object_hash, .. }, empty_files, keep_going, thread_limit, }: index::checkout_exclusive::Options, ) -> anyhow::Result<()> { let repo = repo.map(gix::discover).transpose()?; let dest_directory = dest_directory.as_ref(); if dest_directory.exists() { bail!( "Refusing to checkout index into existing directory '{}' - remove it and try again", dest_directory.display() ) } std::fs::create_dir_all(dest_directory)?; let mut index = parse_file(index_path, object_hash)?; let mut num_skipped = 0; let maybe_symlink_mode = if !empty_files && repo.is_some() { gix::index::entry::Mode::DIR } else { gix::index::entry::Mode::SYMLINK }; for entry in index.entries_mut().iter_mut().filter(|e| { e.mode .contains(maybe_symlink_mode | gix::index::entry::Mode::DIR | gix::index::entry::Mode::COMMIT) }) { entry.flags.insert(gix::index::entry::Flags::SKIP_WORKTREE); num_skipped += 1; } if num_skipped > 0 { progress.info(format!("Skipping {num_skipped} DIR/SYMLINK/COMMIT entries")); } let opts = gix::worktree::state::checkout::Options { fs: gix::fs::Capabilities::probe(dest_directory), destination_is_initially_empty: true, overwrite_existing: false, keep_going, thread_limit, filters: repo .as_ref() .and_then(|repo| repo.filter_pipeline(None).ok().map(|t| t.0.into_parts().0)) .unwrap_or_default(), ..Default::default() }; let mut files = progress.add_child("checkout"); let mut bytes = progress.add_child("writing"); let entries_for_checkout = index.entries().len() - num_skipped; files.init(Some(entries_for_checkout), gix::progress::count("files")); bytes.init(None, gix::progress::bytes()); let start = std::time::Instant::now(); let no_repo = repo.is_none(); let checkout::Outcome { errors, ignored_filter_errors, collisions, files_updated, bytes_written, delayed_paths_unknown, delayed_paths_unprocessed, } = match repo { Some(repo) => gix::worktree::state::checkout( &mut index, dest_directory, EmptyOrDb { empty_files, db: repo.objects.into_arc()?, }, &files, &bytes, should_interrupt, opts, ), None => gix::worktree::state::checkout( &mut index, dest_directory, Empty, &files, &bytes, should_interrupt, opts, ), }?; files.show_throughput(start); bytes.show_throughput(start); progress.done(format!( "Created {} {} files{} ({})", files_updated, if no_repo { "empty" } else { Default::default() }, if should_interrupt.load(Ordering::Relaxed) { { format!( " of {}", entries_for_checkout .saturating_sub(errors.len() + collisions.len() + delayed_paths_unprocessed.len()) ) } } else { Default::default() }, gix::progress::bytes() .unwrap() .display(bytes_written as usize, None, None) )); let mut messages = Vec::new(); crate::report_ignored_filter_errors(ignored_filter_errors, &mut err); if !errors.is_empty() { messages.push(format!("kept going through {} errors(s)", errors.len())); for record in errors { writeln!(err, "{}: {}", record.path, record.error).ok(); } } if !collisions.is_empty() { messages.push(format!("encountered {} collision(s)", collisions.len())); for col in collisions { writeln!(err, "{}: collision ({:?})", col.path, col.error_kind).ok(); } } if !delayed_paths_unknown.is_empty() { messages.push(format!( "A delayed process provided us with {} paths we never sent to it", delayed_paths_unknown.len() )); for unknown in delayed_paths_unknown { writeln!(err, "{unknown}: unknown").ok(); } } if !delayed_paths_unprocessed.is_empty() { messages.push(format!( "A delayed process forgot to process {} paths", delayed_paths_unprocessed.len() )); for unprocessed in delayed_paths_unprocessed { writeln!(err, "{unprocessed}: unprocessed and forgotten").ok(); } } if !messages.is_empty() { bail!( "One or more errors occurred - checkout is incomplete: {}", messages.join(", ") ); } Ok(()) } #[derive(Clone)] struct EmptyOrDb { empty_files: bool, db: Find, } impl gix::objs::Find for EmptyOrDb where Find: gix::objs::Find, { fn try_find<'a>(&self, id: &gix::oid, buf: &'a mut Vec) -> Result>, Error> { if self.empty_files { // We always want to query the ODB here… let Some(kind) = self.db.try_find(id, buf)?.map(|d| d.kind) else { return Ok(None); }; buf.clear(); // …but write nothing Ok(Some(gix::objs::Data { kind, object_hash: id.kind(), data: buf, })) } else { self.db.try_find(id, buf) } } } #[derive(Clone)] struct Empty; impl gix::objs::Find for Empty { fn try_find<'a>(&self, id: &gix::oid, buffer: &'a mut Vec) -> Result>, Error> { buffer.clear(); Ok(Some(gix::objs::Data { kind: gix::object::Kind::Blob, object_hash: id.kind(), data: buffer, })) } }