Skip to content

Commit e181de1

Browse files
authored
Better branch delete confirmation (jesseduffield#3915)
- **PR Description** When deleting a local branch, put up the "This branch is not fully merged, do you want to force delete it" confirmation only when the branch is not merged into any of the main branches. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [x] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [ ] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [ ] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
2 parents 8a328a5 + c712b1d commit e181de1

File tree

8 files changed

+257
-113
lines changed

8 files changed

+257
-113
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: 2 additions & 82 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,91 +520,12 @@ 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-
// TODO: i18n
536-
title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{
537-
"worktreeName": worktree.Name,
538-
"branchName": selectedBranch.Name,
539-
})
540-
return self.c.Menu(types.CreateMenuOptions{
541-
Title: title,
542-
Items: []*types.MenuItem{
543-
{
544-
Label: self.c.Tr.SwitchToWorktree,
545-
OnPress: func() error {
546-
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
547-
},
548-
},
549-
{
550-
Label: self.c.Tr.DetachWorktree,
551-
Tooltip: self.c.Tr.DetachWorktreeTooltip,
552-
OnPress: func() error {
553-
return self.c.Helpers().Worktree.Detach(worktree)
554-
},
555-
},
556-
{
557-
Label: self.c.Tr.RemoveWorktree,
558-
OnPress: func() error {
559-
return self.c.Helpers().Worktree.Remove(worktree, false)
560-
},
561-
},
562-
},
563-
})
564-
}
565-
566523
func (self *BranchesController) localDelete(branch *models.Branch) error {
567-
if self.checkedOutByOtherWorktree(branch) {
568-
return self.promptWorktreeBranchDelete(branch)
569-
}
570-
571-
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error {
572-
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
573-
err := self.c.Git().Branch.LocalDelete(branch.Name, false)
574-
if err != nil && strings.Contains(err.Error(), "git branch -D ") {
575-
return self.forceDelete(branch)
576-
}
577-
if err != nil {
578-
return err
579-
}
580-
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
581-
})
524+
return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branch)
582525
}
583526

584527
func (self *BranchesController) remoteDelete(branch *models.Branch) error {
585-
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.Name)
586-
}
587-
588-
func (self *BranchesController) forceDelete(branch *models.Branch) error {
589-
title := self.c.Tr.ForceDeleteBranchTitle
590-
message := utils.ResolvePlaceholderString(
591-
self.c.Tr.ForceDeleteBranchMessage,
592-
map[string]string{
593-
"selectedBranchName": branch.Name,
594-
},
595-
)
596-
597-
self.c.Confirm(types.ConfirmOpts{
598-
Title: title,
599-
Prompt: message,
600-
HandleConfirm: func() error {
601-
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
602-
return err
603-
}
604-
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
605-
},
606-
})
607-
608-
return nil
528+
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.UpstreamBranch)
609529
}
610530

611531
func (self *BranchesController) delete(branch *models.Branch) error {

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+
}

pkg/i18n/english.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,6 @@ type TranslationSet struct {
472472
RemoveRemoteTooltip string
473473
RemoveRemotePrompt string
474474
DeleteRemoteBranch string
475-
DeleteRemoteBranchMessage string
476475
DeleteRemoteBranchTooltip string
477476
SetAsUpstream string
478477
SetAsUpstreamTooltip string
@@ -849,7 +848,6 @@ type Actions struct {
849848
CheckoutBranch string
850849
ForceCheckoutBranch string
851850
DeleteLocalBranch string
852-
DeleteBranch string
853851
Merge string
854852
SquashMerge string
855853
RebaseBranch string
@@ -1461,9 +1459,8 @@ func EnglishTranslationSet() *TranslationSet {
14611459
EditRemoteUrl: `Enter updated remote url for {{.remoteName}}:`,
14621460
RemoveRemote: `Remove remote`,
14631461
RemoveRemoteTooltip: `Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected.`,
1464-
RemoveRemotePrompt: "Are you sure you want to remove remote",
1462+
RemoveRemotePrompt: "Are you sure you want to remove remote?",
14651463
DeleteRemoteBranch: "Delete remote branch",
1466-
DeleteRemoteBranchMessage: "Are you sure you want to delete remote branch",
14671464
DeleteRemoteBranchTooltip: "Delete the remote branch from the remote.",
14681465
SetAsUpstream: "Set as upstream",
14691466
SetAsUpstreamTooltip: "Set the selected remote branch as the upstream of the checked-out branch.",
@@ -1480,7 +1477,7 @@ func EnglishTranslationSet() *TranslationSet {
14801477
ViewUpstreamRebaseOptionsTooltip: "View options for rebasing the checked-out branch onto {{upstream}}. Note: this will not rebase the selected branch onto the upstream, it will rebase the checked-out branch onto the upstream.",
14811478
UpstreamGenericName: "upstream of selected branch",
14821479
SetUpstreamTitle: "Set upstream branch",
1483-
SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'",
1480+
SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'?",
14841481
EditRemoteTooltip: "Edit the selected remote's name or URL.",
14851482
TagCommit: "Tag commit",
14861483
TagCommitTooltip: "Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description.",
@@ -1797,7 +1794,6 @@ func EnglishTranslationSet() *TranslationSet {
17971794
CheckoutBranch: "Checkout branch",
17981795
ForceCheckoutBranch: "Force checkout branch",
17991796
DeleteLocalBranch: "Delete local branch",
1800-
DeleteBranch: "Delete branch",
18011797
Merge: "Merge",
18021798
SquashMerge: "Squash merge",
18031799
RebaseBranch: "Rebase branch",

0 commit comments

Comments
 (0)