summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefan Haller <stefan@haller-berlin.de>2024-09-13 09:06:29 +0200
committerStefan Haller <stefan@haller-berlin.de>2024-09-15 17:56:13 +0200
commit3cd47f315e498c1c4ef790650da0f28d40f2f9e3 (patch)
treef56ce52e4c62d4a9f21753bc8f6db5bc6f7098e0
parentba7d2f03a77d5024f69a04299abdd789dda9e4e1 (diff)
Add a menu item to delete both local and remote branch at oncedelete-local-and-remote-branch-at-once
-rw-r--r--pkg/gui/controllers/branches_controller.go19
-rw-r--r--pkg/gui/controllers/helpers/branches_helper.go53
-rw-r--r--pkg/i18n/english.go4
-rw-r--r--pkg/integration/tests/branch/delete.go66
4 files changed, 140 insertions, 2 deletions
diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go
index 93b3b93e1..a97168fc1 100644
--- a/pkg/gui/controllers/branches_controller.go
+++ b/pkg/gui/controllers/branches_controller.go
@@ -528,6 +528,10 @@ func (self *BranchesController) remoteDelete(branch *models.Branch) error {
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.UpstreamBranch)
}
+func (self *BranchesController) localAndRemoteDelete(branch *models.Branch) error {
+ return self.c.Helpers().BranchesHelper.ConfirmLocalAndRemoteDelete(branch)
+}
+
func (self *BranchesController) delete(branch *models.Branch) error {
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
@@ -553,6 +557,19 @@ func (self *BranchesController) delete(branch *models.Branch) error {
remoteDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
}
+ deleteBothItem := &types.MenuItem{
+ Label: self.c.Tr.DeleteLocalAndRemoteBranch,
+ Key: 'b',
+ OnPress: func() error {
+ return self.localAndRemoteDelete(branch)
+ },
+ }
+ if checkedOutBranch.Name == branch.Name {
+ deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
+ } else if !branch.IsTrackingRemote() || branch.UpstreamGone {
+ deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
+ }
+
menuTitle := utils.ResolvePlaceholderString(
self.c.Tr.DeleteBranchTitle,
map[string]string{
@@ -562,7 +579,7 @@ func (self *BranchesController) delete(branch *models.Branch) error {
return self.c.Menu(types.CreateMenuOptions{
Title: menuTitle,
- Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem},
+ Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem, deleteBothItem},
})
}
diff --git a/pkg/gui/controllers/helpers/branches_helper.go b/pkg/gui/controllers/helpers/branches_helper.go
index 68976f7a3..fdd72e188 100644
--- a/pkg/gui/controllers/helpers/branches_helper.go
+++ b/pkg/gui/controllers/helpers/branches_helper.go
@@ -97,6 +97,59 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName st
return nil
}
+func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branch *models.Branch) error {
+ if self.checkedOutByOtherWorktree(branch) {
+ return self.promptWorktreeBranchDelete(branch)
+ }
+
+ isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
+ if err != nil {
+ return err
+ }
+
+ prompt := utils.ResolvePlaceholderString(
+ self.c.Tr.DeleteLocalAndRemoteBranchPrompt,
+ map[string]string{
+ "localBranchName": branch.Name,
+ "remoteBranchName": branch.UpstreamBranch,
+ "remoteName": branch.UpstreamRemote,
+ },
+ )
+
+ if !isMerged {
+ prompt += "\n\n" + utils.ResolvePlaceholderString(
+ self.c.Tr.ForceDeleteBranchMessage,
+ map[string]string{
+ "selectedBranchName": branch.Name,
+ },
+ )
+ }
+
+ self.c.Confirm(types.ConfirmOpts{
+ Title: self.c.Tr.DeleteLocalAndRemoteBranch,
+ Prompt: prompt,
+ HandleConfirm: func() error {
+ return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
+ // Delete the remote branch first so that we keep the local one
+ // in case of failure
+ self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
+ if err := self.c.Git().Remote.DeleteRemoteBranch(task, branch.UpstreamRemote, branch.Name); err != nil {
+ return err
+ }
+
+ self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
+ if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
+ return err
+ }
+
+ return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
+ })
+ },
+ })
+
+ return nil
+}
+
func ShortBranchName(fullBranchName string) string {
return strings.TrimPrefix(strings.TrimPrefix(fullBranchName, "refs/heads/"), "refs/remotes/")
}
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 09b4eda1c..e141a614c 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -107,6 +107,7 @@ type TranslationSet struct {
DeleteLocalBranch string
DeleteRemoteBranchOption string
DeleteRemoteBranchPrompt string
+ DeleteLocalAndRemoteBranchPrompt string
ForceDeleteBranchTitle string
ForceDeleteBranchMessage string
RebaseBranch string
@@ -473,6 +474,7 @@ type TranslationSet struct {
RemoveRemotePrompt string
DeleteRemoteBranch string
DeleteRemoteBranchTooltip string
+ DeleteLocalAndRemoteBranch string
SetAsUpstream string
SetAsUpstreamTooltip string
SetUpstream string
@@ -1086,6 +1088,7 @@ func EnglishTranslationSet() *TranslationSet {
DeleteLocalBranch: "Delete local branch",
DeleteRemoteBranchOption: "Delete remote branch",
DeleteRemoteBranchPrompt: "Are you sure you want to delete the remote branch '{{.selectedBranchName}}' from '{{.upstream}}'?",
+ DeleteLocalAndRemoteBranchPrompt: "Are you sure you want to delete both '{{.localBranchName}}' from your machine, and '{{.remoteBranchName}}' from '{{.remoteName}}'?",
ForceDeleteBranchTitle: "Force delete branch",
ForceDeleteBranchMessage: "'{{.selectedBranchName}}' is not fully merged. Are you sure you want to delete it?",
RebaseBranch: "Rebase",
@@ -1462,6 +1465,7 @@ func EnglishTranslationSet() *TranslationSet {
RemoveRemotePrompt: "Are you sure you want to remove remote?",
DeleteRemoteBranch: "Delete remote branch",
DeleteRemoteBranchTooltip: "Delete the remote branch from the remote.",
+ DeleteLocalAndRemoteBranch: "Delete local and remote branch",
SetAsUpstream: "Set as upstream",
SetAsUpstreamTooltip: "Set the selected remote branch as the upstream of the checked-out branch.",
SetUpstream: "Set upstream of selected branch",
diff --git a/pkg/integration/tests/branch/delete.go b/pkg/integration/tests/branch/delete.go
index aab872957..d277f31b4 100644
--- a/pkg/integration/tests/branch/delete.go
+++ b/pkg/integration/tests/branch/delete.go
@@ -31,6 +31,13 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
EmptyCommit("on branch-four 01").
PushBranchAndSetUpstream("origin", "branch-four").
EmptyCommit("on branch-four 02"). // branch-four is not contained in any of these, so we get a delete confirmation
+ NewBranchFrom("branch-five", "master").
+ EmptyCommit("on branch-five 01").
+ PushBranchAndSetUpstream("origin", "branch-five"). // branch-five is contained in its own upstream
+ NewBranchFrom("branch-six", "master").
+ EmptyCommit("on branch-six 01").
+ PushBranchAndSetUpstream("origin", "branch-six").
+ EmptyCommit("on branch-six 02"). // branch-six is not contained in any of these, so we get a delete confirmation
Checkout("current-head")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
@@ -38,6 +45,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
Focus().
Lines(
Contains("current-head").IsSelected(),
+ Contains("branch-six ↑1"),
+ Contains("branch-five ✓"),
Contains("branch-four ↑1"),
Contains("branch-three"),
Contains("branch-two ✓"),
@@ -62,7 +71,7 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
// Delete branch-four. This is the only branch that is not fully merged, so we get
// a confirmation popup.
- SelectNextItem().
+ NavigateToLine(Contains("branch-four")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
@@ -78,6 +87,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("current-head"),
+ Contains("branch-six ↑1"),
+ Contains("branch-five ✓"),
Contains("branch-three").IsSelected(),
Contains("branch-two ✓"),
Contains("master"),
@@ -96,6 +107,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("current-head"),
+ Contains("branch-six ↑1"),
+ Contains("branch-five ✓"),
Contains("branch-two ✓").IsSelected(),
Contains("master"),
Contains("branch-one ↑1"),
@@ -113,6 +126,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("current-head"),
+ Contains("branch-six ↑1"),
+ Contains("branch-five ✓"),
Contains("master").IsSelected(),
Contains("branch-one ↑1"),
).
@@ -143,7 +158,9 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().
RemoteBranches().
Lines(
+ Equals("branch-five"),
Equals("branch-four"),
+ Equals("branch-six"),
Equals("branch-two"),
).
Press(keys.Universal.Return)
@@ -154,6 +171,8 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("current-head"),
+ Contains("branch-six ↑1"),
+ Contains("branch-five ✓"),
Contains("master"),
Contains("branch-one (upstream gone)").IsSelected(),
).
@@ -170,6 +189,51 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
}).
Lines(
Contains("current-head"),
+ Contains("branch-six ↑1"),
+ Contains("branch-five ✓"),
+ Contains("master").IsSelected(),
+ ).
+
+ // Delete both local and remote branch of branch-six. We get the force-delete warning because it is not fully merged.
+ NavigateToLine(Contains("branch-six")).
+ Press(keys.Universal.Remove).
+ Tap(func() {
+ t.ExpectPopup().
+ Menu().
+ Title(Equals("Delete branch 'branch-six'?")).
+ Select(Contains("Delete local and remote branch")).
+ Confirm()
+ t.ExpectPopup().
+ Confirmation().
+ Title(Equals("Delete local and remote branch")).
+ Content(Contains("Are you sure you want to delete both 'branch-six' from your machine, and 'branch-six' from 'origin'?").
+ Contains("'branch-six' is not fully merged. Are you sure you want to delete it?")).
+ Confirm()
+ }).
+ Lines(
+ Contains("current-head"),
+ Contains("branch-five ✓").IsSelected(),
+ Contains("master"),
+ ).
+
+ // Delete both local and remote branch of branch-five. We get the same popups, but the confirmation
+ // doesn't contain the force-delete warning.
+ Press(keys.Universal.Remove).
+ Tap(func() {
+ t.ExpectPopup().
+ Menu().
+ Title(Equals("Delete branch 'branch-five'?")).
+ Select(Contains("Delete local and remote branch")).
+ Confirm()
+ t.ExpectPopup().
+ Confirmation().
+ Title(Equals("Delete local and remote branch")).
+ Content(Equals("Are you sure you want to delete both 'branch-five' from your machine, and 'branch-five' from 'origin'?").
+ DoesNotContain("not fully merged")).
+ Confirm()
+ }).
+ Lines(
+ Contains("current-head"),
Contains("master").IsSelected(),
)
},