Skip to content

Commit c712b1d

Browse files
committed
Better local branch delete confirmation
Currently we try to delete a branch normally, and if git returns an error and its output contains the text "branch -D", then we prompt the user to force delete, and try again using -D. Besides just being ugly, this has the disadvantage that git's logic to decide whether a branch is merged is not very good; it only considers a branch merged if it is either reachable from the current head, or from its own upstream. In many cases I want to delete a branch that has been merged to master, but I don't have master checked out, so the current branch is really irrelevant, and it should rather (or in addition) check whether the branch is reachable from one of the main branches. The problem is that git doesn't know what those are. But lazygit does, so make the check on our side, prompt the user if necessary, and always use -D. This is both cleaner, and works better. See this mailing list discussion for more: https://lore.kernel.org/git/[email protected]/
1 parent c4e5995 commit c712b1d

File tree

5 files changed

+204
-105
lines changed

5 files changed

+204
-105
lines changed

pkg/commands/git_commands/branch.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"strings"
66

7+
"github.com/jesseduffield/lazygit/pkg/commands/models"
78
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
89
"github.com/jesseduffield/lazygit/pkg/utils"
910
"github.com/mgutz/str"
@@ -260,3 +261,26 @@ func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj {
260261

261262
return self.cmd.New(str.ToArgv(candidates[i])).DontLog()
262263
}
264+
265+
func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *MainBranches) (bool, error) {
266+
branchesToCheckAgainst := []string{"HEAD"}
267+
if branch.RemoteBranchStoredLocally() {
268+
branchesToCheckAgainst = append(branchesToCheckAgainst, fmt.Sprintf("%s@{upstream}", branch.Name))
269+
}
270+
branchesToCheckAgainst = append(branchesToCheckAgainst, mainBranches.Get()...)
271+
272+
cmdArgs := NewGitCmd("rev-list").
273+
Arg("--max-count=1").
274+
Arg(branch.Name).
275+
Arg(lo.Map(branchesToCheckAgainst, func(branch string, _ int) string {
276+
return fmt.Sprintf("^%s", branch)
277+
})...).
278+
ToArgv()
279+
280+
stdout, _, err := self.cmd.New(cmdArgs).RunWithOutputs()
281+
if err != nil {
282+
return false, err
283+
}
284+
285+
return stdout == "", nil
286+
}

pkg/gui/controllers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (gui *Gui) resetHelpersAndControllers() {
107107
Files: helpers.NewFilesHelper(helperCommon),
108108
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper),
109109
Tags: helpers.NewTagsHelper(helperCommon, commitsHelper),
110-
BranchesHelper: helpers.NewBranchesHelper(helperCommon),
110+
BranchesHelper: helpers.NewBranchesHelper(helperCommon, worktreeHelper),
111111
GPG: helpers.NewGpgHelper(helperCommon),
112112
MergeAndRebase: rebaseHelper,
113113
MergeConflicts: mergeConflictsHelper,

pkg/gui/controllers/branches_controller.go

Lines changed: 1 addition & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package controllers
33
import (
44
"errors"
55
"fmt"
6-
"strings"
76

87
"github.com/jesseduffield/gocui"
98
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
@@ -521,92 +520,14 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er
521520
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, KeepBranchSelectionIndex: true})
522521
}
523522

524-
func (self *BranchesController) checkedOutByOtherWorktree(branch *models.Branch) bool {
525-
return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees)
526-
}
527-
528-
func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
529-
worktree, ok := self.worktreeForBranch(selectedBranch)
530-
if !ok {
531-
self.c.Log.Error("promptWorktreeBranchDelete out of sync with list of worktrees")
532-
return nil
533-
}
534-
535-
title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{
536-
"worktreeName": worktree.Name,
537-
"branchName": selectedBranch.Name,
538-
})
539-
return self.c.Menu(types.CreateMenuOptions{
540-
Title: title,
541-
Items: []*types.MenuItem{
542-
{
543-
Label: self.c.Tr.SwitchToWorktree,
544-
OnPress: func() error {
545-
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
546-
},
547-
},
548-
{
549-
Label: self.c.Tr.DetachWorktree,
550-
Tooltip: self.c.Tr.DetachWorktreeTooltip,
551-
OnPress: func() error {
552-
return self.c.Helpers().Worktree.Detach(worktree)
553-
},
554-
},
555-
{
556-
Label: self.c.Tr.RemoveWorktree,
557-
OnPress: func() error {
558-
return self.c.Helpers().Worktree.Remove(worktree, false)
559-
},
560-
},
561-
},
562-
})
563-
}
564-
565523
func (self *BranchesController) localDelete(branch *models.Branch) error {
566-
if self.checkedOutByOtherWorktree(branch) {
567-
return self.promptWorktreeBranchDelete(branch)
568-
}
569-
570-
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error {
571-
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
572-
err := self.c.Git().Branch.LocalDelete(branch.Name, false)
573-
if err != nil && strings.Contains(err.Error(), "git branch -D ") {
574-
return self.forceDelete(branch)
575-
}
576-
if err != nil {
577-
return err
578-
}
579-
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
580-
})
524+
return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branch)
581525
}
582526

