fix(worktrees): a worktree whose folder was deleted can still be removed

When a managed worktree's folder is deleted by hand (a user in a file
manager, an agent's rm -rf), git keeps its record and lists it as
prunable. Codeman then had no way out: the dialog showed the row as
"missing" with no action, DELETE answered "No worktree named ...", and
the name could never be used again (the branch still existed, and with
another branch git refused "a missing but already registered
worktree"). Reproduced on git 2.43; git's own `git worktree remove` on
the missing path drops the record with exit 0.

Removal (only removal, never a session start) now accepts a worktree
whose folder is gone when git lists it at exactly the managed path, as
prunable or locked, and no component on the way is a symlink or a file.
The live-session and lock refusals still apply. The dialog offers
Remove... (not Open) on such a row, with a note saying what it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-14 05:34:49 +02:00
1 parent 0ce6e09725
commit 64fea4ff84
10 files changed
+150 -18

No files matched your search

+2
View File
@@ -297,6 +297,8 @@
'Remove…': '移除…',
'git worktree remove refuses while the worktree has uncommitted or untracked files. Codeman never forces it.':
'工作树中有未提交或未跟踪的文件时,git worktree remove 会拒绝执行。{name} 从不强制移除。',
'Its folder is gone. Removing it clears the record git keeps of it, so the name can be used again.':
'它的文件夹已不存在。移除它会清除 git 保存的记录,这个名称便可再次使用。',
'Remove worktree': '移除工作树',
'Kill & remove worktree': '终止并移除工作树',
'git refuses while the worktree has uncommitted or untracked files; nothing is forced':
+16 -10
View File
@@ -5229,21 +5229,26 @@ Object.assign(CodemanApp.prototype, {
badge(`${wt.sessionCount} session${wt.sessionCount === 1 ? '' : 's'}`, 'worktree-badge-live');
}
if (wt.managed && !wt.prunable) {
if (wt.managed) {
const actions = document.createElement('span');
actions.className = 'worktree-row-actions';
const open = document.createElement('button');
open.type = 'button';
open.className = 'wt-btn wt-btn-sm';
open.textContent = 'Open';
open.title = 'Start a session in this worktree';
open.addEventListener('click', () => void this.openWorktreeSession(wt.name));
// A worktree whose folder is gone (`prunable`) cannot host a session, but Remove still
// clears git's record of it, which is what frees its name for a new worktree.
if (!wt.prunable) {
const open = document.createElement('button');
open.type = 'button';
open.className = 'wt-btn wt-btn-sm';
open.textContent = 'Open';
open.title = 'Start a session in this worktree';
open.addEventListener('click', () => void this.openWorktreeSession(wt.name));
actions.appendChild(open);
}
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'wt-btn wt-btn-sm wt-btn-danger';
remove.textContent = 'Remove…';
remove.addEventListener('click', () => this.confirmWorktreeRemoval(wt.name));
actions.append(open, remove);
actions.appendChild(remove);
head.appendChild(actions);
}
row.appendChild(head);
@@ -5266,8 +5271,9 @@ Object.assign(CodemanApp.prototype, {
box.className = 'worktree-remove-confirm';
const note = document.createElement('p');
note.className = 'worktree-remove-note';
note.textContent =
'git worktree remove refuses while the worktree has uncommitted or untracked files. Codeman never forces it.';
note.textContent = wt.prunable
? 'Its folder is gone. Removing it clears the record git keeps of it, so the name can be used again.'
: 'git worktree remove refuses while the worktree has uncommitted or untracked files. Codeman never forces it.';
box.appendChild(note);
let checkbox = null;