Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/content/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ For the full algorithm, see [Branch cleanup](@/remove.md#branch-cleanup) — it

Use `-D` to force-delete branches with unmerged changes. Use `--no-delete-branch` to keep the branch regardless of status.

A branch checked out in a second worktree is retained regardless, `-D` included. Deleting it would leave that worktree unable to resolve `HEAD`; only `git worktree add --force` produces that state.

### Other cleanup

- `wt remove` — besides the target worktree, two cleanup mechanisms run. The removed worktree's own `git fsmonitor--daemon` (git's per-worktree filesystem watcher under `core.fsmonitor=true`, which would leak once its worktree is gone) is sent `git fsmonitor--daemon stop`, then force-terminated (`SIGTERM`, then `SIGKILL`) via the PID resolved from its IPC socket if it didn't exit. A background sweep then deletes `.git/wt/trash/` entries older than 24 hours (directories orphaned when a previous background removal was interrupted) and terminates fsmonitor daemons whose worktree no longer exists (orphans from `git worktree remove`, `rm -rf`, or a crashed `wt`)
Expand Down
2 changes: 2 additions & 0 deletions docs/content/remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ The 'same commit' check uses the local default branch; for other checks, 'target

Branches matching these conditions and with empty working trees are dimmed in `wt list` as safe to delete.

Those six ask whether deleting loses work. A branch checked out in a second worktree (only reachable via `git worktree add --force`) fails a different test: deleting the ref would leave that worktree unable to resolve `HEAD`, which is why `git branch -d` refuses the same delete. Such a branch is retained whatever `-D` asks, and the surviving checkout is named.

## Force flags

Worktrunk has two force flags for different situations:
Expand Down
2 changes: 2 additions & 0 deletions plugins/worktrunk/skills/worktrunk/reference/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ For the full algorithm, see [Branch cleanup](https://worktrunk.dev/remove/#branc

Use `-D` to force-delete branches with unmerged changes. Use `--no-delete-branch` to keep the branch regardless of status.

A branch checked out in a second worktree is retained regardless, `-D` included. Deleting it would leave that worktree unable to resolve `HEAD`; only `git worktree add --force` produces that state.

### Other cleanup

- `wt remove` — besides the target worktree, two cleanup mechanisms run. The removed worktree's own `git fsmonitor--daemon` (git's per-worktree filesystem watcher under `core.fsmonitor=true`, which would leak once its worktree is gone) is sent `git fsmonitor--daemon stop`, then force-terminated (`SIGTERM`, then `SIGKILL`) via the PID resolved from its IPC socket if it didn't exit. A background sweep then deletes `.git/wt/trash/` entries older than 24 hours (directories orphaned when a previous background removal was interrupted) and terminates fsmonitor daemons whose worktree no longer exists (orphans from `git worktree remove`, `rm -rf`, or a crashed `wt`)
Expand Down
2 changes: 2 additions & 0 deletions plugins/worktrunk/skills/worktrunk/reference/remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ The 'same commit' check uses the local default branch; for other checks, 'target

Branches matching these conditions and with empty working trees are dimmed in `wt list` as safe to delete.

Those six ask whether deleting loses work. A branch checked out in a second worktree (only reachable via `git worktree add --force`) fails a different test: deleting the ref would leave that worktree unable to resolve `HEAD`, which is why `git branch -d` refuses the same delete. Such a branch is retained whatever `-D` asks, and the surviving checkout is named.

## Force flags

Worktrunk has two force flags for different situations:
Expand Down
2 changes: 2 additions & 0 deletions skills/worktrunk/reference/faq.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions skills/worktrunk/reference/remove.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,8 @@ The 'same commit' check uses the local default branch; for other checks, 'target

Branches matching these conditions and with empty working trees are dimmed in `wt list` as safe to delete.

Those six ask whether deleting loses work. A branch checked out in a second worktree (only reachable via `git worktree add --force`) fails a different test: deleting the ref would leave that worktree unable to resolve `HEAD`, which is why `git branch -d` refuses the same delete. Such a branch is retained whatever `-D` asks, and the surviving checkout is named.

## Force flags

Worktrunk has two force flags for different situations:
Expand Down
16 changes: 16 additions & 0 deletions src/commands/picker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2803,6 +2803,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};