583527
func (self *BranchesController) remoteDelete(branch *models.Branch) error {
584528
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.UpstreamBranch)
585529
}
586530

587-
func (self *BranchesController) forceDelete(branch *models.Branch) error {
588-
title := self.c.Tr.ForceDeleteBranchTitle
589-
message := utils.ResolvePlaceholderString(
590-
self.c.Tr.ForceDeleteBranchMessage,
591-
map[string]string{
592-
"selectedBranchName": branch.Name,
593-
},
594-
)
595-
596-
self.c.Confirm(types.ConfirmOpts{
597-
Title: title,
598-
Prompt: message,
599-
HandleConfirm: func() error {
600-
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
601-
return err
602-
}
603-
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
604-
},
605-
})
606-
607-
return nil
608-
}
609-
610531
func (self *BranchesController) delete(branch *models.Branch) error {
611532
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
612533

pkg/gui/controllers/helpers/branches_helper.go

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,68 @@ import (
44
"strings"
55

66
"github.com/jesseduffield/gocui"
7+
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
8+
"github.com/jesseduffield/lazygit/pkg/commands/models"
9+
"github.com/jesseduffield/lazygit/pkg/gui/context"
710
"github.com/jesseduffield/lazygit/pkg/gui/types"
811
"github.com/jesseduffield/lazygit/pkg/utils"
912
)
1013

1114
type BranchesHelper struct {
12-
c *HelperCommon
15+
c *HelperCommon
16+
worktreeHelper *WorktreeHelper
1317
}
1418

15-
func NewBranchesHelper(c *HelperCommon) *BranchesHelper {
19+
func NewBranchesHelper(c *HelperCommon, worktreeHelper *WorktreeHelper) *BranchesHelper {
1620
return &BranchesHelper{
17-
c: c,
21+
c: c,
22+
worktreeHelper: worktreeHelper,
1823
}
1924
}
2025

26+
func (self *BranchesHelper) ConfirmLocalDelete(branch *models.Branch) error {
27+
if self.checkedOutByOtherWorktree(branch) {
28+
return self.promptWorktreeBranchDelete(branch)
29+
}
30+
31+
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
32+
if err != nil {
33+
return err
34+
}
35+
36+
doDelete := func() error {
37+
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error {
38+
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
39+
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
40+
return err
41+
}
42+
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
43+
})
44+
}
45+
46+
if isMerged {
47+
return doDelete()
48+
}
49+
50+
title := self.c.Tr.ForceDeleteBranchTitle
51+
message := utils.ResolvePlaceholderString(
52+
self.c.Tr.ForceDeleteBranchMessage,
53+
map[string]string{
54+
"selectedBranchName": branch.Name,
55+
},
56+
)
57+
58+
self.c.Confirm(types.ConfirmOpts{
59+
Title: title,
60+
Prompt: message,
61+
HandleConfirm: func() error {
62+
return doDelete()
63+
},
64+
})
65+
66+
return nil
67+
}
68+
2169
func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName string) error {
2270
title := utils.ResolvePlaceholderString(
2371
self.c.Tr.DeleteBranchTitle,
@@ -52,3 +100,48 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName st
52100
func ShortBranchName(fullBranchName string) string {
53101
return strings.TrimPrefix(strings.TrimPrefix(fullBranchName, "refs/heads/"), "refs/remotes/")
54102
}
103+
104+
func (self *BranchesHelper) checkedOutByOtherWorktree(branch *models.Branch) bool {
105+
return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees)
106+
}
107+
108+
func (self *BranchesHelper) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
109+
return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees)
110+
}
111+
112+
func (self *BranchesHelper) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
113+
worktree, ok := self.worktreeForBranch(selectedBranch)
114+
if !ok {
115+
self.c.Log.Error("promptWorktreeBranchDelete out of sync with list of worktrees")
116+
return nil
117+
}
118+
119+
title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{
120+
"worktreeName": worktree.Name,
121+
"branchName": selectedBranch.Name,
122+
})
123+
return self.c.Menu(types.CreateMenuOptions{
124+
Title: title,
125+
Items: []*types.MenuItem{
126+
{
127+
Label: self.c.Tr.SwitchToWorktree,
128+
OnPress: func() error {
129+
return self.worktreeHelper.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
130+
},
131+
},
132+
{
133+
Label: self.c.Tr.DetachWorktree,
134+
Tooltip: self.c.Tr.DetachWorktreeTooltip,
135+
OnPress: func() error {
136+
return self.worktreeHelper.Detach(worktree)
137+
},
138+
},
139+
{
140+
Label: self.c.Tr.RemoveWorktree,
141+
OnPress: func() error {
142+
return self.worktreeHelper.Remove(worktree, false)
143+
},
144+
},
145+
},
146+
})
147+
}

0 commit comments

Comments
 (0)