use crate::add_timing::{begin_step, end_step, note as note_timing};
use crate::diagnostic_log::{debug, warn};
use crate::fs_btrfs_ioctl::subvolume_destroy_at;
use crate::transaction_git::GitContext;
use crate::worktree_copy::{GitStatus, GitStep};
use crate::worktree_subvolume::{CreatedSubvolume, prepare_subvolume_destination};
use crate::worktree_add_plan::AddPlan;
use crate::worktree_copy_stage_claim::{StageClaim, claim_stage_for_add};
use crate::worktree_identity::WorktreeIdentity;
use crate::worktree_slow_route::{SlowRoute, explain_cheap_refusal, explain_refusal, explain_unsupported};
use crate::worktree_snapshot_source::{SnapshotSource, clone_refusal, snapshot_blocker, snapshot_refusal, source_or_dirty};
use std::path::{Path, PathBuf};
const TAG: &str = "worktreeLadder";
pub(crate) enum Rung {
Git(Option<CreatedSubvolume>),
Clone(Box<SnapshotSource>, Option<CreatedSubvolume>, StageClaim),
Snapshot(Box<SnapshotSource>, StageClaim),
}
pub struct Ladder {
pub(crate) plan: Option<AddPlan>,
pub(crate) rung: Rung,
pub(crate) slow: Option<SlowRoute>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LadderResult {
pub git: GitStatus,
pub carried: Option<PathBuf>,
}
fn log_git_checkout(plan: &AddPlan, reason: &str, snapshot_refusal: &Option<String>, clone_refusal: &Option<String>) {
debug(TAG, format!("{}: Git checks the worktree out ({reason}; snapshot: {snapshot_refusal:?}; clone: {clone_refusal:?})", plan.destination.display()).as_str());
}
fn identity_of_stage_name(stage: &Path) -> Option<crate::fs_beneath::ObjectId> {
let parent: &Path = stage.parent()?;
let held: std::fs::File = crate::fs_beneath::open_root(parent).ok()?;
return crate::fs_beneath::identity_at(&held, stage.file_name()?).ok();
}
fn choose_rung(plan: &AddPlan, context: &GitContext, identity: &WorktreeIdentity) -> (Rung, Option<SlowRoute>) {
let refusals_entered: bool = begin_step("refusals");
let snapshot_refusal: Option<String> = snapshot_refusal(identity, plan);
end_step(refusals_entered);
let refusals_again_entered: bool = begin_step("refusals");
let clone_refusal: Option<String> = clone_refusal(identity, plan);
end_step(refusals_again_entered);
let cheap: Option<SlowRoute> = match snapshot_refusal.as_deref() {
Some(reason) => explain_cheap_refusal(identity, reason),
None => None,
};
if snapshot_refusal.is_some() && clone_refusal.is_some() {
log_git_checkout(plan, "neither a snapshot nor clones apply", &snapshot_refusal, &clone_refusal);
return (Rung::Git(prepare_subvolume_destination(plan)), cheap);
}
let check_source_entered: bool = begin_step("checkSource");
let check_source_outcome = source_or_dirty(context, identity);
end_step(check_source_entered);
let source: SnapshotSource = match check_source_outcome {
Ok(found) => found,
Err(refusal) => {
log_git_checkout(plan, refusal.reason.as_str(), &snapshot_refusal, &clone_refusal);
let slow: Option<SlowRoute> = match cheap {
Some(structural) => Some(structural),
None => explain_refusal(context, identity, &refusal),
};
return (Rung::Git(prepare_subvolume_destination(plan)), slow);
}
};
let snapshot_blocked: Option<String> = match snapshot_refusal.clone() {
Some(reason) => Some(reason),
None => snapshot_blocker(&source),
};
let mut slow: Option<SlowRoute> = match cheap {
Some(structural) => Some(structural),
None => snapshot_blocked.as_deref().map(explain_unsupported),
};
if snapshot_blocked.is_none() {
let claim_stage_entered: bool = begin_step("claimStage");
let claim_stage_outcome = claim_stage_for_add(source.common_dir.as_path(), plan.destination.as_path());
end_step(claim_stage_entered);
match claim_stage_outcome {
Ok(claim) => return (Rung::Snapshot(Box::new(source), claim), None),
Err(error) => {
debug(TAG, format!("the stage cannot be claimed ({}); no snapshot", error.0).as_str());
slow = Some(explain_unsupported(format!("the add could not be recorded durably ({})", error.0).as_str()));
}
}
} else {
debug(TAG, format!("no snapshot for {} ({:?})", plan.destination.display(), snapshot_blocked).as_str());
}
if clone_refusal.is_none() && source.dirty.is_none() {
let claim_stage_again_entered: bool = begin_step("claimStage");
let claim_stage_again_outcome = claim_stage_for_add(source.common_dir.as_path(), plan.destination.as_path());
end_step(claim_stage_again_entered);
match claim_stage_again_outcome {
Ok(claim) => return (Rung::Clone(Box::new(source), prepare_subvolume_destination(plan), claim), slow),
Err(error) => debug(TAG, format!("the add cannot be recorded ({}); no clones", error.0).as_str()),
}
}
log_git_checkout(plan, "no rung applies", &snapshot_refusal, &clone_refusal);
return (Rung::Git(prepare_subvolume_destination(plan)), slow);
}
pub fn begin_ladder(plan: Option<&AddPlan>, context: &GitContext, identity: &WorktreeIdentity) -> Ladder {
let Some(planned) = plan else { return Ladder { plan: None, rung: Rung::Git(None), slow: None } };
let began: std::time::Instant = std::time::Instant::now();
let (rung, slow) = choose_rung(planned, context, identity);
let chosen: &str = match &rung {
Rung::Git(_) => "Git's checkout",
Rung::Clone(..) => "clones",
Rung::Snapshot(source, _claim) if source.dirty.is_some() => "a snapshot with a restore",
Rung::Snapshot(..) => "a snapshot",
};
debug(TAG, format!("{}: {chosen} chosen in {} ms", planned.destination.display(), began.elapsed().as_millis()).as_str());
note_timing("route", chosen);
if let Some(route) = slow.as_ref() {
note_timing("slowRoute", format!("{}: {}", route.word(), route.detail()).as_str());
}
return Ladder { plan: Some(planned.clone()), rung, slow };
}
pub(crate) fn discard_stage(stage: &Path, made: Option<crate::fs_beneath::ObjectId>) {
let Some(expected) = made else {
warn(TAG, format!("the stage {} was not identified and is left in place", stage.display()).as_str());
return;
};
let current = identity_of_stage_name(stage);
if current.is_none() {
return;
}
if current != Some(expected) {
warn(TAG, format!("the stage {} is not the directory this add made and is left in place", stage.display()).as_str());
return;
}
if subvolume_destroy_at(stage).is_ok() {
return;
}
if let Err(error) = crate::subvolume_remove::remove_tree(stage) {
warn(TAG, format!("the stage {} could not be removed ({error}); it stays for the person to remove", stage.display()).as_str());
}
}
impl Ladder {
pub fn first_step(&self) -> GitStep<'static> {
return match (&self.rung, &self.plan) {
(Rung::Snapshot(..) | Rung::Clone(..), Some(plan)) => GitStep::WithoutCheckout(plan.insert_at),
_ => GitStep::Person,
};
}
}
#[cfg(test)]
#[path = "worktree_ladder_run_tests.rs"]
mod tests;