use super::diagnostic_log::{debug, warn};
use super::fs_beneath::ObjectId;
use super::transaction_git::GitContext;
use super::worktree_add_binding::{Bound, bind_registration, verify_association};
use super::worktree_add_plan::AddPlan;
use super::worktree_clone::{Cloned, clone_tracked_files, leftover_report};
use super::worktree_copy::{GitRunner, GitStatus};
use super::worktree_copy_stage_claim::StageClaim;
use super::worktree_ladder_run::{Ladder, LadderResult, Rung, discard_stage};
use super::worktree_ladder_target::{Target, apply_difference, checkout_environment, prepare_target, run_step};
use super::worktree_snapshot::{PlaceError, put_in_place, settle_displaced, take_snapshot};
use super::worktree_snapshot_source::SnapshotSource;
use super::worktree_subvolume::settle_subvolume_destination;
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>, token: Option<&str>, run_git: &mut GitRunner, context: &GitContext) -> Result<String, Ended> {
verify_association(bound)?;
let target: Target = prepare_target(source, plan, context, bound)?;
match take_snapshot(source, plan, stage, &bound.admin, target.pointer.as_slice()) {
Ok(identity) => *made = Some(identity),
Err(failure) => {
*made = failure.stage;
return Err(Ended::from(failure.reason));
}
}
verify_association(bound)?;
apply_difference(source, &target, stage, token, 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;
put_in_place(stage, plan.destination.as_path(), mode, bound.binding.destination_id)?;
for note in settle_displaced(stage, plan.destination.as_path(), bound.binding.destination_id, target.pointer.as_slice()) {
warn(TAG, format!("cli-git: {note}").as_str());
}
return Ok(target.line);
}
fn serve_clones(source: &SnapshotSource, plan: &AddPlan, bound: &Bound, token: Option<&str>, 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(), token, 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, token: Option<&str>, 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, token, 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) {
if let Err(error) = claim.mark_complete() {
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, token: Option<&str>, run_git: &mut GitRunner) -> GitStatus {
let status: GitStatus = checkout_like_git(plan, bound, token, run_git);
if status.succeeded() {
mark_complete(claim);
} else {
report_incomplete(plan);
}
return status;
}
fn bind(claim: &StageClaim, source: &SnapshotSource, plan: &AddPlan) -> Result<Bound, String> {
let bound: Bound = bind_registration(source.common_dir.as_path(), plan.destination.as_path())?;
if let Err(error) = 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, token: Option<&str>, run_git: &mut GitRunner, context: &GitContext, out: &mut dyn Write) -> 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, token, run_git, context) {
Ok(line) => {
debug(TAG, format!("clones served {}", plan.destination.display()).as_str());
mark_complete(claim);
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());
LadderResult { git: complete_like_git(plan, claim, &bound, token, run_git), carried: None }
}
};
}
fn finish_snapshot(source: &SnapshotSource, plan: &AddPlan, stage: &Path, claim: &StageClaim, token: Option<&str>, run_git: &mut GitRunner, context: &GitContext, out: &mut dyn Write) -> 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, token, run_git, context) {
Ok(line) => {
debug(TAG, format!("the snapshot served {}", plan.destination.display()).as_str());
mark_complete(claim);
let _written = out.write_all(line.as_bytes());
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);
LadderResult { git: complete_like_git(plan, claim, &bound, token, 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) {
if let Err(error) = claim.release() {
warn(TAG, format!("the stage claim could not be released ({})", error.0).as_str());
}
}
impl Ladder {
pub fn finish(self, git: GitStatus, token: Option<&str>, run_git: &mut GitRunner, context: &GitContext, out: &mut dyn Write) -> LadderResult {
match (self.rung, self.plan) {
(Rung::Snapshot(source, claim), Some(plan)) => {
if !git.succeeded() {
release_claim(claim);
return LadderResult { git, carried: None };
}
let stage: PathBuf = claim.container().to_path_buf();
let result: LadderResult = finish_snapshot(&source, &plan, stage.as_path(), &claim, token, run_git, context, out);
release_claim(claim);
return result;
}
(Rung::Snapshot(_, claim), None) => {
release_claim(claim);
return LadderResult { git, carried: None };
}
(Rung::Clone(source, subvolume, claim), Some(plan)) => {
let result: LadderResult = finish_clones(&source, &plan, git, &claim, token, run_git, context, out);
if let Some(created) = subvolume.as_ref() {
settle_subvolume_destination(created);
}
release_claim(claim);
return result;
}
(Rung::Clone(_, subvolume, claim), None) => {
if let Some(created) = subvolume.as_ref() {
settle_subvolume_destination(created);
}
release_claim(claim);
return LadderResult { git, carried: None };
}
(Rung::Git(subvolume), _) => {
if let Some(created) = subvolume.as_ref() {
settle_subvolume_destination(created);
}
return LadderResult { git, carried: None };
}
}
}
}
#[cfg(test)]
#[path = "worktree_ladder_finish_tests.rs"]
mod tests;