AltXRemover::do_removal(&repo, &result, &Approvals::default()).unwrap();
Expand All @@ -2826,6 +2827,7 @@ pub mod tests {
pruned: false,
target_branch: None,
integration_reason: None,
branch_checked_out_at: None,
};
AltXRemover::do_removal(&repo, &result, &Approvals::default()).unwrap();

Expand All @@ -2852,6 +2854,7 @@ pub mod tests {
pruned: false,
target_branch: None,
integration_reason: None,
branch_checked_out_at: None,
};
AltXRemover::do_removal(&repo, &result, &Approvals::default()).unwrap();

Expand Down Expand Up @@ -2898,6 +2901,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};

AltXRemover::do_removal(&repo, &result, &Approvals::default()).unwrap();
Expand Down Expand Up @@ -2989,6 +2993,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};

// Empty approvals → `approve_readonly` drops the unapproved project
Expand Down Expand Up @@ -3676,6 +3681,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};
assert_eq!(
super::removal_failure_subject(&branched),
Expand All @@ -3692,6 +3698,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};
assert_eq!(
super::removal_failure_subject(&detached),
Expand All @@ -3704,6 +3711,7 @@ pub mod tests {
pruned: false,
target_branch: None,
integration_reason: None,
branch_checked_out_at: None,
};
assert_eq!(
super::removal_failure_subject(&branch_only),
Expand Down Expand Up @@ -4040,6 +4048,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};

assert_eq!(
Expand Down Expand Up @@ -4138,6 +4147,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};
assert!(super::removal_target_still_present(
&repo,
Expand All @@ -4155,6 +4165,7 @@ pub mod tests {
pruned: false,
target_branch: None,
integration_reason: None,
branch_checked_out_at: None,
};
assert!(super::removal_target_still_present(&repo, &present_branch));

Expand All @@ -4164,6 +4175,7 @@ pub mod tests {
pruned: false,
target_branch: None,
integration_reason: None,
branch_checked_out_at: None,
};
assert!(!super::removal_target_still_present(&repo, &gone_branch));
}
Expand All @@ -4184,6 +4196,7 @@ pub mod tests {
pruned: false,
target_branch: None,
integration_reason: integration,
branch_checked_out_at: None,
}
};

Expand All @@ -4197,6 +4210,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};
assert!(
super::removal_will_remove_target(&worktree),
Expand Down Expand Up @@ -4243,6 +4257,7 @@ pub mod tests {
integration_reason: None,
force_worktree: false,
removed_commit: None,
branch_checked_out_at: None,
};
assert!(
super::removal_targets_current_worktree(&worktree(true)),
Expand All @@ -4259,6 +4274,7 @@ pub mod tests {
pruned: false,
target_branch: None,
integration_reason: None,
branch_checked_out_at: None,
}),
"a branch-only row has no worktree to be standing in"
);
Expand Down
17 changes: 9 additions & 8 deletions src/commands/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fn validate_remove_targets(
};

match resolved {
ResolvedWorktree::Worktree { path, branch } => {
ResolvedWorktree::Worktree { path, branch: _ } => {
// Use canonical paths to avoid symlink/normalization mismatches
let path_canonical = dunce::canonicalize(&path).unwrap_or(path);
let is_current = current_worktree.as_ref() == Some(&path_canonical);
Expand All @@ -121,13 +121,14 @@ fn validate_remove_targets(
continue;
}

// Non-current worktree: remove by branch name, or by path for
// detached worktrees (which have no branch).
let target = if let Some(ref branch_name) = branch {
RemoveTarget::Branch(branch_name)
} else {
RemoveTarget::Path(&path_canonical)
};
// Remove exactly the resolved worktree by its path. Targeting by
// branch name would resolve an ambiguous branch (one checked out
// in several worktrees via `git worktree add --force`) back to
// git's first-listed worktree — removing the wrong one — whereas
// the path is unambiguous. `prepare_worktree_removal` still
// deletes the branch when it's the sole checkout, and retains it
// otherwise (see its shared-branch handling).
let target = RemoveTarget::Path(&path_canonical);
match repo.prepare_worktree_removal(
target,
deletion_mode,
Expand Down
Loading
Loading