Delete the branch when deleting it via its worktree

When you delete a local branch that's checked out in another worktree, the
menu offered to remove or detach the worktree but then stopped there, leaving
the branch you asked to delete still around. Now both actions delete the branch
afterwards, and the labels say so ("Remove worktree and delete branch" /
"Detach worktree and delete branch") to avoid surprises.

Also drop the "Switch to worktree" item: switching abandons the delete the user
asked for, and it's already reachable by checking out the branch or via the
worktrees panel. And drop the now-redundant "remove worktree?" confirmation:
the explicit menu pick is the confirmation (the dirty-worktree force prompt and
the unmerged-branch warning still appear when relevant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-07-03 19:04:44 +02:00
co-authored by Claude Opus 4.8
parent 9a8244110f
commit 4f078f5463
4 changed files with 59 additions and 28 deletions
+46 -15
View File
@@ -8,7 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
@@ -32,7 +31,12 @@ func (self *BranchesHelper) ConfirmLocalDelete(branches []*models.Branch) error
return errors.New(self.c.Tr.SomeBranchesCheckedOutByWorktreeError)
}
} else if self.checkedOutByOtherWorktree(branches[0]) {
return self.promptWorktreeBranchDelete(branches[0])
return self.promptWorktreeBranchDelete(
branches[0],
self.c.Tr.RemoveWorktreeAndDeleteBranch,
self.c.Tr.DetachWorktreeAndDeleteBranch,
self.deleteLocalBranchesContinuation(branches),
)
}
return self.confirmForceIfUnmerged(branches, func() error {
@@ -160,8 +164,18 @@ func (self *BranchesHelper) worktreeForBranch(branch *models.Branch) (*models.Wo
return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees)
}
func (self *BranchesHelper) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
worktree, ok := self.worktreeForBranch(selectedBranch)
// promptWorktreeBranchDelete handles deleting a branch that's checked out by
// another worktree: the worktree has to be removed or detached first to free the
// branch, so we offer both as menu items. Either way the branch is deleted
// afterwards (that's what the user asked for), via deleteBranches, which knows
// whether to delete just the local branch or the remote one too.
func (self *BranchesHelper) promptWorktreeBranchDelete(
branch *models.Branch,
removeLabel string,
detachLabel string,
deleteBranches func(gocui.Task) error,
) error {
worktree, ok := self.worktreeForBranch(branch)
if !ok {
self.c.Log.Error("promptWorktreeBranchDelete out of sync with list of worktrees")
return nil
@@ -169,28 +183,28 @@ func (self *BranchesHelper) promptWorktreeBranchDelete(selectedBranch *models.Br
title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{
"worktreeName": worktree.Name,
"branchName": selectedBranch.Name,
"branchName": branch.Name,
})
return self.c.Menu(types.CreateMenuOptions{
Title: title,
Items: []*types.MenuItem{
{
Label: self.c.Tr.SwitchToWorktree,
Label: removeLabel,
Keys: menuKey('r'),
OnPress: func() error {
return self.worktreeHelper.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
return self.confirmForceIfUnmerged([]*models.Branch{branch}, func() error {
return self.worktreeHelper.remove(worktree, false, deleteBranches)
})
},
},
{
Label: self.c.Tr.DetachWorktree,
Label: detachLabel,
Keys: menuKey('d'),
Tooltip: self.c.Tr.DetachWorktreeTooltip,
OnPress: func() error {
return self.worktreeHelper.Detach(worktree, nil)
},
},
{
Label: self.c.Tr.RemoveWorktree,
OnPress: func() error {
return self.worktreeHelper.Remove(worktree)
return self.confirmForceIfUnmerged([]*models.Branch{branch}, func() error {
return self.worktreeHelper.Detach(worktree, deleteBranches)
})
},
},
},
@@ -250,6 +264,23 @@ func (self *BranchesHelper) doDeleteLocalAndRemoteBranches(task gocui.Task, bran
return self.doDeleteLocalBranches(branches)
}
// deleteLocalBranchesContinuation returns a worktree-removal continuation that
// deletes the local branches and refreshes once the worktree is out of the way.
func (self *BranchesHelper) deleteLocalBranchesContinuation(branches []*models.Branch) func(gocui.Task) error {
return func(gocui.Task) error {
if err := self.doDeleteLocalBranches(branches); err != nil {
return err
}
self.c.Contexts().Branches.CollapseRangeSelectionToTop()
self.c.Refresh(types.RefreshOptions{
Mode: types.ASYNC,
Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES},
})
return nil
}
}
func (self *BranchesHelper) allBranchesMerged(branches []*models.Branch) (bool, error) {
allBranchesMerged := true
for _, branch := range branches {
+4
View File
@@ -874,7 +874,9 @@ type TranslationSet struct {
Switching string
RemoveWorktree string
RemoveWorktreeTitle string
RemoveWorktreeAndDeleteBranch string
DetachWorktree string
DetachWorktreeAndDeleteBranch string
DetachingWorktree string
WorktreesTitle string
WorktreeTitle string
@@ -2018,10 +2020,12 @@ func EnglishTranslationSet() *TranslationSet {
Switching: "Switching",
RemoveWorktree: "Remove worktree",
RemoveWorktreeTitle: "Remove worktree",
RemoveWorktreeAndDeleteBranch: "Remove worktree and delete branch",
RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?",
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?",
RemovingWorktree: "Deleting worktree",
DetachWorktree: "Detach worktree",
DetachWorktreeAndDeleteBranch: "Detach worktree and delete branch",
DetachingWorktree: "Detaching worktree",
AddingWorktree: "Adding worktree",
CantDeleteCurrentWorktree: "You cannot remove the current worktree!",
@@ -6,7 +6,7 @@ import (
)
var DetachWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Detach a worktree from the branches view",
Description: "Delete a branch that's checked out in another worktree by detaching that worktree",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
@@ -37,12 +37,12 @@ var DetachWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Branch newbranch is checked out by worktree linked-worktree")).
Select(Equals("Detach worktree")).
Select(Contains("Detach worktree and delete branch")).
Confirm()
}).
// The branch is gone; the worktree stays around (now detached)
Lines(
Contains("mybranch"),
Contains("newbranch").DoesNotContain("(worktree)").IsSelected(),
Contains("mybranch").IsSelected(),
)
t.Views().Worktrees().
@@ -6,7 +6,7 @@ import (
)
var RemoveWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Remove a worktree from the branches view",
Description: "Delete a branch that's checked out in another worktree by removing that worktree",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
@@ -38,22 +38,18 @@ var RemoveWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Branch newbranch is checked out by worktree linked-worktree")).
Select(Equals("Remove worktree")).
Confirm()
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Equals("Are you sure you want to remove worktree 'linked-worktree'?")).
Select(Contains("Remove worktree and delete branch")).
Confirm()
// The worktree is dirty, so we get asked to force-remove it
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Equals("'linked-worktree' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?")).
Confirm()
}).
// The branch is gone, not just unlinked from its worktree
Lines(
Contains("mybranch"),
Contains("newbranch").DoesNotContain("(worktree)").IsSelected(),
Contains("mybranch").IsSelected(),
)
t.Views().Worktrees().