mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-25 12:24:47 +02:00
Allow deleting a range selection of branches
We allow deleting remote branches (or local and remote branches) only if *all* selected branches have one. We show the a warning about force-deleting as soon as at least one of the selected branches is not fully merged. The added test only tests a few of the most interesting cases; I didn't try to cover the whole space of possible combinations, that would have been too much.
This commit is contained in:
parent
0b0910573b
commit
c1b4201726
@ -109,10 +109,10 @@ func (self *BranchCommands) CurrentBranchName() (string, error) {
|
||||
}
|
||||
|
||||
// LocalDelete delete branch locally
|
||||
func (self *BranchCommands) LocalDelete(branch string, force bool) error {
|
||||
func (self *BranchCommands) LocalDelete(branches []string, force bool) error {
|
||||
cmdArgs := NewGitCmd("branch").
|
||||
ArgIfElse(force, "-D", "-d").
|
||||
Arg(branch).
|
||||
Arg(branches...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
|
@ -62,36 +62,57 @@ func TestBranchNewBranch(t *testing.T) {
|
||||
|
||||
func TestBranchDeleteBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
force bool
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
testName string
|
||||
branchNames []string
|
||||
force bool
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Delete a branch",
|
||||
[]string{"test"},
|
||||
false,
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-d", "test"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Delete multiple branches",
|
||||
[]string{"test1", "test2", "test3"},
|
||||
false,
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-d", "test1", "test2", "test3"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Force delete a branch",
|
||||
[]string{"test"},
|
||||
true,
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-D", "test"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Force delete multiple branches",
|
||||
[]string{"test1", "test2", "test3"},
|
||||
true,
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-D", "test1", "test2", "test3"}, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildBranchCommands(commonDeps{runner: s.runner})
|
||||
|
||||
s.test(instance.LocalDelete("test", s.force))
|
||||
s.test(instance.LocalDelete(s.branchNames, s.force))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
|
@ -49,9 +49,10 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchName string) error {
|
||||
func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchNames []string) error {
|
||||
cmdArgs := NewGitCmd("push").
|
||||
Arg(remoteName, "--delete", branchName).
|
||||
Arg(remoteName, "--delete").
|
||||
Arg(branchNames...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
|
||||
|
@ -91,8 +91,8 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Remove),
|
||||
Handler: self.withItem(self.delete),
|
||||
GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)),
|
||||
Handler: self.withItems(self.delete),
|
||||
GetDisabledReason: self.require(self.itemRangeSelected(self.branchesAreReal)),
|
||||
Description: self.c.Tr.Delete,
|
||||
Tooltip: self.c.Tr.BranchDeleteTooltip,
|
||||
OpensMenu: true,
|
||||
@ -520,29 +520,35 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, KeepBranchSelectionIndex: true})
|
||||
}
|
||||
|
||||
func (self *BranchesController) localDelete(branch *models.Branch) error {
|
||||
return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branch)
|
||||
func (self *BranchesController) localDelete(branches []*models.Branch) error {
|
||||
return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branches)
|
||||
}
|
||||
|
||||
func (self *BranchesController) remoteDelete(branch *models.Branch) error {
|
||||
remoteBranch := &models.RemoteBranch{Name: branch.UpstreamBranch, RemoteName: branch.UpstreamRemote}
|
||||
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(remoteBranch)
|
||||
func (self *BranchesController) remoteDelete(branches []*models.Branch) error {
|
||||
remoteBranches := lo.Map(branches, func(branch *models.Branch, _ int) *models.RemoteBranch {
|
||||
return &models.RemoteBranch{Name: branch.UpstreamBranch, RemoteName: branch.UpstreamRemote}
|
||||
})
|
||||
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(remoteBranches)
|
||||
}
|
||||
|
||||
func (self *BranchesController) localAndRemoteDelete(branch *models.Branch) error {
|
||||
return self.c.Helpers().BranchesHelper.ConfirmLocalAndRemoteDelete(branch)
|
||||
func (self *BranchesController) localAndRemoteDelete(branches []*models.Branch) error {
|
||||
return self.c.Helpers().BranchesHelper.ConfirmLocalAndRemoteDelete(branches)
|
||||
}
|
||||
|
||||
func (self *BranchesController) delete(branch *models.Branch) error {
|
||||
func (self *BranchesController) delete(branches []*models.Branch) error {
|
||||
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
||||
isBranchCheckedOut := checkedOutBranch.Name == branch.Name
|
||||
hasUpstream := branch.IsTrackingRemote() && !branch.UpstreamGone
|
||||
isBranchCheckedOut := lo.SomeBy(branches, func(branch *models.Branch) bool {
|
||||
return checkedOutBranch.Name == branch.Name
|
||||
})
|
||||
hasUpstream := lo.EveryBy(branches, func(branch *models.Branch) bool {
|
||||
return branch.IsTrackingRemote() && !branch.UpstreamGone
|
||||
})
|
||||
|
||||
localDeleteItem := &types.MenuItem{
|
||||
Label: self.c.Tr.DeleteLocalBranch,
|
||||
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteLocalBranches, self.c.Tr.DeleteLocalBranch),
|
||||
Key: 'c',
|
||||
OnPress: func() error {
|
||||
return self.localDelete(branch)
|
||||
return self.localDelete(branches)
|
||||
},
|
||||
}
|
||||
if isBranchCheckedOut {
|
||||
@ -550,35 +556,44 @@ func (self *BranchesController) delete(branch *models.Branch) error {
|
||||
}
|
||||
|
||||
remoteDeleteItem := &types.MenuItem{
|
||||
Label: self.c.Tr.DeleteRemoteBranch,
|
||||
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteRemoteBranches, self.c.Tr.DeleteRemoteBranch),
|
||||
Key: 'r',
|
||||
OnPress: func() error {
|
||||
return self.remoteDelete(branch)
|
||||
return self.remoteDelete(branches)
|
||||
},
|
||||
}
|
||||
if !hasUpstream {
|
||||
remoteDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
|
||||
remoteDeleteItem.DisabledReason = &types.DisabledReason{
|
||||
Text: lo.Ternary(len(branches) > 1, self.c.Tr.UpstreamsNotSetError, self.c.Tr.UpstreamNotSetError),
|
||||
}
|
||||
}
|
||||
|
||||
deleteBothItem := &types.MenuItem{
|
||||
Label: self.c.Tr.DeleteLocalAndRemoteBranch,
|
||||
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteLocalAndRemoteBranches, self.c.Tr.DeleteLocalAndRemoteBranch),
|
||||
Key: 'b',
|
||||
OnPress: func() error {
|
||||
return self.localAndRemoteDelete(branch)
|
||||
return self.localAndRemoteDelete(branches)
|
||||
},
|
||||
}
|
||||
if isBranchCheckedOut {
|
||||
deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
|
||||
} else if !hasUpstream {
|
||||
deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
|
||||
deleteBothItem.DisabledReason = &types.DisabledReason{
|
||||
Text: lo.Ternary(len(branches) > 1, self.c.Tr.UpstreamsNotSetError, self.c.Tr.UpstreamNotSetError),
|
||||
}
|
||||
}
|
||||
|
||||
menuTitle := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteBranchTitle,
|
||||
map[string]string{
|
||||
"selectedBranchName": branch.Name,
|
||||
},
|
||||
)
|
||||
var menuTitle string
|
||||
if len(branches) == 1 {
|
||||
menuTitle = utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteBranchTitle,
|
||||
map[string]string{
|
||||
"selectedBranchName": branches[0].Name,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
menuTitle = self.c.Tr.DeleteBranchesTitle
|
||||
}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: menuTitle,
|
||||
@ -822,6 +837,16 @@ func (self *BranchesController) branchIsReal(branch *models.Branch) *types.Disab
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesController) branchesAreReal(selectedBranches []*models.Branch, startIdx int, endIdx int) *types.DisabledReason {
|
||||
if !lo.EveryBy(selectedBranches, func(branch *models.Branch) bool {
|
||||
return branch.IsRealBranch()
|
||||
}) {
|
||||
return &types.DisabledReason{Text: self.c.Tr.SelectedItemIsNotABranch}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesController) notMergingIntoYourself(branch *models.Branch) *types.DisabledReason {
|
||||
selectedBranchName := branch.Name
|
||||
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
|
||||
|
@ -1,6 +1,7 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type BranchesHelper struct {
|
||||
@ -23,12 +25,16 @@ func NewBranchesHelper(c *HelperCommon, worktreeHelper *WorktreeHelper) *Branche
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BranchesHelper) ConfirmLocalDelete(branch *models.Branch) error {
|
||||
if self.checkedOutByOtherWorktree(branch) {
|
||||
return self.promptWorktreeBranchDelete(branch)
|
||||
func (self *BranchesHelper) ConfirmLocalDelete(branches []*models.Branch) error {
|
||||
if len(branches) > 1 {
|
||||
if lo.SomeBy(branches, func(branch *models.Branch) bool { return self.checkedOutByOtherWorktree(branch) }) {
|
||||
return errors.New(self.c.Tr.SomeBranchesCheckedOutByWorktreeError)
|
||||
}
|
||||
} else if self.checkedOutByOtherWorktree(branches[0]) {
|
||||
return self.promptWorktreeBranchDelete(branches[0])
|
||||
}
|
||||
|
||||
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
|
||||
allBranchesMerged, err := self.allBranchesMerged(branches)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -36,24 +42,32 @@ func (self *BranchesHelper) ConfirmLocalDelete(branch *models.Branch) error {
|
||||
doDelete := func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
|
||||
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
|
||||
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string { return branch.Name })
|
||||
if err := self.c.Git().Branch.LocalDelete(branchNames, true); err != nil {
|
||||
return err
|
||||
}
|
||||
selectionStart, _ := self.c.Contexts().Branches.GetSelectionRange()
|
||||
self.c.Contexts().Branches.SetSelectedLineIdx(selectionStart)
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
|
||||
})
|
||||
}
|
||||
|
||||
if isMerged {
|
||||
if allBranchesMerged {
|
||||
return doDelete()
|
||||
}
|
||||
|
||||
title := self.c.Tr.ForceDeleteBranchTitle
|
||||
message := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.ForceDeleteBranchMessage,
|
||||
map[string]string{
|
||||
"selectedBranchName": branch.Name,
|
||||
},
|
||||
)
|
||||
var message string
|
||||
if len(branches) == 1 {
|
||||
message = utils.ResolvePlaceholderString(
|
||||
self.c.Tr.ForceDeleteBranchMessage,
|
||||
map[string]string{
|
||||
"selectedBranchName": branches[0].Name,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
message = self.c.Tr.ForceDeleteBranchesMessage
|
||||
}
|
||||
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: title,
|
||||
@ -66,27 +80,36 @@ func (self *BranchesHelper) ConfirmLocalDelete(branch *models.Branch) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesHelper) ConfirmDeleteRemote(remoteBranch *models.RemoteBranch) error {
|
||||
title := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteBranchTitle,
|
||||
map[string]string{
|
||||
"selectedBranchName": remoteBranch.Name,
|
||||
},
|
||||
)
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteRemoteBranchPrompt,
|
||||
map[string]string{
|
||||
"selectedBranchName": remoteBranch.Name,
|
||||
"upstream": remoteBranch.RemoteName,
|
||||
},
|
||||
)
|
||||
func (self *BranchesHelper) ConfirmDeleteRemote(remoteBranches []*models.RemoteBranch) error {
|
||||
var title string
|
||||
if len(remoteBranches) == 1 {
|
||||
title = utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteBranchTitle,
|
||||
map[string]string{
|
||||
"selectedBranchName": remoteBranches[0].Name,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
title = self.c.Tr.DeleteBranchesTitle
|
||||
}
|
||||
var prompt string
|
||||
if len(remoteBranches) == 1 {
|
||||
prompt = utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteRemoteBranchPrompt,
|
||||
map[string]string{
|
||||
"selectedBranchName": remoteBranches[0].Name,
|
||||
"upstream": remoteBranches[0].RemoteName,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
prompt = self.c.Tr.DeleteRemoteBranchesPrompt
|
||||
}
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: title,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
|
||||
if err := self.c.Git().Remote.DeleteRemoteBranch(task, remoteBranch.RemoteName, remoteBranch.Name); err != nil {
|
||||
if err := self.deleteRemoteBranches(remoteBranches, task); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
|
||||
@ -97,32 +120,41 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteBranch *models.RemoteBranc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branch *models.Branch) error {
|
||||
if self.checkedOutByOtherWorktree(branch) {
|
||||
return self.promptWorktreeBranchDelete(branch)
|
||||
func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branches []*models.Branch) error {
|
||||
if lo.SomeBy(branches, func(branch *models.Branch) bool { return self.checkedOutByOtherWorktree(branch) }) {
|
||||
return errors.New(self.c.Tr.SomeBranchesCheckedOutByWorktreeError)
|
||||
}
|
||||
|
||||
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
|
||||
allBranchesMerged, err := self.allBranchesMerged(branches)
|
||||
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,
|
||||
var prompt string
|
||||
if len(branches) == 1 {
|
||||
prompt = utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteLocalAndRemoteBranchPrompt,
|
||||
map[string]string{
|
||||
"selectedBranchName": branch.Name,
|
||||
"localBranchName": branches[0].Name,
|
||||
"remoteBranchName": branches[0].UpstreamBranch,
|
||||
"remoteName": branches[0].UpstreamRemote,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
prompt = self.c.Tr.DeleteLocalAndRemoteBranchesPrompt
|
||||
}
|
||||
|
||||
if !allBranchesMerged {
|
||||
if len(branches) == 1 {
|
||||
prompt += "\n\n" + utils.ResolvePlaceholderString(
|
||||
self.c.Tr.ForceDeleteBranchMessage,
|
||||
map[string]string{
|
||||
"selectedBranchName": branches[0].Name,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
prompt += "\n\n" + self.c.Tr.ForceDeleteBranchesMessage
|
||||
}
|
||||
}
|
||||
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
@ -130,18 +162,24 @@ func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branch *models.Branch) e
|
||||
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
|
||||
// Delete the remote branches first so that we keep the local ones
|
||||
// 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 {
|
||||
remoteBranches := lo.Map(branches, func(branch *models.Branch, _ int) *models.RemoteBranch {
|
||||
return &models.RemoteBranch{Name: branch.UpstreamBranch, RemoteName: branch.UpstreamRemote}
|
||||
})
|
||||
if err := self.deleteRemoteBranches(remoteBranches, task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
|
||||
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
|
||||
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string { return branch.Name })
|
||||
if err := self.c.Git().Branch.LocalDelete(branchNames, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectionStart, _ := self.c.Contexts().Branches.GetSelectionRange()
|
||||
self.c.Contexts().Branches.SetSelectedLineIdx(selectionStart)
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
|
||||
})
|
||||
},
|
||||
@ -198,3 +236,30 @@ func (self *BranchesHelper) promptWorktreeBranchDelete(selectedBranch *models.Br
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *BranchesHelper) allBranchesMerged(branches []*models.Branch) (bool, error) {
|
||||
allBranchesMerged := true
|
||||
for _, branch := range branches {
|
||||
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !isMerged {
|
||||
allBranchesMerged = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return allBranchesMerged, nil
|
||||
}
|
||||
|
||||
func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.RemoteBranch, task gocui.Task) error {
|
||||
remotes := lo.GroupBy(remoteBranches, func(branch *models.RemoteBranch) string { return branch.RemoteName })
|
||||
for remote, branches := range remotes {
|
||||
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
|
||||
branchNames := lo.Map(branches, func(branch *models.RemoteBranch, _ int) string { return branch.Name })
|
||||
if err := self.c.Git().Remote.DeleteRemoteBranch(task, remote, branchNames); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -66,8 +66,8 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts)
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Remove),
|
||||
Handler: self.withItem(self.delete),
|
||||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
Handler: self.withItems(self.delete),
|
||||
GetDisabledReason: self.require(self.itemRangeSelected()),
|
||||
Description: self.c.Tr.Delete,
|
||||
Tooltip: self.c.Tr.DeleteRemoteBranchTooltip,
|
||||
DisplayOnScreen: true,
|
||||
@ -132,8 +132,8 @@ func (self *RemoteBranchesController) context() *context.RemoteBranchesContext {
|
||||
return self.c.Contexts().RemoteBranches
|
||||
}
|
||||
|
||||
func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error {
|
||||
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranch)
|
||||
func (self *RemoteBranchesController) delete(selectedBranches []*models.RemoteBranch) error {
|
||||
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranches)
|
||||
}
|
||||
|
||||
func (self *RemoteBranchesController) merge(selectedBranch *models.RemoteBranch) error {
|
||||
|
@ -105,12 +105,17 @@ type TranslationSet struct {
|
||||
NewBranchNameBranchOff string
|
||||
CantDeleteCheckOutBranch string
|
||||
DeleteBranchTitle string
|
||||
DeleteBranchesTitle string
|
||||
DeleteLocalBranch string
|
||||
DeleteLocalBranches string
|
||||
DeleteRemoteBranchOption string
|
||||
DeleteRemoteBranchPrompt string
|
||||
DeleteRemoteBranchesPrompt string
|
||||
DeleteLocalAndRemoteBranchPrompt string
|
||||
DeleteLocalAndRemoteBranchesPrompt string
|
||||
ForceDeleteBranchTitle string
|
||||
ForceDeleteBranchMessage string
|
||||
ForceDeleteBranchesMessage string
|
||||
RebaseBranch string
|
||||
RebaseBranchTooltip string
|
||||
CantRebaseOntoSelf string
|
||||
@ -472,8 +477,10 @@ type TranslationSet struct {
|
||||
RemoveRemoteTooltip string
|
||||
RemoveRemotePrompt string
|
||||
DeleteRemoteBranch string
|
||||
DeleteRemoteBranches string
|
||||
DeleteRemoteBranchTooltip string
|
||||
DeleteLocalAndRemoteBranch string
|
||||
DeleteLocalAndRemoteBranches string
|
||||
SetAsUpstream string
|
||||
SetAsUpstreamTooltip string
|
||||
SetUpstream string
|
||||
@ -542,6 +549,7 @@ type TranslationSet struct {
|
||||
ViewBranchUpstreamOptions string
|
||||
ViewBranchUpstreamOptionsTooltip string
|
||||
UpstreamNotSetError string
|
||||
UpstreamsNotSetError string
|
||||
NewGitFlowBranchPrompt string
|
||||
RenameBranchWarning string
|
||||
OpenKeybindingsMenu string
|
||||
@ -750,6 +758,7 @@ type TranslationSet struct {
|
||||
SwitchToWorktreeTooltip string
|
||||
AlreadyCheckedOutByWorktree string
|
||||
BranchCheckedOutByWorktree string
|
||||
SomeBranchesCheckedOutByWorktreeError string
|
||||
DetachWorktreeTooltip string
|
||||
Switching string
|
||||
RemoveWorktree string
|
||||
@ -1087,12 +1096,17 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
NewBranchNameBranchOff: "New branch name (branch is off of '{{.branchName}}')",
|
||||
CantDeleteCheckOutBranch: "You cannot delete the checked out branch!",
|
||||
DeleteBranchTitle: "Delete branch '{{.selectedBranchName}}'?",
|
||||
DeleteBranchesTitle: "Delete selected branches?",
|
||||
DeleteLocalBranch: "Delete local branch",
|
||||
DeleteLocalBranches: "Delete local branches",
|
||||
DeleteRemoteBranchOption: "Delete remote branch",
|
||||
DeleteRemoteBranchPrompt: "Are you sure you want to delete the remote branch '{{.selectedBranchName}}' from '{{.upstream}}'?",
|
||||
DeleteRemoteBranchesPrompt: "Are you sure you want to delete the remote branches of the selected branches from their respective remotes?",
|
||||
DeleteLocalAndRemoteBranchPrompt: "Are you sure you want to delete both '{{.localBranchName}}' from your machine, and '{{.remoteBranchName}}' from '{{.remoteName}}'?",
|
||||
DeleteLocalAndRemoteBranchesPrompt: "Are you sure you want to delete both the selected branches from your machine, and their remote branches from their respective remotes?",
|
||||
ForceDeleteBranchTitle: "Force delete branch",
|
||||
ForceDeleteBranchMessage: "'{{.selectedBranchName}}' is not fully merged. Are you sure you want to delete it?",
|
||||
ForceDeleteBranchesMessage: "Some of the selected branches are not fully merged. Are you sure you want to delete them?",
|
||||
RebaseBranch: "Rebase",
|
||||
RebaseBranchTooltip: "Rebase the checked-out branch onto the selected branch.",
|
||||
CantRebaseOntoSelf: "You cannot rebase a branch onto itself",
|
||||
@ -1464,8 +1478,10 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
RemoveRemoteTooltip: `Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected.`,
|
||||
RemoveRemotePrompt: "Are you sure you want to remove remote?",
|
||||
DeleteRemoteBranch: "Delete remote branch",
|
||||
DeleteRemoteBranches: "Delete remote branches",
|
||||
DeleteRemoteBranchTooltip: "Delete the remote branch from the remote.",
|
||||
DeleteLocalAndRemoteBranch: "Delete local and remote branch",
|
||||
DeleteLocalAndRemoteBranches: "Delete local and remote branches",
|
||||
SetAsUpstream: "Set as upstream",
|
||||
SetAsUpstreamTooltip: "Set the selected remote branch as the upstream of the checked-out branch.",
|
||||
SetUpstream: "Set upstream of selected branch",
|
||||
@ -1530,6 +1546,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
ViewBranchUpstreamOptions: "View upstream options",
|
||||
ViewBranchUpstreamOptionsTooltip: "View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream.",
|
||||
UpstreamNotSetError: "The selected branch has no upstream (or the upstream is not stored locally)",
|
||||
UpstreamsNotSetError: "Some of the selected branches have no upstream (or the upstream is not stored locally)",
|
||||
Upstream: "Upstream",
|
||||
UpstreamTooltip: "View upstream options for selected branch e.g. setting/unsetting the upstream and resetting to the upstream.",
|
||||
NewBranchNamePrompt: "Enter new branch name for branch",
|
||||
@ -1741,6 +1758,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
SwitchToWorktreeTooltip: "Switch to the selected worktree.",
|
||||
AlreadyCheckedOutByWorktree: "This branch is checked out by worktree {{.worktreeName}}. Do you want to switch to that worktree?",
|
||||
BranchCheckedOutByWorktree: "Branch {{.branchName}} is checked out by worktree {{.worktreeName}}",
|
||||
SomeBranchesCheckedOutByWorktreeError: "Some of the selected branches are checked out by other worktrees. Select them one by one to delete them.",
|
||||
DetachWorktreeTooltip: "This will run `git checkout --detach` on the worktree so that it stops hogging the branch, but the worktree's working tree will be left alone.",
|
||||
Switching: "Switching",
|
||||
RemoveWorktree: "Remove worktree",
|
||||
|
186
pkg/integration/tests/branch/delete_multiple.go
Normal file
186
pkg/integration/tests/branch/delete_multiple.go
Normal file
@ -0,0 +1,186 @@
|
||||
package branch
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var DeleteMultiple = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Try some combinations of local and remote branch deletions with a range selection of branches",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetAppState().LocalBranchSortOrder = "alphabetic"
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
CloneIntoRemote("origin").
|
||||
CloneIntoRemote("other-remote").
|
||||
EmptyCommit("blah").
|
||||
NewBranch("branch-01").
|
||||
EmptyCommit("on branch-01 01").
|
||||
PushBranchAndSetUpstream("origin", "branch-01").
|
||||
EmptyCommit("on branch-01 02").
|
||||
NewBranch("branch-02").
|
||||
EmptyCommit("on branch-02 01").
|
||||
PushBranchAndSetUpstream("origin", "branch-02").
|
||||
NewBranchFrom("branch-03", "master").
|
||||
EmptyCommit("on branch-03 01").
|
||||
NewBranch("current-head").
|
||||
EmptyCommit("on current-head").
|
||||
NewBranchFrom("branch-04", "master").
|
||||
EmptyCommit("on branch-04 01").
|
||||
PushBranchAndSetUpstream("other-remote", "branch-04").
|
||||
EmptyCommit("on branch-04 02").
|
||||
NewBranchFrom("branch-05", "master").
|
||||
EmptyCommit("on branch-05 01").
|
||||
PushBranchAndSetUpstream("origin", "branch-05").
|
||||
NewBranchFrom("branch-06", "master").
|
||||
EmptyCommit("on branch-06 01").
|
||||
PushBranch("origin", "branch-06").
|
||||
PushBranchAndSetUpstream("other-remote", "branch-06").
|
||||
EmptyCommit("on branch-06 02").
|
||||
Checkout("current-head")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("current-head").IsSelected(),
|
||||
Contains("branch-01 ↑1"),
|
||||
Contains("branch-02 ✓"),
|
||||
Contains("branch-03"),
|
||||
Contains("branch-04 ↑1"),
|
||||
Contains("branch-05 ✓"),
|
||||
Contains("branch-06 ↑1"),
|
||||
Contains("master"),
|
||||
).
|
||||
Press(keys.Universal.RangeSelectDown).
|
||||
|
||||
// Deleting a range that includes the current branch is not possible
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().
|
||||
Menu().
|
||||
Tooltip(Contains("You cannot delete the checked out branch!")).
|
||||
Title(Equals("Delete selected branches?")).
|
||||
Select(Contains("Delete local branches")).
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Contains("You cannot delete the checked out branch!"))
|
||||
}).
|
||||
Cancel()
|
||||
}).
|
||||
|
||||
// Delete branch-03 and branch-04. 04 is not fully merged, so we get
|
||||
// a confirmation popup.
|
||||
NavigateToLine(Contains("branch-03")).
|
||||
Press(keys.Universal.RangeSelectDown).
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().
|
||||
Menu().
|
||||
Title(Equals("Delete selected branches?")).
|
||||
Select(Contains("Delete local branches")).
|
||||
Confirm()
|
||||
t.ExpectPopup().
|
||||
Confirmation().
|
||||
Title(Equals("Force delete branch")).
|
||||
Content(Equals("Some of the selected branches are not fully merged. Are you sure you want to delete them?")).
|
||||
Confirm()
|
||||
}).
|
||||
Lines(
|
||||
Contains("current-head"),
|
||||
Contains("branch-01 ↑1"),
|
||||
Contains("branch-02 ✓"),
|
||||
Contains("branch-05 ✓").IsSelected(),
|
||||
Contains("branch-06 ↑1"),
|
||||
Contains("master"),
|
||||
).
|
||||
|
||||
// Delete remote branches of branch-05 and branch-06. They are on different remotes.
|
||||
NavigateToLine(Contains("branch-05")).
|
||||
Press(keys.Universal.RangeSelectDown).
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().
|
||||
Menu().
|
||||
Title(Equals("Delete selected branches?")).
|
||||
Select(Contains("Delete remote branches")).
|
||||
Confirm()
|
||||
}).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().
|
||||
Confirmation().
|
||||
Title(Equals("Delete selected branches?")).
|
||||
Content(Equals("Are you sure you want to delete the remote branches of the selected branches from their respective remotes?")).
|
||||
Confirm()
|
||||
}).
|
||||
Tap(func() {
|
||||
checkRemoteBranches(t, keys, "origin", []string{
|
||||
"branch-01",
|
||||
"branch-02",
|
||||
"branch-06",
|
||||
})
|
||||
checkRemoteBranches(t, keys, "other-remote", []string{
|
||||
"branch-04",
|
||||
})
|
||||
}).
|
||||
Lines(
|
||||
Contains("current-head"),
|
||||
Contains("branch-01 ↑1"),
|
||||
Contains("branch-02 ✓"),
|
||||
Contains("branch-05 (upstream gone)").IsSelected(),
|
||||
Contains("branch-06 (upstream gone)").IsSelected(),
|
||||
Contains("master"),
|
||||
).
|
||||
|
||||
// Try to delete both local and remote branches of branch-02 and
|
||||
// branch-05; not possible because branch-05's upstream is gone
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().
|
||||
Menu().
|
||||
Title(Equals("Delete selected branches?")).
|
||||
Select(Contains("Delete local and remote branches")).
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Contains("Some of the selected branches have no upstream (or the upstream is not stored locally)"))
|
||||
}).
|
||||
Cancel()
|
||||
}).
|
||||
|
||||
// Delete both local and remote branches of branch-01 and branch-02. We get
|
||||
// the force-delete warning because branch-01 it is not fully merged.
|
||||
NavigateToLine(Contains("branch-01")).
|
||||
Press(keys.Universal.RangeSelectDown).
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().
|
||||
Menu().
|
||||
Title(Equals("Delete selected branches?")).
|
||||
Select(Contains("Delete local and remote branches")).
|
||||
Confirm()
|
||||
t.ExpectPopup().
|
||||
Confirmation().
|
||||
Title(Equals("Delete local and remote branch")).
|
||||
Content(Contains("Are you sure you want to delete both the selected branches from your machine, and their remote branches from their respective remotes?").
|
||||
Contains("Some of the selected branches are not fully merged. Are you sure you want to delete them?")).
|
||||
Confirm()
|
||||
}).
|
||||
Lines(
|
||||
Contains("current-head"),
|
||||
Contains("branch-05 (upstream gone)").IsSelected(),
|
||||
Contains("branch-06 (upstream gone)"),
|
||||
Contains("master"),
|
||||
).
|
||||
Tap(func() {
|
||||
checkRemoteBranches(t, keys, "origin", []string{
|
||||
"branch-06",
|
||||
})
|
||||
checkRemoteBranches(t, keys, "other-remote", []string{
|
||||
"branch-04",
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
@ -42,6 +42,7 @@ var tests = []*components.IntegrationTest{
|
||||
branch.CheckoutByName,
|
||||
branch.CreateTag,
|
||||
branch.Delete,
|
||||
branch.DeleteMultiple,
|
||||
branch.DeleteRemoteBranchWithCredentialPrompt,
|
||||
branch.DeleteRemoteBranchWithDifferentName,
|
||||
branch.DetachedHead,
|
||||
|
Loading…
x
Reference in New Issue
Block a user