use crate::add_timing::{begin as begin_timing, finish as finish_timing, step as timed};
use crate::json_record::quote;
use crate::transaction_git::GitContext;
use crate::worktree_copy_error::{WorktreeCopyError, lifecycle_failure, raw_failure};
use crate::worktree_copy_recovery::recover_pending_worktree_copies;
use crate::worktree_copy_selection::CopySelection;
use crate::worktree_copy_registry::{
CreatedWorktrees, WorktreeCopyObservation, destination_is_registered, find_created_worktree, observe_copy_source,
};
use crate::worktree_copy_transaction::synchronize_created_worktree_noting;
use crate::worktree_identity::WorktreeIdentity;
use crate::worktree_add_plan::AddPlan;
use crate::worktree_ladder::{LadderResult, begin_ladder};
#[cfg(target_os = "linux")]
use crate::package_repair::{RepairEnvironment, RepairRequest, RepairSwitch, repair_worktree};
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GitStatus {
pub code: Option<i32>,
}
impl GitStatus {
pub fn succeeded(&self) -> bool {
return self.code == Some(0);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Invocation {
pub identity: WorktreeIdentity,
pub effective_cwd: PathBuf,
pub creates: bool,
pub destination: Option<PathBuf>,
pub opted_out: bool,
pub short_circuit: bool,
pub context: GitContext,
pub add_plan: Option<AddPlan>,
#[cfg(target_os = "linux")]
pub repair: RepairSwitch,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Settlement {
pub git: GitStatus,
pub copy_failure: Option<WorktreeCopyError>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitStep<'a> {
Person,
WithoutCheckout(usize),
Other {
arguments: &'a [std::ffi::OsString],
environment: &'a [(std::ffi::OsString, std::ffi::OsString)],
},
}
pub type GitRunner<'a> = dyn FnMut(&GitStep<'_>) -> Result<GitStatus, WorktreeCopyError> + 'a;
pub fn summary_line(copied: usize, destinations: usize, source: Option<&Path>) -> String {
let source_text: String = match source {
None => String::from("bare repository with an empty source set"),
Some(path) => quote(path.to_string_lossy().as_ref()),
};
let worktree_noun: &str = if destinations == 1 {
"worktree"
} else {
"worktrees"
};
let entry_noun: &str = if copied == 1 { "entry" } else { "entries" };
return format!(
"cli-git: copied {copied} ignored filesystem {entry_noun} from {source_text} into {destinations} new {worktree_noun}.\n"
);
}
pub fn exit_code(settlement: &Settlement) -> i32 {
let git_code: i32 = settlement.git.code.unwrap_or(1);
match (
settlement.git.succeeded(),
settlement.copy_failure.is_some(),
) {
(true, false) => return 0,
(false, false) => return git_code,
(true, true) => return 2,
(false, true) => return git_code,
}
}
fn forward_only(run_git: &mut GitRunner) -> Result<Settlement, WorktreeCopyError> {
let git: GitStatus = run_git(&GitStep::Person)?;
return Ok(Settlement {
git,
copy_failure: None,
});
}
const UNKNOWN_DESTINATION_NOTICE: &str = "cli-git: this `git worktree add` names its worktree in a form cli-git does not read, so the worktree received no ignored-state copy. Name the path as a plain operand (`git worktree add [-b <branch>] <path> [<commit-ish>]`, with `-C <directory>` or `-c <name>=<value>` before `worktree`) to have it copied.\n";
fn unconfirmed_destination_notice(destination: &Path) -> String {
return format!(
"cli-git: {} was made, but its registration could not be confirmed as this command's, so it received no ignored-state copy. `git worktree list` shows whether Git registered it.\n",
quote(destination.to_string_lossy().as_ref())
);
}
fn synchronize_after(
invocation: &Invocation,
observation: &WorktreeCopyObservation,
registered_before: bool,
git_succeeded: bool,
carried: Option<&Path>,
stderr: &mut dyn Write,
) -> Result<Vec<PathBuf>, WorktreeCopyError> {
if matches!(&observation.selection, CopySelection::Paths(paths) if paths.is_empty()) {
return Ok(carried.map(Path::to_path_buf).into_iter().collect());
}
let Some(destination) = invocation.destination.as_deref() else {
stderr
.write_all(UNKNOWN_DESTINATION_NOTICE.as_bytes())
.map_err(|error| return raw_failure(&error))?;
return Ok(Vec::new());
};
let mut found: CreatedWorktrees =
find_created_worktree(&invocation.context, observation, destination, registered_before)?;
if found.created.is_empty()
&& git_succeeded
&& !registered_before
&& std::fs::symlink_metadata(destination.join(".git")).is_ok()
{
let line: String = unconfirmed_destination_notice(destination);
stderr
.write_all(line.as_bytes())
.map_err(|error| return raw_failure(&error))?;
return Ok(Vec::new());
}
let created_roots: Vec<PathBuf> = found.created.iter().map(|worktree| { return worktree.root.clone(); }).collect();
found.created.retain(|worktree| { return carried != Some(worktree.root.as_path()); });
if found.created.is_empty() {
return Ok(created_roots);
}
let mut copied: usize = 0;
match observation.source_root.as_ref() {
None => {}
Some(source) => {
for created in &found.created {
copied += synchronize_created_worktree_noting(
&invocation.context,
observation.common_dir.as_path(),
source.as_path(),
created.root.as_path(),
found.registered_roots.as_slice(),
&observation.selection,
stderr,
)?;
}
}
}
let line: String = summary_line(
copied,
found.created.len(),
observation.source_root.as_deref(),
);
stderr
.write_all(line.as_bytes())
.map_err(|error| return raw_failure(&error))?;
return Ok(created_roots);
}
struct AfterLock {
source_root: Option<PathBuf>,
created: Vec<PathBuf>,
}
#[cfg(target_os = "linux")]
fn repair_created(invocation: &Invocation, after: &AfterLock, stderr: &mut dyn Write) {
let Some(source) = after.source_root.as_deref() else { return };
let environment: RepairEnvironment = RepairEnvironment::from_process();
for root in &after.created {
let request: RepairRequest<'_> = RepairRequest { git: &invocation.context, source, destination: root.as_path(), switch: invocation.repair, environment: &environment };
repair_worktree(&request, stderr);
}
}
fn settle_creation(
invocation: &Invocation,
observation: &WorktreeCopyObservation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<(Settlement, AfterLock), WorktreeCopyError> {
let registered_before: bool = invocation
.destination
.as_deref()
.is_some_and(|destination| { return destination_is_registered(observation.common_dir.as_path(), destination); });
let ladder = timed("chooseRung", || { return begin_ladder(invocation.add_plan.as_ref(), &invocation.context, &invocation.identity); });
let ran: Result<GitStatus, WorktreeCopyError> = timed("gitAdd", || { return run_git(&ladder.first_step()); });
let settled: Result<LadderResult, WorktreeCopyError> = match ran {
Ok(status) => Ok(timed("finishRung", || { return ladder.finish(status, run_git, &invocation.context, &mut std::io::stdout()); })),
Err(error) => {
ladder.finish(GitStatus { code: None }, run_git, &invocation.context, &mut std::io::sink());
Err(error)
}
};
let LadderResult { git, carried } = settled?;
let (copy_failure, created): (Option<WorktreeCopyError>, Vec<PathBuf>) =
if git.succeeded() || invocation.destination.is_some() {
match timed("copyIgnored", || { return synchronize_after(invocation, observation, registered_before, git.succeeded(), carried.as_deref(), stderr); }) {
Ok(roots) => (None, roots),
Err(failure) => (Some(failure), Vec::new()),
}
} else {
(None, Vec::new())
};
return Ok((Settlement { git, copy_failure }, AfterLock { source_root: observation.source_root.clone(), created }));
}
pub fn run_with_worktree_copy(
invocation: &Invocation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<Settlement, WorktreeCopyError> {
return settle_invocation(invocation, run_git, stderr).map_err(lifecycle_failure);
}
fn settle_invocation(
invocation: &Invocation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<Settlement, WorktreeCopyError> {
if invocation.opted_out || invocation.short_circuit {
return forward_only(run_git);
}
let Some(observation) = observe_copy_source(
&invocation.identity,
invocation.effective_cwd.as_path(),
)?
else {
return forward_only(run_git);
};
let recorded: bool = invocation.creates && invocation.add_plan.is_some();
if recorded {
begin_timing(observation.common_dir.clone());
}
let settled: Result<Settlement, WorktreeCopyError> = settle_observed(invocation, &observation, run_git, stderr);
if recorded {
finish_timing(settled.as_ref().map_or(1, exit_code));
}
return settled;
}
fn settle_observed(
invocation: &Invocation,
observation: &WorktreeCopyObservation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<Settlement, WorktreeCopyError> {
timed("recover", || { return recover_pending_worktree_copies(observation.common_dir.as_path(), stderr); })?;
if !invocation.creates {
return forward_only(run_git);
}
let (settlement, after) = settle_creation(invocation, observation, run_git, stderr)?;
#[cfg(target_os = "linux")]
timed("repair", || { repair_created(invocation, &after, stderr); });
#[cfg(not(target_os = "linux"))]
let _after = after;
return Ok(settlement);
}
#[cfg(test)]
#[path = "worktree_copy_lifecycle_tests.rs"]
mod tests;