use crate::worktree_copy_compare::{entry_matches, lstat_or_absent};
use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause};
use crate::parallel_budget::{HostResources, sliced_workers};
use crate::worktree_copy_stage::PARALLEL_COPY_MAX_WORKERS;
use crate::worktree_copy_install_entry::{
InstalledPath, capture_installed_path, create_selected_entry, ensure_parents_remembering,
};
use crate::worktree_copy_manifest::{EntryKind, WorktreeCopyEntry, apply_entry_modes};
use crate::worktree_copy_rollback::rollback_created;
use crate::worktree_copy_selection::filesystem_path;
use crate::json_record::quote;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
pub const INSTALL_BATCH_ENTRIES: usize = 4096;
pub trait InstallRecorder {
fn record_intents(&mut self, relative_paths: &[String]) -> Result<(), WorktreeCopyError>;
fn record_creations(&mut self, entries: &[InstalledPath]) -> Result<(), WorktreeCopyError>;
}
#[derive(Debug, Default)]
pub struct NoRecord;
impl InstallRecorder for NoRecord {
fn record_intents(&mut self, _relative_paths: &[String]) -> Result<(), WorktreeCopyError> {
return Ok(());
}
fn record_creations(&mut self, _entries: &[InstalledPath]) -> Result<(), WorktreeCopyError> {
return Ok(());
}
}
pub fn preflight_destination(
stage_root: &Path,
destination_root: &Path,
entries: &[WorktreeCopyEntry],
claimed: &BTreeSet<String>,
) -> Result<BTreeSet<String>, WorktreeCopyError> {
let mut absent: BTreeSet<String> = BTreeSet::new();
for entry in entries {
let destination_path: PathBuf =
filesystem_path(destination_root, entry.relative_path.as_str())?;
match lstat_or_absent(destination_path.as_path())? {
None => {
absent.insert(entry.relative_path.clone());
}
Some(metadata) => {
let claimed_directory: bool = entry.kind == EntryKind::Directory
&& metadata.is_dir()
&& claimed.contains(&entry.relative_path);
if claimed_directory {
continue;
}
if !entry_matches(stage_root, destination_root, entry)? {
let message: String = format!(
"cli-git: ignored-state copy would overwrite differing destination entry {} in {}.",
quote(entry.relative_path.as_str()),
quote(destination_root.to_string_lossy().as_ref())
);
return Err(failure_with_cause(message.as_str(), None));
}
}
}
}
return Ok(absent);
}
fn install_batch(
stage_root: &Path,
destination_root: &Path,
batch: &[WorktreeCopyEntry],
absent: &BTreeSet<String>,
created: &mut Vec<InstalledPath>,
recorder: &mut dyn InstallRecorder,
known: &mut std::collections::HashSet<String>,
) -> Result<(), WorktreeCopyError> {
let claims: Vec<String> = batch
.iter()
.map(|entry| return entry.relative_path.clone())
.filter(|relative_path| return absent.contains(relative_path))
.collect();
recorder.record_intents(claims.as_slice())?;
let batch_start: usize = created.len();
let mut leaves: Vec<&WorktreeCopyEntry> = Vec::new();
for entry in batch {
ensure_parents_remembering(destination_root, entry, created, known)?;
if entry.kind != EntryKind::Directory {
leaves.push(entry);
continue;
}
let destination_path: PathBuf =
filesystem_path(destination_root, entry.relative_path.as_str())?;
if lstat_or_absent(destination_path.as_path())?.is_none() {
create_selected_entry(stage_root, destination_root, entry)?;
created.push(capture_installed_path(
destination_path.as_path(),
entry.relative_path.as_str(),
true,
)?);
}
known.insert(entry.relative_path.clone());
}
install_leaves(stage_root, destination_root, leaves.as_slice(), created)?;
recorder.record_creations(&created[batch_start..])?;
return Ok(());
}
const PARALLEL_INSTALL_MIN_LEAVES: usize = crate::parallel_budget::PARALLEL_BREAK_EVEN_ENTRIES;
fn install_slice(
stage_root: &Path,
destination_root: &Path,
slice: &[&WorktreeCopyEntry],
) -> (Vec<InstalledPath>, Option<WorktreeCopyError>) {
let mut made: Vec<InstalledPath> = Vec::new();
for entry in slice {
let outcome: Result<Option<InstalledPath>, WorktreeCopyError> = (|| {
let destination_path: PathBuf =
filesystem_path(destination_root, entry.relative_path.as_str())?;
if lstat_or_absent(destination_path.as_path())?.is_some() {
return Ok(None);
}
create_selected_entry(stage_root, destination_root, entry)?;
return capture_installed_path(destination_path.as_path(), entry.relative_path.as_str(), true).map(Some);
})();
match outcome {
Ok(Some(installed)) => made.push(installed),
Ok(None) => {}
Err(error) => return (made, Some(error)),
}
}
return (made, None);
}
fn install_leaves(
stage_root: &Path,
destination_root: &Path,
leaves: &[&WorktreeCopyEntry],
created: &mut Vec<InstalledPath>,
) -> Result<(), WorktreeCopyError> {
let workers: usize = if leaves.len() < PARALLEL_INSTALL_MIN_LEAVES {
1
} else {
sliced_workers(leaves.len(), PARALLEL_COPY_MAX_WORKERS, &HostResources::read())
};
let slice_length: usize = leaves.len().div_ceil(workers).max(1);
let results: Vec<(Vec<InstalledPath>, Option<WorktreeCopyError>)> = std::thread::scope(|scope| {
let handles: Vec<_> = leaves
.chunks(slice_length)
.map(|slice| return scope.spawn(move || return install_slice(stage_root, destination_root, slice)))
.collect();
return handles
.into_iter()
.map(|handle| {
return handle.join().unwrap_or_else(|_panic| {
return (Vec::new(), Some(failure_with_cause("cli-git: an install worker panicked.", None)));
});
})
.collect();
});
let mut failure: Option<WorktreeCopyError> = None;
for (made, stopped) in results {
created.extend(made);
failure = failure.or(stopped);
}
return match failure {
Some(error) => Err(error),
None => Ok(()),
};
}
pub fn install_snapshot(
stage_root: &Path,
destination_root: &Path,
entries: &[WorktreeCopyEntry],
claimed: &BTreeSet<String>,
prior: &[InstalledPath],
recorder: &mut dyn InstallRecorder,
) -> Result<usize, WorktreeCopyError> {
let mut created: Vec<InstalledPath> = prior.to_vec();
match install_all(
stage_root,
destination_root,
entries,
claimed,
&mut created,
recorder,
) {
Ok(copied) => return Ok(copied),
Err(error) => {
let retained: Vec<String> =
rollback_created(stage_root, destination_root, entries, created.as_slice())?;
let suffix: String = if retained.is_empty() {
String::new()
} else {
let names: Vec<String> = retained
.iter()
.map(|path| return quote(path.as_str()))
.collect();
format!(" Rollback retained: {}.", names.join(", "))
};
let message: String = format!(
"cli-git: ignored-state installation failed for {}.{suffix}",
quote(destination_root.to_string_lossy().as_ref())
);
return Err(failure_with_cause(message.as_str(), Some(&error.0)));
}
}
}
fn install_all(
stage_root: &Path,
destination_root: &Path,
entries: &[WorktreeCopyEntry],
claimed: &BTreeSet<String>,
created: &mut Vec<InstalledPath>,
recorder: &mut dyn InstallRecorder,
) -> Result<usize, WorktreeCopyError> {
let started: std::time::Instant = std::time::Instant::now();
let absent: BTreeSet<String> =
preflight_destination(stage_root, destination_root, entries, claimed)?;
let preflighted: std::time::Duration = started.elapsed();
let mut known: std::collections::HashSet<String> = std::collections::HashSet::new();
for batch in entries.chunks(INSTALL_BATCH_ENTRIES) {
install_batch(
stage_root,
destination_root,
batch,
&absent,
created,
recorder,
&mut known,
)?;
}
let created_after: std::time::Duration = started.elapsed();
apply_entry_modes(destination_root, entries)?;
crate::diagnostic_log::debug(
"worktreeCopyInstall",
format!(
"installed {} entries: preflight {} ms, create and record {} ms, modes {} ms",
entries.len(),
preflighted.as_millis(),
(created_after - preflighted).as_millis(),
(started.elapsed() - created_after).as_millis()
)
.as_str(),
);
let selected: usize = created
.iter()
.filter(|installed| return installed.selected)
.count();
return Ok(selected);
}
#[cfg(test)]
#[path = "worktree_copy_install_tests.rs"]
mod tests;