mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
The rule below the header of a branch's pull request is drawn as wide as the main view. Its width is read when the render is asked for, before the layout has settled it. If the layout then changes the width of the main view, for example because it unsplits the view for the content that comes next, the rule comes out as wide as the view used to be. Make a prefix a function that is called with the width the render is laid out to, once the layout has run. The function it returns produces the text on the render's own goroutine before the command starts. This way a prefix that takes a while to produce, such as one that runs commands of its own, holds up only the render and not the UI. All prefixes are still static here; the next commit lays out the rule of the pull request header to the width. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
894 lines
30 KiB
Go
894 lines
30 KiB
Go
package controllers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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/presentation"
|
|
"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(
|
|
c,
|
|
c.Contexts().Branches,
|
|
c.Contexts().Branches.GetSelected,
|
|
c.Contexts().Branches.GetSelectedItems,
|
|
),
|
|
}
|
|
}
|
|
|
|
func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
return []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(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,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.New),
|
|
Handler: self.withItem(self.newBranch),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.NewBranch,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.MoveCommitsToNewBranch),
|
|
Handler: self.c.Helpers().Refs.MoveCommitsToNewBranch,
|
|
GetDisabledReason: self.c.Helpers().Refs.CanMoveCommitsToNewBranch,
|
|
Description: self.c.Tr.MoveCommitsToNewBranch,
|
|
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.NewWorktree),
|
|
Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForBranch),
|
|
Description: self.c.Tr.NewWorktree,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.CreatePullRequest),
|
|
Handler: self.withItem(self.handleCreatePullRequest),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.CreatePullRequest,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.ViewPullRequestOptions),
|
|
Handler: self.withItem(self.handleCreatePullRequestMenu),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.CreatePullRequestOptions,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.OpenPullRequestInBrowser),
|
|
Handler: self.withItem(self.openPRInBrowser),
|
|
GetDisabledReason: self.require(self.singleItemSelected(self.branchHasPR)),
|
|
Description: self.c.Tr.OpenPullRequestInBrowser,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.CopyPullRequestURL),
|
|
Handler: self.copyPullRequestURL,
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.CopyPullRequestURL,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.CheckoutBranchByName),
|
|
Handler: self.checkoutByName,
|
|
Description: self.c.Tr.CheckoutByName,
|
|
Tooltip: self.c.Tr.CheckoutByNameTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.CheckoutPreviousBranch),
|
|
Handler: self.checkoutPreviousBranch,
|
|
Description: self.c.Tr.CheckoutPreviousBranch,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.ForceCheckoutBranch),
|
|
Handler: self.forceCheckout,
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.ForceCheckout,
|
|
Tooltip: self.c.Tr.ForceCheckoutTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
|
Handler: self.withItems(self.delete),
|
|
GetDisabledReason: self.require(self.itemRangeSelected(self.branchesAreReal)),
|
|
Description: self.c.Tr.Delete,
|
|
Tooltip: self.c.Tr.BranchDeleteTooltip,
|
|
OpensMenu: true,
|
|
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,
|
|
OpensMenu: true,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.MergeIntoCurrentBranch),
|
|
Handler: opts.Guards.OutsideFilterMode(self.merge),
|
|
GetDisabledReason: self.require(self.singleItemSelected(self.notMergingIntoYourself)),
|
|
Description: self.c.Tr.Merge,
|
|
Tooltip: self.c.Tr.MergeBranchTooltip,
|
|
DisplayOnScreen: true,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.FastForward),
|
|
Handler: self.withItems(self.fastForward),
|
|
GetDisabledReason: self.require(self.itemRangeSelected(self.branchesAreReal)),
|
|
Description: self.c.Tr.FastForward,
|
|
Tooltip: self.c.Tr.FastForwardTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.CreateTag),
|
|
Handler: self.withItem(self.createTag),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.NewTag,
|
|
},
|
|
{
|
|
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,
|
|
OpensMenu: true,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Branches.RenameBranch),
|
|
Handler: self.withItem(self.rename),
|
|
GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)),
|
|
Description: self.c.Tr.RenameBranch,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(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,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.OpenDiffTool),
|
|
Handler: self.withItem(func(selectedBranch *models.Branch) error {
|
|
return self.c.Helpers().Diff.OpenDiffToolForRef(selectedBranch)
|
|
}),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.OpenDiffTool,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (self *BranchesController) GetOnRenderToMain() func() {
|
|
return func() {
|
|
self.c.Helpers().Diff.WithDiffModeCheck(func() {
|
|
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())
|
|
|
|
rendererTask := types.NewRunDiffRendererTask(cmdObj.GetCmd())
|
|
task = rendererTask
|
|
|
|
pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name)
|
|
if ok && presentation.ShouldShowPrForBranch(pr, branch.Name, self.c.UserConfig()) {
|
|
rendererTask.Prefix = types.StaticPrefix(presentation.FormatPullRequestHeader(pr, self.c.Tr) +
|
|
strings.Repeat("─", self.c.Contexts().Normal.GetView().InnerWidth()) + "\n")
|
|
}
|
|
}
|
|
|
|
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 {
|
|
upstream := lo.Ternary(selectedBranch.RemoteBranchStoredLocally(),
|
|
selectedBranch.ShortUpstreamRefName(),
|
|
self.c.Tr.UpstreamGenericName)
|
|
|
|
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(), upstream),
|
|
RefToShowDivergenceFrom: branch.FullUpstreamRefName(),
|
|
Context: self.context(),
|
|
ShowBranchHeads: false,
|
|
})
|
|
},
|
|
}
|
|
|
|
var disabledReason *types.DisabledReason
|
|
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(selectedBranch, self.c.Model().MainBranches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if baseBranch == "" {
|
|
baseBranch = self.c.Tr.CouldNotDetermineBaseBranch
|
|
disabledReason = &types.DisabledReason{Text: self.c.Tr.CouldNotDetermineBaseBranch}
|
|
}
|
|
shortBaseBranchName := helpers.ShortBranchName(baseBranch)
|
|
label := utils.ResolvePlaceholderString(
|
|
self.c.Tr.ViewDivergenceFromBaseBranch,
|
|
map[string]string{"baseBranch": shortBaseBranchName},
|
|
)
|
|
viewDivergenceFromBaseBranchItem := &types.MenuItem{
|
|
LabelColumns: []string{label},
|
|
Keys: menuKey('b'),
|
|
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(), shortBaseBranchName),
|
|
RefToShowDivergenceFrom: baseBranch,
|
|
Context: self.context(),
|
|
ShowBranchHeads: false,
|
|
})
|
|
},
|
|
DisabledReason: disabledReason,
|
|
}
|
|
|
|
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
|
|
}
|
|
self.c.Refresh(types.RefreshOptions{
|
|
Scope: []types.RefreshableView{
|
|
types.BRANCHES,
|
|
types.COMMITS,
|
|
},
|
|
})
|
|
return nil
|
|
},
|
|
Keys: menuKey('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
|
|
}
|
|
self.c.Refresh(types.RefreshOptions{
|
|
Scope: []types.RefreshableView{
|
|
types.BRANCHES,
|
|
types.COMMITS,
|
|
},
|
|
})
|
|
return nil
|
|
})
|
|
},
|
|
Keys: menuKey('s'),
|
|
}
|
|
|
|
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 {
|
|
// We only can invoke this when the remote branch is stored locally, so using the selectedBranch here is fine.
|
|
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream, selectedBranch.FullUpstreamRefName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
Tooltip: upstreamResetTooltip,
|
|
Keys: menuKey('g'),
|
|
}
|
|
|
|
upstreamRebaseItem := &types.MenuItem{
|
|
LabelColumns: []string{upstreamRebaseOptions},
|
|
OpensMenu: true,
|
|
OnPress: func() error {
|
|
if err := self.c.Helpers().MergeAndRebase.RebaseOntoRef(upstream); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
Tooltip: upstreamRebaseTooltip,
|
|
Keys: menuKey('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,
|
|
viewDivergenceFromBaseBranchItem,
|
|
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 errors.New(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.ConfirmIf(!self.c.UserConfig().Gui.SkipSwitchWorktreeOnCheckoutWarning, 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 errors.New(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) getPullRequestURL() (string, error) {
|
|
branch := self.context().GetSelected()
|
|
if pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name); ok {
|
|
return pr.Url, nil
|
|
}
|
|
|
|
branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name)
|
|
|
|
if !branchExistsOnRemote {
|
|
return "", errors.New(self.c.Tr.NoBranchOnRemote)
|
|
}
|
|
|
|
return self.c.Helpers().Host.GetPullRequestURL(branch.Name, "")
|
|
}
|
|
|
|
func (self *BranchesController) copyPullRequestURL() error {
|
|
url, err := self.getPullRequestURL()
|
|
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
|
|
|
|
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
|
|
}
|
|
self.c.Refresh(types.RefreshOptions{})
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *BranchesController) checkoutPreviousBranch() error {
|
|
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
|
|
return self.c.Helpers().Refs.CheckoutPreviousRef()
|
|
}
|
|
|
|
func (self *BranchesController) checkoutByName() error {
|
|
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 {
|
|
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)
|
|
},
|
|
})
|
|
|
|
return nil
|
|
},
|
|
})
|
|
},
|
|
},
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
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.c.Refresh(types.RefreshOptions{
|
|
BranchSelection: types.SelectCheckedOutBranch,
|
|
CommitSelection: types.SelectHeadCommit,
|
|
SelectTopReflogCommit: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (self *BranchesController) localDelete(branches []*models.Branch) error {
|
|
return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branches)
|
|
}
|
|
|
|
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, false)
|
|
}
|
|
|
|
func (self *BranchesController) localAndRemoteDelete(branches []*models.Branch) error {
|
|
return self.c.Helpers().BranchesHelper.ConfirmLocalAndRemoteDelete(branches)
|
|
}
|
|
|
|
func (self *BranchesController) delete(branches []*models.Branch) error {
|
|
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
|
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: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteLocalBranches, self.c.Tr.DeleteLocalBranch),
|
|
Keys: menuKey('c'),
|
|
OnPress: func() error {
|
|
return self.localDelete(branches)
|
|
},
|
|
}
|
|
if isBranchCheckedOut {
|
|
localDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
|
|
}
|
|
|
|
remoteDeleteItem := &types.MenuItem{
|
|
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteRemoteBranches, self.c.Tr.DeleteRemoteBranch),
|
|
Keys: menuKey('r'),
|
|
OnPress: func() error {
|
|
return self.remoteDelete(branches)
|
|
},
|
|
}
|
|
if !hasUpstream {
|
|
remoteDeleteItem.DisabledReason = &types.DisabledReason{
|
|
Text: lo.Ternary(len(branches) > 1, self.c.Tr.UpstreamsNotSetError, self.c.Tr.UpstreamNotSetError),
|
|
}
|
|
}
|
|
|
|
deleteBothItem := &types.MenuItem{
|
|
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteLocalAndRemoteBranches, self.c.Tr.DeleteLocalAndRemoteBranch),
|
|
Keys: menuKey('b'),
|
|
OnPress: func() error {
|
|
return self.localAndRemoteDelete(branches)
|
|
},
|
|
}
|
|
if isBranchCheckedOut {
|
|
deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
|
|
} else if !hasUpstream {
|
|
deleteBothItem.DisabledReason = &types.DisabledReason{
|
|
Text: lo.Ternary(len(branches) > 1, self.c.Tr.UpstreamsNotSetError, self.c.Tr.UpstreamNotSetError),
|
|
}
|
|
}
|
|
|
|
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,
|
|
Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem, deleteBothItem},
|
|
})
|
|
}
|
|
|
|
func (self *BranchesController) merge() error {
|
|
selectedBranchName := self.context().GetSelected().Name
|
|
return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName)
|
|
}
|
|
|
|
func (self *BranchesController) rebase(branch *models.Branch) error {
|
|
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(branch.Name)
|
|
}
|
|
|
|
func (self *BranchesController) fastForward(branches []*models.Branch) error {
|
|
if !lo.EveryBy(branches, func(branch *models.Branch) bool { return branch.IsTrackingRemote() }) {
|
|
return errors.New(self.c.Tr.FwdNoUpstream)
|
|
}
|
|
if !lo.EveryBy(branches, func(branch *models.Branch) bool { return branch.RemoteBranchStoredLocally() }) {
|
|
return errors.New(self.c.Tr.FwdNoLocalUpstream)
|
|
}
|
|
// A branch that is only ahead has nothing to fast-forward to. One that is
|
|
// both ahead and behind may still be reset to its upstream, so let the
|
|
// helper look into it.
|
|
if lo.SomeBy(branches, func(branch *models.Branch) bool {
|
|
return branch.IsAheadForPull() && !branch.IsBehindForPull()
|
|
}) {
|
|
return errors.New(self.c.Tr.FwdCommitsToPush)
|
|
}
|
|
|
|
return self.c.Helpers().BranchesHelper.FastForwardBranches(branches)
|
|
}
|
|
|
|
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"},
|
|
self.c.Tr.SortOrderPromptLocalBranches,
|
|
func(sortOrder string) error {
|
|
if self.c.UserConfig().Git.LocalBranchSortOrder != sortOrder {
|
|
self.c.UserConfig().Git.LocalBranchSortOrder = sortOrder
|
|
self.c.Contexts().Branches.SetSelection(0)
|
|
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}})
|
|
return nil
|
|
}
|
|
return nil
|
|
},
|
|
self.c.UserConfig().Git.LocalBranchSortOrder)
|
|
}
|
|
|
|
func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
|
|
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name, selectedBranch.FullRefName())
|
|
}
|
|
|
|
func (self *BranchesController) rename(branch *models.Branch) error {
|
|
promptForNewName := func() error {
|
|
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 and then find our branch. The branches model update is bounced
|
|
// onto the UI thread, so the re-selection (which reads Model.Branches) has to run in
|
|
// Then; reading it inline here would see the previous model.
|
|
self.c.Refresh(types.RefreshOptions{
|
|
Scope: []types.RefreshableView{types.BRANCHES, types.WORKTREES},
|
|
Then: func() error {
|
|
// 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)
|
|
self.context().HandleRender()
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
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.
|
|
return self.c.ConfirmIf(branch.IsTrackingRemote(), 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 {
|
|
if !branch.IsTrackingRemote() {
|
|
return errors.New(self.c.Tr.PullRequestNoUpstream)
|
|
}
|
|
|
|
if len(self.c.Model().Remotes) == 1 {
|
|
toRemote := self.c.Model().Remotes[0].Name
|
|
self.c.Log.Debugf("PR will target the only existing remote '%s'", toRemote)
|
|
return self.promptForTargetBranchNameAndCreatePullRequest(branch, toRemote)
|
|
}
|
|
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: self.c.Tr.SelectTargetRemote,
|
|
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(),
|
|
HandleConfirm: func(toRemote string) error {
|
|
self.c.Log.Debugf("PR will target remote '%s'", toRemote)
|
|
|
|
return self.promptForTargetBranchNameAndCreatePullRequest(branch, toRemote)
|
|
},
|
|
})
|
|
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
if selectedBranch != checkedOutBranch {
|
|
menuItems = append(menuItems,
|
|
&types.MenuItem{
|
|
LabelColumns: fromToLabelColumns(checkedOutBranch.Name, selectedBranch.Name),
|
|
OnPress: func() error {
|
|
if !checkedOutBranch.IsTrackingRemote() || !selectedBranch.IsTrackingRemote() {
|
|
return errors.New(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.Sprint(self.c.Tr.CreatePullRequestOptions), Items: menuItems})
|
|
}
|
|
|
|
func (self *BranchesController) promptForTargetBranchNameAndCreatePullRequest(fromBranch *models.Branch, toRemote string) error {
|
|
remoteDoesNotExist := lo.NoneBy(self.c.Model().Remotes, func(remote *models.Remote) bool {
|
|
return remote.Name == toRemote
|
|
})
|
|
if remoteDoesNotExist {
|
|
return fmt.Errorf(self.c.Tr.NoValidRemoteName, toRemote)
|
|
}
|
|
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: fmt.Sprintf("%s → %s/", fromBranch.UpstreamBranch, toRemote),
|
|
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteBranchesForRemoteSuggestionsFunc(toRemote),
|
|
HandleConfirm: func(toBranch string) error {
|
|
self.c.Log.Debugf("PR will target branch '%s' on remote '%s'", toBranch, toRemote)
|
|
return self.createPullRequest(fromBranch.UpstreamBranch, toBranch)
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (self *BranchesController) branchHasPR(branch *models.Branch) *types.DisabledReason {
|
|
return self.c.Helpers().Host.NoPullRequestDisabledReason(branch.Name)
|
|
}
|
|
|
|
func (self *BranchesController) openPRInBrowser(branch *models.Branch) error {
|
|
pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name)
|
|
if !ok {
|
|
// Should be guarded against by the DisabledReason check, but be defensive in case
|
|
// PullRequestsMap was updated concurrently by a background refresh
|
|
return errors.New(self.c.Tr.NoPullRequestForBranch)
|
|
}
|
|
|
|
self.c.LogAction(self.c.Tr.Actions.OpenPullRequest)
|
|
|
|
return self.c.OS().OpenLink(pr.Url)
|
|
}
|
|
|
|
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
|
|
|
|
if checkedOutBranch == selectedBranchName {
|
|
return &types.DisabledReason{Text: self.c.Tr.CantMergeBranchIntoItself}
|
|
}
|
|
|
|
return nil
|
|
}
|