1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-26 05:37:18 +02:00

Add a DisabledReason mechanism for menu items and keybindings (#2992)

This commit is contained in:
Stefan Haller 2023-09-18 10:26:11 +02:00 committed by GitHub
commit e2a966443b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 348 additions and 213 deletions

View File

@ -90,6 +90,9 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
return lo.Map(menuItems, func(item *types.MenuItem, _ int) []string { return lo.Map(menuItems, func(item *types.MenuItem, _ int) []string {
displayStrings := item.LabelColumns displayStrings := item.LabelColumns
if item.DisabledReason != "" {
displayStrings[0] = style.FgDefault.SetStrikethrough().Sprint(displayStrings[0])
}
if !showKeys { if !showKeys {
return displayStrings return displayStrings
@ -169,6 +172,10 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin
} }
func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error { func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
if selectedItem != nil && selectedItem.DisabledReason != "" {
return self.c.ErrorMsg(selectedItem.DisabledReason)
}
if err := self.c.PopContext(); err != nil { if err := self.c.PopContext(); err != nil {
return err return err
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
) )
type BranchesController struct { type BranchesController struct {
@ -75,9 +76,10 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
OpensMenu: true, OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.RebaseBranch), Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
Handler: opts.Guards.OutsideFilterMode(self.rebase), Handler: opts.Guards.OutsideFilterMode(self.rebase),
Description: self.c.Tr.RebaseBranch, Description: self.c.Tr.RebaseBranch,
GetDisabledReason: self.getDisabledReasonForRebase,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch),
@ -140,32 +142,55 @@ func (self *BranchesController) GetOnRenderToMain() func() error {
} }
func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error { func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error {
options := []*types.MenuItem{ viewDivergenceItem := &types.MenuItem{
{ LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream},
LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream}, OnPress: func() error {
OnPress: func() error { branch := self.context().GetSelected()
branch := self.context().GetSelected() if branch == nil {
if branch == nil { return 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,
})
},
Key: 'v',
}
unsetUpstreamItem := &types.MenuItem{
LabelColumns: []string{self.c.Tr.UnsetUpstream},
OnPress: func() error {
if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil {
return self.c.Error(err)
}
if err := self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC,
Scope: []types.RefreshableView{
types.BRANCHES,
types.COMMITS,
},
}); err != nil {
return self.c.Error(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 self.c.Error(err)
} }
if !branch.RemoteBranchStoredLocally() { if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil {
return self.c.ErrorMsg(self.c.Tr.DivergenceNoUpstream)
}
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,
})
},
Key: 'v',
},
{
LabelColumns: []string{self.c.Tr.UnsetUpstream},
OnPress: func() error {
if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
if err := self.c.Refresh(types.RefreshOptions{ if err := self.c.Refresh(types.RefreshOptions{
@ -178,75 +203,48 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error
return self.c.Error(err) return self.c.Error(err)
} }
return nil return nil
}, })
Key: 'u',
},
{
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 self.c.Error(err)
}
if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil {
return self.c.Error(err)
}
if err := self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC,
Scope: []types.RefreshableView{
types.BRANCHES,
types.COMMITS,
},
}); err != nil {
return self.c.Error(err)
}
return nil
})
},
Key: 's',
}, },
Key: 's',
} }
if selectedBranch.IsTrackingRemote() { upstream := lo.Ternary(selectedBranch.RemoteBranchStoredLocally(),
upstream := fmt.Sprintf("%s/%s", selectedBranch.UpstreamRemote, selectedBranch.Name) fmt.Sprintf("%s/%s", selectedBranch.UpstreamRemote, selectedBranch.Name),
upstreamResetOptions := utils.ResolvePlaceholderString( self.c.Tr.UpstreamGenericName)
self.c.Tr.ViewUpstreamResetOptions, upstreamResetOptions := utils.ResolvePlaceholderString(
map[string]string{"upstream": upstream}, self.c.Tr.ViewUpstreamResetOptions,
) map[string]string{"upstream": upstream},
upstreamResetTooltip := utils.ResolvePlaceholderString( )
self.c.Tr.ViewUpstreamResetOptionsTooltip, upstreamResetTooltip := utils.ResolvePlaceholderString(
map[string]string{"upstream": upstream}, self.c.Tr.ViewUpstreamResetOptionsTooltip,
) map[string]string{"upstream": upstream},
)
options = append(options, &types.MenuItem{ upstreamResetItem := &types.MenuItem{
LabelColumns: []string{upstreamResetOptions}, LabelColumns: []string{upstreamResetOptions},
OpensMenu: true, OpensMenu: true,
OnPress: func() error { OnPress: func() error {
if selectedBranch.RemoteBranchNotStoredLocally() { err := self.c.Helpers().Refs.CreateGitResetMenu(upstream)
return self.c.ErrorMsg(self.c.Tr.UpstreamNotStoredLocallyError) if err != nil {
} return self.c.Error(err)
}
return nil
},
Tooltip: upstreamResetTooltip,
Key: 'g',
}
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream) if !selectedBranch.RemoteBranchStoredLocally() {
if err != nil { viewDivergenceItem.DisabledReason = self.c.Tr.UpstreamNotSetError
return self.c.Error(err) unsetUpstreamItem.DisabledReason = self.c.Tr.UpstreamNotSetError
} upstreamResetItem.DisabledReason = self.c.Tr.UpstreamNotSetError
return nil }
},
Tooltip: upstreamResetTooltip, options := []*types.MenuItem{
Key: 'g', viewDivergenceItem,
}) unsetUpstreamItem,
} else { setUpstreamItem,
options = append(options, &types.MenuItem{ upstreamResetItem,
LabelColumns: []string{self.c.Tr.ViewUpstreamDisabledResetOptions},
OpensMenu: true,
OnPress: func() error {
return self.c.ErrorMsg(self.c.Tr.UpstreamNotSetError)
},
Tooltip: self.c.Tr.UpstreamNotSetError,
Key: 'g',
})
} }
return self.c.Menu(types.CreateMenuOptions{ return self.c.Menu(types.CreateMenuOptions{
@ -468,7 +466,6 @@ func (self *BranchesController) forceDelete(branch *models.Branch) error {
} }
func (self *BranchesController) delete(branch *models.Branch) error { func (self *BranchesController) delete(branch *models.Branch) error {
menuItems := []*types.MenuItem{}
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
localDeleteItem := &types.MenuItem{ localDeleteItem := &types.MenuItem{
@ -479,25 +476,18 @@ func (self *BranchesController) delete(branch *models.Branch) error {
}, },
} }
if checkedOutBranch.Name == branch.Name { if checkedOutBranch.Name == branch.Name {
localDeleteItem = &types.MenuItem{ localDeleteItem.DisabledReason = self.c.Tr.CantDeleteCheckOutBranch
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 { remoteDeleteItem := &types.MenuItem{
menuItems = append(menuItems, &types.MenuItem{ Label: self.c.Tr.DeleteRemoteBranch,
Label: self.c.Tr.DeleteRemoteBranch, Key: 'r',
Key: 'r', OnPress: func() error {
OnPress: func() error { return self.remoteDelete(branch)
return self.remoteDelete(branch) },
}, }
}) if !branch.IsTrackingRemote() || branch.UpstreamGone {
remoteDeleteItem.DisabledReason = self.c.Tr.UpstreamNotSetError
} }
menuTitle := utils.ResolvePlaceholderString( menuTitle := utils.ResolvePlaceholderString(
@ -509,7 +499,7 @@ func (self *BranchesController) delete(branch *models.Branch) error {
return self.c.Menu(types.CreateMenuOptions{ return self.c.Menu(types.CreateMenuOptions{
Title: menuTitle, Title: menuTitle,
Items: menuItems, Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem},
}) })
} }
@ -523,6 +513,16 @@ func (self *BranchesController) rebase() error {
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName) return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName)
} }
func (self *BranchesController) getDisabledReasonForRebase() string {
selectedBranchName := self.context().GetSelected().Name
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
if selectedBranchName == checkedOutBranch {
return self.c.Tr.CantRebaseOntoSelf
}
return ""
}
func (self *BranchesController) fastForward(branch *models.Branch) error { func (self *BranchesController) fastForward(branch *models.Branch) error {
if !branch.IsTrackingRemote() { if !branch.IsTrackingRemote() {
return self.c.ErrorMsg(self.c.Tr.FwdNoUpstream) return self.c.ErrorMsg(self.c.Tr.FwdNoUpstream)

View File

@ -85,6 +85,10 @@ func (self *CherryPickHelper) Paste() error {
}) })
} }
func (self *CherryPickHelper) CanPaste() bool {
return self.getData().Active()
}
func (self *CherryPickHelper) Reset() error { func (self *CherryPickHelper) Reset() error {
self.getData().ContextKey = "" self.getData().ContextKey = ""
self.getData().CherryPickedCommits = nil self.getData().CherryPickedCommits = nil

View File

@ -333,7 +333,7 @@ func (self *ConfirmationHelper) resizeMenu() {
tooltip := "" tooltip := ""
selectedItem := self.c.Contexts().Menu.GetSelected() selectedItem := self.c.Contexts().Menu.GetSelected()
if selectedItem != nil { if selectedItem != nil {
tooltip = selectedItem.Tooltip tooltip = self.TooltipForMenuItem(selectedItem)
} }
tooltipHeight := getMessageHeight(true, tooltip, panelWidth) + 2 // plus 2 for the frame tooltipHeight := getMessageHeight(true, tooltip, panelWidth) + 2 // plus 2 for the frame
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0) _, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
@ -382,3 +382,14 @@ func (self *ConfirmationHelper) IsPopupPanel(viewName string) bool {
func (self *ConfirmationHelper) IsPopupPanelFocused() bool { func (self *ConfirmationHelper) IsPopupPanelFocused() bool {
return self.IsPopupPanel(self.c.CurrentContext().GetViewName()) return self.IsPopupPanel(self.c.CurrentContext().GetViewName())
} }
func (self *ConfirmationHelper) TooltipForMenuItem(menuItem *types.MenuItem) string {
tooltip := menuItem.Tooltip
if menuItem.DisabledReason != "" {
if tooltip != "" {
tooltip += "\n\n"
}
tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + menuItem.DisabledReason
}
return tooltip
}

View File

@ -220,9 +220,6 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error { func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
checkedOutBranch := self.refsHelper.GetCheckedOutRef().Name checkedOutBranch := self.refsHelper.GetCheckedOutRef().Name
if ref == checkedOutBranch {
return self.c.ErrorMsg(self.c.Tr.CantRebaseOntoSelf)
}
menuItems := []*types.MenuItem{ menuItems := []*types.MenuItem{
{ {
Label: self.c.Tr.SimpleRebase, Label: self.c.Tr.SimpleRebase,

View File

@ -44,70 +44,83 @@ func NewLocalCommitsController(
func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
outsideFilterModeBindings := []*types.Binding{ outsideFilterModeBindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Commits.SquashDown), Key: opts.GetKey(opts.Config.Commits.SquashDown),
Handler: self.checkSelected(self.squashDown), Handler: self.checkSelected(self.squashDown),
Description: self.c.Tr.SquashDown, GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashDown),
Description: self.c.Tr.SquashDown,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup), Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup),
Handler: self.checkSelected(self.fixup), Handler: self.checkSelected(self.fixup),
Description: self.c.Tr.FixupCommit, GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForFixup),
Description: self.c.Tr.FixupCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.RenameCommit), Key: opts.GetKey(opts.Config.Commits.RenameCommit),
Handler: self.checkSelected(self.reword), Handler: self.checkSelected(self.reword),
Description: self.c.Tr.RewordCommit, GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword),
Description: self.c.Tr.RewordCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor), Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor),
Handler: self.checkSelected(self.rewordEditor), Handler: self.checkSelected(self.rewordEditor),
Description: self.c.Tr.RenameCommitEditor, GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword),
Description: self.c.Tr.RenameCommitEditor,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.drop), Handler: self.checkSelected(self.drop),
Description: self.c.Tr.DeleteCommit, GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Drop),
Description: self.c.Tr.DeleteCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Edit), Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelected(self.edit), Handler: self.checkSelected(self.edit),
Description: self.c.Tr.EditCommit, GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Edit),
Description: self.c.Tr.EditCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.PickCommit), Key: opts.GetKey(opts.Config.Commits.PickCommit),
Handler: self.checkSelected(self.pick), Handler: self.checkSelected(self.pick),
Description: self.c.Tr.PickCommit, GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Pick),
Description: self.c.Tr.PickCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit), Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit),
Handler: self.checkSelected(self.createFixupCommit), Handler: self.checkSelected(self.createFixupCommit),
Description: self.c.Tr.CreateFixupCommitDescription, GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.CreateFixupCommitDescription,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits), Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits),
Handler: self.checkSelected(self.squashAllAboveFixupCommits), Handler: self.checkSelected(self.squashAllAboveFixupCommits),
Description: self.c.Tr.SquashAboveCommits, GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashAllAboveFixupCommits),
Description: self.c.Tr.SquashAboveCommits,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MoveDownCommit), Key: opts.GetKey(opts.Config.Commits.MoveDownCommit),
Handler: self.checkSelected(self.moveDown), Handler: self.checkSelected(self.moveDown),
Description: self.c.Tr.MoveDownCommit, GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.MoveDownCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MoveUpCommit), Key: opts.GetKey(opts.Config.Commits.MoveUpCommit),
Handler: self.checkSelected(self.moveUp), Handler: self.checkSelected(self.moveUp),
Description: self.c.Tr.MoveUpCommit, GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.MoveUpCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.PasteCommits), Key: opts.GetKey(opts.Config.Commits.PasteCommits),
Handler: self.paste, Handler: self.paste,
Description: self.c.Tr.PasteCommits, GetDisabledReason: self.getDisabledReasonForPaste,
Description: self.c.Tr.PasteCommits,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsBaseForRebase), Key: opts.GetKey(opts.Config.Commits.MarkCommitAsBaseForRebase),
Handler: self.checkSelected(self.markAsBaseCommit), Handler: self.checkSelected(self.markAsBaseCommit),
Description: self.c.Tr.MarkAsBaseCommit, GetDisabledReason: self.disabledIfNoSelectedCommit(),
Tooltip: self.c.Tr.MarkAsBaseCommitTooltip, Description: self.c.Tr.MarkAsBaseCommit,
Tooltip: self.c.Tr.MarkAsBaseCommitTooltip,
}, },
// overriding these navigation keybindings because we might need to load // overriding these navigation keybindings because we might need to load
// more commits on demand // more commits on demand
@ -131,25 +144,29 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
bindings := append(outsideFilterModeBindings, []*types.Binding{ bindings := append(outsideFilterModeBindings, []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Commits.AmendToCommit), Key: opts.GetKey(opts.Config.Commits.AmendToCommit),
Handler: self.checkSelected(self.amendTo), Handler: self.checkSelected(self.amendTo),
Description: self.c.Tr.AmendToCommit, GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo),
Description: self.c.Tr.AmendToCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor), Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor),
Handler: self.checkSelected(self.amendAttribute), Handler: self.checkSelected(self.amendAttribute),
Description: self.c.Tr.SetResetCommitAuthor, GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo),
OpensMenu: true, Description: self.c.Tr.SetResetCommitAuthor,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.RevertCommit), Key: opts.GetKey(opts.Config.Commits.RevertCommit),
Handler: self.checkSelected(self.revert), Handler: self.checkSelected(self.revert),
Description: self.c.Tr.RevertCommit, GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.RevertCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.CreateTag), Key: opts.GetKey(opts.Config.Commits.CreateTag),
Handler: self.checkSelected(self.createTag), Handler: self.checkSelected(self.createTag),
Description: self.c.Tr.TagCommit, GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.TagCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.OpenLogMenu), Key: opts.GetKey(opts.Config.Commits.OpenLogMenu),
@ -208,10 +225,6 @@ func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts {
} }
func (self *LocalCommitsController) squashDown(commit *models.Commit) error { func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
}
applied, err := self.handleMidRebaseCommand(todo.Squash, commit) applied, err := self.handleMidRebaseCommand(todo.Squash, commit)
if err != nil { if err != nil {
return err return err
@ -232,11 +245,15 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
}) })
} }
func (self *LocalCommitsController) fixup(commit *models.Commit) error { func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *models.Commit) string {
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit) return self.c.Tr.CannotSquashOrFixupFirstCommit
} }
return self.rebaseCommandEnabled(todo.Squash, commit)
}
func (self *LocalCommitsController) fixup(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Fixup, commit) applied, err := self.handleMidRebaseCommand(todo.Fixup, commit)
if err != nil { if err != nil {
return err return err
@ -257,6 +274,14 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error {
}) })
} }
func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Commit) string {
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.Tr.CannotSquashOrFixupFirstCommit
}
return self.rebaseCommandEnabled(todo.Squash, commit)
}
func (self *LocalCommitsController) reword(commit *models.Commit) error { func (self *LocalCommitsController) reword(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Reword, commit) applied, err := self.handleMidRebaseCommand(todo.Reword, commit)
if err != nil { if err != nil {
@ -428,34 +453,10 @@ func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) e
// commit meaning you are trying to edit the todo file rather than actually // commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action // begin a rebase. It then updates the todo file with that action
func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) { func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) {
if commit.Action == models.ActionConflict {
return true, self.c.ErrorMsg(self.c.Tr.ChangingThisActionIsNotAllowed)
}
if !commit.IsTODO() { if !commit.IsTODO() {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit
if !(action == todo.Reword && self.isHeadCommit()) {
return true, self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
}
return false, nil return false, nil
} }
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == todo.Reword {
return true, self.c.ErrorMsg(self.c.Tr.RewordNotSupported)
}
if allowed := isChangeOfRebaseTodoAllowed(action); !allowed {
return true, self.c.ErrorMsg(self.c.Tr.ChangingThisActionIsNotAllowed)
}
self.c.LogAction("Update rebase TODO") self.c.LogAction("Update rebase TODO")
msg := utils.ResolvePlaceholderString( msg := utils.ResolvePlaceholderString(
@ -476,6 +477,38 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
}) })
} }
func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand, commit *models.Commit) string {
if commit.Action == models.ActionConflict {
return self.c.Tr.ChangingThisActionIsNotAllowed
}
if !commit.IsTODO() {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit
if !(action == todo.Reword && self.isHeadCommit()) {
return self.c.Tr.AlreadyRebasing
}
}
return ""
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == todo.Reword {
return self.c.Tr.RewordNotSupported
}
if allowed := isChangeOfRebaseTodoAllowed(action); !allowed {
return self.c.Tr.ChangingThisActionIsNotAllowed
}
return ""
}
func (self *LocalCommitsController) moveDown(commit *models.Commit) error { func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
index := self.context().GetSelectedLineIdx() index := self.context().GetSelectedLineIdx()
commits := self.c.Model().Commits commits := self.c.Model().Commits
@ -601,6 +634,14 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
}) })
} }
func (self *LocalCommitsController) getDisabledReasonForAmendTo(commit *models.Commit) string {
if !self.isHeadCommit() && self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.Tr.AlreadyRebasing
}
return ""
}
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error { func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
@ -772,6 +813,14 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
}) })
} }
func (self *LocalCommitsController) getDisabledReasonForSquashAllAboveFixupCommits(commit *models.Commit) string {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.Tr.AlreadyRebasing
}
return ""
}
func (self *LocalCommitsController) createTag(commit *models.Commit) error { func (self *LocalCommitsController) createTag(commit *models.Commit) error {
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {}) return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {})
} }
@ -896,13 +945,35 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit)
return func() error { return func() error {
commit := self.context().GetSelected() commit := self.context().GetSelected()
if commit == nil { if commit == nil {
return nil // The enabled callback should have checked for this
panic("no commit selected")
} }
return callback(commit) return callback(commit)
} }
} }
func (self *LocalCommitsController) callGetDisabledReasonFuncWithSelectedCommit(callback func(*models.Commit) string) func() string {
return func() string {
commit := self.context().GetSelected()
if commit == nil {
return self.c.Tr.NoCommitSelected
}
return callback(commit)
}
}
func (self *LocalCommitsController) disabledIfNoSelectedCommit() func() string {
return self.callGetDisabledReasonFuncWithSelectedCommit(func(*models.Commit) string { return "" })
}
func (self *LocalCommitsController) getDisabledReasonForRebaseCommandWithSelectedCommit(action todo.TodoCommand) func() string {
return self.callGetDisabledReasonFuncWithSelectedCommit(func(commit *models.Commit) string {
return self.rebaseCommandEnabled(action, commit)
})
}
func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error { func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error { return func(types.OnFocusOpts) error {
context := self.context() context := self.context()
@ -931,6 +1002,14 @@ func (self *LocalCommitsController) paste() error {
return self.c.Helpers().CherryPick.Paste() return self.c.Helpers().CherryPick.Paste()
} }
func (self *LocalCommitsController) getDisabledReasonForPaste() string {
if !self.c.Helpers().CherryPick.CanPaste() {
return self.c.Tr.NoCopiedCommits
}
return ""
}
func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) error { func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) error {
if commit.Sha == self.c.Modes().MarkedBaseCommit.GetSha() { if commit.Sha == self.c.Modes().MarkedBaseCommit.GetSha() {
// Reset when invoking it again on the marked commit // Reset when invoking it again on the marked commit

View File

@ -54,7 +54,7 @@ func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error { return func(types.OnFocusOpts) error {
selectedMenuItem := self.context().GetSelected() selectedMenuItem := self.context().GetSelected()
if selectedMenuItem != nil { if selectedMenuItem != nil {
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip) self.c.Views().Tooltip.SetContent(self.c.Helpers().Confirmation.TooltipForMenuItem(selectedMenuItem))
} }
return nil return nil
} }

View File

@ -25,6 +25,10 @@ func (self *OptionsMenuAction) Call() error {
appendBindings := func(bindings []*types.Binding, section *types.MenuSection) { appendBindings := func(bindings []*types.Binding, section *types.MenuSection) {
menuItems = append(menuItems, menuItems = append(menuItems,
lo.Map(bindings, func(binding *types.Binding, _ int) *types.MenuItem { lo.Map(bindings, func(binding *types.Binding, _ int) *types.MenuItem {
disabledReason := ""
if binding.GetDisabledReason != nil {
disabledReason = binding.GetDisabledReason()
}
return &types.MenuItem{ return &types.MenuItem{
OpensMenu: binding.OpensMenu, OpensMenu: binding.OpensMenu,
Label: binding.Description, Label: binding.Description,
@ -33,11 +37,12 @@ func (self *OptionsMenuAction) Call() error {
return nil return nil
} }
return binding.Handler() return self.c.IGuiCommon.CallKeybindingHandler(binding)
}, },
Key: binding.Key, Key: binding.Key,
Tooltip: binding.Tooltip, Tooltip: binding.Tooltip,
Section: section, DisabledReason: disabledReason,
Section: section,
} }
})...) })...)
} }

View File

@ -171,6 +171,10 @@ func (self *guiCommon) KeybindingsOpts() types.KeybindingsOpts {
return self.gui.keybindingOpts() return self.gui.keybindingOpts()
} }
func (self *guiCommon) CallKeybindingHandler(binding *types.Binding) error {
return self.gui.callKeybindingHandler(binding)
}
func (self *guiCommon) IsAnyModeActive() bool { func (self *guiCommon) IsAnyModeActive() bool {
return self.gui.helpers.Mode.IsAnyModeActive() return self.gui.helpers.Mode.IsAnyModeActive()
} }

