use crate::forwarding::{ChildOutcome, exit_code as child_exit_code, run_real_git};
use crate::worktree_add_plan::AddPlan;
use crate::worktree_remove_plan::RemovePlan;
use crate::worktree_identity::WorktreeIdentity;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CopyFacts {
pub identity: WorktreeIdentity,
pub creates: bool,
pub destination: Option<PathBuf>,
pub add_plan: Option<AddPlan>,
pub remove_plan: Option<RemovePlan>,
}
pub fn copy_applies(identity: &WorktreeIdentity, creates: bool) -> bool {
return copy_applies_with(identity, creates, false);
}
pub fn copy_applies_with(identity: &WorktreeIdentity, creates: bool, serves_ladder: bool) -> bool {
match identity {
WorktreeIdentity::OutsideWorktree => return false,
WorktreeIdentity::MainWorktree { .. } => {
return serves_ladder || creates;
}
WorktreeIdentity::LinkedWorktree { .. } | WorktreeIdentity::BareRepository { .. } => {
return true;
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ForwardedRun {
Ran(i32),
Failed {
stderr: String,
code: i32,
},
}
#[cfg(unix)]
fn query_directory(identity: &WorktreeIdentity) -> PathBuf {
match identity {
WorktreeIdentity::MainWorktree { worktree_root, .. }
| WorktreeIdentity::LinkedWorktree { worktree_root, .. } => return worktree_root.clone(),
WorktreeIdentity::BareRepository { common_dir, .. } => return common_dir.clone(),
WorktreeIdentity::OutsideWorktree => return PathBuf::new(),
}
}
fn run_plain(
real_git: &Path,
arguments: &[OsString],
overlay: &[(OsString, OsString)],
) -> ForwardedRun {
match run_real_git(real_git, arguments, overlay) {
Ok(outcome) => return ForwardedRun::Ran(child_exit_code(outcome)),
Err(error) => {
return ForwardedRun::Failed {
stderr: format!("cli-git: cannot start {}: {error}\n", real_git.display()),
code: crate::action::ENGINE_FAILURE_EXIT_CODE,
};
}
}
}
pub fn run_forwarded_git(
real_git: &Path,
arguments: &[OsString],
overlay: &[(OsString, OsString)],
copy: Option<&CopyFacts>,
) -> ForwardedRun {
#[cfg(unix)]
{
if let Some(facts) = copy {
#[cfg(target_os = "linux")]
if let Some(finished) = remove_by_subvolume(real_git, overlay, facts) {
return finished;
}
return run_settled(real_git, arguments, overlay, facts);
}
}
#[cfg(not(unix))]
{
let _ = copy;
}
return run_plain(real_git, arguments, overlay);
}
#[cfg(all(unix, target_os = "linux"))]
fn remove_by_subvolume(real_git: &Path, overlay: &[(OsString, OsString)], facts: &CopyFacts) -> Option<ForwardedRun> {
use std::io::Write;
use crate::worktree_remove::{RemoveOutcome, RemoveRequest, remove_subvolume_worktree};
use crate::worktree_identity::WorktreeIdentity;
let plan = facts.remove_plan.as_ref()?;
let common_dir: &Path = match &facts.identity {
WorktreeIdentity::MainWorktree { common_dir, .. }
| WorktreeIdentity::LinkedWorktree { common_dir, .. }
| WorktreeIdentity::BareRepository { common_dir, .. } => common_dir.as_path(),
WorktreeIdentity::OutsideWorktree => return None,
};
let context: crate::transaction_git::GitContext = crate::transaction_git::GitContext {
real_git: real_git.to_path_buf(),
overlay: overlay.to_vec(),
global_prefix: Vec::new(),
common_dir: None,
};
match remove_subvolume_worktree(&RemoveRequest { context: &context, plan, common_dir }) {
RemoveOutcome::LeftToGit => return None,
RemoveOutcome::Removed => {
let _closed = writeln!(std::io::stderr(), "{}", crate::worktree_remove::DESTROY_NOTICE);
return Some(ForwardedRun::Ran(0));
}
RemoveOutcome::Incomplete(report) => {
return Some(ForwardedRun::Failed { stderr: format!("{report}\n"), code: crate::action::ENGINE_FAILURE_EXIT_CODE });
}
}
}
#[cfg(unix)]
fn run_settled(
real_git: &Path,
arguments: &[OsString],
overlay: &[(OsString, OsString)],
facts: &CopyFacts,
) -> ForwardedRun {
use std::io::Write;
use crate::checkout_workers::{insert_parallel_checkout, is_fallback_checkout, parallel_checkout_wanted};
use crate::transaction_git::GitContext;
use crate::worktree_copy::{GitStatus, GitStep, Invocation, exit_code, run_with_worktree_copy};
use crate::worktree_copy_error::WorktreeCopyError;
let context: GitContext = GitContext {
real_git: real_git.to_path_buf(),
overlay: overlay.to_vec(),
global_prefix: Vec::new(),
common_dir: None,
};
let invocation: Invocation = Invocation {
identity: facts.identity.clone(),
effective_cwd: query_directory(&facts.identity),
creates: facts.creates,
destination: facts.destination.clone(),
opted_out: false,
short_circuit: false,
context,
add_plan: facts.add_plan.clone(),
#[cfg(target_os = "linux")]
repair: crate::package_repair::RepairSwitch::from_arguments(arguments),
};
let mut stderr = std::io::stderr();
let mut parallel: Option<bool> = None;
let mut run_git = |step: &GitStep<'_>| -> Result<GitStatus, WorktreeCopyError> {
let mut child_overlay: Vec<(OsString, OsString)> = overlay.to_vec();
let mut child_arguments: Vec<OsString> = arguments.to_vec();
match step {
GitStep::Person => {
if facts.creates && *parallel.get_or_insert_with(|| { return parallel_checkout_wanted(&invocation.context, invocation.effective_cwd.as_path(), arguments); }) {
insert_parallel_checkout(&mut child_arguments);
}
}
GitStep::WithoutCheckout(at) => {
if *at == 0 || *at > child_arguments.len() || child_arguments[*at - 1] != "add" {
return Err(WorktreeCopyError(String::from("cli-git: the worktree plan does not match the arguments.")));
}
child_arguments.insert(*at, OsString::from("--no-checkout"));
}
GitStep::Other { arguments: other, environment } => {
child_arguments = other.to_vec();
child_overlay.extend(environment.iter().cloned());
if is_fallback_checkout(child_arguments.as_slice()) && *parallel.get_or_insert_with(|| { return parallel_checkout_wanted(&invocation.context, invocation.effective_cwd.as_path(), arguments); }) {
insert_parallel_checkout(&mut child_arguments);
}
}
}
match run_real_git(real_git, child_arguments.as_slice(), child_overlay.as_slice()) {
Ok(ChildOutcome::Exited(code)) => return Ok(GitStatus { code: Some(code) }),
Ok(ChildOutcome::Signaled(_)) => return Ok(GitStatus { code: None }),
Err(error) => {
return Err(WorktreeCopyError(format!(
"cli-git: cannot start {}: {error}",
real_git.display()
)));
}
}
};
match run_with_worktree_copy(&invocation, &mut run_git, &mut stderr) {
Ok(settlement) => {
if let Some(failure) = &settlement.copy_failure {
let _ = writeln!(stderr, "{}", failure.0);
}
return ForwardedRun::Ran(exit_code(&settlement));
}
Err(failure) => {
return ForwardedRun::Failed {
stderr: format!("{}\n", failure.0),
code: crate::action::ENGINE_FAILURE_EXIT_CODE,
};
}
}
}
#[cfg(test)]
#[path = "worktree_copy_forward_tests.rs"]
mod tests;