1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use anyhow::{Result, anyhow};
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;
/// Remove a worktree without merging
pub fn remove(
handle: &str,
force: bool,
keep_branch: bool,
context: &WorkflowContext,
) -> Result<RemoveResult> {
info!(handle = handle, force, keep_branch, "remove:start");
// Get worktree path and branch - this also validates that the worktree exists
// Smart resolution: try handle first, then branch name
let (worktree_path, branch_name) = git::find_worktree(handle).map_err(|_| {
anyhow!(
"Worktree '{}' not found. Use 'workmux list' to see available worktrees.",
handle
)
})?;
// Extract actual handle from worktree path (directory name)
// User may have provided branch name (with slashes) but window names use handle (with dashes)
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");
// Capture mode BEFORE cleanup (cleanup removes the metadata)
let mode = get_worktree_mode(actual_handle);
// Safety Check: Prevent deleting the main worktree itself, regardless of branch.
let is_main_worktree = match (
worktree_path.canonicalize(),
context.main_worktree_root.canonicalize(),
) {
(Ok(canon_wt_path), Ok(canon_main_path)) => {
// Best case: both paths exist and can be resolved. This is the most reliable check.
canon_wt_path == canon_main_path
}
_ => {
// Fallback: If canonicalization fails on either path (e.g., directory was
// manually removed, broken symlink), compare the raw paths provided by git.
// This is a critical safety net.
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
));
}
// Safety Check: Prevent deleting the main branch by name (secondary check)
if branch_name == context.main_branch {
return Err(anyhow!(
"Cannot delete the main branch ('{}')",
context.main_branch
));
}
if worktree_path.exists() && git::has_uncommitted_changes(&worktree_path)? && !force {
return Err(anyhow!(
"Worktree has uncommitted changes. Use --force to delete anyway."
));
}
// Note: Unmerged branch check removed - git branch -d/D handles this natively
// The CLI provides a user-friendly confirmation prompt before calling this function
// Stop any running containers for this worktree before killing the window.
// This is necessary because tmux kill-window sends SIGHUP which doesn't allow
// the supervisor's Drop handler to run. We try unconditionally since sandbox
// may have been enabled via --sandbox flag even if disabled in config.
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, // no_hooks: run hooks normally for user-initiated remove
)?;
// Navigate to the main branch window/session and close the source
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(),
})
}