View File

@ -375,7 +375,10 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
} }
func (gui *Gui) SetKeybinding(binding *types.Binding) error { func (gui *Gui) SetKeybinding(binding *types.Binding) error {
handler := binding.Handler handler := func() error {
return gui.callKeybindingHandler(binding)
}
// TODO: move all mouse-ey stuff into new mouse approach // TODO: move all mouse-ey stuff into new mouse approach
if gocui.IsMouseKey(binding.Key) { if gocui.IsMouseKey(binding.Key) {
handler = func() error { handler = func() error {
@ -406,3 +409,14 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {
return gui.g.SetViewClickBinding(binding) return gui.g.SetViewClickBinding(binding)
} }
func (gui *Gui) callKeybindingHandler(binding *types.Binding) error {
disabledReason := ""
if binding.GetDisabledReason != nil {
disabledReason = binding.GetDisabledReason()
}
if disabledReason != "" {
return gui.c.ErrorMsg(disabledReason)
}
return binding.Handler()
}

View File

@ -104,6 +104,7 @@ type IGuiCommon interface {
State() IStateAccessor State() IStateAccessor
KeybindingsOpts() KeybindingsOpts KeybindingsOpts() KeybindingsOpts
CallKeybindingHandler(binding *Binding) error
// hopefully we can remove this once we've moved all our keybinding stuff out of the gui god struct. // hopefully we can remove this once we've moved all our keybinding stuff out of the gui god struct.
GetInitialKeybindingsWithCustomCommands() ([]*Binding, []*gocui.ViewMouseBinding) GetInitialKeybindingsWithCustomCommands() ([]*Binding, []*gocui.ViewMouseBinding)
@ -204,6 +205,10 @@ type MenuItem struct {
// The tooltip will be displayed upon highlighting the menu item // The tooltip will be displayed upon highlighting the menu item
Tooltip string Tooltip string
// If non-empty, show this in a tooltip, style the menu item as disabled,
// and refuse to invoke the command
DisabledReason string
// Can be used to group menu items into sections with headers. MenuItems // Can be used to group menu items into sections with headers. MenuItems
// with the same Section should be contiguous, and will automatically get a // with the same Section should be contiguous, and will automatically get a
// section header. If nil, the item is not part of a section. // section header. If nil, the item is not part of a section.

View File

@ -25,6 +25,13 @@ type Binding struct {
// to be displayed if the keybinding is highlighted from within a menu // to be displayed if the keybinding is highlighted from within a menu
Tooltip string Tooltip string
// Function to decide whether the command is enabled, and why. If this
// returns an empty string, it is; if it returns a non-empty string, it is
// disabled and we show the given text in an error message when trying to
// invoke it. When left nil, the command is always enabled. Note that this
// function must not do expensive calls.
GetDisabledReason func() string
} }
// A guard is a decorator which checks something before executing a handler // A guard is a decorator which checks something before executing a handler

View File

@ -352,12 +352,11 @@ type TranslationSet struct {
SetUpstream string SetUpstream string
UnsetUpstream string UnsetUpstream string
ViewDivergenceFromUpstream string ViewDivergenceFromUpstream string
DivergenceNoUpstream string
DivergenceSectionHeaderLocal string DivergenceSectionHeaderLocal string
DivergenceSectionHeaderRemote string DivergenceSectionHeaderRemote string
ViewUpstreamResetOptions string ViewUpstreamResetOptions string
ViewUpstreamResetOptionsTooltip string ViewUpstreamResetOptionsTooltip string
ViewUpstreamDisabledResetOptions string UpstreamGenericName string
SetUpstreamTitle string SetUpstreamTitle string
SetUpstreamMessage string SetUpstreamMessage string
EditRemote string EditRemote string
@ -405,7 +404,6 @@ type TranslationSet struct {
ViewBranchUpstreamOptions string ViewBranchUpstreamOptions string
BranchUpstreamOptionsTitle string BranchUpstreamOptionsTitle string
ViewBranchUpstreamOptionsTooltip string ViewBranchUpstreamOptionsTooltip string
UpstreamNotStoredLocallyError string
UpstreamNotSetError string UpstreamNotSetError string
NewGitFlowBranchPrompt string NewGitFlowBranchPrompt string
RenameBranchWarning string RenameBranchWarning string
@ -612,6 +610,9 @@ type TranslationSet struct {
MarkAsBaseCommitTooltip string MarkAsBaseCommitTooltip string
MarkedCommitMarker string MarkedCommitMarker string
PleaseGoToURL string PleaseGoToURL string
DisabledMenuItemPrefix string
NoCommitSelected string
NoCopiedCommits string
Actions Actions Actions Actions
Bisect Bisect Bisect Bisect
Log Log Log Log
@ -1147,12 +1148,11 @@ func EnglishTranslationSet() TranslationSet {
SetUpstream: "Set upstream of selected branch", SetUpstream: "Set upstream of selected branch",
UnsetUpstream: "Unset upstream of selected branch", UnsetUpstream: "Unset upstream of selected branch",
ViewDivergenceFromUpstream: "View divergence from upstream", ViewDivergenceFromUpstream: "View divergence from upstream",
DivergenceNoUpstream: "Cannot show divergence of a branch that has no (locally tracked) upstream",
DivergenceSectionHeaderLocal: "Local", DivergenceSectionHeaderLocal: "Local",
DivergenceSectionHeaderRemote: "Remote", DivergenceSectionHeaderRemote: "Remote",
ViewUpstreamResetOptions: "Reset checked-out branch onto {{.upstream}}", ViewUpstreamResetOptions: "Reset checked-out branch onto {{.upstream}}",
ViewUpstreamResetOptionsTooltip: "View options for resetting the checked-out branch onto {{upstream}}. Note: this will not reset the selected branch onto the upstream, it will reset the checked-out branch onto the upstream", ViewUpstreamResetOptionsTooltip: "View options for resetting the checked-out branch onto {{upstream}}. Note: this will not reset the selected branch onto the upstream, it will reset the checked-out branch onto the upstream",
ViewUpstreamDisabledResetOptions: "Reset checked-out branch onto upstream of selected branch", UpstreamGenericName: "upstream of selected branch",
SetUpstreamTitle: "Set upstream branch", SetUpstreamTitle: "Set upstream branch",
SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'", SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'",
EditRemote: "Edit remote", EditRemote: "Edit remote",
@ -1196,8 +1196,7 @@ func EnglishTranslationSet() TranslationSet {
RenameBranch: "Rename branch", RenameBranch: "Rename branch",
BranchUpstreamOptionsTitle: "Upstream options", BranchUpstreamOptionsTitle: "Upstream options",
ViewBranchUpstreamOptionsTooltip: "View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream", ViewBranchUpstreamOptionsTooltip: "View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream",
UpstreamNotStoredLocallyError: "Cannot reset to upstream branch because it is not stored locally", UpstreamNotSetError: "The selected branch has no upstream (or the upstream is not stored locally)",
UpstreamNotSetError: "The selected branch has no upstream",
ViewBranchUpstreamOptions: "View upstream options", ViewBranchUpstreamOptions: "View upstream options",
NewBranchNamePrompt: "Enter new branch name for branch", NewBranchNamePrompt: "Enter new branch name for branch",
RenameBranchWarning: "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?", RenameBranchWarning: "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
@ -1403,6 +1402,9 @@ func EnglishTranslationSet() TranslationSet {
MarkAsBaseCommitTooltip: "Select a base commit for the next rebase; this will effectively perform a 'git rebase --onto'.", MarkAsBaseCommitTooltip: "Select a base commit for the next rebase; this will effectively perform a 'git rebase --onto'.",
MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑", MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑",
PleaseGoToURL: "Please go to {{.url}}", PleaseGoToURL: "Please go to {{.url}}",
DisabledMenuItemPrefix: "Disabled: ",
NoCommitSelected: "No commit selected",
NoCopiedCommits: "No copied commits",
Actions: Actions{ Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit", CheckoutCommit: "Checkout commit",

View File

@ -41,11 +41,11 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Menu(). t.ExpectPopup().Menu().
Title(Equals("Upstream options")). Title(Equals("Upstream options")).
Select(Contains("Reset checked-out branch onto upstream of selected branch")). Select(Contains("Reset checked-out branch onto upstream of selected branch")).
Tooltip(Contains("The selected branch has no upstream")). Tooltip(Contains("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")).
Confirm() Confirm()
t.ExpectPopup().Alert(). t.ExpectPopup().Alert().
Title(Equals("Error")). Title(Equals("Error")).
Content(Equals("The selected branch has no upstream")). Content(Equals("The selected branch has no upstream (or the upstream is not stored locally)")).
Confirm() Confirm()
}). }).
SelectNextItem(). SelectNextItem().