package controllers import ( "errors" "fmt" "strings" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) type BranchesController struct { baseController *ListControllerTrait[*models.Branch] c *ControllerCommon } var _ types.IController = &BranchesController{} func NewBranchesController( c *ControllerCommon, ) *BranchesController { return &BranchesController{ baseController: baseController{}, c: c, ListControllerTrait: NewListControllerTrait[*models.Branch]( c, c.Contexts().Branches, c.Contexts().Branches.GetSelected, c.Contexts().Branches.GetSelectedItems, ), } } func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { return []*types.Binding{ { Key: opts.GetKey(opts.Config.Universal.Select), Handler: self.withItem(self.press), GetDisabledReason: self.require( self.singleItemSelected(), self.notPulling, ), Description: self.c.Tr.Checkout, Tooltip: self.c.Tr.CheckoutTooltip, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Universal.New), Handler: self.withItem(self.newBranch), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.NewBranch, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Branches.CreatePullRequest), Handler: self.withItem(self.handleCreatePullRequest), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.CreatePullRequest, }, { Key: opts.GetKey(opts.Config.Branches.ViewPullRequestOptions), Handler: self.withItem(self.handleCreatePullRequestMenu), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.CreatePullRequestOptions, OpensMenu: true, }, { Key: opts.GetKey(opts.Config.Branches.CopyPullRequestURL), Handler: self.copyPullRequestURL, GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.CopyPullRequestURL, }, { Key: opts.GetKey(opts.Config.Branches.CheckoutBranchByName), Handler: self.checkoutByName, Description: self.c.Tr.CheckoutByName, Tooltip: self.c.Tr.CheckoutByNameTooltip, }, { Key: opts.GetKey(opts.Config.Branches.ForceCheckoutBranch), Handler: self.forceCheckout, GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.ForceCheckout, Tooltip: self.c.Tr.ForceCheckoutTooltip, }, { Key: opts.GetKey(opts.Config.Universal.Remove), Handler: self.withItem(self.delete), GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)), Description: self.c.Tr.Delete, Tooltip: self.c.Tr.BranchDeleteTooltip, OpensMenu: true, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Branches.RebaseBranch), Handler: opts.Guards.OutsideFilterMode(self.rebase), GetDisabledReason: self.require( self.singleItemSelected(self.notRebasingOntoSelf), ), Description: self.c.Tr.RebaseBranch, Tooltip: self.c.Tr.RebaseBranchTooltip, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), Handler: opts.Guards.OutsideFilterMode(self.merge), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.Merge, Tooltip: self.c.Tr.MergeBranchTooltip, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Branches.FastForward), Handler: self.withItem(self.fastForward), GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)), Description: self.c.Tr.FastForward, Tooltip: self.c.Tr.FastForwardTooltip, }, { Key: opts.GetKey(opts.Config.Branches.CreateTag), Handler: self.withItem(self.createTag), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.NewTag, }, { Key: opts.GetKey(opts.Config.Branches.SortOrder), Handler: self.createSortMenu, Description: self.c.Tr.SortOrder, }, { Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), Handler: self.withItem(self.createResetMenu), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.ViewResetOptions, OpensMenu: true, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Branches.RenameBranch), Handler: self.withItem(self.rename), GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)), Description: self.c.Tr.RenameBranch, }, { Key: opts.GetKey(opts.Config.Branches.SetUpstream), Handler: self.withItem(self.viewUpstreamOptions), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.ViewBranchUpstreamOptions, Tooltip: self.c.Tr.ViewBranchUpstreamOptionsTooltip, ShortDescription: self.c.Tr.Upstream, OpensMenu: true, DisplayOnScreen: true, }, } } func (self *BranchesController) GetOnRenderToMain() func() error { return func() error { return self.c.Helpers().Diff.WithDiffModeCheck(func() error { var task types.UpdateTask branch := self.context().GetSelected() if branch == nil { task = types.NewRenderStringTask(self.c.Tr.NoBranchesThisRepo) } else { cmdObj := self.c.Git().Branch.GetGraphCmdObj(branch.FullRefName()) task = types.NewRunPtyTask(cmdObj.GetCmd()) } return self.c.RenderToMainViews(types.RefreshMainOpts{ Pair: self.c.MainViewPairs().Normal, Main: &types.ViewUpdateOpts{ Title: self.c.Tr.LogTitle, Task: task, }, }) }) } } func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branch) error { viewDivergenceItem := &types.MenuItem{ LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream}, OnPress: func() error { branch := self.context().GetSelected() if branch == nil { return nil } return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{ Ref: branch, TitleRef: fmt.Sprintf("%s <-> %s", branch.RefName(), branch.ShortUpstreamRefName()), RefToShowDivergenceFrom: branch.FullUpstreamRefName(), Context: self.context(), ShowBranchHeads: false, }) }, } unsetUpstreamItem := &types.MenuItem{ LabelColumns: []string{self.c.Tr.UnsetUpstream}, OnPress: func() error { if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil { return err } if err := self.c.Refresh(types.RefreshOptions{ Mode: types.SYNC, Scope: []types.RefreshableView{ types.BRANCHES, types.COMMITS, }, }); err != nil { return err } return nil }, Key: 'u', } setUpstreamItem := &types.MenuItem{ LabelColumns: []string{self.c.Tr.SetUpstream}, OnPress: func() error { return self.c.Helpers().Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error { upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) if err != nil { return err } if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil { return err } if err := self.c.Refresh(types.RefreshOptions{ Mode: types.SYNC, Scope: []types.RefreshableView{ types.BRANCHES, types.COMMITS, }, }); err != nil { return err } return nil }) }, Key: 's', } upstream := lo.Ternary(selectedBranch.RemoteBranchStoredLocally(), fmt.Sprintf("%s/%s", selectedBranch.UpstreamRemote, selectedBranch.Name), self.c.Tr.UpstreamGenericName) upstreamResetOptions := utils.ResolvePlaceholderString( self.c.Tr.ViewUpstreamResetOptions, map[string]string{"upstream": upstream}, ) upstreamResetTooltip := utils.ResolvePlaceholderString( self.c.Tr.ViewUpstreamResetOptionsTooltip, map[string]string{"upstream": upstream}, ) upstreamRebaseOptions := utils.ResolvePlaceholderString( self.c.Tr.ViewUpstreamRebaseOptions, map[string]string{"upstream": upstream}, ) upstreamRebaseTooltip := utils.ResolvePlaceholderString( self.c.Tr.ViewUpstreamRebaseOptionsTooltip, map[string]string{"upstream": upstream}, ) upstreamResetItem := &types.MenuItem{ LabelColumns: []string{upstreamResetOptions}, OpensMenu: true, OnPress: func() error { err := self.c.Helpers().Refs.CreateGitResetMenu(upstream) if err != nil { return err } return nil }, Tooltip: upstreamResetTooltip, Key: 'g', } upstreamRebaseItem := &types.MenuItem{ LabelColumns: []string{upstreamRebaseOptions}, OpensMenu: true, OnPress: func() error { if err := self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranch.ShortUpstreamRefName()); err != nil { return err } return nil }, Tooltip: upstreamRebaseTooltip, Key: 'r', } if !selectedBranch.IsTrackingRemote() { unsetUpstreamItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} } if !selectedBranch.RemoteBranchStoredLocally() { viewDivergenceItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} upstreamResetItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} upstreamRebaseItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} } options := []*types.MenuItem{ viewDivergenceItem, unsetUpstreamItem, setUpstreamItem, upstreamResetItem, upstreamRebaseItem, } return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.BranchUpstreamOptionsTitle, Items: options, }) } func (self *BranchesController) Context() types.Context { return self.context() } func (self *BranchesController) context() *context.BranchesContext { return self.c.Contexts().Branches } func (self *BranchesController) press(selectedBranch *models.Branch) error { if selectedBranch == self.c.Helpers().Refs.GetCheckedOutRef() { return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch) } worktreeForRef, ok := self.worktreeForBranch(selectedBranch) if ok && !worktreeForRef.IsCurrent { return self.promptToCheckoutWorktree(worktreeForRef) } self.c.LogAction(self.c.Tr.Actions.CheckoutBranch) return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) } func (self *BranchesController) notPulling() *types.DisabledReason { currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() if currentBranch != nil { op := self.c.State().GetItemOperation(currentBranch) if op == types.ItemOperationFastForwarding || op == types.ItemOperationPulling { return &types.DisabledReason{Text: self.c.Tr.CantCheckoutBranchWhilePulling} } } return nil } func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) { return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees) } func (self *BranchesController) promptToCheckoutWorktree(worktree *models.Worktree) error { prompt := utils.ResolvePlaceholderString(self.c.Tr.AlreadyCheckedOutByWorktree, map[string]string{ "worktreeName": worktree.Name, }) return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.SwitchToWorktree, Prompt: prompt, HandleConfirm: func() error { return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY) }, }) } func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error { if !selectedBranch.IsTrackingRemote() { return self.c.ErrorMsg(self.c.Tr.PullRequestNoUpstream) } return self.createPullRequest(selectedBranch.UpstreamBranch, "") } func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *models.Branch) error { checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() return self.createPullRequestMenu(selectedBranch, checkedOutBranch) } func (self *BranchesController) copyPullRequestURL() error { branch := self.context().GetSelected() branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name) if !branchExistsOnRemote { return errors.New(self.c.Tr.NoBranchOnRemote) } url, err := self.c.Helpers().Host.GetPullRequestURL(branch.Name, "") if err != nil { return err } self.c.LogAction(self.c.Tr.Actions.CopyPullRequestURL) if err := self.c.OS().CopyToClipboard(url); err != nil { return err } self.c.Toast(self.c.Tr.PullRequestURLCopiedToClipboard) return nil } func (self *BranchesController) forceCheckout() error { branch := self.context().GetSelected() message := self.c.Tr.SureForceCheckout title := self.c.Tr.ForceCheckoutBranch return self.c.Confirm(types.ConfirmOpts{ Title: title, Prompt: message, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.ForceCheckoutBranch) if err := self.c.Git().Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil { return err } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) }, }) } func (self *BranchesController) checkoutByName() error { return self.c.Prompt(types.PromptOpts{ Title: self.c.Tr.BranchName + ":", FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(), HandleConfirm: func(response string) error { self.c.LogAction("Checkout branch") _, branchName, found := self.c.Helpers().Refs.ParseRemoteBranchName(response) if found { return self.c.Helpers().Refs.CheckoutRemoteBranch(response, branchName) } return self.c.Helpers().Refs.CheckoutRef(response, types.CheckoutRefOptions{ OnRefNotFound: func(ref string) error { return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.BranchNotFoundTitle, Prompt: fmt.Sprintf("%s %s%s", self.c.Tr.BranchNotFoundPrompt, ref, "?"), HandleConfirm: func() error { return self.createNewBranchWithName(ref) }, }) }, }) }, }, ) } func (self *BranchesController) createNewBranchWithName(newBranchName string) error { branch := self.context().GetSelected() if branch == nil { return nil } if err := self.c.Git().Branch.New(newBranchName, branch.FullRefName()); err != nil { return err } self.context().SetSelection(0) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, KeepBranchSelectionIndex: true}) } func (self *BranchesController) checkedOutByOtherWorktree(branch *models.Branch) bool { return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees) } func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *models.Branch) error { worktree, ok := self.worktreeForBranch(selectedBranch) if !ok { self.c.Log.Error("promptWorktreeBranchDelete out of sync with list of worktrees") return nil } // TODO: i18n title := utils.ResolvePlaceholderString(self.c.Tr.BranchCheckedOutByWorktree, map[string]string{ "worktreeName": worktree.Name, "branchName": selectedBranch.Name, }) return self.c.Menu(types.CreateMenuOptions{ Title: title, Items: []*types.MenuItem{ { Label: self.c.Tr.SwitchToWorktree, OnPress: func() error { return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY) }, }, { Label: self.c.Tr.DetachWorktree, Tooltip: self.c.Tr.DetachWorktreeTooltip, OnPress: func() error { return self.c.Helpers().Worktree.Detach(worktree) }, }, { Label: self.c.Tr.RemoveWorktree, OnPress: func() error { return self.c.Helpers().Worktree.Remove(worktree, false) }, }, }, }) } 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 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( self.c.Tr.ForceDeleteBranchMessage, map[string]string{ "selectedBranchName": branch.Name, }, ) return self.c.Confirm(types.ConfirmOpts{ Title: title, Prompt: message, HandleConfirm: func() error { 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 { 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.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch} } remoteDeleteItem := &types.MenuItem{ Label: self.c.Tr.DeleteRemoteBranch, Key: 'r', OnPress: func() error { return self.remoteDelete(branch) }, } if !branch.IsTrackingRemote() || branch.UpstreamGone { remoteDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} } menuTitle := utils.ResolvePlaceholderString( self.c.Tr.DeleteBranchTitle, map[string]string{ "selectedBranchName": branch.Name, }, ) return self.c.Menu(types.CreateMenuOptions{ Title: menuTitle, Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem}, }) } func (self *BranchesController) merge() error { selectedBranchName := self.context().GetSelected().Name return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName) } func (self *BranchesController) rebase() error { selectedBranchName := self.context().GetSelected().Name return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName) } func (self *BranchesController) notRebasingOntoSelf(branch *models.Branch) *types.DisabledReason { selectedBranchName := branch.Name checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name if selectedBranchName == checkedOutBranch { return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf} } return nil } func (self *BranchesController) fastForward(branch *models.Branch) error { if !branch.IsTrackingRemote() { return self.c.ErrorMsg(self.c.Tr.FwdNoUpstream) } if !branch.RemoteBranchStoredLocally() { return self.c.ErrorMsg(self.c.Tr.FwdNoLocalUpstream) } if branch.HasCommitsToPush() { return self.c.ErrorMsg(self.c.Tr.FwdCommitsToPush) } action := self.c.Tr.Actions.FastForwardBranch return self.c.WithInlineStatus(branch, types.ItemOperationFastForwarding, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error { worktree, ok := self.worktreeForBranch(branch) if ok { self.c.LogAction(action) worktreeGitDir := "" // if it is the current worktree path, no need to specify the path if !worktree.IsCurrent { worktreeGitDir = worktree.GitDir } err := self.c.Git().Sync.Pull( task, git_commands.PullOptions{ RemoteName: branch.UpstreamRemote, BranchName: branch.UpstreamBranch, FastForwardOnly: true, WorktreeGitDir: worktreeGitDir, }, ) _ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return err } else { self.c.LogAction(action) err := self.c.Git().Sync.FastForward( task, branch.Name, branch.UpstreamRemote, branch.UpstreamBranch, ) _ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) return err } }) } func (self *BranchesController) createTag(branch *models.Branch) error { return self.c.Helpers().Tags.OpenCreateTagPrompt(branch.FullRefName(), func() {}) } func (self *BranchesController) createSortMenu() error { return self.c.Helpers().Refs.CreateSortOrderMenu([]string{"recency", "alphabetical", "date"}, func(sortOrder string) error { if self.c.GetAppState().LocalBranchSortOrder != sortOrder { self.c.GetAppState().LocalBranchSortOrder = sortOrder self.c.SaveAppStateAndLogError() self.c.Contexts().Branches.SetSelection(0) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) } return nil }) } func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error { return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name) } func (self *BranchesController) rename(branch *models.Branch) error { promptForNewName := func() error { return self.c.Prompt(types.PromptOpts{ Title: self.c.Tr.NewBranchNamePrompt + " " + branch.Name + ":", InitialContent: branch.Name, HandleConfirm: func(newBranchName string) error { self.c.LogAction(self.c.Tr.Actions.RenameBranch) if err := self.c.Git().Branch.Rename(branch.Name, helpers.SanitizedBranchName(newBranchName)); err != nil { return err } // need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch _ = self.c.Refresh(types.RefreshOptions{ Mode: types.SYNC, Scope: []types.RefreshableView{types.BRANCHES, types.WORKTREES}, }) // now that we've got our stuff again we need to find that branch and reselect it. for i, newBranch := range self.c.Model().Branches { if newBranch.Name == newBranchName { self.context().SetSelection(i) if err := self.context().HandleRender(); err != nil { return err } } } return nil }, }) } // I could do an explicit check here for whether the branch is tracking a remote branch // but if we've selected it we'll already know that via Pullables and Pullables. // Bit of a hack but I'm lazy. if !branch.IsTrackingRemote() { return promptForNewName() } return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.RenameBranch, Prompt: self.c.Tr.RenameBranchWarning, HandleConfirm: promptForNewName, }) } func (self *BranchesController) newBranch(selectedBranch *models.Branch) error { return self.c.Helpers().Refs.NewBranch(selectedBranch.FullRefName(), selectedBranch.RefName(), "") } func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error { menuItems := make([]*types.MenuItem, 0, 4) fromToLabelColumns := func(from string, to string) []string { return []string{fmt.Sprintf("%s → %s", from, to)} } menuItemsForBranch := func(branch *models.Branch) []*types.MenuItem { return []*types.MenuItem{ { LabelColumns: fromToLabelColumns(branch.Name, self.c.Tr.DefaultBranch), OnPress: func() error { return self.handleCreatePullRequest(branch) }, }, { LabelColumns: fromToLabelColumns(branch.Name, self.c.Tr.SelectBranch), OnPress: func() error { return self.c.Prompt(types.PromptOpts{ Title: branch.Name + " →", FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteBranchesSuggestionsFunc("/"), HandleConfirm: func(targetBranchName string) error { return self.createPullRequest(branch.Name, targetBranchName) }, }) }, }, } } if selectedBranch != checkedOutBranch { menuItems = append(menuItems, &types.MenuItem{ LabelColumns: fromToLabelColumns(checkedOutBranch.Name, selectedBranch.Name), OnPress: func() error { if !checkedOutBranch.IsTrackingRemote() || !selectedBranch.IsTrackingRemote() { return self.c.ErrorMsg(self.c.Tr.PullRequestNoUpstream) } return self.createPullRequest(checkedOutBranch.UpstreamBranch, selectedBranch.UpstreamBranch) }, }, ) menuItems = append(menuItems, menuItemsForBranch(checkedOutBranch)...) } menuItems = append(menuItems, menuItemsForBranch(selectedBranch)...) return self.c.Menu(types.CreateMenuOptions{Title: fmt.Sprintf(self.c.Tr.CreatePullRequestOptions), Items: menuItems}) } func (self *BranchesController) createPullRequest(from string, to string) error { url, err := self.c.Helpers().Host.GetPullRequestURL(from, to) if err != nil { return err } self.c.LogAction(self.c.Tr.Actions.OpenPullRequest) if err := self.c.OS().OpenLink(url); err != nil { return err } return nil } func (self *BranchesController) branchIsReal(branch *models.Branch) *types.DisabledReason { if !branch.IsRealBranch() { return &types.DisabledReason{Text: self.c.Tr.SelectedItemIsNotABranch} } return nil }