use anyhow::{Result, anyhow};
use std::path::PathBuf;
use crate::git;
use crate::sandbox;
use tracing::{debug, info};
use super::cleanup::{self, get_worktree_mode};
use super::context::WorkflowContext;
use super::types::RemoveResult;
pub fn fallback_worktree_path(handle: &str, context: &WorkflowContext) -> Result<Option<PathBuf>> {
let base_dir = if let Some(ref worktree_dir) = context.config.worktree_dir {
crate::util::expand_worktree_dir(worktree_dir, &context.main_worktree_root)?
} else {
let project_name = context
.main_worktree_root
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow!("Could not determine project name"))?;
context
.main_worktree_root
.parent()
.ok_or_else(|| anyhow!("Could not determine parent directory"))?
.join(format!("{}__worktrees", project_name))
};
let path = base_dir.join(handle);
let Some(admin_dir) = git::linked_worktree_admin_dir(&path) else {
return Ok(None);
};
let expected_parent = context.git_common_dir.join("worktrees");
Ok((!admin_dir.is_dir() && admin_dir.starts_with(expected_parent)).then_some(path))
}
pub fn remove(
handle: &str,
force: bool,
keep_branch: bool,
context: &WorkflowContext,
) -> Result<RemoveResult> {
info!(handle = handle, force, keep_branch, "remove:start");
let (worktree_path, branch_name) = match git::find_worktree(handle) {
Ok(worktree) => worktree,
Err(e) => {
if let Some(path) = fallback_worktree_path(handle, context)? {
(path, String::new())
} else {
return Err(anyhow!(
"Worktree '{}' not found. Use 'workmux list' to see available worktrees.",
handle
)
.context(e));
}
}
};
let actual_handle = worktree_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| {
anyhow!(
"Could not derive handle from worktree path: {}",
worktree_path.display()
)
})?;
debug!(handle = actual_handle, branch = branch_name, path = %worktree_path.display(), "remove:worktree resolved");
let mode = get_worktree_mode(actual_handle);
let is_main_worktree = match (
worktree_path.canonicalize(),
context.main_worktree_root.canonicalize(),
) {
(Ok(canon_wt_path), Ok(canon_main_path)) => {
canon_wt_path == canon_main_path
}
_ => {
worktree_path == context.main_worktree_root
}
};
if is_main_worktree {
return Err(anyhow!(
"Cannot remove branch '{}' because it is checked out in the main worktree at '{}'. \
Switch the main worktree to a different branch first, or create a linked worktree for '{}'.",
branch_name,
context.main_worktree_root.display(),
branch_name
));
}
if branch_name.is_empty() && !keep_branch {
return Err(anyhow!(
"Worktree '{}' has broken Git metadata, so its branch cannot be determined. \
Use --keep-branch to remove only the worktree directory.",
actual_handle
));
}
if branch_name == context.main_branch {
return Err(anyhow!(
"Cannot delete the main branch ('{}')",
context.main_branch
));
}
if worktree_path.exists()
&& !git::has_missing_admin_dir(&worktree_path)
&& git::has_uncommitted_changes(&worktree_path)?
&& !force
{
return Err(anyhow!(
"Worktree has uncommitted changes. Use --force to delete anyway."
));
}
sandbox::stop_containers_for_handle(actual_handle);
info!(branch = %branch_name, keep_branch, "remove:cleanup start");
let cleanup_result = cleanup::cleanup(
context,
&branch_name,
actual_handle,
&worktree_path,
force,
keep_branch,
false, )?;
cleanup::navigate_to_target_and_close(
context.mux.as_ref(),
&context.prefix,
&context.main_branch,
actual_handle,
&cleanup_result,
mode,
)?;
Ok(RemoveResult {
branch_removed: branch_name.to_string(),
})
}