use crate::add_timing::step as timed;
use crate::diagnostic_log::{debug, warn};
use crate::fs_beneath::ObjectId;
use crate::transaction_git::GitContext;
use crate::worktree_add_binding::{Bound, bind_registration, verify_association};
use crate::worktree_add_plan::AddPlan;
use crate::worktree_clone::{Cloned, clone_tracked_files, leftover_report};
use crate::worktree_copy::{GitRunner, GitStatus};
use crate::worktree_copy_error::WorktreeCopyError;
use crate::worktree_copy_stage_claim::StageClaim;
use crate::worktree_ladder_run::{Ladder, LadderResult, Rung, discard_stage};
use crate::worktree_dirty_restore::{left_out_note, record_left_out, restore_dirty};
use crate::worktree_ladder_target::{Target, apply_difference, checkout_environment, prepare_target, run_step};
use crate::worktree_slow_route::{SlowRoute, explain_failure};
use crate::worktree_snapshot::{PlaceError, put_in_place, settle_displaced, take_snapshot};
use crate::worktree_snapshot_source::{ChangedPaths, SnapshotSource};
use crate::worktree_subvolume::settle_subvolume_destination;
use crate::worktree_trash::{TrashManifest, dispose_pending_trash};
use std::ffi::OsString;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
const TAG: &str = "worktreeLadder";
struct Ended {
reason: String,
stage_is_ours: bool,
}
impl From<String> for Ended {
fn from(reason: String) -> Ended {
return Ended { reason, stage_is_ours: true };
}
}
impl From<PlaceError> for Ended {
fn from(error: PlaceError) -> Ended {
return Ended { reason: error.reason, stage_is_ours: error.stage_is_ours };
}
}
fn serve_snapshot(source: &SnapshotSource, plan: &AddPlan, stage: &Path, bound: &Bound, made: &mut Option<ObjectId>, run_git: &mut GitRunner, context: &GitContext) -> Result<String, Ended> {
verify_association(bound)?;
let target: Target = timed("prepareTarget", || { return prepare_target(source, plan, context, bound); })?;
let trash: Option<TrashManifest> = match timed("takeSnapshot", || { return take_snapshot(source, plan, stage, &bound.admin, target.pointer.as_slice()); }) {
Ok(taken) => {
*made = Some(taken.stage);
taken.trash
}
Err(failure) => {
*made = failure.stage;
return Err(Ended::from(failure.reason));
}
};
verify_association(bound)?;
if source.dirty.is_some() {
timed("restoreDirty", || { return restore_dirty(source, &target, stage, run_git, context); })?;
} else {
timed("applyDifference", || { return apply_difference(source, &target, stage, run_git); })?;
}
let mode: u32 = std::fs::metadata(source.root.as_path()).map_err(|error| { return format!("the source's root is unreadable ({error})"); })?.mode() & 0o7777;
timed("exchange", || { return put_in_place(stage, plan.destination.as_path(), mode, bound.binding.destination_id); })?;
for note in timed("settleDisplaced", || { return settle_displaced(stage, plan.destination.as_path(), bound.binding.destination_id, target.pointer.as_slice()); }) {
warn(TAG, format!("cli-git: {note}").as_str());
}
let mut notices: Box<dyn std::io::Write> = if plan.quiet { Box::new(std::io::sink()) } else { Box::new(std::io::stderr()) };
if let Err(reason) = timed("disposeTrash", || { return dispose_pending_trash(context, plan.destination.as_path(), target.admin.as_path(), trash.as_ref(), &mut notices); }) {
warn(TAG, format!("cli-git: {reason}").as_str());
}
return Ok(target.line);
}
fn serve_clones(source: &SnapshotSource, plan: &AddPlan, bound: &Bound, run_git: &mut GitRunner, context: &GitContext) -> Result<String, String> {
verify_association(bound)?;
let target: Target = prepare_target(source, plan, context, bound)?;
let cloned: Cloned = clone_tracked_files(source, &bound.destination, &bound.admin)?;
let applied: Result<(), String> = verify_association(bound).and_then(|()| { return apply_difference(source, &target, plan.destination.as_path(), run_git); });
if let Err(reason) = applied {
warn(TAG, format!("cli-git: {}", leftover_report(cloned.created(), plan.destination.as_path())).as_str());
return Err(reason);
}
return Ok(target.line);
}
fn checkout_like_git(plan: &AddPlan, bound: &Bound, run_git: &mut GitRunner) -> GitStatus {
let destination: &Path = plan.destination.as_path();
if let Err(reason) = verify_association(bound) {
warn(TAG, format!("cli-git: the checkout of {} was not started: {reason}", destination.display()).as_str());
return GitStatus { code: Some(1) };
}
let mut arguments: Vec<OsString> = ["-C"].iter().map(OsString::from).chain([destination.as_os_str().to_os_string()]).chain(["reset", "--hard", "--no-recurse-submodules"].iter().map(OsString::from)).collect();
if plan.quiet {
arguments.push(OsString::from("--quiet"));
}
let environment: Vec<(OsString, OsString)> = checkout_environment(bound.binding.admin.as_path(), destination);
let status: GitStatus = match run_step(run_git, arguments.as_slice(), environment.as_slice()) {
Ok(status) => status,
Err(reason) => {
warn(TAG, format!("Git's checkout of {} could not start ({reason})", destination.display()).as_str());
return GitStatus { code: Some(1) };
}
};
if let (true, Err(reason)) = (status.succeeded(), verify_association(bound)) {
warn(TAG, format!("cli-git: the checkout of {} ran, but it cannot be taken for the add's: {reason}", destination.display()).as_str());
return GitStatus { code: Some(1) };
}
return status;
}
fn report_incomplete(plan: &AddPlan) {
let shown: String = plan.destination.display().to_string();
warn(TAG, format!("cli-git: the `git worktree add` of {shown} could not be completed; the worktree and its registration were left as they are. `git -C {shown} reset --hard` retries its checkout, and `git worktree remove --force {shown}` removes it.").as_str());
}
fn mark_complete(claim: &StageClaim, swapped: bool) {
let marked: Result<(), WorktreeCopyError> = timed("markComplete", || { return if swapped { claim.mark_complete_unsynced() } else { claim.mark_complete() }; });
if let Err(error) = marked {
warn(TAG, format!("the completion of the add could not be recorded ({}); a later command may report it as interrupted although it is whole", error.0).as_str());
}
}
fn complete_like_git(plan: &AddPlan, claim: &StageClaim, bound: &Bound, run_git: &mut GitRunner) -> GitStatus {
let status: GitStatus = checkout_like_git(plan, bound, run_git);
if status.succeeded() {
mark_complete(claim, false);
} else {
report_incomplete(plan);
}
return status;
}
fn bind(claim: &StageClaim, source: &SnapshotSource, plan: &AddPlan) -> Result<Bound, String> {
let bound: Bound = timed("bindRegistration", || { return bind_registration(source.common_dir.as_path(), plan.destination.as_path()); })?;
if let Err(error) = timed("recordBinding", || { return claim.record_binding(&bound.binding); }) {
warn(TAG, format!("the binding of the add could not be recorded ({}); an interruption will be reported by path only", error.0).as_str());
}
return Ok(bound);
}
fn finish_clones(source: &SnapshotSource, plan: &AddPlan, git: GitStatus, claim: &StageClaim, run_git: &mut GitRunner, context: &GitContext, out: &mut dyn Write, slow: &mut Option<SlowRoute>) -> LadderResult {
if !git.succeeded() {
return LadderResult { git, carried: None };
}
let bound: Bound = match bind(claim, source, plan) {
Ok(found) => found,
Err(reason) => {
warn(TAG, format!("cli-git: {} was left as Git made it, without a checkout: {reason}", plan.destination.display()).as_str());
report_incomplete(plan);
return LadderResult { git: GitStatus { code: Some(1) }, carried: None };
}
};
return match serve_clones(source, plan, &bound, run_git, context) {
Ok(line) => {
debug(TAG, format!("clones served {}", plan.destination.display()).as_str());
mark_complete(claim, false);
let _written = out.write_all(line.as_bytes());
LadderResult { git, carried: None }
}
Err(reason) => {
debug(TAG, format!("the clone rung ended: {reason}; Git checks the worktree out").as_str());
*slow = Some(explain_failure(reason.as_str()));
LadderResult { git: complete_like_git(plan, claim, &bound, run_git), carried: None }
}
};
}
fn finish_snapshot(source: &SnapshotSource, plan: &AddPlan, stage: &Path, claim: &StageClaim, run_git: &mut GitRunner, context: &GitContext, out: &mut dyn Write, slow: &mut Option<SlowRoute>) -> LadderResult {
let bound: Bound = match bind(claim, source, plan) {
Ok(found) => found,
Err(reason) => {
warn(TAG, format!("cli-git: {} was left as Git made it, without a checkout: {reason}", plan.destination.display()).as_str());
report_incomplete(plan);
return LadderResult { git: GitStatus { code: Some(1) }, carried: None };
}
};
let mut made: Option<ObjectId> = None;
return match serve_snapshot(source, plan, stage, &bound, &mut made, run_git, context) {
Ok(line) => {
debug(TAG, format!("the snapshot served {}", plan.destination.display()).as_str());
mark_complete(claim, true);
let _written = out.write_all(line.as_bytes());
if source.git_dir == source.common_dir && !plan.quiet {
let _noted = std::io::stderr().write_all(crate::worktree_trash::MAIN_SNAPSHOT_NOTE.as_bytes());
if let Some(changed) = source.dirty.as_ref().filter(|found: &&ChangedPaths| { return found.has_changes(); }) {
let _left_out = std::io::stderr().write_all(left_out_note(changed).as_bytes());
}
}
if let Some(changed) = source.dirty.as_ref().filter(|found: &&ChangedPaths| { return found.has_changes(); }) {
record_left_out(plan.destination.as_path(), changed);
}
LadderResult { git: GitStatus { code: Some(0) }, carried: Some(plan.destination.clone()) }
}
Err(ended) if ended.stage_is_ours => {
debug(TAG, format!("the snapshot rung ended: {}; Git checks the worktree out", ended.reason).as_str());
discard_stage(stage, made);
*slow = Some(explain_failure(ended.reason.as_str()));
LadderResult { git: complete_like_git(plan, claim, &bound, run_git), carried: None }
}
Err(ended) => {
warn(TAG, format!("cli-git: the add of {} ended in a state the wrapper cannot untangle: {}; {} is not touched", plan.destination.display(), ended.reason, stage.display()).as_str());
report_incomplete(plan);
LadderResult { git: GitStatus { code: Some(1) }, carried: None }
}
};
}
fn release_claim(claim: StageClaim, swapped: bool) {
let released: Result<(), WorktreeCopyError> = timed("releaseClaim", || { return if swapped { claim.release_unsynced() } else { claim.release() }; });
if let Err(error) = released {
warn(TAG, format!("the stage claim could not be released ({})", error.0).as_str());
}
}
impl Ladder {
pub fn finish(self, git: GitStatus, run_git: &mut GitRunner, context: &GitContext, out: &mut dyn Write) -> LadderResult {
let mut slow: Option<SlowRoute> = self.slow;
let result: LadderResult = match (self.rung, self.plan.as_ref()) {
(Rung::Snapshot(source, claim), Some(plan)) => {
if !git.succeeded() {
release_claim(claim, false);
return LadderResult { git, carried: None };
}
let stage: PathBuf = claim.container().to_path_buf();
let result: LadderResult = finish_snapshot(&source, plan, stage.as_path(), &claim, run_git, context, out, &mut slow);
release_claim(claim, result.carried.is_some());
result
}
(Rung::Snapshot(_, claim), None) => {
release_claim(claim, false);
LadderResult { git, carried: None }
}
(Rung::Clone(source, subvolume, claim), Some(plan)) => {
let result: LadderResult = finish_clones(&source, plan, git, &claim, run_git, context, out, &mut slow);
if let Some(created) = subvolume.as_ref() {
settle_subvolume_destination(created);
}
release_claim(claim, false);
result
}
(Rung::Clone(_, subvolume, claim), None) => {
if let Some(created) = subvolume.as_ref() {
settle_subvolume_destination(created);
}
release_claim(claim, false);
LadderResult { git, carried: None }
}
(Rung::Git(subvolume), _) => {
if let Some(created) = subvolume.as_ref() {
settle_subvolume_destination(created);
}
LadderResult { git, carried: None }
}
};
if let (Some(route), Some(plan), true, None) = (slow.as_ref(), self.plan.as_ref(), result.git.succeeded(), result.carried.as_ref()) {
route.tell(plan);
}
return result;
}
}
#[cfg(test)]
#[path = "worktree_ladder_finish_tests.rs"]
mod tests;