From 0df5cb1286a1f6fb442789ea8afacf7cdcd53735 Mon Sep 17 00:00:00 2001 From: Federico Date: Thu, 10 Aug 2023 09:39:26 +0200 Subject: Allow deleting remote tags/branches from local tag/branch views (#2738) --- pkg/gui/controllers.go | 1 + pkg/gui/controllers/branches_controller.go | 105 +++++++++++++++------- pkg/gui/controllers/helpers/branches_helper.go | 46 ++++++++++ pkg/gui/controllers/helpers/helpers.go | 1 + pkg/gui/controllers/remote_branches_controller.go | 22 +---- pkg/gui/controllers/tags_controller.go | 91 ++++++++++++++++--- 6 files changed, 204 insertions(+), 62 deletions(-) create mode 100644 pkg/gui/controllers/helpers/branches_helper.go (limited to 'pkg/gui') diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index cc45d833a..1430ad239 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -86,6 +86,7 @@ func (gui *Gui) resetHelpersAndControllers() { Files: helpers.NewFilesHelper(helperCommon), WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper), Tags: helpers.NewTagsHelper(helperCommon, commitsHelper), + BranchesHelper: helpers.NewBranchesHelper(helperCommon), GPG: helpers.NewGpgHelper(helperCommon), MergeAndRebase: rebaseHelper, MergeConflicts: mergeConflictsHelper, diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index e7023959f..84d9e838e 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -70,7 +70,8 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty { Key: opts.GetKey(opts.Config.Universal.Remove), Handler: self.checkSelectedAndReal(self.delete), - Description: self.c.Tr.DeleteBranch, + Description: self.c.Tr.ViewDeleteOptions, + OpensMenu: true, }, { Key: opts.GetKey(opts.Config.Branches.RebaseBranch), @@ -316,19 +317,6 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) } -func (self *BranchesController) delete(branch *models.Branch) error { - checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() - if checkedOutBranch.Name == branch.Name { - return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch) - } - - if self.checkedOutByOtherWorktree(branch) { - return self.promptWorktreeBranchDelete(branch) - } - - return self.deleteWithForce(branch, false) -} - func (self *BranchesController) checkedOutByOtherWorktree(branch *models.Branch) bool { return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees) } @@ -371,18 +359,34 @@ func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *model }) } -func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error { - title := self.c.Tr.DeleteBranch - var templateStr string - if force { - templateStr = self.c.Tr.ForceDeleteBranchMessage - } else { - templateStr = self.c.Tr.DeleteBranchMessage +func (self *BranchesController) localDelete(branch *models.Branch) error { + if self.checkedOutByOtherWorktree(branch) { + return self.promptWorktreeBranchDelete(branch) } + + return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error { + self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch) + err := self.c.Git().Branch.LocalDelete(branch.Name, false) + if err != nil && strings.Contains(err.Error(), "git branch -D ") { + return self.forceDelete(branch) + } + if err != nil { + return self.c.Error(err) + } + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) + }) +} + +func (self *BranchesController) remoteDelete(branch *models.Branch) error { + return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.Name) +} + +func (self *BranchesController) forceDelete(branch *models.Branch) error { + title := self.c.Tr.ForceDeleteBranchTitle message := utils.ResolvePlaceholderString( - templateStr, + self.c.Tr.ForceDeleteBranchMessage, map[string]string{ - "selectedBranchName": selectedBranch.Name, + "selectedBranchName": branch.Name, }, ) @@ -390,19 +394,60 @@ func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, f Title: title, Prompt: message, HandleConfirm: func() error { - self.c.LogAction(self.c.Tr.Actions.DeleteBranch) - if err := self.c.Git().Branch.Delete(selectedBranch.Name, force); err != nil { - errMessage := err.Error() - if !force && strings.Contains(errMessage, "git branch -D ") { - return self.deleteWithForce(selectedBranch, true) - } - return self.c.ErrorMsg(errMessage) + if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil { + return self.c.ErrorMsg(err.Error()) } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) }, }) } +func (self *BranchesController) delete(branch *models.Branch) error { + menuItems := []*types.MenuItem{} + checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() + + localDeleteItem := &types.MenuItem{ + Label: self.c.Tr.DeleteLocalBranch, + Key: 'c', + OnPress: func() error { + return self.localDelete(branch) + }, + } + if checkedOutBranch.Name == branch.Name { + localDeleteItem = &types.MenuItem{ + Label: self.c.Tr.DeleteLocalBranch, + Key: 'c', + Tooltip: self.c.Tr.CantDeleteCheckOutBranch, + OnPress: func() error { + return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch) + }, + } + } + menuItems = append(menuItems, localDeleteItem) + + if branch.IsTrackingRemote() && !branch.UpstreamGone { + menuItems = append(menuItems, &types.MenuItem{ + Label: self.c.Tr.DeleteRemoteBranch, + Key: 'r', + OnPress: func() error { + return self.remoteDelete(branch) + }, + }) + } + + menuTitle := utils.ResolvePlaceholderString( + self.c.Tr.DeleteBranchTitle, + map[string]string{ + "selectedBranchName": branch.Name, + }, + ) + + return self.c.Menu(types.CreateMenuOptions{ + Title: menuTitle, + Items: menuItems, + }) +} + func (self *BranchesController) merge() error { selectedBranchName := self.context().GetSelected().Name return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName) diff --git a/pkg/gui/controllers/helpers/branches_helper.go b/pkg/gui/controllers/helpers/branches_helper.go new file mode 100644 index 000000000..6bc336e8e --- /dev/null +++ b/pkg/gui/controllers/helpers/branches_helper.go @@ -0,0 +1,46 @@ +package helpers + +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type BranchesHelper struct { + c *HelperCommon +} + +func NewBranchesHelper(c *HelperCommon) *BranchesHelper { + return &BranchesHelper{ + c: c, + } +} + +func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName string) error { + title := utils.ResolvePlaceholderString( + self.c.Tr.DeleteBranchTitle, + map[string]string{ + "selectedBranchName": branchName, + }, + ) + prompt := utils.ResolvePlaceholderString( + self.c.Tr.DeleteRemoteBranchPrompt, + map[string]string{ + "selectedBranchName": branchName, + "upstream": remoteName, + }, + ) + return 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, remoteName, branchName); err != nil { + return self.c.Error(err) + } + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) + }) + }, + }) +} diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index e87b57eb0..22a7ea91b 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -22,6 +22,7 @@ type Helpers struct { Suggestions *SuggestionsHelper Files *FilesHelper WorkingTree *WorkingTreeHelper + BranchesHelper *BranchesHelper Tags *TagsHelper MergeAndRebase *MergeAndRebaseHelper MergeConflicts *MergeConflictsHelper diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index 529b00a90..ffb55d5ca 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -1,10 +1,8 @@ package controllers import ( - "fmt" "strings" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -53,7 +51,7 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts) { Key: opts.GetKey(opts.Config.Universal.Remove), Handler: self.checkSelected(self.delete), - Description: self.c.Tr.DeleteBranch, + Description: self.c.Tr.DeleteRemoteTag, }, { Key: opts.GetKey(opts.Config.Branches.SetUpstream), @@ -112,23 +110,7 @@ func (self *RemoteBranchesController) checkSelected(callback func(*models.Remote } func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error { - message := fmt.Sprintf("%s '%s'?", self.c.Tr.DeleteRemoteBranchMessage, selectedBranch.FullName()) - - return self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.DeleteRemoteBranch, - Prompt: message, - HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error { - self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch) - err := self.c.Git().Remote.DeleteRemoteBranch(task, selectedBranch.RemoteName, selectedBranch.Name) - if err != nil { - _ = self.c.Error(err) - } - - return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) - }) - }, - }) + return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranch.RemoteName, selectedBranch.Name) } func (self *RemoteBranchesController) merge(selectedBranch *models.RemoteBranch) error { diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index 80248391e..91d590c32 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -34,7 +34,8 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types. { Key: opts.GetKey(opts.Config.Universal.Remove), Handler: self.withSelectedTag(self.delete), - Description: self.c.Tr.DeleteTag, + Description: self.c.Tr.ViewDeleteOptions, + OpensMenu: true, }, { Key: opts.GetKey(opts.Config.Branches.PushTag), @@ -88,24 +89,90 @@ func (self *TagsController) checkout(tag *models.Tag) error { return self.c.PushContext(self.c.Contexts().Branches) } +func (self *TagsController) localDelete(tag *models.Tag) error { + return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(gocui.Task) error { + self.c.LogAction(self.c.Tr.Actions.DeleteLocalTag) + if err := self.c.Git().Tag.LocalDelete(tag.Name); err != nil { + return self.c.Error(err) + } + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) + }) +} + +func (self *TagsController) remoteDelete(tag *models.Tag) error { + title := utils.ResolvePlaceholderString( + self.c.Tr.SelectRemoteTagUpstream, + map[string]string{ + "tagName": tag.Name, + }, + ) + + return self.c.Prompt(types.PromptOpts{ + Title: title, + InitialContent: "origin", + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(), + HandleConfirm: func(upstream string) error { + confirmTitle := utils.ResolvePlaceholderString( + self.c.Tr.DeleteTagTitle, + map[string]string{ + "tagName": tag.Name, + }, + ) + confirmPrompt := utils.ResolvePlaceholderString( + self.c.Tr.DeleteRemoteTagPrompt, + map[string]string{ + "tagName": tag.Name, + "upstream": upstream, + }, + ) + + return self.c.Confirm(types.ConfirmOpts{ + Title: confirmTitle, + Prompt: confirmPrompt, + HandleConfirm: func() error { + return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(t gocui.Task) error { + self.c.LogAction(self.c.Tr.Actions.DeleteRemoteTag) + if err := self.c.Git().Remote.DeleteRemoteTag(t, upstream, tag.Name); err != nil { + return self.c.Error(err) + } + self.c.Toast(self.c.Tr.RemoteTagDeletedMessage) + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) + }) + }, + }) + }, + }) +} + func (self *TagsController) delete(tag *models.Tag) error { - prompt := utils.ResolvePlaceholderString( - self.c.Tr.DeleteTagPrompt, + menuTitle := utils.ResolvePlaceholderString( + self.c.Tr.DeleteTagTitle, map[string]string{ "tagName": tag.Name, }, ) - return self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.DeleteTagTitle, - Prompt: prompt, - HandleConfirm: func() error { - self.c.LogAction(self.c.Tr.Actions.DeleteTag) - if err := self.c.Git().Tag.Delete(tag.Name); err != nil { - return self.c.Error(err) - } - return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) + menuItems := []*types.MenuItem{ + { + Label: self.c.Tr.DeleteLocalTag, + Key: 'c', + OnPress: func() error { + return self.localDelete(tag) + }, }, + { + Label: self.c.Tr.DeleteRemoteTag, + Key: 'r', + OpensMenu: true, + OnPress: func() error { + return self.remoteDelete(tag) + }, + }, + } + + return self.c.Menu(types.CreateMenuOptions{ + Title: menuTitle, + Items: menuItems, }) } -- cgit v1.2.3