package controllers import ( "fmt" "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) type RemoteBranchesController struct { baseController *ListControllerTrait[*models.RemoteBranch] c *ControllerCommon } var _ types.IController = &RemoteBranchesController{} func NewRemoteBranchesController( c *ControllerCommon, ) *RemoteBranchesController { return &RemoteBranchesController{ baseController: baseController{}, ListControllerTrait: NewListControllerTrait( c, c.Contexts().RemoteBranches, c.Contexts().RemoteBranches.GetSelected, c.Contexts().RemoteBranches.GetSelectedItems, ), c: c, } } func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { return []*types.Binding{ { Keys: opts.GetKeys(opts.Config.Universal.Select), Handler: self.withItem(self.checkoutBranch), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.Checkout, Tooltip: self.c.Tr.RemoteBranchCheckoutTooltip, DisplayOnScreen: true, }, { Keys: opts.GetKeys(opts.Config.Universal.New), Handler: self.withItem(self.newLocalBranch), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.NewBranch, }, { Keys: opts.GetKeys(opts.Config.Universal.NewWorktree), Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForRemoteBranch), Description: self.c.Tr.NewWorktree, OpensMenu: true, }, { Keys: opts.GetKeys(opts.Config.Branches.MergeIntoCurrentBranch), Handler: opts.Guards.OutsideFilterMode(self.withItem(self.merge)), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.Merge, Tooltip: self.c.Tr.MergeBranchTooltip, DisplayOnScreen: true, }, { Keys: opts.GetKeys(opts.Config.Branches.RebaseBranch), Handler: opts.Guards.OutsideFilterMode(self.withItem(self.rebase)), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.RebaseBranch, Tooltip: self.c.Tr.RebaseBranchTooltip, DisplayOnScreen: true, }, { Keys: opts.GetKeys(opts.Config.Universal.Remove), Handler: self.withItems(self.delete), GetDisabledReason: self.require(self.itemRangeSelected()), Description: self.c.Tr.Delete, Tooltip: self.c.Tr.DeleteRemoteBranchTooltip, DisplayOnScreen: true, }, { Keys: opts.GetKeys(opts.Config.Branches.SetUpstream), Handler: self.withItem(self.setAsUpstream), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.SetAsUpstream, Tooltip: self.c.Tr.SetAsUpstreamTooltip, DisplayOnScreen: true, }, { Keys: opts.GetKeys(opts.Config.Branches.SortOrder), Handler: self.createSortMenu, Description: self.c.Tr.SortOrder, OpensMenu: true, }, { Keys: opts.GetKeys(opts.Config.Commits.ViewResetOptions), Handler: self.withItem(self.createResetMenu), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.ViewResetOptions, Tooltip: self.c.Tr.ResetTooltip, OpensMenu: true, }, { Keys: opts.GetKeys(opts.Config.Universal.OpenDiffTool), Handler: self.withItem(func(selectedBranch *models.RemoteBranch) error { return self.c.Helpers().Diff.OpenDiffToolForRef(selectedBranch) }), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.OpenDiffTool, }, } } func (self *RemoteBranchesController) GetOnRenderToMain() func() { return func() { self.c.Helpers().Diff.WithDiffModeCheck(func() { var task types.UpdateTask remoteBranch := self.context().GetSelected() if remoteBranch == nil { task = types.NewRenderStringTask("No branches for this remote") } else { cmdObj := self.c.Git().Branch.GetGraphCmdObj(remoteBranch.FullRefName()) task = types.NewRunCommandTask(cmdObj.GetCmd()) } self.c.RenderToMainViews(types.RefreshMainOpts{ Pair: self.c.MainViewPairs().Normal, Main: &types.ViewUpdateOpts{ Title: "Remote Branch", Task: task, }, }) }) } } func (self *RemoteBranchesController) context() *context.RemoteBranchesContext { return self.c.Contexts().RemoteBranches } func (self *RemoteBranchesController) delete(selectedBranches []*models.RemoteBranch) error { if len(selectedBranches) > 0 && selectedBranches[0].RemoteName == "git-svn" { return self.deleteSvnRemoteBranches(selectedBranches) } return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranches, true) } func (self *RemoteBranchesController) merge(selectedBranch *models.RemoteBranch) error { return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranch.FullName()) } func (self *RemoteBranchesController) rebase(selectedBranch *models.RemoteBranch) error { return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranch.FullName()) } func (self *RemoteBranchesController) createSortMenu() error { return self.c.Helpers().Refs.CreateSortOrderMenu( []string{"alphabetical", "date"}, self.c.Tr.SortOrderPromptRemoteBranches, func(sortOrder string) error { if self.c.UserConfig().Git.RemoteBranchSortOrder != sortOrder { self.c.UserConfig().Git.RemoteBranchSortOrder = sortOrder self.c.Contexts().RemoteBranches.SetSelection(0) self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.REMOTES}}) } return nil }, self.c.UserConfig().Git.RemoteBranchSortOrder) } func (self *RemoteBranchesController) createResetMenu(selectedBranch *models.RemoteBranch) error { return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.FullName(), selectedBranch.FullRefName()) } func (self *RemoteBranchesController) setAsUpstream(selectedBranch *models.RemoteBranch) error { checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() message := utils.ResolvePlaceholderString( self.c.Tr.SetUpstreamMessage, map[string]string{ "checkedOut": checkedOutBranch.Name, "selected": selectedBranch.FullName(), }, ) self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.SetUpstreamTitle, Prompt: message, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.SetBranchUpstream) if err := self.c.Git().Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil { return err } self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) return nil }, }) return nil } func (self *RemoteBranchesController) newLocalBranch(selectedBranch *models.RemoteBranch) error { // will set to the remote's branch name without the remote name nameSuggestion := strings.SplitAfterN(selectedBranch.RefName(), "/", 2)[1] return self.c.Helpers().Refs.NewBranch(selectedBranch.RefName(), selectedBranch.RefName(), nameSuggestion) } func (self *RemoteBranchesController) checkoutBranch(selectedBranch *models.RemoteBranch) error { return self.c.Helpers().Refs.CheckoutRemoteBranch(selectedBranch.FullName(), selectedBranch.Name) } func (self *RemoteBranchesController) deleteSvnRemoteBranches(selectedBranches []*models.RemoteBranch) error { var menuTitle string if len(selectedBranches) == 1 { menuTitle = utils.ResolvePlaceholderString( self.c.Tr.SvnDeleteBranchTitle, map[string]string{"selectedBranchName": selectedBranches[0].Name}, ) } else { menuTitle = self.c.Tr.SvnDeleteBranchTitle } return self.c.Menu(types.CreateMenuOptions{ Title: menuTitle, Items: []*types.MenuTitle{ { LabelColumns: []string{self.c.Tr.DeleteSvnLocalRef}, Key: 'l', OnPress: func() error { return self.deleteSvnLocalRefs(selectedBranches) }, }, { LabelColumns: []string{self.c.Tr.DeleteSvnBoth}, Key: 'b', OnPress: func() error { return self.confirmDeleteSvnBoth(selectedBranches) }, }, }, }) } func (self *RemoteBranchesController) deleteSvnLocalRefs(selectedBranches []*models.RemoteBranch) error { return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error { for _, branch := range selectedBranches { refName := branch.RemoteName + "/" + branch.Name if err := self.c.Git().Svn.DeleteLocalRef(refName); err != nil { return err } } self.c.Contexts().RemoteBranches.CollapseRangeSelectionToTop() self.c.Refresh(types.RefreshOptions{ Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}, }) return nil }) } func (self *RemoteBranchesController) confirmDeleteSvnBoth(selectedBranches []*models.RemoteBranch) error { var prompt string if len(selectedBranches) == 1 { prompt = utils.ResolvePlaceholderString( self.c.Tr.DeleteSvnBothConfirm, map[string]string{"branchPath": selectedBranches[0].Name}, ) } else { prompt = fmt.Sprintf("This will delete %d branches from both local refs and SVN server. Continue?", len(selectedBranches)) } self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.SvnDeleteBranchTitle, Prompt: prompt, HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task,) error { for _, branch := range selectedBranches { if err := self.c.Git().Svn.DeleteServerBranch(task, branch.Name); err != nil { return fmt.Errorf(self.c.Tr.SvnOperationFailed, map[string]string{"error": err.Error()}) } refName := branch.RemoteName + "/" + branch.Name _ = self.c.Git().Svn.DeleteLocalRef(refName) } self.c.Contexts().RemoteBranches.CollapseRangeSelectionToTop() self.c.Refresh(types.RefreshOptions{ Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}, }) return nil }) }, }) return nil }