1use crate::add_timing::step as timed;
20use crate::diagnostic_log::{debug, warn};
21use crate::fs_beneath::ObjectId;
22use crate::transaction_git::GitContext;
23use crate::worktree_add_binding::{Bound, bind_registration, verify_association};
24use crate::worktree_add_plan::AddPlan;
25use crate::worktree_clone::{Cloned, clone_tracked_files, leftover_report};
26use crate::worktree_copy::{GitRunner, GitStatus};
27use crate::worktree_copy_error::WorktreeCopyError;
28use crate::worktree_copy_stage_claim::StageClaim;
29use crate::worktree_ladder_run::{Ladder, LadderResult, Rung, discard_stage};
30use crate::worktree_dirty_restore::{left_out_note, record_left_out, restore_dirty};
31use crate::worktree_ladder_target::{Target, apply_difference, checkout_environment, prepare_target, run_step};
32use crate::worktree_slow_route::{SlowRoute, explain_failure};
33use crate::worktree_snapshot::{PlaceError, put_in_place, settle_displaced, take_snapshot};
34use crate::worktree_snapshot_source::{ChangedPaths, SnapshotSource};
35use crate::worktree_subvolume::settle_subvolume_destination;
36use crate::worktree_trash::{TrashManifest, dispose_pending_trash};
37use std::ffi::OsString;
38use std::io::Write;
39use std::os::unix::fs::MetadataExt;
40use std::path::{Path, PathBuf};
41
42const TAG: &str = "worktreeLadder";
44
45struct Ended {
47 reason: String,
49 stage_is_ours: bool,
51}
52
53impl From<String> for Ended {
54 fn from(reason: String) -> Ended {
55 return Ended { reason, stage_is_ours: true };
56 }
57}
58
59impl From<PlaceError> for Ended {
60 fn from(error: PlaceError) -> Ended {
61 return Ended { reason: error.reason, stage_is_ours: error.stage_is_ours };
62 }
63}
64
65fn serve_snapshot(source: &SnapshotSource, plan: &AddPlan, stage: &Path, bound: &Bound, made: &mut Option<ObjectId>, run_git: &mut GitRunner, context: &GitContext) -> Result<String, Ended> {
76 verify_association(bound)?;
77 let target: Target = timed("prepareTarget", || { return prepare_target(source, plan, context, bound); })?;
78 let trash: Option<TrashManifest> = match timed("takeSnapshot", || { return take_snapshot(source, plan, stage, &bound.admin, target.pointer.as_slice()); }) {
79 Ok(taken) => {
80 *made = Some(taken.stage);
81 taken.trash
82 }
83 Err(failure) => {
84 *made = failure.stage;
85 return Err(Ended::from(failure.reason));
86 }
87 };
88 verify_association(bound)?;
89 if source.dirty.is_some() {
90 timed("restoreDirty", || { return restore_dirty(source, &target, stage, run_git, context); })?;
91 } else {
92 timed("applyDifference", || { return apply_difference(source, &target, stage, run_git); })?;
93 }
94 let mode: u32 = std::fs::metadata(source.root.as_path()).map_err(|error| { return format!("the source's root is unreadable ({error})"); })?.mode() & 0o7777;
95 timed("exchange", || { return put_in_place(stage, plan.destination.as_path(), mode, bound.binding.destination_id); })?;
96 for note in timed("settleDisplaced", || { return settle_displaced(stage, plan.destination.as_path(), bound.binding.destination_id, target.pointer.as_slice()); }) {
97 warn(TAG, format!("cli-git: {note}").as_str());
98 }
99 let mut notices: Box<dyn std::io::Write> = if plan.quiet { Box::new(std::io::sink()) } else { Box::new(std::io::stderr()) };
103 if let Err(reason) = timed("disposeTrash", || { return dispose_pending_trash(context, plan.destination.as_path(), target.admin.as_path(), trash.as_ref(), &mut notices); }) {
104 warn(TAG, format!("cli-git: {reason}").as_str());
105 }
106 return Ok(target.line);
107}
108
109fn serve_clones(source: &SnapshotSource, plan: &AddPlan, bound: &Bound, run_git: &mut GitRunner, context: &GitContext) -> Result<String, String> {
120 verify_association(bound)?;
121 let target: Target = prepare_target(source, plan, context, bound)?;
122 let cloned: Cloned = clone_tracked_files(source, &bound.destination, &bound.admin)?;
123 let applied: Result<(), String> = verify_association(bound).and_then(|()| { return apply_difference(source, &target, plan.destination.as_path(), run_git); });
124 if let Err(reason) = applied {
125 warn(TAG, format!("cli-git: {}", leftover_report(cloned.created(), plan.destination.as_path())).as_str());
126 return Err(reason);
127 }
128 return Ok(target.line);
129}
130
131fn checkout_like_git(plan: &AddPlan, bound: &Bound, run_git: &mut GitRunner) -> GitStatus {
145 let destination: &Path = plan.destination.as_path();
146 if let Err(reason) = verify_association(bound) {
147 warn(TAG, format!("cli-git: the checkout of {} was not started: {reason}", destination.display()).as_str());
148 return GitStatus { code: Some(1) };
149 }
150 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();
151 if plan.quiet {
152 arguments.push(OsString::from("--quiet"));
153 }
154 let environment: Vec<(OsString, OsString)> = checkout_environment(bound.binding.admin.as_path(), destination);
155 let status: GitStatus = match run_step(run_git, arguments.as_slice(), environment.as_slice()) {
156 Ok(status) => status,
157 Err(reason) => {
158 warn(TAG, format!("Git's checkout of {} could not start ({reason})", destination.display()).as_str());
159 return GitStatus { code: Some(1) };
160 }
161 };
162 if let (true, Err(reason)) = (status.succeeded(), verify_association(bound)) {
163 warn(TAG, format!("cli-git: the checkout of {} ran, but it cannot be taken for the add's: {reason}", destination.display()).as_str());
164 return GitStatus { code: Some(1) };
165 }
166 return status;
167}
168
169fn report_incomplete(plan: &AddPlan) {
180 let shown: String = plan.destination.display().to_string();
181 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());
182}
183
184fn mark_complete(claim: &StageClaim, swapped: bool) {
195 let marked: Result<(), WorktreeCopyError> = timed("markComplete", || { return if swapped { claim.mark_complete_unsynced() } else { claim.mark_complete() }; });
196 if let Err(error) = marked {
197 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());
198 }
199}
200
201fn complete_like_git(plan: &AddPlan, claim: &StageClaim, bound: &Bound, run_git: &mut GitRunner) -> GitStatus {
211 let status: GitStatus = checkout_like_git(plan, bound, run_git);
212 if status.succeeded() {
213 mark_complete(claim, false);
214 } else {
215 report_incomplete(plan);
216 }
217 return status;
218}
219
220fn bind(claim: &StageClaim, source: &SnapshotSource, plan: &AddPlan) -> Result<Bound, String> {
231 let bound: Bound = timed("bindRegistration", || { return bind_registration(source.common_dir.as_path(), plan.destination.as_path()); })?;
232 if let Err(error) = timed("recordBinding", || { return claim.record_binding(&bound.binding); }) {
233 warn(TAG, format!("the binding of the add could not be recorded ({}); an interruption will be reported by path only", error.0).as_str());
234 }
235 return Ok(bound);
236}
237
238fn 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 {
249 if !git.succeeded() {
250 return LadderResult { git, carried: None };
251 }
252 let bound: Bound = match bind(claim, source, plan) {
253 Ok(found) => found,
254 Err(reason) => {
255 warn(TAG, format!("cli-git: {} was left as Git made it, without a checkout: {reason}", plan.destination.display()).as_str());
256 report_incomplete(plan);
257 return LadderResult { git: GitStatus { code: Some(1) }, carried: None };
258 }
259 };
260 return match serve_clones(source, plan, &bound, run_git, context) {
261 Ok(line) => {
262 debug(TAG, format!("clones served {}", plan.destination.display()).as_str());
263 mark_complete(claim, false);
264 let _written = out.write_all(line.as_bytes());
266 LadderResult { git, carried: None }
267 }
268 Err(reason) => {
269 debug(TAG, format!("the clone rung ended: {reason}; Git checks the worktree out").as_str());
270 *slow = Some(explain_failure(reason.as_str()));
272 LadderResult { git: complete_like_git(plan, claim, &bound, run_git), carried: None }
273 }
274 };
275}
276
277fn 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 {
289 let bound: Bound = match bind(claim, source, plan) {
290 Ok(found) => found,
291 Err(reason) => {
292 warn(TAG, format!("cli-git: {} was left as Git made it, without a checkout: {reason}", plan.destination.display()).as_str());
293 report_incomplete(plan);
294 return LadderResult { git: GitStatus { code: Some(1) }, carried: None };
295 }
296 };
297 let mut made: Option<ObjectId> = None;
298 return match serve_snapshot(source, plan, stage, &bound, &mut made, run_git, context) {
299 Ok(line) => {
300 debug(TAG, format!("the snapshot served {}", plan.destination.display()).as_str());
301 mark_complete(claim, true);
302 let _written = out.write_all(line.as_bytes());
304 if source.git_dir == source.common_dir && !plan.quiet {
306 let _noted = std::io::stderr().write_all(crate::worktree_trash::MAIN_SNAPSHOT_NOTE.as_bytes());
307 if let Some(changed) = source.dirty.as_ref().filter(|found: &&ChangedPaths| { return found.has_changes(); }) {
308 let _left_out = std::io::stderr().write_all(left_out_note(changed).as_bytes());
309 }
310 }
311 if let Some(changed) = source.dirty.as_ref().filter(|found: &&ChangedPaths| { return found.has_changes(); }) {
312 record_left_out(plan.destination.as_path(), changed);
313 }
314 LadderResult { git: GitStatus { code: Some(0) }, carried: Some(plan.destination.clone()) }
315 }
316 Err(ended) if ended.stage_is_ours => {
317 debug(TAG, format!("the snapshot rung ended: {}; Git checks the worktree out", ended.reason).as_str());
318 discard_stage(stage, made);
319 *slow = Some(explain_failure(ended.reason.as_str()));
320 LadderResult { git: complete_like_git(plan, claim, &bound, run_git), carried: None }
321 }
322 Err(ended) => {
323 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());
324 report_incomplete(plan);
325 LadderResult { git: GitStatus { code: Some(1) }, carried: None }
326 }
327 };
328}
329
330fn release_claim(claim: StageClaim, swapped: bool) {
332 let released: Result<(), WorktreeCopyError> = timed("releaseClaim", || { return if swapped { claim.release_unsynced() } else { claim.release() }; });
333 if let Err(error) = released {
334 warn(TAG, format!("the stage claim could not be released ({})", error.0).as_str());
335 }
336}
337
338impl Ladder {
339 pub fn finish(self, git: GitStatus, run_git: &mut GitRunner, context: &GitContext, out: &mut dyn Write) -> LadderResult {
351 let mut slow: Option<SlowRoute> = self.slow;
352 let result: LadderResult = match (self.rung, self.plan.as_ref()) {
353 (Rung::Snapshot(source, claim), Some(plan)) => {
354 if !git.succeeded() {
355 release_claim(claim, false);
356 return LadderResult { git, carried: None };
357 }
358 let stage: PathBuf = claim.container().to_path_buf();
359 let result: LadderResult = finish_snapshot(&source, plan, stage.as_path(), &claim, run_git, context, out, &mut slow);
360 release_claim(claim, result.carried.is_some());
362 result
363 }
364 (Rung::Snapshot(_, claim), None) => {
365 release_claim(claim, false);
366 LadderResult { git, carried: None }
367 }
368 (Rung::Clone(source, subvolume, claim), Some(plan)) => {
369 let result: LadderResult = finish_clones(&source, plan, git, &claim, run_git, context, out, &mut slow);
370 if let Some(created) = subvolume.as_ref() {
371 settle_subvolume_destination(created);
372 }
373 release_claim(claim, false);
374 result
375 }
376 (Rung::Clone(_, subvolume, claim), None) => {
377 if let Some(created) = subvolume.as_ref() {
378 settle_subvolume_destination(created);
379 }
380 release_claim(claim, false);
381 LadderResult { git, carried: None }
382 }
383 (Rung::Git(subvolume), _) => {
384 if let Some(created) = subvolume.as_ref() {
385 settle_subvolume_destination(created);
386 }
387 LadderResult { git, carried: None }
388 }
389 };
390 if let (Some(route), Some(plan), true, None) = (slow.as_ref(), self.plan.as_ref(), result.git.succeeded(), result.carried.as_ref()) {
392 route.tell(plan);
393 }
394 return result;
395 }
396}
397
398#[cfg(test)]
400#[path = "worktree_ladder_finish_tests.rs"]
401mod tests;