use std::path::Path;
use super::driver::{ClaimEnder, Git, Landing};
use super::ownership::{self, ClaimOwnership};
use super::plan::{self, StepLine};
use super::{CleanupRequest, DirtProbe, landed, recheck, run_git};
use crate::session_manager::worktree_ownership::{
restore_worktree_sentinel, take_worktree_sentinel,
};
#[allow(clippy::too_many_arguments)]
pub(super) async fn remove_one<T: Git, C: ClaimEnder>(
git: &T,
claims: &C,
ownership: &dyn ClaimOwnership,
landing: &dyn Landing,
probe_dirt: DirtProbe<'_>,
req: &CleanupRequest,
merge: &landed::Merge,
entry: &plan::WorktreeEntry,
) -> StepLine {
const STEP: &str = "worktree";
let path = entry.path.as_path();
let shown = path.display().to_string();
let mut landed_note = String::new();
let dirt = probe_dirt(path);
let before = recheck::first(dirt.as_ref(), &entry.head);
if let Some(dirt) = &dirt {
if !landed::ahead_only(dirt) {
return StepLine::failed(
STEP,
format!(
"{shown} holds unsaved work ({}) — refusing to remove it; cleanup never \
forces (#7275)",
dirt.reason
),
);
}
match landed::landed(git, landing, req, merge, entry) {
Ok(pr) => {
landed_note = format!(
" ({} landed: #{pr} merged it and re-merging changes nothing)",
dirt.reason
);
}
Err(why) => {
return StepLine::failed(STEP, landed::ahead_refusal(&shown, &dirt.reason, &why));
}
}
}
let holders = match claims.claims_on(path).await {
Ok(h) => h,
Err(e) => return StepLine::failed(STEP, format!("{e:#}")),
};
if let Err(why) = ownership::gate(ownership, path, &holders).await {
return StepLine::failed(STEP, format!("{shown} kept: {why}"));
}
if req.dry_run {
return StepLine::ok(
STEP,
format!(
"would remove {shown}{}{landed_note}",
claim_note(&holders, "end ")
),
);
}
let holders = match ownership::regate(claims, ownership, path).await {
Ok(holders) => holders,
Err(why) => {
return StepLine::failed(
STEP,
format!("{shown} kept at the pre-removal check: {why}"),
);
}
};
match recheck::again(git, probe_dirt, path) {
Ok(now) => {
if let Some(changed) = recheck::drift(&before, &now) {
return StepLine::failed(
STEP,
format!(
"{shown} changed while cleanup was checking it ({changed}) — refusing to \
remove it (#7275)"
),
);
}
}
Err(why) => {
return StepLine::failed(
STEP,
format!("{shown} could not be re-read before removal: {why} (#7275)"),
);
}
}
if let Err(why) = ownership.release_stale_lock(path).await {
return StepLine::failed(
STEP,
format!("{shown} kept: its lock was judged again at release: {why}"),
);
}
let marker = match take_worktree_sentinel(path) {
Ok(bytes) => bytes,
Err(e) => {
return StepLine::failed(
STEP,
format!("{shown}: the harness ownership marker could not be cleared: {e} (#7185)"),
);
}
};
for id in &holders {
if let Err(e) = claims.end_claim(id).await {
return StepLine::failed(
STEP,
format!(
"{shown} is claimed by session {id} and the claim could not be ended: \
{e:#}{}",
restore_marker(path, &marker)
),
);
}
}
match run_git(git, req, &["worktree", "remove", &shown]) {
Ok(_) => StepLine::ok(
STEP,
format!(
"removed {shown}{}{landed_note}",
claim_note(&holders, "ended ")
),
),
Err(e) => StepLine::failed(
STEP,
format!(
"{e:#}{}{}",
claim_note(&holders, "ended "),
restore_marker(path, &marker)
),
),
}
}
fn restore_marker(path: &Path, marker: &Option<Vec<u8>>) -> String {
let Some(bytes) = marker.as_deref() else {
return String::new();
};
match restore_worktree_sentinel(path, bytes) {
Ok(()) => String::new(),
Err(e) => format!(
" — and its harness ownership marker could not be restored ({e}), so the tree is \
now unattributed and no sweep will reclaim it (#7185)"
),
}
}
fn claim_note(holders: &[String], verb: &str) -> String {
if holders.is_empty() {
return String::new();
}
format!(" ({verb}claim held by {})", holders.join(", "))
}