Skip to content

Commit c6a7722

Browse files
authored
Add a menu item to delete both local and remote branch at once (jesseduffield#3916)
- **PR Description** Add a menu item to delete both local and remote branch at once. - **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 e181de1 + 1ab70ec commit c6a7722

File tree

4 files changed

+140
-2
lines changed

4 files changed

+140
-2
lines changed

pkg/gui/controllers/branches_controller.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,10 @@ func (self *BranchesController) remoteDelete(branch *models.Branch) error {
528528
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.UpstreamBranch)
529529
}
530530

531+
func (self *BranchesController) localAndRemoteDelete(branch *models.Branch) error {
532+
return self.c.Helpers().BranchesHelper.ConfirmLocalAndRemoteDelete(branch)
533+
}
534+
531535
func (self *BranchesController) delete(branch *models.Branch) error {
532536
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
533537

@@ -553,6 +557,19 @@ func (self *BranchesController) delete(branch *models.Branch) error {
553557
remoteDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
554558
}
555559

560+
deleteBothItem := &types.MenuItem{
561+
Label: self.c.Tr.DeleteLocalAndRemoteBranch,
562+
Key: 'b',
563+
OnPress: func() error {
564+
return self.localAndRemoteDelete(branch)
565+
},
566+
}
567+
if checkedOutBranch.Name == branch.Name {
568+
deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
569+
} else if !branch.IsTrackingRemote() || branch.UpstreamGone {
570+
deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
571+
}
572+
556573
menuTitle := utils.ResolvePlaceholderString(
557574
self.c.Tr.DeleteBranchTitle,
558575
map[string]string{
@@ -562,7 +579,7 @@ func (self *BranchesController) delete(branch *models.Branch) error {
562579

563580
return self.c.Menu(types.CreateMenuOptions{
564581
Title: menuTitle,
565-
Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem},
582+
Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem, deleteBothItem},
566583
})
567584
}
568585

pkg/gui/controllers/helpers/branches_helper.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,59 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName st
9797
return nil
9898
}
9999

100+
func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branch *models.Branch) error {
101+
if self.checkedOutByOtherWorktree(branch) {
102+
return self.promptWorktreeBranchDelete(branch)
103+
}
104+
105+
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
106+
if err != nil {
107+
return err
108+
}
109+
110+
prompt := utils.ResolvePlaceholderString(
111+
self.c.Tr.DeleteLocalAndRemoteBranchPrompt,
112+
map[string]string{
113+
"localBranchName": branch.Name,
114+
"remoteBranchName": branch.UpstreamBranch,
115+
"remoteName": branch.UpstreamRemote,
116+
},
117+
)
118+
119+
if !isMerged {
120+
prompt += "\n\n" + utils.ResolvePlaceholderString(
121+
self.c.Tr.ForceDeleteBranchMessage,
122+
map[string]string{
123+
"selectedBranchName": branch.Name,
124+
},
125+
)
126+
}
127+
128+
self.c.Confirm(types.ConfirmOpts{
129+
Title: self.c.Tr.DeleteLocalAndRemoteBranch,
130+
Prompt: prompt,
131+
HandleConfirm: func() error {
132+
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
133+
// Delete the remote branch first so that we keep the local one
134+
// in case of failure
135+
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
136+
if err := self.c.Git().Remote.DeleteRemoteBranch(task, branch.UpstreamRemote, branch.Name); err != nil {
137+
return err
138+
}
139+
140+
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
141+
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
142+
return err
143+
}
144+
145+
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
146+
})
147+
},
148+
})
149+
150+
return nil
151+
}
152+
100153
func ShortBranchName(fullBranchName string) string {
101154
return strings.TrimPrefix(strings.TrimPrefix(fullBranchName, "refs/heads/"), "refs/remotes/")
102155
}

