mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-06 14:06:32 -04:00
When somebody else rebases a stack and force-pushes it, pulling the topmost branch leaves the branches below it diverged. They can be updated with `f`, but pushing the stack offers to push the branches below along with the current one, and pulling should be symmetric. When pulling, find the branches stacked below the current one that can be updated without losing anything. These are those that are behind their upstream branches, and those that diverged from them only because they were rewritten. If there are any, show a menu that lists them and offers to update them in addition to pulling the current branch, or to pull only the current branch. The current branch is pulled as before. The decision is based on the last fetch, not on a new one, so that the menu appears right away. If the remote has changed since then, a branch might not be offered although it could be updated; it then shows as diverged after the pull, and pulling again offers it. The update itself fetches the upstream branches and checks them again, so it never loses commits. Branches that are checked out in another worktree are not offered, since updating them would change the files there. The branches below are updated before the current branch is pulled. If one of them pointed into the commits that a rebasing pull rebases, the pull would move it when rebase.updateRefs is set, and updating it afterwards would fail. If updating them fails, the current branch isn't pulled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
528 lines
18 KiB
Go
528 lines
18 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/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type SyncController struct {
|
|
baseController
|
|
c *ControllerCommon
|
|
}
|
|
|
|
var _ types.IController = &SyncController{}
|
|
|
|
func NewSyncController(
|
|
common *ControllerCommon,
|
|
) *SyncController {
|
|
return &SyncController{
|
|
baseController: baseController{},
|
|
c: common,
|
|
}
|
|
}
|
|
|
|
func (self *SyncController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
bindings := []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Push),
|
|
Handler: opts.Guards.NoPopupPanel(self.HandlePush),
|
|
GetDisabledReason: self.getDisabledReasonForPushOrPull,
|
|
Description: self.c.Tr.Push,
|
|
Tooltip: self.c.Tr.PushTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Pull),
|
|
Handler: opts.Guards.NoPopupPanel(self.HandlePull),
|
|
GetDisabledReason: self.getDisabledReasonForPushOrPull,
|
|
Description: self.c.Tr.Pull,
|
|
Tooltip: self.c.Tr.PullTooltip,
|
|
},
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func (self *SyncController) Context() types.Context {
|
|
return nil
|
|
}
|
|
|
|
func (self *SyncController) HandlePush() error {
|
|
return self.branchCheckedOut(self.push)()
|
|
}
|
|
|
|
func (self *SyncController) HandlePull() error {
|
|
return self.branchCheckedOut(self.pull)()
|
|
}
|
|
|
|
func (self *SyncController) getDisabledReasonForPushOrPull() *types.DisabledReason {
|
|
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
|
if currentBranch != nil {
|
|
op := self.c.State().GetItemOperation(currentBranch)
|
|
if op != types.ItemOperationNone {
|
|
return &types.DisabledReason{Text: self.c.Tr.CantPullOrPushSameBranchTwice}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error {
|
|
return func() error {
|
|
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
|
if currentBranch == nil {
|
|
// need to wait for branches to refresh
|
|
return nil
|
|
}
|
|
|
|
return f(currentBranch)
|
|
}
|
|
}
|
|
|
|
func (self *SyncController) push(currentBranch *models.Branch) error {
|
|
branchesBelow := self.unpushedBranchesBelow(currentBranch)
|
|
if len(branchesBelow) == 0 {
|
|
return self.pushCurrentBranch(currentBranch)
|
|
}
|
|
|
|
branchName := map[string]string{"branchName": currentBranch.Name}
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: self.c.Tr.Push,
|
|
Prompt: self.branchesBelowPrompt(currentBranch, branchesBelow),
|
|
Items: []*types.MenuItem{
|
|
{
|
|
Label: utils.ResolvePlaceholderString(self.c.Tr.PushBranchAndBranchesBelow, branchName),
|
|
OnPress: func() error {
|
|
return self.pushWithBranchesBelow(currentBranch, branchesBelow)
|
|
},
|
|
},
|
|
{
|
|
Label: utils.ResolvePlaceholderString(self.c.Tr.PushOnlyCurrentBranch, branchName),
|
|
OnPress: func() error {
|
|
return self.pushCurrentBranch(currentBranch)
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) pushCurrentBranch(currentBranch *models.Branch) error {
|
|
return self.resolvePushOfCurrentBranch(currentBranch, func(opts pushOpts) error {
|
|
// if we are behind our upstream branch we'll ask if the user wants to force push
|
|
if currentBranch.IsBehindForPush() {
|
|
return self.requestToForcePush(currentBranch, opts)
|
|
}
|
|
|
|
return self.pushAux(currentBranch, opts)
|
|
})
|
|
}
|
|
|
|
// The branches stacked below the current one that have commits to push
|
|
func (self *SyncController) unpushedBranchesBelow(currentBranch *models.Branch) []*models.Branch {
|
|
branchesBelow := helpers.BranchesBelowInStack(
|
|
self.c.Model().Commits, self.c.Model().Branches, currentBranch, self.c.UserConfig().Git.MainBranches)
|
|
|
|
return lo.Filter(branchesBelow, func(branch *models.Branch, _ int) bool {
|
|
// Pushing a branch whose remote branch was deleted would recreate it.
|
|
// A branch that is only behind its remote branch has nothing to push,
|
|
// and force-pushing it would move the remote branch back to an older
|
|
// commit.
|
|
return branch.PushRemote != "" && !branch.UpstreamGone && branch.IsAheadForPush()
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) branchesBelowPrompt(currentBranch *models.Branch, branchesBelow []*models.Branch) string {
|
|
intro := utils.ResolvePlaceholderString(
|
|
self.c.Tr.BranchesBelowHaveCommitsToPush,
|
|
map[string]string{"branchName": currentBranch.Name},
|
|
)
|
|
lines := lo.Map(branchesBelow, func(branch *models.Branch, _ int) string {
|
|
divergence := "↑" + branch.AheadForPush
|
|
if branch.IsBehindForPush() {
|
|
divergence = "↓" + branch.BehindForPush + divergence
|
|
}
|
|
return fmt.Sprintf("%s %s", branch.Name, style.FgYellow.Sprint(divergence))
|
|
})
|
|
|
|
return intro + "\n\n " + strings.Join(lines, "\n ")
|
|
}
|
|
|
|
// Pushes the current branch and the branches stacked below it, after asking
|
|
// for confirmation if any of them has to be force-pushed
|
|
func (self *SyncController) pushWithBranchesBelow(currentBranch *models.Branch, branchesBelow []*models.Branch) error {
|
|
if currentBranch.RemoteBranchStoredLocally() && currentBranch.PushRemote != "" {
|
|
// We know where the current branch goes and whether it needs to be
|
|
// forced, so it is pushed like the branches below it, in the same
|
|
// command as those that go to the same remote
|
|
branches := append([]*models.Branch{currentBranch}, branchesBelow...)
|
|
return self.confirmForcePushIfNeeded(branches, func(forceWithLease bool) error {
|
|
return self.pushBranchesAux(currentBranch, branchesBelow, forceWithLease)
|
|
})
|
|
}
|
|
|
|
// The current branch has no upstream yet, or its remote branch isn't
|
|
// stored locally. Push it the way it is pushed on its own, and the branches
|
|
// below it after it.
|
|
return self.resolvePushOfCurrentBranch(currentBranch, func(opts pushOpts) error {
|
|
return self.confirmForcePushIfNeeded(branchesBelow, func(forceWithLease bool) error {
|
|
opts.branchesBelow = branchesBelow
|
|
opts.forceWithLeaseBelow = forceWithLease
|
|
return self.pushAux(currentBranch, opts)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Calls push right away if none of the branches has diverged from its remote
|
|
// branch, and after the user confirmed force-pushing otherwise
|
|
func (self *SyncController) confirmForcePushIfNeeded(branches []*models.Branch, push func(forceWithLease bool) error) error {
|
|
diverged := lo.Filter(branches, func(branch *models.Branch, _ int) bool {
|
|
return branch.IsBehindForPush()
|
|
})
|
|
if len(diverged) == 0 {
|
|
return push(false)
|
|
}
|
|
|
|
if self.c.UserConfig().Git.DisableForcePushing {
|
|
return errors.New(self.c.Tr.ForcePushBranchesDisabled)
|
|
}
|
|
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.ForcePush,
|
|
Prompt: utils.ResolvePlaceholderString(
|
|
self.c.Tr.ForcePushBranchesPrompt,
|
|
map[string]string{
|
|
"branches": " " + strings.Join(lo.Map(diverged, func(branch *models.Branch, _ int) string { return branch.Name }), "\n "),
|
|
"cancelKey": self.c.UserConfig().Keybinding.Universal.Return.String(),
|
|
"confirmKey": self.c.UserConfig().Keybinding.Universal.Confirm.String(),
|
|
},
|
|
),
|
|
HandleConfirm: func() error {
|
|
return push(true)
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Works out where the current branch is pushed to: to its upstream, to a
|
|
// branch of the same name if push.default is "current", or to an upstream the
|
|
// user enters in a prompt. Calls onResolved with the options for that push.
|
|
func (self *SyncController) resolvePushOfCurrentBranch(currentBranch *models.Branch, onResolved func(pushOpts) error) error {
|
|
if currentBranch.IsTrackingRemote() {
|
|
return onResolved(pushOpts{remoteBranchStoredLocally: currentBranch.RemoteBranchStoredLocally()})
|
|
}
|
|
|
|
if self.c.Git().Config.GetPushToCurrent() {
|
|
return onResolved(pushOpts{setUpstream: true})
|
|
}
|
|
|
|
return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error {
|
|
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return onResolved(pushOpts{
|
|
setUpstream: true,
|
|
upstreamRemote: upstreamRemote,
|
|
upstreamBranch: upstreamBranch,
|
|
})
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) pull(currentBranch *models.Branch) error {
|
|
branchesBelow := self.updatableBranchesBelow(currentBranch)
|
|
if len(branchesBelow) == 0 {
|
|
return self.pullCurrentBranch(currentBranch, nil)
|
|
}
|
|
|
|
branchName := map[string]string{"branchName": currentBranch.Name}
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: self.c.Tr.Pull,
|
|
Prompt: self.branchesBelowToUpdatePrompt(currentBranch, branchesBelow),
|
|
Items: []*types.MenuItem{
|
|
{
|
|
Label: utils.ResolvePlaceholderString(self.c.Tr.PullBranchAndBranchesBelow, branchName),
|
|
OnPress: func() error {
|
|
return self.pullCurrentBranch(currentBranch, branchesBelow)
|
|
},
|
|
},
|
|
{
|
|
Label: utils.ResolvePlaceholderString(self.c.Tr.PullOnlyCurrentBranch, branchName),
|
|
OnPress: func() error {
|
|
return self.pullCurrentBranch(currentBranch, nil)
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// Pulls the current branch, after updating the given branches stacked below it
|
|
// to their upstream branches
|
|
func (self *SyncController) pullCurrentBranch(currentBranch *models.Branch, branchesBelow []*models.Branch) error {
|
|
opts := PullFilesOptions{Action: self.c.Tr.Actions.Pull, BranchesBelow: branchesBelow}
|
|
|
|
// if we have no upstream branch we need to set that first
|
|
if !currentBranch.IsTrackingRemote() {
|
|
return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error {
|
|
if err := self.setCurrentBranchUpstream(upstream); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.PullAux(currentBranch, opts)
|
|
})
|
|
}
|
|
|
|
return self.PullAux(currentBranch, opts)
|
|
}
|
|
|
|
// The branches stacked below the current one that can be updated to their
|
|
// upstream branches without losing anything: those that are behind them, and
|
|
// those that diverged from them only because they were rewritten
|
|
func (self *SyncController) updatableBranchesBelow(currentBranch *models.Branch) []*models.Branch {
|
|
branchesBelow := helpers.BranchesBelowInStack(
|
|
self.c.Model().Commits, self.c.Model().Branches, currentBranch, self.c.UserConfig().Git.MainBranches)
|
|
|
|
return lo.Filter(branchesBelow, func(branch *models.Branch, _ int) bool {
|
|
// Updating a branch that is checked out in another worktree changes
|
|
// the files there, which is more than pulling the current branch
|
|
// should do
|
|
if git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees) {
|
|
return false
|
|
}
|
|
|
|
return branch.IsBehindForPull() && (!branch.IsAheadForPull() || branch.UpstreamRewritten.Load())
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) branchesBelowToUpdatePrompt(currentBranch *models.Branch, branchesBelow []*models.Branch) string {
|
|
intro := utils.ResolvePlaceholderString(
|
|
self.c.Tr.BranchesBelowHaveChangedOnRemote,
|
|
map[string]string{"branchName": currentBranch.Name},
|
|
)
|
|
lines := lo.Map(branchesBelow, func(branch *models.Branch, _ int) string {
|
|
divergence := "↓" + branch.BehindForPull
|
|
if branch.IsAheadForPull() {
|
|
divergence += "↑" + branch.AheadForPull
|
|
}
|
|
return fmt.Sprintf("%s %s", branch.Name, style.FgYellow.Sprint(divergence))
|
|
})
|
|
|
|
return intro + "\n\n " + strings.Join(lines, "\n ")
|
|
}
|
|
|
|
func (self *SyncController) setCurrentBranchUpstream(upstream string) error {
|
|
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := self.c.Git().Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil {
|
|
if strings.Contains(err.Error(), "does not exist") {
|
|
return fmt.Errorf(
|
|
"upstream branch %s/%s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')",
|
|
upstreamRemote, upstreamBranch,
|
|
)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type PullFilesOptions struct {
|
|
UpstreamRemote string
|
|
UpstreamBranch string
|
|
FastForwardOnly bool
|
|
Action string
|
|
|
|
// Branches stacked below the current one, updated to their upstream
|
|
// branches before the current one is pulled
|
|
BranchesBelow []*models.Branch
|
|
}
|
|
|
|
func (self *SyncController) PullAux(currentBranch *models.Branch, opts PullFilesOptions) error {
|
|
var updateBranchesBelow func(gocui.Task) error
|
|
if len(opts.BranchesBelow) > 0 {
|
|
var err error
|
|
updateBranchesBelow, err = self.c.Helpers().BranchesHelper.PrepareFastForward(opts.BranchesBelow)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
branches := append([]*models.Branch{currentBranch}, opts.BranchesBelow...)
|
|
return self.c.Helpers().BranchesHelper.WithInlineStatusOnBranches(branches, types.ItemOperationPulling, func(task gocui.Task) error {
|
|
// Update the branches below first. If one of them pointed into the
|
|
// commits that a rebasing pull rebases, the pull would move it when
|
|
// rebase.updateRefs is set, and updating it afterwards would fail. If
|
|
// updating them fails, the current branch isn't pulled either; the
|
|
// user can still choose to pull only the current branch.
|
|
if updateBranchesBelow != nil {
|
|
if err := updateBranchesBelow(task); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return self.pullWithLock(task, opts)
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) pullWithLock(task gocui.Task, opts PullFilesOptions) error {
|
|
self.c.LogAction(opts.Action)
|
|
|
|
err := self.c.Git().Sync.Pull(
|
|
task,
|
|
git_commands.PullOptions{
|
|
RemoteName: opts.UpstreamRemote,
|
|
BranchName: opts.UpstreamBranch,
|
|
FastForwardOnly: opts.FastForwardOnly,
|
|
},
|
|
)
|
|
|
|
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseAndSelectHeadCommit(err)
|
|
}
|
|
|
|
type pushOpts struct {
|
|
force bool
|
|
forceWithLease bool
|
|
upstreamRemote string
|
|
upstreamBranch string
|
|
setUpstream bool
|
|
|
|
// If this is false, we can't tell ahead of time whether a force-push will
|
|
// be necessary, so we start with a normal push and offer to force-push if
|
|
// the server rejected. If this is true, we don't offer to force-push if the
|
|
// server rejected, but rather ask the user to fetch.
|
|
remoteBranchStoredLocally bool
|
|
|
|
// Branches stacked below the current one, pushed after it with one command
|
|
// per remote, with --force-with-lease if forceWithLeaseBelow is set. The
|
|
// force options above apply to the current branch's own push only.
|
|
branchesBelow []*models.Branch
|
|
forceWithLeaseBelow bool
|
|
}
|
|
|
|
func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts) error {
|
|
return self.withPushingStatus(currentBranch, opts.branchesBelow, func(task gocui.Task) error {
|
|
refspecs := []string{}
|
|
if opts.upstreamBranch != "" {
|
|
refspecs = append(refspecs, fmt.Sprintf("refs/heads/%s:%s", currentBranch.Name, opts.upstreamBranch))
|
|
}
|
|
err := self.c.Git().Sync.Push(
|
|
task,
|
|
git_commands.PushOpts{
|
|
Force: opts.force,
|
|
ForceWithLease: opts.forceWithLease,
|
|
SetUpstream: opts.setUpstream,
|
|
Remote: opts.upstreamRemote,
|
|
Refspecs: refspecs,
|
|
})
|
|
if err != nil {
|
|
if !opts.force && !opts.forceWithLease && strings.Contains(err.Error(), "Updates were rejected") {
|
|
if opts.remoteBranchStoredLocally {
|
|
return errors.New(self.c.Tr.UpdatesRejected)
|
|
}
|
|
|
|
forcePushDisabled := self.c.UserConfig().Git.DisableForcePushing
|
|
if forcePushDisabled {
|
|
return errors.New(self.c.Tr.UpdatesRejectedAndForcePushDisabled)
|
|
}
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.ForcePush,
|
|
Prompt: self.forcePushPrompt(),
|
|
HandleConfirm: func() error {
|
|
newOpts := opts
|
|
newOpts.force = true
|
|
|
|
return self.pushAux(currentBranch, newOpts)
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
err = self.pushBranches(task, opts.branchesBelow, opts.forceWithLeaseBelow)
|
|
self.c.RefreshFromWorker(types.RefreshOptions{})
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Pushes the current branch along with the branches stacked below it, all of
|
|
// them with explicit refspecs
|
|
func (self *SyncController) pushBranchesAux(currentBranch *models.Branch, branchesBelow []*models.Branch, forceWithLease bool) error {
|
|
return self.withPushingStatus(currentBranch, branchesBelow, func(task gocui.Task) error {
|
|
branches := append([]*models.Branch{currentBranch}, branchesBelow...)
|
|
err := self.pushBranches(task, branches, forceWithLease)
|
|
self.c.RefreshFromWorker(types.RefreshOptions{})
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Runs f as a push of the current branch, showing it and the other branches
|
|
// as being pushed while it runs
|
|
func (self *SyncController) withPushingStatus(currentBranch *models.Branch, otherBranches []*models.Branch, f func(gocui.Task) error) error {
|
|
branches := append([]*models.Branch{currentBranch}, otherBranches...)
|
|
return self.c.Helpers().BranchesHelper.WithInlineStatusOnBranches(branches, types.ItemOperationPushing, func(task gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.Push)
|
|
return f(task)
|
|
})
|
|
}
|
|
|
|
// Pushes the branches to their push destinations, one command per remote
|
|
func (self *SyncController) pushBranches(task gocui.Task, branches []*models.Branch, forceWithLease bool) error {
|
|
remotes := lo.Uniq(lo.Map(branches, func(branch *models.Branch, _ int) string { return branch.PushRemote }))
|
|
for _, remote := range remotes {
|
|
refspecs := lo.FilterMap(branches, func(branch *models.Branch, _ int) (string, bool) {
|
|
return fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch.Name, branch.PushBranch), branch.PushRemote == remote
|
|
})
|
|
err := self.c.Git().Sync.Push(task, git_commands.PushOpts{
|
|
ForceWithLease: forceWithLease,
|
|
Remote: remote,
|
|
Refspecs: refspecs,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SyncController) requestToForcePush(currentBranch *models.Branch, opts pushOpts) error {
|
|
forcePushDisabled := self.c.UserConfig().Git.DisableForcePushing
|
|
if forcePushDisabled {
|
|
return errors.New(self.c.Tr.ForcePushDisabled)
|
|
}
|
|
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.ForcePush,
|
|
Prompt: self.forcePushPrompt(),
|
|
HandleConfirm: func() error {
|
|
opts.forceWithLease = true
|
|
return self.pushAux(currentBranch, opts)
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SyncController) forcePushPrompt() string {
|
|
return utils.ResolvePlaceholderString(
|
|
self.c.Tr.ForcePushPrompt,
|
|
map[string]string{
|
|
"cancelKey": self.c.UserConfig().Keybinding.Universal.Return.String(),
|
|
"confirmKey": self.c.UserConfig().Keybinding.Universal.Confirm.String(),
|
|
},
|
|
)
|
|
}
|