Let worktree removal/detach chain follow-up work

Split the actual worktree removal out of the confirmation in Remove into a
non-confirming helper, and give both Remove and Detach an optional `then`
continuation that runs after a successful removal in place of the default
refresh. Upcoming flows need to delete the worktree's branch once the worktree
is out of the way; threading a continuation through (rather than the caller
firing branch deletion independently) keeps it ordered after the git command
that actually frees the branch. No behavior change yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-06-30 18:57:21 +02:00
parent d6016d6286
commit 9a8244110f
3 changed files with 58 additions and 32 deletions

View file

@ -184,13 +184,13 @@ func (self *BranchesHelper) promptWorktreeBranchDelete(selectedBranch *models.Br
Label: self.c.Tr.DetachWorktree,
Tooltip: self.c.Tr.DetachWorktreeTooltip,
OnPress: func() error {
return self.worktreeHelper.Detach(worktree)
return self.worktreeHelper.Detach(worktree, nil)
},
},
{
Label: self.c.Tr.RemoveWorktree,
OnPress: func() error {
return self.worktreeHelper.Remove(worktree, false)
return self.worktreeHelper.Remove(worktree)
},
},
},

View file

@ -119,56 +119,82 @@ func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.C
return self.reposHelper.DispatchSwitchTo(worktree.Path, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
}
func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error {
title := self.c.Tr.RemoveWorktreeTitle
var templateStr string
if force {
templateStr = self.c.Tr.ForceRemoveWorktreePrompt
} else {
templateStr = self.c.Tr.RemoveWorktreePrompt
}
func (self *WorktreeHelper) Remove(worktree *models.Worktree) error {
message := utils.ResolvePlaceholderString(
templateStr,
self.c.Tr.RemoveWorktreePrompt,
map[string]string{
"worktreeName": worktree.Name,
},
)
self.c.Confirm(types.ConfirmOpts{
Title: title,
Title: self.c.Tr.RemoveWorktreeTitle,
Prompt: message,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.RemoveWorktree)
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
errMessage := err.Error()
if !strings.Contains(errMessage, "--force") &&
!strings.Contains(errMessage, "fatal: working trees containing submodules cannot be moved or removed") {
return err
}
if !force {
return self.Remove(worktree, true)
}
return err
}
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
return nil
})
return self.remove(worktree, false, nil)
},
})
return nil
}
func (self *WorktreeHelper) Detach(worktree *models.Worktree) error {
return self.c.WithWaitingStatus(self.c.Tr.DetachingWorktree, func(gocui.Task) error {
// remove deletes the worktree without confirming first; callers must have done
// so (or shown a menu) already. If git refuses because the worktree is dirty or
// contains submodules, we ask for confirmation and retry with --force. When then
// is non-nil it runs in place of the default refresh after a successful removal,
// letting callers chain further work such as deleting the worktree's branch.
func (self *WorktreeHelper) remove(worktree *models.Worktree, force bool, then func(gocui.Task) error) error {
return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(task gocui.Task) error {
self.c.LogAction(self.c.Tr.RemoveWorktree)
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
errMessage := err.Error()
if !strings.Contains(errMessage, "--force") &&
!strings.Contains(errMessage, "fatal: working trees containing submodules cannot be moved or removed") {
return err
}
if force {
return err
}
message := utils.ResolvePlaceholderString(
self.c.Tr.ForceRemoveWorktreePrompt,
map[string]string{
"worktreeName": worktree.Name,
},
)
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.RemoveWorktreeTitle,
Prompt: message,
HandleConfirm: func() error {
return self.remove(worktree, true, then)
},
})
return nil
}
if then != nil {
return then(task)
}
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
return nil
})
}
func (self *WorktreeHelper) Detach(worktree *models.Worktree, then func(gocui.Task) error) error {
return self.c.WithWaitingStatus(self.c.Tr.DetachingWorktree, func(task gocui.Task) error {
self.c.LogAction(self.c.Tr.RemovingWorktree)
err := self.c.Git().Worktree.Detach(worktree.Path)
if err != nil {
return err
}
if then != nil {
return then(task)
}
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
return nil
})

View file

@ -130,7 +130,7 @@ func (self *WorktreesController) remove(worktree *models.Worktree) error {
return errors.New(self.c.Tr.CantDeleteCurrentWorktree)
}
return self.c.Helpers().Worktree.Remove(worktree, false)
return self.c.Helpers().Worktree.Remove(worktree)
}
func (self *WorktreesController) GetOnDoubleClick() func() error {