pkg/i18n/english.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ type TranslationSet struct {
107107
DeleteLocalBranch string
108108
DeleteRemoteBranchOption string
109109
DeleteRemoteBranchPrompt string
110+
DeleteLocalAndRemoteBranchPrompt string
110111
ForceDeleteBranchTitle string
111112
ForceDeleteBranchMessage string
112113
RebaseBranch string
@@ -473,6 +474,7 @@ type TranslationSet struct {
473474
RemoveRemotePrompt string
474475
DeleteRemoteBranch string
475476
DeleteRemoteBranchTooltip string
477+
DeleteLocalAndRemoteBranch string
476478
SetAsUpstream string
477479
SetAsUpstreamTooltip string
478480
SetUpstream string
@@ -1086,6 +1088,7 @@ func EnglishTranslationSet() *TranslationSet {
10861088
DeleteLocalBranch: "Delete local branch",
10871089
DeleteRemoteBranchOption: "Delete remote branch",
10881090
DeleteRemoteBranchPrompt: "Are you sure you want to delete the remote branch '{{.selectedBranchName}}' from '{{.upstream}}'?",
1091+
DeleteLocalAndRemoteBranchPrompt: "Are you sure you want to delete both '{{.localBranchName}}' from your machine, and '{{.remoteBranchName}}' from '{{.remoteName}}'?",
10891092
ForceDeleteBranchTitle: "Force delete branch",
10901093
ForceDeleteBranchMessage: "'{{.selectedBranchName}}' is not fully merged. Are you sure you want to delete it?",
10911094
RebaseBranch: "Rebase",
@@ -1462,6 +1465,7 @@ func EnglishTranslationSet() *TranslationSet {
14621465
RemoveRemotePrompt: "Are you sure you want to remove remote?",
14631466
DeleteRemoteBranch: "Delete remote branch",
14641467
DeleteRemoteBranchTooltip: "Delete the remote branch from the remote.",
1468+
DeleteLocalAndRemoteBranch: "Delete local and remote branch",
14651469
SetAsUpstream: "Set as upstream",
14661470
SetAsUpstreamTooltip: "Set the selected remote branch as the upstream of the checked-out branch.",
14671471
SetUpstream: "Set upstream of selected branch",

pkg/integration/tests/branch/delete.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,22 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
3131
EmptyCommit("on branch-four 01").
3232
PushBranchAndSetUpstream("origin", "branch-four").
3333
EmptyCommit("on branch-four 02"). // branch-four is not contained in any of these, so we get a delete confirmation
34+
NewBranchFrom("branch-five", "master").
35+
EmptyCommit("on branch-five 01").
36+
PushBranchAndSetUpstream("origin", "branch-five"). // branch-five is contained in its own upstream
37+
NewBranchFrom("branch-six", "master").
38+
EmptyCommit("on branch-six 01").
39+
PushBranchAndSetUpstream("origin", "branch-six").
40+
EmptyCommit("on branch-six 02"). // branch-six is not contained in any of these, so we get a delete confirmation
3441
Checkout("current-head")
3542
},
3643
Run: func(t *TestDriver, keys config.KeybindingConfig) {
3744
t.Views().Branches().
3845
Focus().
3946
Lines(
4047
Contains("current-head").IsSelected(),
48+
Contains("branch-six ↑1"),
49+
Contains("branch-five ✓"),
4150
Contains("branch-four ↑1"),
4251
Contains("branch-three"),
4352
Contains("branch-two ✓"),
@@ -62,7 +71,7 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
6271

6372
// Delete branch-four. This is the only branch that is not fully merged, so we get
6473
// a confirmation popup.
65-
SelectNextItem().
74+
NavigateToLine(Contains("branch-four")).
6675
Press(keys.Universal.Remove).
6776
Tap(func() {
6877
t.ExpectPopup().
@@ -78,6 +87,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
7887
}).
7988
Lines(
8089
Contains("current-head"),
90+
Contains("branch-six ↑1"),
91+
Contains("branch-five ✓"),
8192
Contains("branch-three").IsSelected(),
8293
Contains("branch-two ✓"),
8394
Contains("master"),
@@ -96,6 +107,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
96107
}).
97108
Lines(
98109
Contains("current-head"),
110+
Contains("branch-six ↑1"),
111+
Contains("branch-five ✓"),
99112
Contains("branch-two ✓").IsSelected(),
100113
Contains("master"),
101114
Contains("branch-one ↑1"),
@@ -113,6 +126,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
113126
}).
114127
Lines(
115128
Contains("current-head"),
129+
Contains("branch-six ↑1"),
130+
Contains("branch-five ✓"),
116131
Contains("master").IsSelected(),
117132
Contains("branch-one ↑1"),
118133
).
@@ -143,7 +158,9 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
143158
t.Views().
144159
RemoteBranches().
145160
Lines(
161+
Equals("branch-five"),
146162
Equals("branch-four"),
163+
Equals("branch-six"),
147164
Equals("branch-two"),
148165
).
149166
Press(keys.Universal.Return)
@@ -154,6 +171,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
154171
}).
155172
Lines(
156173
Contains("current-head"),
174+
Contains("branch-six ↑1"),
175+
Contains("branch-five ✓"),
157176
Contains("master"),
158177
Contains("branch-one (upstream gone)").IsSelected(),
159178
).
@@ -168,6 +187,51 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
168187
Select(Contains("Delete local branch")).
169188
Confirm()
170189
}).
190+
Lines(
191+
Contains("current-head"),
192+
Contains("branch-six ↑1"),
193+
Contains("branch-five ✓"),
194+
Contains("master").IsSelected(),
195+
).
196+
197+
// Delete both local and remote branch of branch-six. We get the force-delete warning because it is not fully merged.
198+
NavigateToLine(Contains("branch-six")).
199+
Press(keys.Universal.Remove).
200+
Tap(func() {
201+
t.ExpectPopup().
202+
Menu().
203+
Title(Equals("Delete branch 'branch-six'?")).
204+
Select(Contains("Delete local and remote branch")).
205+
Confirm()
206+
t.ExpectPopup().
207+
Confirmation().
208+
Title(Equals("Delete local and remote branch")).
209+
Content(Contains("Are you sure you want to delete both 'branch-six' from your machine, and 'branch-six' from 'origin'?").
210+
Contains("'branch-six' is not fully merged. Are you sure you want to delete it?")).
211+
Confirm()
212+
}).
213+
Lines(
214+
Contains("current-head"),
215+
Contains("branch-five ✓").IsSelected(),
216+
Contains("master"),
217+
).
218+
219+
// Delete both local and remote branch of branch-five. We get the same popups, but the confirmation
220+
// doesn't contain the force-delete warning.
221+
Press(keys.Universal.Remove).
222+
Tap(func() {
223+
t.ExpectPopup().
224+
Menu().
225+
Title(Equals("Delete branch 'branch-five'?")).
226+
Select(Contains("Delete local and remote branch")).
227+
Confirm()
228+
t.ExpectPopup().
229+
Confirmation().
230+
Title(Equals("Delete local and remote branch")).
231+
Content(Equals("Are you sure you want to delete both 'branch-five' from your machine, and 'branch-five' from 'origin'?").
232+
DoesNotContain("not fully merged")).
233+
Confirm()
234+
}).
171235
Lines(
172236
Contains("current-head"),
173237
Contains("master").IsSelected(),

0 commit comments

Comments
 (0)