mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-31 14:24:25 +03:00
start moving commit panel handlers into controller
more and more move rebase commit refreshing into existing abstraction and more and more WIP and more handling clicks properly fix merge conflicts update cheatsheet lots more preparation to start moving things into controllers WIP better typing expand on remotes controller moving more code into controllers
This commit is contained in:
273
pkg/gui/controllers/bisect_controller.go
Normal file
273
pkg/gui/controllers/bisect_controller.go
Normal file
@ -0,0 +1,273 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type BisectController struct {
|
||||
c *ControllerCommon
|
||||
context types.IListContext
|
||||
git *commands.GitCommand
|
||||
|
||||
getSelectedLocalCommit func() *models.Commit
|
||||
getCommits func() []*models.Commit
|
||||
}
|
||||
|
||||
var _ types.IController = &BisectController{}
|
||||
|
||||
func NewBisectController(
|
||||
c *ControllerCommon,
|
||||
context types.IListContext,
|
||||
git *commands.GitCommand,
|
||||
|
||||
getSelectedLocalCommit func() *models.Commit,
|
||||
getCommits func() []*models.Commit,
|
||||
) *BisectController {
|
||||
return &BisectController{
|
||||
c: c,
|
||||
context: context,
|
||||
git: git,
|
||||
|
||||
getSelectedLocalCommit: getSelectedLocalCommit,
|
||||
getCommits: getCommits,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BisectController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Commits.ViewBisectOptions),
|
||||
Handler: guards.OutsideFilterMode(self.checkSelected(self.openMenu)),
|
||||
Description: self.c.Tr.LcViewBisectOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (self *BisectController) openMenu(commit *models.Commit) error {
|
||||
// no shame in getting this directly rather than using the cached value
|
||||
// given how cheap it is to obtain
|
||||
info := self.git.Bisect.GetInfo()
|
||||
if info.Started() {
|
||||
return self.openMidBisectMenu(info, commit)
|
||||
} else {
|
||||
return self.openStartBisectMenu(info, commit)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
||||
// if there is not yet a 'current' bisect commit, or if we have
|
||||
// selected the current commit, we need to jump to the next 'current' commit
|
||||
// after we perform a bisect action. The reason we don't unconditionally jump
|
||||
// is that sometimes the user will want to go and mark a few commits as skipped
|
||||
// in a row and they wouldn't want to be jumped back to the current bisect
|
||||
// commit each time.
|
||||
// Originally we were allowing the user to, from the bisect menu, select whether
|
||||
// they were talking about the selected commit or the current bisect commit,
|
||||
// and that was a bit confusing (and required extra keypresses).
|
||||
selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha
|
||||
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
|
||||
// ref, because we'll be reloading our commits in that case.
|
||||
waitToReselect := selectCurrentAfter && !self.git.Bisect.ReachableFromStart(info)
|
||||
|
||||
menuItems := []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()),
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.BisectMark)
|
||||
if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.afterMark(selectCurrentAfter, waitToReselect)
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()),
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.BisectMark)
|
||||
if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.afterMark(selectCurrentAfter, waitToReselect)
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Skip, commit.ShortSha()),
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.BisectSkip)
|
||||
if err := self.git.Bisect.Skip(commit.Sha); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.afterMark(selectCurrentAfter, waitToReselect)
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: self.c.Tr.Bisect.ResetOption,
|
||||
OnPress: func() error {
|
||||
return self.Reset()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.Bisect.BisectMenuTitle,
|
||||
Items: menuItems,
|
||||
})
|
||||
}
|
||||
|
||||
func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.Bisect.BisectMenuTitle,
|
||||
Items: []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()),
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.StartBisect)
|
||||
if err := self.git.Bisect.Start(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.postBisectCommandRefresh()
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()),
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.StartBisect)
|
||||
if err := self.git.Bisect.Start(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.postBisectCommandRefresh()
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *BisectController) Reset() error {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.Bisect.ResetTitle,
|
||||
Prompt: self.c.Tr.Bisect.ResetPrompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.ResetBisect)
|
||||
if err := self.git.Bisect.Reset(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.postBisectCommandRefresh()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *BisectController) showBisectCompleteMessage(candidateShas []string) error {
|
||||
prompt := self.c.Tr.Bisect.CompletePrompt
|
||||
if len(candidateShas) > 1 {
|
||||
prompt = self.c.Tr.Bisect.CompletePromptIndeterminate
|
||||
}
|
||||
|
||||
formattedCommits, err := self.git.Commit.GetCommitsOneline(candidateShas)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.Bisect.CompleteTitle,
|
||||
Prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.ResetBisect)
|
||||
if err := self.git.Bisect.Reset(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.postBisectCommandRefresh()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool) error {
|
||||
done, candidateShas, err := self.git.Bisect.IsDone()
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
if err := self.afterBisectMarkRefresh(selectCurrent, waitToReselect); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
if done {
|
||||
return self.showBisectCompleteMessage(candidateShas)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BisectController) postBisectCommandRefresh() error {
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{}})
|
||||
}
|
||||
|
||||
func (self *BisectController) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error {
|
||||
selectFn := func() {
|
||||
if selectCurrent {
|
||||
self.selectCurrentBisectCommit()
|
||||
}
|
||||
}
|
||||
|
||||
if waitToReselect {
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{}, Then: selectFn})
|
||||
} else {
|
||||
selectFn()
|
||||
|
||||
return self.postBisectCommandRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BisectController) selectCurrentBisectCommit() {
|
||||
info := self.git.Bisect.GetInfo()
|
||||
if info.GetCurrentSha() != "" {
|
||||
// find index of commit with that sha, move cursor to that.
|
||||
for i, commit := range self.getCommits() {
|
||||
if commit.Sha == info.GetCurrentSha() {
|
||||
self.context.GetPanelState().SetSelectedLineIdx(i)
|
||||
_ = self.context.HandleFocus()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BisectController) checkSelected(callback func(*models.Commit) error) func() error {
|
||||
return func() error {
|
||||
commit := self.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(commit)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BisectController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
10
pkg/gui/controllers/controller_common.go
Normal file
10
pkg/gui/controllers/controller_common.go
Normal file
@ -0,0 +1,10 @@
|
||||
package controllers
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/common"
|
||||
|
||||
// if Go let me do private struct embedding of structs with public fields (which it should)
|
||||
// I would just do that. But alas.
|
||||
type ControllerCommon struct {
|
||||
*common.Common
|
||||
IGuiCommon
|
||||
}
|
737
pkg/gui/controllers/files_controller.go
Normal file
737
pkg/gui/controllers/files_controller.go
Normal file
@ -0,0 +1,737 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type FilesController struct {
|
||||
// I've said publicly that I'm against single-letter variable names but in this
|
||||
// case I would actually prefer a _zero_ letter variable name in the form of
|
||||
// struct embedding, but Go does not allow hiding public fields in an embedded struct
|
||||
// to the client
|
||||
c *ControllerCommon
|
||||
context types.IListContext
|
||||
git *commands.GitCommand
|
||||
os *oscommands.OSCommand
|
||||
|
||||
getSelectedFileNode func() *filetree.FileNode
|
||||
allContexts context.ContextTree
|
||||
fileTreeViewModel *filetree.FileTreeViewModel
|
||||
enterSubmodule func(submodule *models.SubmoduleConfig) error
|
||||
getSubmodules func() []*models.SubmoduleConfig
|
||||
setCommitMessage func(message string)
|
||||
getCheckedOutBranch func() *models.Branch
|
||||
withGpgHandling func(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error
|
||||
getFailedCommitMessage func() string
|
||||
getCommits func() []*models.Commit
|
||||
getSelectedPath func() string
|
||||
switchToMergeFn func(path string) error
|
||||
suggestionsHelper ISuggestionsHelper
|
||||
refHelper IRefHelper
|
||||
fileHelper IFileHelper
|
||||
workingTreeHelper IWorkingTreeHelper
|
||||
}
|
||||
|
||||
var _ types.IController = &FilesController{}
|
||||
|
||||
func NewFilesController(
|
||||
c *ControllerCommon,
|
||||
context types.IListContext,
|
||||
git *commands.GitCommand,
|
||||
os *oscommands.OSCommand,
|
||||
getSelectedFileNode func() *filetree.FileNode,
|
||||
allContexts context.ContextTree,
|
||||
fileTreeViewModel *filetree.FileTreeViewModel,
|
||||
enterSubmodule func(submodule *models.SubmoduleConfig) error,
|
||||
getSubmodules func() []*models.SubmoduleConfig,
|
||||
setCommitMessage func(message string),
|
||||
withGpgHandling func(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error,
|
||||
getFailedCommitMessage func() string,
|
||||
getCommits func() []*models.Commit,
|
||||
getSelectedPath func() string,
|
||||
switchToMergeFn func(path string) error,
|
||||
suggestionsHelper ISuggestionsHelper,
|
||||
refHelper IRefHelper,
|
||||
fileHelper IFileHelper,
|
||||
workingTreeHelper IWorkingTreeHelper,
|
||||
) *FilesController {
|
||||
return &FilesController{
|
||||
c: c,
|
||||
context: context,
|
||||
git: git,
|
||||
os: os,
|
||||
getSelectedFileNode: getSelectedFileNode,
|
||||
allContexts: allContexts,
|
||||
fileTreeViewModel: fileTreeViewModel,
|
||||
enterSubmodule: enterSubmodule,
|
||||
getSubmodules: getSubmodules,
|
||||
setCommitMessage: setCommitMessage,
|
||||
withGpgHandling: withGpgHandling,
|
||||
getFailedCommitMessage: getFailedCommitMessage,
|
||||
getCommits: getCommits,
|
||||
getSelectedPath: getSelectedPath,
|
||||
switchToMergeFn: switchToMergeFn,
|
||||
suggestionsHelper: suggestionsHelper,
|
||||
refHelper: refHelper,
|
||||
fileHelper: fileHelper,
|
||||
workingTreeHelper: workingTreeHelper,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Universal.Select),
|
||||
Handler: self.checkSelectedFileNode(self.press),
|
||||
Description: self.c.Tr.LcToggleStaged,
|
||||
},
|
||||
{
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: func() error { return self.context.HandleClick(self.checkSelectedFileNode(self.press)) },
|
||||
},
|
||||
{
|
||||
Key: getKey("<c-b>"), // TODO: softcode
|
||||
Handler: self.handleStatusFilterPressed,
|
||||
Description: self.c.Tr.LcFileFilter,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.CommitChanges),
|
||||
Handler: self.HandleCommitPress,
|
||||
Description: self.c.Tr.CommitChanges,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.CommitChangesWithoutHook),
|
||||
Handler: self.HandleWIPCommitPress,
|
||||
Description: self.c.Tr.LcCommitChangesWithoutHook,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.AmendLastCommit),
|
||||
Handler: self.handleAmendCommitPress,
|
||||
Description: self.c.Tr.AmendLastCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.CommitChangesWithEditor),
|
||||
Handler: self.HandleCommitEditorPress,
|
||||
Description: self.c.Tr.CommitChangesWithEditor,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Edit),
|
||||
Handler: self.edit,
|
||||
Description: self.c.Tr.LcEditFile,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.OpenFile),
|
||||
Handler: self.Open,
|
||||
Description: self.c.Tr.LcOpenFile,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.IgnoreFile),
|
||||
Handler: self.ignore,
|
||||
Description: self.c.Tr.LcIgnoreFile,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.RefreshFiles),
|
||||
Handler: self.refresh,
|
||||
Description: self.c.Tr.LcRefreshFiles,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.StashAllChanges),
|
||||
Handler: self.stash,
|
||||
Description: self.c.Tr.LcStashAllChanges,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.ViewStashOptions),
|
||||
Handler: self.createStashMenu,
|
||||
Description: self.c.Tr.LcViewStashOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.ToggleStagedAll),
|
||||
Handler: self.stageAll,
|
||||
Description: self.c.Tr.LcToggleStagedAll,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.GoInto),
|
||||
Handler: self.enter,
|
||||
Description: self.c.Tr.FileEnter,
|
||||
},
|
||||
{
|
||||
ViewName: "",
|
||||
Key: getKey(config.Universal.ExecuteCustomCommand),
|
||||
Handler: self.handleCustomCommand,
|
||||
Description: self.c.Tr.LcExecuteCustomCommand,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.ViewResetOptions),
|
||||
Handler: self.createResetMenu,
|
||||
Description: self.c.Tr.LcViewResetToUpstreamOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.ToggleTreeView),
|
||||
Handler: self.toggleTreeView,
|
||||
Description: self.c.Tr.LcToggleTreeView,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Files.OpenMergeTool),
|
||||
Handler: self.OpenMergeTool,
|
||||
Description: self.c.Tr.LcOpenMergeTool,
|
||||
},
|
||||
}
|
||||
|
||||
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
|
||||
}
|
||||
|
||||
func (self *FilesController) press(node *filetree.FileNode) error {
|
||||
if node.IsLeaf() {
|
||||
file := node.File
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return self.c.PushContext(self.allContexts.Merging)
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
self.c.LogAction(self.c.Tr.Actions.StageFile)
|
||||
if err := self.git.WorkingTree.StageFile(file.Name); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
} else {
|
||||
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
|
||||
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if any files within have inline merge conflicts we can't stage or unstage,
|
||||
// or it'll end up with those >>>>>> lines actually staged
|
||||
if node.GetHasInlineMergeConflicts() {
|
||||
return self.c.ErrorMsg(self.c.Tr.ErrStageDirWithInlineMergeConflicts)
|
||||
}
|
||||
|
||||
if node.GetHasUnstagedChanges() {
|
||||
self.c.LogAction(self.c.Tr.Actions.StageFile)
|
||||
if err := self.git.WorkingTree.StageFile(node.Path); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
} else {
|
||||
// pretty sure it doesn't matter that we're always passing true here
|
||||
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
|
||||
if err := self.git.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.context.HandleFocus()
|
||||
}
|
||||
|
||||
func (self *FilesController) checkSelectedFileNode(callback func(*filetree.FileNode) error) func() error {
|
||||
return func() error {
|
||||
node := self.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(node)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilesController) checkSelectedFile(callback func(*models.File) error) func() error {
|
||||
return func() error {
|
||||
file := self.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(file)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilesController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
||||
|
||||
func (self *FilesController) getSelectedFile() *models.File {
|
||||
node := self.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return node.File
|
||||
}
|
||||
|
||||
func (self *FilesController) enter() error {
|
||||
return self.EnterFile(types.OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
|
||||
}
|
||||
|
||||
func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
|
||||
node := self.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.File == nil {
|
||||
return self.handleToggleDirCollapsed()
|
||||
}
|
||||
|
||||
file := node.File
|
||||
|
||||
submoduleConfigs := self.getSubmodules()
|
||||
if file.IsSubmodule(submoduleConfigs) {
|
||||
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
||||
return self.enterSubmodule(submoduleConfig)
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return self.switchToMerge()
|
||||
}
|
||||
if file.HasMergeConflicts {
|
||||
return self.c.ErrorMsg(self.c.Tr.FileStagingRequirements)
|
||||
}
|
||||
|
||||
return self.c.PushContext(self.allContexts.Staging, opts)
|
||||
}
|
||||
|
||||
func (self *FilesController) allFilesStaged() bool {
|
||||
for _, file := range self.fileTreeViewModel.GetAllFiles() {
|
||||
if file.HasUnstagedChanges {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *FilesController) stageAll() error {
|
||||
var err error
|
||||
if self.allFilesStaged() {
|
||||
self.c.LogAction(self.c.Tr.Actions.UnstageAllFiles)
|
||||
err = self.git.WorkingTree.UnstageAll()
|
||||
} else {
|
||||
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
|
||||
err = self.git.WorkingTree.StageAll()
|
||||
}
|
||||
if err != nil {
|
||||
_ = self.c.Error(err)
|
||||
}
|
||||
|
||||
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.allContexts.Files.HandleFocus()
|
||||
}
|
||||
|
||||
func (self *FilesController) ignore() error {
|
||||
node := self.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.GetPath() == ".gitignore" {
|
||||
return self.c.ErrorMsg("Cannot ignore .gitignore")
|
||||
}
|
||||
|
||||
unstageFiles := func() error {
|
||||
return node.ForEachFile(func(file *models.File) error {
|
||||
if file.HasStagedChanges {
|
||||
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if node.GetIsTracked() {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.IgnoreTracked,
|
||||
Prompt: self.c.Tr.IgnoreTrackedPrompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.IgnoreFile)
|
||||
// not 100% sure if this is necessary but I'll assume it is
|
||||
if err := unstageFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.git.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.git.WorkingTree.Ignore(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.IgnoreFile)
|
||||
|
||||
if err := unstageFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.git.WorkingTree.Ignore(node.GetPath()); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
}
|
||||
|
||||
func (self *FilesController) HandleWIPCommitPress() error {
|
||||
skipHookPrefix := self.c.UserConfig.Git.SkipHookPrefix
|
||||
if skipHookPrefix == "" {
|
||||
return self.c.ErrorMsg(self.c.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
self.setCommitMessage(skipHookPrefix)
|
||||
|
||||
return self.HandleCommitPress()
|
||||
}
|
||||
|
||||
func (self *FilesController) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
|
||||
cfg, ok := self.c.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (self *FilesController) prepareFilesForCommit() error {
|
||||
noStagedFiles := !self.workingTreeHelper.AnyStagedFiles()
|
||||
if noStagedFiles && self.c.UserConfig.Gui.SkipNoStagedFilesWarning {
|
||||
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
|
||||
err := self.git.WorkingTree.StageAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.syncRefresh()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// for when you need to refetch files before continuing an action. Runs synchronously.
|
||||
func (self *FilesController) syncRefresh() error {
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}})
|
||||
}
|
||||
|
||||
func (self *FilesController) refresh() error {
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
}
|
||||
|
||||
func (self *FilesController) HandleCommitPress() error {
|
||||
if err := self.prepareFilesForCommit(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
if self.fileTreeViewModel.GetItemsLength() == 0 {
|
||||
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if !self.workingTreeHelper.AnyStagedFiles() {
|
||||
return self.promptToStageAllAndRetry(self.HandleCommitPress)
|
||||
}
|
||||
|
||||
failedCommitMessage := self.getFailedCommitMessage()
|
||||
if len(failedCommitMessage) > 0 {
|
||||
self.setCommitMessage(failedCommitMessage)
|
||||
} else {
|
||||
commitPrefixConfig := self.commitPrefixConfigForRepo()
|
||||
if commitPrefixConfig != nil {
|
||||
prefixPattern := commitPrefixConfig.Pattern
|
||||
prefixReplace := commitPrefixConfig.Replace
|
||||
rgx, err := regexp.Compile(prefixPattern)
|
||||
if err != nil {
|
||||
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(self.getCheckedOutBranch().Name, prefixReplace)
|
||||
self.setCommitMessage(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.c.PushContext(self.allContexts.CommitMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FilesController) promptToStageAllAndRetry(retry func() error) error {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.NoFilesStagedTitle,
|
||||
Prompt: self.c.Tr.NoFilesStagedPrompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
|
||||
if err := self.git.WorkingTree.StageAll(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
if err := self.syncRefresh(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return retry()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) handleAmendCommitPress() error {
|
||||
if self.fileTreeViewModel.GetItemsLength() == 0 {
|
||||
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if !self.workingTreeHelper.AnyStagedFiles() {
|
||||
return self.promptToStageAllAndRetry(self.handleAmendCommitPress)
|
||||
}
|
||||
|
||||
if len(self.getCommits()) == 0 {
|
||||
return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend)
|
||||
}
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: strings.Title(self.c.Tr.AmendLastCommit),
|
||||
Prompt: self.c.Tr.SureToAmend,
|
||||
HandleConfirm: func() error {
|
||||
cmdObj := self.git.Commit.AmendHeadCmdObj()
|
||||
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
|
||||
return self.withGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// HandleCommitEditorPress - handle when the user wants to commit changes via
|
||||
// their editor rather than via the popup panel
|
||||
func (self *FilesController) HandleCommitEditorPress() error {
|
||||
if self.fileTreeViewModel.GetItemsLength() == 0 {
|
||||
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if !self.workingTreeHelper.AnyStagedFiles() {
|
||||
return self.promptToStageAllAndRetry(self.HandleCommitEditorPress)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.Commit)
|
||||
return self.c.RunSubprocessAndRefresh(
|
||||
self.git.Commit.CommitEditorCmdObj(),
|
||||
)
|
||||
}
|
||||
|
||||
func (self *FilesController) handleStatusFilterPressed() error {
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.FilteringMenuTitle,
|
||||
Items: []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: self.c.Tr.FilterStagedFiles,
|
||||
OnPress: func() error {
|
||||
return self.setStatusFiltering(filetree.DisplayStaged)
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: self.c.Tr.FilterUnstagedFiles,
|
||||
OnPress: func() error {
|
||||
return self.setStatusFiltering(filetree.DisplayUnstaged)
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: self.c.Tr.ResetCommitFilterState,
|
||||
OnPress: func() error {
|
||||
return self.setStatusFiltering(filetree.DisplayAll)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
|
||||
self.fileTreeViewModel.SetFilter(filter)
|
||||
return self.c.PostRefreshUpdate(self.context)
|
||||
}
|
||||
|
||||
func (self *FilesController) edit() error {
|
||||
node := self.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.File == nil {
|
||||
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory)
|
||||
}
|
||||
|
||||
return self.fileHelper.EditFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (self *FilesController) Open() error {
|
||||
node := self.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.fileHelper.OpenFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (self *FilesController) switchToMerge() error {
|
||||
file := self.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.switchToMergeFn(path)
|
||||
}
|
||||
|
||||
func (self *FilesController) handleCustomCommand() error {
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.CustomCommand,
|
||||
FindSuggestionsFunc: self.suggestionsHelper.GetCustomCommandsHistorySuggestionsFunc(),
|
||||
HandleConfirm: func(command string) error {
|
||||
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
|
||||
utils.Uniq(
|
||||
append(self.c.GetAppState().CustomCommandsHistory, command),
|
||||
),
|
||||
1000,
|
||||
)
|
||||
|
||||
err := self.c.SaveAppState()
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.CustomCommand)
|
||||
return self.c.RunSubprocessAndRefresh(
|
||||
self.os.Cmd.NewShell(command),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) createStashMenu() error {
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.LcStashOptions,
|
||||
Items: []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: self.c.Tr.LcStashAllChanges,
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.StashAllChanges)
|
||||
return self.handleStashSave(self.git.Stash.Save)
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: self.c.Tr.LcStashStagedChanges,
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.StashStagedChanges)
|
||||
return self.handleStashSave(self.git.Stash.SaveStagedChanges)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) stash() error {
|
||||
return self.handleStashSave(self.git.Stash.Save)
|
||||
}
|
||||
|
||||
func (self *FilesController) createResetMenu() error {
|
||||
return self.refHelper.CreateGitResetMenu("@{upstream}")
|
||||
}
|
||||
|
||||
func (self *FilesController) handleToggleDirCollapsed() error {
|
||||
node := self.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.fileTreeViewModel.ToggleCollapsed(node.GetPath())
|
||||
|
||||
if err := self.c.PostRefreshUpdate(self.allContexts.Files); err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FilesController) toggleTreeView() error {
|
||||
// get path of currently selected file
|
||||
path := self.getSelectedPath()
|
||||
|
||||
self.fileTreeViewModel.ToggleShowTree()
|
||||
|
||||
// find that same node in the new format and move the cursor to it
|
||||
if path != "" {
|
||||
self.fileTreeViewModel.ExpandToPath(path)
|
||||
index, found := self.fileTreeViewModel.GetIndexForPath(path)
|
||||
if found {
|
||||
self.context.GetPanelState().SetSelectedLineIdx(index)
|
||||
}
|
||||
}
|
||||
|
||||
return self.c.PostRefreshUpdate(self.context)
|
||||
}
|
||||
|
||||
func (self *FilesController) OpenMergeTool() error {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.MergeToolTitle,
|
||||
Prompt: self.c.Tr.MergeToolPrompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.OpenMergeTool)
|
||||
return self.c.RunSubprocessAndRefresh(
|
||||
self.git.WorkingTree.OpenMergeToolCmdObj(),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) ResetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.LcResettingSubmoduleStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.ResetSubmodule)
|
||||
|
||||
file := self.workingTreeHelper.FileForSubmodule(submodule)
|
||||
if file != nil {
|
||||
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.git.Submodule.Stash(submodule); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
if err := self.git.Submodule.Reset(submodule); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilesController) handleStashSave(stashFunc func(message string) error) error {
|
||||
if !self.workingTreeHelper.IsWorkingTreeDirty() {
|
||||
return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash)
|
||||
}
|
||||
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.StashChanges,
|
||||
HandleConfirm: func(stashComment string) error {
|
||||
if err := stashFunc(stashComment); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}})
|
||||
},
|
||||
})
|
||||
}
|
783
pkg/gui/controllers/local_commits_controller.go
Normal file
783
pkg/gui/controllers/local_commits_controller.go
Normal file
@ -0,0 +1,783 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
CheckoutRefFn func(refName string, opts types.CheckoutRefOptions) error
|
||||
CreateGitResetMenuFn func(refName string) error
|
||||
SwitchToCommitFilesContextFn func(SwitchToCommitFilesContextOpts) error
|
||||
CreateTagMenuFn func(commitSha string) error
|
||||
GetHostingServiceMgrFn func() *hosting_service.HostingServiceMgr
|
||||
PullFilesFn func() error
|
||||
CheckMergeOrRebase func(error) error
|
||||
OpenSearchFn func(viewName string) error
|
||||
)
|
||||
|
||||
type LocalCommitsController struct {
|
||||
c *ControllerCommon
|
||||
context types.IListContext
|
||||
os *oscommands.OSCommand
|
||||
git *commands.GitCommand
|
||||
refHelper IRefHelper
|
||||
|
||||
getSelectedLocalCommit func() *models.Commit
|
||||
getCommits func() []*models.Commit
|
||||
getSelectedLocalCommitIdx func() int
|
||||
checkMergeOrRebase CheckMergeOrRebase
|
||||
pullFiles PullFilesFn
|
||||
createTagMenu CreateTagMenuFn
|
||||
getHostingServiceMgr GetHostingServiceMgrFn
|
||||
switchToCommitFilesContext SwitchToCommitFilesContextFn
|
||||
openSearch OpenSearchFn
|
||||
getLimitCommits func() bool
|
||||
setLimitCommits func(bool)
|
||||
getShowWholeGitGraph func() bool
|
||||
setShowWholeGitGraph func(bool)
|
||||
}
|
||||
|
||||
var _ types.IController = &LocalCommitsController{}
|
||||
|
||||
func NewLocalCommitsController(
|
||||
c *ControllerCommon,
|
||||
context types.IListContext,
|
||||
os *oscommands.OSCommand,
|
||||
git *commands.GitCommand,
|
||||
refHelper IRefHelper,
|
||||
getSelectedLocalCommit func() *models.Commit,
|
||||
getCommits func() []*models.Commit,
|
||||
getSelectedLocalCommitIdx func() int,
|
||||
checkMergeOrRebase CheckMergeOrRebase,
|
||||
pullFiles PullFilesFn,
|
||||
createTagMenu CreateTagMenuFn,
|
||||
getHostingServiceMgr GetHostingServiceMgrFn,
|
||||
switchToCommitFilesContext SwitchToCommitFilesContextFn,
|
||||
openSearch OpenSearchFn,
|
||||
getLimitCommits func() bool,
|
||||
setLimitCommits func(bool),
|
||||
getShowWholeGitGraph func() bool,
|
||||
setShowWholeGitGraph func(bool),
|
||||
) *LocalCommitsController {
|
||||
return &LocalCommitsController{
|
||||
c: c,
|
||||
context: context,
|
||||
os: os,
|
||||
git: git,
|
||||
refHelper: refHelper,
|
||||
getSelectedLocalCommit: getSelectedLocalCommit,
|
||||
getCommits: getCommits,
|
||||
getSelectedLocalCommitIdx: getSelectedLocalCommitIdx,
|
||||
checkMergeOrRebase: checkMergeOrRebase,
|
||||
pullFiles: pullFiles,
|
||||
createTagMenu: createTagMenu,
|
||||
getHostingServiceMgr: getHostingServiceMgr,
|
||||
switchToCommitFilesContext: switchToCommitFilesContext,
|
||||
openSearch: openSearch,
|
||||
getLimitCommits: getLimitCommits,
|
||||
setLimitCommits: setLimitCommits,
|
||||
getShowWholeGitGraph: getShowWholeGitGraph,
|
||||
setShowWholeGitGraph: setShowWholeGitGraph,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) Keybindings(
|
||||
getKey func(key string) interface{},
|
||||
config config.KeybindingConfig,
|
||||
guards types.KeybindingGuards,
|
||||
) []*types.Binding {
|
||||
outsideFilterModeBindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Commits.SquashDown),
|
||||
Handler: self.squashDown,
|
||||
Description: self.c.Tr.LcSquashDown,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.MarkCommitAsFixup),
|
||||
Handler: self.fixup,
|
||||
Description: self.c.Tr.LcFixupCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.RenameCommit),
|
||||
Handler: self.checkSelected(self.reword),
|
||||
Description: self.c.Tr.LcRewordCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.RenameCommitWithEditor),
|
||||
Handler: self.rewordEditor,
|
||||
Description: self.c.Tr.LcRenameCommitEditor,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Remove),
|
||||
Handler: self.drop,
|
||||
Description: self.c.Tr.LcDeleteCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Edit),
|
||||
Handler: self.edit,
|
||||
Description: self.c.Tr.LcEditCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.PickCommit),
|
||||
Handler: self.pick,
|
||||
Description: self.c.Tr.LcPickCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.CreateFixupCommit),
|
||||
Handler: self.checkSelected(self.handleCreateFixupCommit),
|
||||
Description: self.c.Tr.LcCreateFixupCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.SquashAboveCommits),
|
||||
Handler: self.checkSelected(self.handleSquashAllAboveFixupCommits),
|
||||
Description: self.c.Tr.LcSquashAboveCommits,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.MoveDownCommit),
|
||||
Handler: self.handleCommitMoveDown,
|
||||
Description: self.c.Tr.LcMoveDownCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.MoveUpCommit),
|
||||
Handler: self.handleCommitMoveUp,
|
||||
Description: self.c.Tr.LcMoveUpCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.AmendToCommit),
|
||||
Handler: self.handleCommitAmendTo,
|
||||
Description: self.c.Tr.LcAmendToCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.RevertCommit),
|
||||
Handler: self.checkSelected(self.handleCommitRevert),
|
||||
Description: self.c.Tr.LcRevertCommit,
|
||||
},
|
||||
// overriding these navigation keybindings because we might need to load
|
||||
// more commits on demand
|
||||
{
|
||||
Key: getKey(config.Universal.StartSearch),
|
||||
Handler: func() error { return self.handleOpenSearch("commits") },
|
||||
Description: self.c.Tr.LcStartSearch,
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.GotoBottom),
|
||||
Handler: self.gotoBottom,
|
||||
Description: self.c.Tr.LcGotoBottom,
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: func() error { return self.context.HandleClick(self.checkSelected(self.enter)) },
|
||||
},
|
||||
}
|
||||
|
||||
for _, binding := range outsideFilterModeBindings {
|
||||
binding.Handler = guards.OutsideFilterMode(binding.Handler)
|
||||
}
|
||||
|
||||
bindings := append(outsideFilterModeBindings, []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Commits.OpenLogMenu),
|
||||
Handler: self.handleOpenLogMenu,
|
||||
Description: self.c.Tr.LcOpenLogMenu,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.ViewResetOptions),
|
||||
Handler: self.checkSelected(self.handleCreateCommitResetMenu),
|
||||
Description: self.c.Tr.LcResetToThisCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.GoInto),
|
||||
Handler: self.checkSelected(self.enter),
|
||||
Description: self.c.Tr.LcViewCommitFiles,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.CheckoutCommit),
|
||||
Handler: self.checkSelected(self.handleCheckoutCommit),
|
||||
Description: self.c.Tr.LcCheckoutCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.TagCommit),
|
||||
Handler: self.checkSelected(self.handleTagCommit),
|
||||
Description: self.c.Tr.LcTagCommit,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.CopyCommitMessageToClipboard),
|
||||
Handler: self.checkSelected(self.handleCopySelectedCommitMessageToClipboard),
|
||||
Description: self.c.Tr.LcCopyCommitMessageToClipboard,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.OpenInBrowser),
|
||||
Handler: self.checkSelected(self.handleOpenCommitInBrowser),
|
||||
Description: self.c.Tr.LcOpenCommitInBrowser,
|
||||
},
|
||||
}...)
|
||||
|
||||
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) squashDown() error {
|
||||
if len(self.getCommits()) <= 1 {
|
||||
return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
|
||||
}
|
||||
|
||||
applied, err := self.handleMidRebaseCommand("squash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.Squash,
|
||||
Prompt: self.c.Tr.SureSquashThisCommit,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.SquashCommitDown)
|
||||
return self.interactiveRebase("squash")
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) fixup() error {
|
||||
if len(self.getCommits()) <= 1 {
|
||||
return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
|
||||
}
|
||||
|
||||
applied, err := self.handleMidRebaseCommand("fixup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.Fixup,
|
||||
Prompt: self.c.Tr.SureFixupThisCommit,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.FixupCommit)
|
||||
return self.interactiveRebase("fixup")
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
applied, err := self.handleMidRebaseCommand("reword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
message, err := self.git.Commit.GetCommitMessage(commit.Sha)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
// TODO: use the commit message panel here
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.LcRewordCommit,
|
||||
InitialContent: message,
|
||||
HandleConfirm: func(response string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
|
||||
if err := self.git.Rebase.RewordCommit(self.getCommits(), self.getSelectedLocalCommitIdx(), response); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) rewordEditor() error {
|
||||
applied, err := self.handleMidRebaseCommand("reword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
|
||||
subProcess, err := self.git.Rebase.RewordCommitInEditor(
|
||||
self.getCommits(), self.getSelectedLocalCommitIdx(),
|
||||
)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
if subProcess != nil {
|
||||
return self.c.RunSubprocessAndRefresh(subProcess)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) drop() error {
|
||||
applied, err := self.handleMidRebaseCommand("drop")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.DeleteCommitTitle,
|
||||
Prompt: self.c.Tr.DeleteCommitPrompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.DropCommit)
|
||||
return self.interactiveRebase("drop")
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) edit() error {
|
||||
applied, err := self.handleMidRebaseCommand("edit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.EditCommit)
|
||||
return self.interactiveRebase("edit")
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) pick() error {
|
||||
applied, err := self.handleMidRebaseCommand("pick")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
// at this point we aren't actually rebasing so we will interpret this as an
|
||||
// attempt to pull. We might revoke this later after enabling configurable keybindings
|
||||
return self.pullFiles()
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) interactiveRebase(action string) error {
|
||||
err := self.git.Rebase.InteractiveRebase(self.getCommits(), self.getSelectedLocalCommitIdx(), action)
|
||||
return self.checkMergeOrRebase(err)
|
||||
}
|
||||
|
||||
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
|
||||
// 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
|
||||
func (self *LocalCommitsController) handleMidRebaseCommand(action string) (bool, error) {
|
||||
selectedCommit := self.getSelectedLocalCommit()
|
||||
if selectedCommit.Status != "rebasing" {
|
||||
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 == "reword" {
|
||||
return true, self.c.ErrorMsg(self.c.Tr.LcRewordNotSupported)
|
||||
}
|
||||
|
||||
self.c.LogAction("Update rebase TODO")
|
||||
self.c.LogCommand(
|
||||
fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
|
||||
false,
|
||||
)
|
||||
|
||||
if err := self.git.Rebase.EditRebaseTodo(
|
||||
self.getSelectedLocalCommitIdx(), action,
|
||||
); err != nil {
|
||||
return false, self.c.Error(err)
|
||||
}
|
||||
|
||||
return true, self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCommitMoveDown() error {
|
||||
index := self.context.GetPanelState().GetSelectedLineIdx()
|
||||
commits := self.getCommits()
|
||||
selectedCommit := self.getCommits()[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
if commits[index+1].Status != "rebasing" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
|
||||
self.c.LogCommand(fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()), false)
|
||||
|
||||
if err := self.git.Rebase.MoveTodoDown(index); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.context.HandleNextLine()
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
|
||||
})
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
|
||||
err := self.git.Rebase.MoveCommitDown(self.getCommits(), index)
|
||||
if err == nil {
|
||||
self.context.HandleNextLine()
|
||||
}
|
||||
return self.checkMergeOrRebase(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCommitMoveUp() error {
|
||||
index := self.context.GetPanelState().GetSelectedLineIdx()
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
selectedCommit := self.getCommits()[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
|
||||
self.c.LogCommand(
|
||||
fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
|
||||
false,
|
||||
)
|
||||
|
||||
if err := self.git.Rebase.MoveTodoDown(index - 1); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.context.HandlePrevLine()
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
|
||||
})
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
|
||||
err := self.git.Rebase.MoveCommitDown(self.getCommits(), index-1)
|
||||
if err == nil {
|
||||
self.context.HandlePrevLine()
|
||||
}
|
||||
return self.checkMergeOrRebase(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCommitAmendTo() error {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.AmendCommitTitle,
|
||||
Prompt: self.c.Tr.AmendCommitPrompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
|
||||
err := self.git.Rebase.AmendTo(self.getSelectedLocalCommit().Sha)
|
||||
return self.checkMergeOrRebase(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCommitRevert(commit *models.Commit) error {
|
||||
if commit.IsMerge() {
|
||||
return self.createRevertMergeCommitMenu(commit)
|
||||
} else {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.Actions.RevertCommit,
|
||||
Prompt: utils.ResolvePlaceholderString(
|
||||
self.c.Tr.ConfirmRevertCommit,
|
||||
map[string]string{
|
||||
"selectedCommit": commit.ShortSha(),
|
||||
}),
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
|
||||
if err := self.git.Commit.Revert(commit.Sha); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.afterRevertCommit()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.Commit) error {
|
||||
menuItems := make([]*popup.MenuItem, len(commit.Parents))
|
||||
for i, parentSha := range commit.Parents {
|
||||
i := i
|
||||
message, err := self.git.Commit.GetCommitMessageFirstLine(parentSha)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
menuItems[i] = &popup.MenuItem{
|
||||
DisplayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message),
|
||||
OnPress: func() error {
|
||||
parentNumber := i + 1
|
||||
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
|
||||
if err := self.git.Commit.RevertMerge(commit.Sha, parentNumber); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.afterRevertCommit()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return self.c.Menu(popup.CreateMenuOptions{Title: self.c.Tr.SelectParentCommitForMerge, Items: menuItems})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) afterRevertCommit() error {
|
||||
self.context.HandleNextLine()
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.BLOCK_UI, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) enter(commit *models.Commit) error {
|
||||
return self.switchToCommitFilesContext(SwitchToCommitFilesContextOpts{
|
||||
RefName: commit.Sha,
|
||||
CanRebase: true,
|
||||
Context: self.context,
|
||||
WindowName: "commits",
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCreateFixupCommit(commit *models.Commit) error {
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.SureCreateFixupCommit,
|
||||
map[string]string{
|
||||
"commit": commit.Sha,
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.CreateFixupCommit,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit)
|
||||
if err := self.git.Commit.CreateFixupCommit(commit.Sha); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleSquashAllAboveFixupCommits(commit *models.Commit) error {
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.SureSquashAboveCommits,
|
||||
map[string]string{
|
||||
"commit": commit.Sha,
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.SquashAboveCommits,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
|
||||
err := self.git.Rebase.SquashAllAboveFixupCommits(commit.Sha)
|
||||
return self.checkMergeOrRebase(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleTagCommit(commit *models.Commit) error {
|
||||
return self.createTagMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCheckoutCommit(commit *models.Commit) error {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.LcCheckoutCommit,
|
||||
Prompt: self.c.Tr.SureCheckoutThisCommit,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CheckoutCommit)
|
||||
return self.refHelper.CheckoutRef(commit.Sha, types.CheckoutRefOptions{})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCreateCommitResetMenu(commit *models.Commit) error {
|
||||
return self.refHelper.CreateGitResetMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleOpenSearch(string) error {
|
||||
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||
if self.getLimitCommits() {
|
||||
self.setLimitCommits(false)
|
||||
if err := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return self.openSearch("commits")
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) gotoBottom() error {
|
||||
// we usually lazyload these commits but now that we're jumping to the bottom we need to load them now
|
||||
if self.getLimitCommits() {
|
||||
self.setLimitCommits(false)
|
||||
if err := self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
self.context.HandleGotoBottom()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleCopySelectedCommitMessageToClipboard(commit *models.Commit) error {
|
||||
message, err := self.git.Commit.GetCommitMessage(commit.Sha)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.CopyCommitMessageToClipboard)
|
||||
if err := self.os.CopyToClipboard(message); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
self.c.Toast(self.c.Tr.CommitMessageCopiedToClipboard)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleOpenLogMenu() error {
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.LogMenuTitle,
|
||||
Items: []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: self.c.Tr.ToggleShowGitGraphAll,
|
||||
OnPress: func() error {
|
||||
self.setShowWholeGitGraph(!self.getShowWholeGitGraph())
|
||||
|
||||
if self.getShowWholeGitGraph() {
|
||||
self.setLimitCommits(false)
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.LcLoadingCommits, func() error {
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}})
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: self.c.Tr.ShowGitGraph,
|
||||
OpensMenu: true,
|
||||
OnPress: func() error {
|
||||
onPress := func(value string) func() error {
|
||||
return func() error {
|
||||
self.c.UserConfig.Git.Log.ShowGraph = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.LogMenuTitle,
|
||||
Items: []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: "always",
|
||||
OnPress: onPress("always"),
|
||||
},
|
||||
{
|
||||
DisplayString: "never",
|
||||
OnPress: onPress("never"),
|
||||
},
|
||||
{
|
||||
DisplayString: "when maximised",
|
||||
OnPress: onPress("when-maximised"),
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: self.c.Tr.SortCommits,
|
||||
OpensMenu: true,
|
||||
OnPress: func() error {
|
||||
onPress := func(value string) func() error {
|
||||
return func() error {
|
||||
self.c.UserConfig.Git.Log.Order = value
|
||||
return self.c.WithWaitingStatus(self.c.Tr.LcLoadingCommits, func() error {
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.LogMenuTitle,
|
||||
Items: []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: "topological (topo-order)",
|
||||
OnPress: onPress("topo-order"),
|
||||
},
|
||||
{
|
||||
DisplayString: "date-order",
|
||||
OnPress: onPress("date-order"),
|
||||
},
|
||||
{
|
||||
DisplayString: "author-date-order",
|
||||
OnPress: onPress("author-date-order"),
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleOpenCommitInBrowser(commit *models.Commit) error {
|
||||
hostingServiceMgr := self.getHostingServiceMgr()
|
||||
|
||||
url, err := hostingServiceMgr.GetCommitURL(commit.Sha)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.OpenCommitInBrowser)
|
||||
if err := self.os.OpenLink(url); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) checkSelected(callback func(*models.Commit) error) func() error {
|
||||
return func() error {
|
||||
commit := self.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(commit)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
70
pkg/gui/controllers/menu_controller.go
Normal file
70
pkg/gui/controllers/menu_controller.go
Normal file
@ -0,0 +1,70 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type MenuController struct {
|
||||
c *ControllerCommon
|
||||
context types.IListContext
|
||||
|
||||
getSelectedMenuItem func() *popup.MenuItem
|
||||
}
|
||||
|
||||
var _ types.IController = &MenuController{}
|
||||
|
||||
func NewMenuController(
|
||||
c *ControllerCommon,
|
||||
context types.IListContext,
|
||||
getSelectedMenuItem func() *popup.MenuItem,
|
||||
) *MenuController {
|
||||
return &MenuController{
|
||||
c: c,
|
||||
context: context,
|
||||
getSelectedMenuItem: getSelectedMenuItem,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MenuController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Universal.Select),
|
||||
Handler: self.press,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Confirm),
|
||||
Handler: self.press,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.ConfirmAlt1),
|
||||
Handler: self.press,
|
||||
},
|
||||
{
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: func() error { return self.context.HandleClick(self.press) },
|
||||
},
|
||||
}
|
||||
|
||||
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
|
||||
}
|
||||
|
||||
func (self *MenuController) press() error {
|
||||
selectedItem := self.getSelectedMenuItem()
|
||||
|
||||
if err := self.c.PopContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := selectedItem.OnPress(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MenuController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
204
pkg/gui/controllers/remotes_controller.go
Normal file
204
pkg/gui/controllers/remotes_controller.go
Normal file
@ -0,0 +1,204 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type RemotesController struct {
|
||||
c *ControllerCommon
|
||||
context types.IListContext
|
||||
git *commands.GitCommand
|
||||
|
||||
getSelectedRemote func() *models.Remote
|
||||
setRemoteBranches func([]*models.RemoteBranch)
|
||||
allContexts context.ContextTree
|
||||
fetchMutex *sync.Mutex
|
||||
}
|
||||
|
||||
var _ types.IController = &RemotesController{}
|
||||
|
||||
func NewRemotesController(
|
||||
c *ControllerCommon,
|
||||
context types.IListContext,
|
||||
git *commands.GitCommand,
|
||||
allContexts context.ContextTree,
|
||||
getSelectedRemote func() *models.Remote,
|
||||
setRemoteBranches func([]*models.RemoteBranch),
|
||||
fetchMutex *sync.Mutex,
|
||||
) *RemotesController {
|
||||
return &RemotesController{
|
||||
c: c,
|
||||
git: git,
|
||||
allContexts: allContexts,
|
||||
context: context,
|
||||
getSelectedRemote: getSelectedRemote,
|
||||
setRemoteBranches: setRemoteBranches,
|
||||
fetchMutex: fetchMutex,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RemotesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Universal.GoInto),
|
||||
Handler: self.checkSelected(self.enter),
|
||||
},
|
||||
{
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: func() error { return self.context.HandleClick(self.checkSelected(self.enter)) },
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Branches.FetchRemote),
|
||||
Handler: self.checkSelected(self.fetch),
|
||||
Description: self.c.Tr.LcFetchRemote,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.New),
|
||||
Handler: self.add,
|
||||
Description: self.c.Tr.LcAddNewRemote,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Remove),
|
||||
Handler: self.checkSelected(self.remove),
|
||||
Description: self.c.Tr.LcRemoveRemote,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Edit),
|
||||
Handler: self.checkSelected(self.edit),
|
||||
Description: self.c.Tr.LcEditRemote,
|
||||
},
|
||||
}
|
||||
|
||||
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
|
||||
}
|
||||
|
||||
func (self *RemotesController) enter(remote *models.Remote) error {
|
||||
// naive implementation: get the branches from the remote and render them to the list, change the context
|
||||
self.setRemoteBranches(remote.Branches)
|
||||
|
||||
newSelectedLine := 0
|
||||
if len(remote.Branches) == 0 {
|
||||
newSelectedLine = -1
|
||||
}
|
||||
self.allContexts.RemoteBranches.GetPanelState().SetSelectedLineIdx(newSelectedLine)
|
||||
|
||||
return self.c.PushContext(self.allContexts.RemoteBranches)
|
||||
}
|
||||
|
||||
func (self *RemotesController) add() error {
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.LcNewRemoteName,
|
||||
HandleConfirm: func(remoteName string) error {
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.LcNewRemoteUrl,
|
||||
HandleConfirm: func(remoteUrl string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.AddRemote)
|
||||
if err := self.git.Remote.AddRemote(remoteName, remoteUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.REMOTES}})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RemotesController) remove(remote *models.Remote) error {
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.LcRemoveRemote,
|
||||
Prompt: self.c.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?",
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RemoveRemote)
|
||||
if err := self.git.Remote.RemoveRemote(remote.Name); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RemotesController) edit(remote *models.Remote) error {
|
||||
editNameMessage := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.LcEditRemoteName,
|
||||
map[string]string{
|
||||
"remoteName": remote.Name,
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: editNameMessage,
|
||||
InitialContent: remote.Name,
|
||||
HandleConfirm: func(updatedRemoteName string) error {
|
||||
if updatedRemoteName != remote.Name {
|
||||
self.c.LogAction(self.c.Tr.Actions.UpdateRemote)
|
||||
if err := self.git.Remote.RenameRemote(remote.Name, updatedRemoteName); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
editUrlMessage := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.LcEditRemoteUrl,
|
||||
map[string]string{
|
||||
"remoteName": updatedRemoteName,
|
||||
},
|
||||
)
|
||||
|
||||
urls := remote.Urls
|
||||
url := ""
|
||||
if len(urls) > 0 {
|
||||
url = urls[0]
|
||||
}
|
||||
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: editUrlMessage,
|
||||
InitialContent: url,
|
||||
HandleConfirm: func(updatedRemoteUrl string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.UpdateRemote)
|
||||
if err := self.git.Remote.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RemotesController) fetch(remote *models.Remote) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FetchingRemoteStatus, func() error {
|
||||
self.fetchMutex.Lock()
|
||||
defer self.fetchMutex.Unlock()
|
||||
|
||||
err := self.git.Sync.FetchRemote(remote.Name)
|
||||
if err != nil {
|
||||
_ = self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RemotesController) checkSelected(callback func(*models.Remote) error) func() error {
|
||||
return func() error {
|
||||
file := self.getSelectedRemote()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(file)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RemotesController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
@ -5,65 +5,57 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// if Go let me do private struct embedding of structs with public fields (which it should)
|
||||
// I would just do that. But alas.
|
||||
type ControllerCommon struct {
|
||||
*common.Common
|
||||
IGuiCommon
|
||||
type SubmodulesController struct {
|
||||
c *ControllerCommon
|
||||
context types.IListContext
|
||||
git *commands.GitCommand
|
||||
|
||||
enterSubmodule func(submodule *models.SubmoduleConfig) error
|
||||
getSelectedSubmodule func() *models.SubmoduleConfig
|
||||
}
|
||||
|
||||
type SubmodulesController struct {
|
||||
// I've said publicly that I'm against single-letter variable names but in this
|
||||
// case I would actually prefer a _zero_ letter variable name in the form of
|
||||
// struct embedding, but Go does not allow hiding public fields in an embedded struct
|
||||
// to the client
|
||||
c *ControllerCommon
|
||||
enterSubmoduleFn func(submodule *models.SubmoduleConfig) error
|
||||
getSelectedSubmodule func() *models.SubmoduleConfig
|
||||
git *commands.GitCommand
|
||||
submodules []*models.SubmoduleConfig
|
||||
}
|
||||
var _ types.IController = &SubmodulesController{}
|
||||
|
||||
func NewSubmodulesController(
|
||||
c *ControllerCommon,
|
||||
enterSubmoduleFn func(submodule *models.SubmoduleConfig) error,
|
||||
context types.IListContext,
|
||||
git *commands.GitCommand,
|
||||
submodules []*models.SubmoduleConfig,
|
||||
enterSubmodule func(submodule *models.SubmoduleConfig) error,
|
||||
getSelectedSubmodule func() *models.SubmoduleConfig,
|
||||
) *SubmodulesController {
|
||||
return &SubmodulesController{
|
||||
c: c,
|
||||
enterSubmoduleFn: enterSubmoduleFn,
|
||||
context: context,
|
||||
git: git,
|
||||
submodules: submodules,
|
||||
enterSubmodule: enterSubmodule,
|
||||
getSelectedSubmodule: getSelectedSubmodule,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SubmodulesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig) []*types.Binding {
|
||||
return []*types.Binding{
|
||||
func (self *SubmodulesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Universal.GoInto),
|
||||
Handler: self.forSubmodule(self.enter),
|
||||
Handler: self.checkSelected(self.enter),
|
||||
Description: self.c.Tr.LcEnterSubmodule,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Remove),
|
||||
Handler: self.forSubmodule(self.remove),
|
||||
Handler: self.checkSelected(self.remove),
|
||||
Description: self.c.Tr.LcRemoveSubmodule,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Submodules.Update),
|
||||
Handler: self.forSubmodule(self.update),
|
||||
Handler: self.checkSelected(self.update),
|
||||
Description: self.c.Tr.LcSubmoduleUpdate,
|
||||
},
|
||||
{
|
||||
@ -73,12 +65,12 @@ func (self *SubmodulesController) Keybindings(getKey func(key string) interface{
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Edit),
|
||||
Handler: self.forSubmodule(self.editURL),
|
||||
Handler: self.checkSelected(self.editURL),
|
||||
Description: self.c.Tr.LcEditSubmoduleUrl,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Submodules.Init),
|
||||
Handler: self.forSubmodule(self.init),
|
||||
Handler: self.checkSelected(self.init),
|
||||
Description: self.c.Tr.LcInitSubmodule,
|
||||
},
|
||||
{
|
||||
@ -87,11 +79,17 @@ func (self *SubmodulesController) Keybindings(getKey func(key string) interface{
|
||||
Description: self.c.Tr.LcViewBulkSubmoduleOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: func() error { return self.context.HandleClick(self.checkSelected(self.enter)) },
|
||||
},
|
||||
}
|
||||
|
||||
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
|
||||
}
|
||||
|
||||
func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error {
|
||||
return self.enterSubmoduleFn(submodule)
|
||||
return self.enterSubmodule(submodule)
|
||||
}
|
||||
|
||||
func (self *SubmodulesController) add() error {
|
||||
@ -231,7 +229,7 @@ func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) erro
|
||||
})
|
||||
}
|
||||
|
||||
func (self *SubmodulesController) forSubmodule(callback func(*models.SubmoduleConfig) error) func() error {
|
||||
func (self *SubmodulesController) checkSelected(callback func(*models.SubmoduleConfig) error) func() error {
|
||||
return func() error {
|
||||
submodule := self.getSelectedSubmodule()
|
||||
if submodule == nil {
|
||||
@ -241,3 +239,7 @@ func (self *SubmodulesController) forSubmodule(callback func(*models.SubmoduleCo
|
||||
return callback(submodule)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SubmodulesController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
||||
|
253
pkg/gui/controllers/sync_controller.go
Normal file
253
pkg/gui/controllers/sync_controller.go
Normal file
@ -0,0 +1,253 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type SyncController struct {
|
||||
// I've said publicly that I'm against single-letter variable names but in this
|
||||
// case I would actually prefer a _zero_ letter variable name in the form of
|
||||
// struct embedding, but Go does not allow hiding public fields in an embedded struct
|
||||
// to the client
|
||||
c *ControllerCommon
|
||||
git *commands.GitCommand
|
||||
|
||||
getCheckedOutBranch func() *models.Branch
|
||||
suggestionsHelper ISuggestionsHelper
|
||||
getSuggestedRemote func() string
|
||||
checkMergeOrRebase func(error) error
|
||||
}
|
||||
|
||||
var _ types.IController = &SyncController{}
|
||||
|
||||
func NewSyncController(
|
||||
c *ControllerCommon,
|
||||
git *commands.GitCommand,
|
||||
getCheckedOutBranch func() *models.Branch,
|
||||
suggestionsHelper ISuggestionsHelper,
|
||||
getSuggestedRemote func() string,
|
||||
checkMergeOrRebase func(error) error,
|
||||
) *SyncController {
|
||||
return &SyncController{
|
||||
c: c,
|
||||
git: git,
|
||||
|
||||
getCheckedOutBranch: getCheckedOutBranch,
|
||||
suggestionsHelper: suggestionsHelper,
|
||||
getSuggestedRemote: getSuggestedRemote,
|
||||
checkMergeOrRebase: checkMergeOrRebase,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SyncController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Universal.PushFiles),
|
||||
Handler: guards.NoPopupPanel(self.HandlePush),
|
||||
Description: self.c.Tr.LcPush,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.PullFiles),
|
||||
Handler: guards.NoPopupPanel(self.HandlePull),
|
||||
Description: self.c.Tr.LcPull,
|
||||
},
|
||||
}
|
||||
|
||||
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) branchCheckedOut(f func(*models.Branch) error) func() error {
|
||||
return func() error {
|
||||
currentBranch := self.getCheckedOutBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return nil
|
||||
}
|
||||
|
||||
return f(currentBranch)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SyncController) push(currentBranch *models.Branch) error {
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
if currentBranch.IsTrackingRemote() {
|
||||
opts := pushOpts{
|
||||
force: false,
|
||||
upstreamRemote: currentBranch.UpstreamRemote,
|
||||
upstreamBranch: currentBranch.UpstreamBranch,
|
||||
}
|
||||
if currentBranch.HasCommitsToPull() {
|
||||
opts.force = true
|
||||
return self.requestToForcePush(opts)
|
||||
} else {
|
||||
return self.pushAux(opts)
|
||||
}
|
||||
} else {
|
||||
if self.git.Config.GetPushToCurrent() {
|
||||
return self.pushAux(pushOpts{setUpstream: true})
|
||||
} else {
|
||||
return self.promptForUpstream(currentBranch, func(upstream string) error {
|
||||
var upstreamBranch, upstreamRemote string
|
||||
split := strings.Split(upstream, " ")
|
||||
if len(split) == 2 {
|
||||
upstreamRemote = split[0]
|
||||
upstreamBranch = split[1]
|
||||
} else {
|
||||
upstreamRemote = upstream
|
||||
upstreamBranch = ""
|
||||
}
|
||||
|
||||
return self.pushAux(pushOpts{
|
||||
force: false,
|
||||
upstreamRemote: upstreamRemote,
|
||||
upstreamBranch: upstreamBranch,
|
||||
setUpstream: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SyncController) pull(currentBranch *models.Branch) error {
|
||||
action := self.c.Tr.Actions.Pull
|
||||
|
||||
// if we have no upstream branch we need to set that first
|
||||
if !currentBranch.IsTrackingRemote() {
|
||||
return self.promptForUpstream(currentBranch, func(upstream string) error {
|
||||
var upstreamBranch, upstreamRemote string
|
||||
split := strings.Split(upstream, " ")
|
||||
if len(split) != 2 {
|
||||
return self.c.ErrorMsg(self.c.Tr.InvalidUpstream)
|
||||
}
|
||||
|
||||
upstreamRemote = split[0]
|
||||
upstreamBranch = split[1]
|
||||
|
||||
if err := self.git.Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil {
|
||||
errorMessage := err.Error()
|
||||
if strings.Contains(errorMessage, "does not exist") {
|
||||
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
|
||||
}
|
||||
return self.c.ErrorMsg(errorMessage)
|
||||
}
|
||||
return self.PullAux(PullFilesOptions{UpstreamRemote: upstreamRemote, UpstreamBranch: upstreamBranch, Action: action})
|
||||
})
|
||||
}
|
||||
|
||||
return self.PullAux(PullFilesOptions{UpstreamRemote: currentBranch.UpstreamRemote, UpstreamBranch: currentBranch.UpstreamBranch, Action: action})
|
||||
}
|
||||
|
||||
func (self *SyncController) promptForUpstream(currentBranch *models.Branch, onConfirm func(string) error) error {
|
||||
suggestedRemote := self.getSuggestedRemote()
|
||||
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.EnterUpstream,
|
||||
InitialContent: suggestedRemote + " " + currentBranch.Name,
|
||||
FindSuggestionsFunc: self.suggestionsHelper.GetRemoteBranchesSuggestionsFunc(" "),
|
||||
HandleConfirm: onConfirm,
|
||||
})
|
||||
}
|
||||
|
||||
type PullFilesOptions struct {
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
FastForwardOnly bool
|
||||
Action string
|
||||
}
|
||||
|
||||
func (self *SyncController) PullAux(opts PullFilesOptions) error {
|
||||
return self.c.WithLoaderPanel(self.c.Tr.PullWait, func() error {
|
||||
return self.pullWithLock(opts)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *SyncController) pullWithLock(opts PullFilesOptions) error {
|
||||
self.c.LogAction(opts.Action)
|
||||
|
||||
err := self.git.Sync.Pull(
|
||||
git_commands.PullOptions{
|
||||
RemoteName: opts.UpstreamRemote,
|
||||
BranchName: opts.UpstreamBranch,
|
||||
FastForwardOnly: opts.FastForwardOnly,
|
||||
},
|
||||
)
|
||||
|
||||
return self.checkMergeOrRebase(err)
|
||||
}
|
||||
|
||||
type pushOpts struct {
|
||||
force bool
|
||||
upstreamRemote string
|
||||
upstreamBranch string
|
||||
setUpstream bool
|
||||
}
|
||||
|
||||
func (self *SyncController) pushAux(opts pushOpts) error {
|
||||
return self.c.WithLoaderPanel(self.c.Tr.PushWait, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.Push)
|
||||
err := self.git.Sync.Push(git_commands.PushOpts{
|
||||
Force: opts.force,
|
||||
UpstreamRemote: opts.upstreamRemote,
|
||||
UpstreamBranch: opts.upstreamBranch,
|
||||
SetUpstream: opts.setUpstream,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
_ = self.c.ErrorMsg(self.c.Tr.UpdatesRejectedAndForcePushDisabled)
|
||||
return nil
|
||||
}
|
||||
_ = self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.ForcePush,
|
||||
Prompt: self.c.Tr.ForcePushPrompt,
|
||||
HandleConfirm: func() error {
|
||||
newOpts := opts
|
||||
newOpts.force = true
|
||||
|
||||
return self.pushAux(newOpts)
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
_ = self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *SyncController) requestToForcePush(opts pushOpts) error {
|
||||
forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
return self.c.ErrorMsg(self.c.Tr.ForcePushDisabled)
|
||||
}
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.ForcePush,
|
||||
Prompt: self.c.Tr.ForcePushPrompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.pushAux(opts)
|
||||
},
|
||||
})
|
||||
}
|
229
pkg/gui/controllers/tags_controller.go
Normal file
229
pkg/gui/controllers/tags_controller.go
Normal file
@ -0,0 +1,229 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type TagsController struct {
|
||||
c *ControllerCommon
|
||||
context types.IListContext
|
||||
git *commands.GitCommand
|
||||
allContexts context.ContextTree
|
||||
|
||||
refHelper IRefHelper
|
||||
suggestionsHelper ISuggestionsHelper
|
||||
|
||||
getSelectedTag func() *models.Tag
|
||||
switchToSubCommitsContext func(string) error
|
||||
}
|
||||
|
||||
var _ types.IController = &TagsController{}
|
||||
|
||||
func NewTagsController(
|
||||
c *ControllerCommon,
|
||||
context types.IListContext,
|
||||
git *commands.GitCommand,
|
||||
allContexts context.ContextTree,
|
||||
refHelper IRefHelper,
|
||||
suggestionsHelper ISuggestionsHelper,
|
||||
|
||||
getSelectedTag func() *models.Tag,
|
||||
switchToSubCommitsContext func(string) error,
|
||||
) *TagsController {
|
||||
return &TagsController{
|
||||
c: c,
|
||||
context: context,
|
||||
git: git,
|
||||
allContexts: allContexts,
|
||||
refHelper: refHelper,
|
||||
suggestionsHelper: suggestionsHelper,
|
||||
|
||||
getSelectedTag: getSelectedTag,
|
||||
switchToSubCommitsContext: switchToSubCommitsContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagsController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Universal.Select),
|
||||
Handler: self.withSelectedTag(self.checkout),
|
||||
Description: self.c.Tr.LcCheckout,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Remove),
|
||||
Handler: self.withSelectedTag(self.delete),
|
||||
Description: self.c.Tr.LcDeleteTag,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Branches.PushTag),
|
||||
Handler: self.withSelectedTag(self.push),
|
||||
Description: self.c.Tr.LcPushTag,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.New),
|
||||
Handler: self.create,
|
||||
Description: self.c.Tr.LcCreateTag,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Commits.ViewResetOptions),
|
||||
Handler: self.withSelectedTag(self.createResetMenu),
|
||||
Description: self.c.Tr.LcViewResetOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.GoInto),
|
||||
Handler: self.withSelectedTag(self.enter),
|
||||
Description: self.c.Tr.LcViewCommits,
|
||||
},
|
||||
}
|
||||
|
||||
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
|
||||
}
|
||||
|
||||
func (self *TagsController) checkout(tag *models.Tag) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CheckoutTag)
|
||||
if err := self.refHelper.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.c.PushContext(self.allContexts.Branches)
|
||||
}
|
||||
|
||||
func (self *TagsController) enter(tag *models.Tag) error {
|
||||
return self.switchToSubCommitsContext(tag.Name)
|
||||
}
|
||||
|
||||
func (self *TagsController) delete(tag *models.Tag) error {
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.DeleteTagPrompt,
|
||||
map[string]string{
|
||||
"tagName": tag.Name,
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.DeleteTagTitle,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.DeleteTag)
|
||||
if err := self.git.Tag.Delete(tag.Name); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsController) push(tag *models.Tag) error {
|
||||
title := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.PushTagTitle,
|
||||
map[string]string{
|
||||
"tagName": tag.Name,
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: title,
|
||||
InitialContent: "origin",
|
||||
FindSuggestionsFunc: self.suggestionsHelper.GetRemoteSuggestionsFunc(),
|
||||
HandleConfirm: func(response string) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.PushingTagStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.PushTag)
|
||||
err := self.git.Tag.Push(response, tag.Name)
|
||||
if err != nil {
|
||||
_ = self.c.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsController) createResetMenu(tag *models.Tag) error {
|
||||
return self.refHelper.CreateGitResetMenu(tag.Name)
|
||||
}
|
||||
|
||||
func (self *TagsController) CreateTagMenu(commitSha string) error {
|
||||
return self.c.Menu(popup.CreateMenuOptions{
|
||||
Title: self.c.Tr.TagMenuTitle,
|
||||
Items: []*popup.MenuItem{
|
||||
{
|
||||
DisplayString: self.c.Tr.LcLightweightTag,
|
||||
OnPress: func() error {
|
||||
return self.handleCreateLightweightTag(commitSha)
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayString: self.c.Tr.LcAnnotatedTag,
|
||||
OnPress: func() error {
|
||||
return self.handleCreateAnnotatedTag(commitSha)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsController) afterTagCreate() error {
|
||||
self.context.GetPanelState().SetSelectedLineIdx(0)
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsController) handleCreateAnnotatedTag(commitSha string) error {
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.TagNameTitle,
|
||||
HandleConfirm: func(tagName string) error {
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.TagMessageTitle,
|
||||
HandleConfirm: func(msg string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
|
||||
if err := self.git.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.afterTagCreate()
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsController) handleCreateLightweightTag(commitSha string) error {
|
||||
return self.c.Prompt(popup.PromptOpts{
|
||||
Title: self.c.Tr.TagNameTitle,
|
||||
HandleConfirm: func(tagName string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
|
||||
if err := self.git.Tag.CreateLightweight(tagName, commitSha); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.afterTagCreate()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsController) create() error {
|
||||
// leaving commit SHA blank so that we're just creating the tag for the current commit
|
||||
return self.CreateTagMenu("")
|
||||
}
|
||||
|
||||
func (self *TagsController) withSelectedTag(f func(tag *models.Tag) error) func() error {
|
||||
return func() error {
|
||||
tag := self.getSelectedTag()
|
||||
if tag == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f(tag)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagsController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
@ -8,6 +11,54 @@ import (
|
||||
type IGuiCommon interface {
|
||||
popup.IPopupHandler
|
||||
|
||||
LogAction(string)
|
||||
LogAction(action string)
|
||||
LogCommand(cmdStr string, isCommandLine bool)
|
||||
// we call this when we want to refetch some models and render the result. Internally calls PostRefreshUpdate
|
||||
Refresh(types.RefreshOptions) error
|
||||
// we call this when we've changed something in the view model but not the actual model,
|
||||
// e.g. expanding or collapsing a folder in a file view. Calling 'Refresh' in this
|
||||
// case would be overkill, although refresh will internally call 'PostRefreshUpdate'
|
||||
PostRefreshUpdate(types.Context) error
|
||||
RunSubprocessAndRefresh(oscommands.ICmdObj) error
|
||||
PushContext(context types.Context, opts ...types.OnFocusOpts) error
|
||||
PopContext() error
|
||||
|
||||
GetAppState() *config.AppState
|
||||
SaveAppState() error
|
||||
}
|
||||
|
||||
type IRefHelper interface {
|
||||
CheckoutRef(ref string, options types.CheckoutRefOptions) error
|
||||
CreateGitResetMenu(ref string) error
|
||||
ResetToRef(ref string, strength string, envVars []string) error
|
||||
}
|
||||
|
||||
type ISuggestionsHelper interface {
|
||||
GetRemoteSuggestionsFunc() func(string) []*types.Suggestion
|
||||
GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion
|
||||
GetFilePathSuggestionsFunc() func(string) []*types.Suggestion
|
||||
GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion
|
||||
GetRefsSuggestionsFunc() func(string) []*types.Suggestion
|
||||
GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion
|
||||
}
|
||||
|
||||
type IFileHelper interface {
|
||||
EditFile(filename string) error
|
||||
EditFileAtLine(filename string, lineNumber int) error
|
||||
OpenFile(filename string) error
|
||||
}
|
||||
|
||||
type IWorkingTreeHelper interface {
|
||||
AnyStagedFiles() bool
|
||||
AnyTrackedFiles() bool
|
||||
IsWorkingTreeDirty() bool
|
||||
FileForSubmodule(submodule *models.SubmoduleConfig) *models.File
|
||||
}
|
||||
|
||||
// all fields mandatory (except `CanRebase` because it's boolean)
|
||||
type SwitchToCommitFilesContextOpts struct {
|
||||
RefName string
|
||||
CanRebase bool
|
||||
Context types.Context
|
||||
WindowName string
|
||||
}
|
||||
|
266
pkg/gui/controllers/undo_controller.go
Normal file
266
pkg/gui/controllers/undo_controller.go
Normal file
@ -0,0 +1,266 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Quick summary of how this all works:
|
||||
// when you want to undo or redo, we start from the top of the reflog and work
|
||||
// down until we've reached the last user-initiated reflog entry that hasn't already been undone
|
||||
// we then do the reverse of what that reflog describes.
|
||||
// When we do this, we create a new reflog entry, and tag it as either an undo or redo
|
||||
// Then, next time we want to undo, we'll use those entries to know which user-initiated
|
||||
// actions we can skip. E.g. if I do do three things, A, B, and C, and hit undo twice,
|
||||
// the reflog will read UUCBA, and when I read the first two undos, I know to skip the following
|
||||
// two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way.
|
||||
|
||||
type UndoController struct {
|
||||
c *ControllerCommon
|
||||
git *commands.GitCommand
|
||||
|
||||
refHelper IRefHelper
|
||||
workingTreeHelper IWorkingTreeHelper
|
||||
|
||||
getFilteredReflogCommits func() []*models.Commit
|
||||
}
|
||||
|
||||
var _ types.IController = &UndoController{}
|
||||
|
||||
func NewUndoController(
|
||||
c *ControllerCommon,
|
||||
git *commands.GitCommand,
|
||||
refHelper IRefHelper,
|
||||
workingTreeHelper IWorkingTreeHelper,
|
||||
|
||||
getFilteredReflogCommits func() []*models.Commit,
|
||||
) *UndoController {
|
||||
return &UndoController{
|
||||
c: c,
|
||||
git: git,
|
||||
refHelper: refHelper,
|
||||
workingTreeHelper: workingTreeHelper,
|
||||
|
||||
getFilteredReflogCommits: getFilteredReflogCommits,
|
||||
}
|
||||
}
|
||||
|
||||
type ReflogActionKind int
|
||||
|
||||
const (
|
||||
CHECKOUT ReflogActionKind = iota
|
||||
COMMIT
|
||||
REBASE
|
||||
CURRENT_REBASE
|
||||
)
|
||||
|
||||
type reflogAction struct {
|
||||
kind ReflogActionKind
|
||||
from string
|
||||
to string
|
||||
}
|
||||
|
||||
func (self *UndoController) Keybindings(
|
||||
getKey func(key string) interface{},
|
||||
config config.KeybindingConfig,
|
||||
guards types.KeybindingGuards,
|
||||
) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: getKey(config.Universal.Undo),
|
||||
Handler: self.reflogUndo,
|
||||
Description: self.c.Tr.LcUndoReflog,
|
||||
},
|
||||
{
|
||||
Key: getKey(config.Universal.Redo),
|
||||
Handler: self.reflogRedo,
|
||||
Description: self.c.Tr.LcRedoReflog,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (self *UndoController) Context() types.Context {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *UndoController) reflogUndo() error {
|
||||
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
|
||||
undoingStatus := self.c.Tr.UndoingStatus
|
||||
|
||||
if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
return self.c.ErrorMsg(self.c.Tr.LcCantUndoWhileRebasing)
|
||||
}
|
||||
|
||||
return self.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||
if counter != 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch action.kind {
|
||||
case COMMIT, REBASE:
|
||||
self.c.LogAction(self.c.Tr.Actions.Undo)
|
||||
return true, self.hardResetWithAutoStash(action.from, hardResetOptions{
|
||||
EnvVars: undoEnvVars,
|
||||
WaitingStatus: undoingStatus,
|
||||
})
|
||||
case CHECKOUT:
|
||||
self.c.LogAction(self.c.Tr.Actions.Undo)
|
||||
return true, self.refHelper.CheckoutRef(action.from, types.CheckoutRefOptions{
|
||||
EnvVars: undoEnvVars,
|
||||
WaitingStatus: undoingStatus,
|
||||
})
|
||||
case CURRENT_REBASE:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
self.c.Log.Error("didn't match on the user action when trying to undo")
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (self *UndoController) reflogRedo() error {
|
||||
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
|
||||
redoingStatus := self.c.Tr.RedoingStatus
|
||||
|
||||
if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
return self.c.ErrorMsg(self.c.Tr.LcCantRedoWhileRebasing)
|
||||
}
|
||||
|
||||
return self.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||
// if we're redoing and the counter is zero, we just return
|
||||
if counter == 0 {
|
||||
return true, nil
|
||||
} else if counter > 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch action.kind {
|
||||
case COMMIT, REBASE:
|
||||
self.c.LogAction(self.c.Tr.Actions.Redo)
|
||||
return true, self.hardResetWithAutoStash(action.to, hardResetOptions{
|
||||
EnvVars: redoEnvVars,
|
||||
WaitingStatus: redoingStatus,
|
||||
})
|
||||
case CHECKOUT:
|
||||
self.c.LogAction(self.c.Tr.Actions.Redo)
|
||||
return true, self.refHelper.CheckoutRef(action.to, types.CheckoutRefOptions{
|
||||
EnvVars: redoEnvVars,
|
||||
WaitingStatus: redoingStatus,
|
||||
})
|
||||
case CURRENT_REBASE:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
self.c.Log.Error("didn't match on the user action when trying to redo")
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Here we're going through the reflog and maintaining a counter that represents how many
|
||||
// undos/redos/user actions we've seen. when we hit a user action we call the callback specifying
|
||||
// what the counter is up to and the nature of the action.
|
||||
// If we find ourselves mid-rebase, we just return because undo/redo mid rebase
|
||||
// requires knowledge of previous TODO file states, which you can't just get from the reflog.
|
||||
// Though we might support this later, hence the use of the CURRENT_REBASE action kind.
|
||||
func (self *UndoController) parseReflogForActions(onUserAction func(counter int, action reflogAction) (bool, error)) error {
|
||||
counter := 0
|
||||
reflogCommits := self.getFilteredReflogCommits()
|
||||
rebaseFinishCommitSha := ""
|
||||
var action *reflogAction
|
||||
for reflogCommitIdx, reflogCommit := range reflogCommits {
|
||||
action = nil
|
||||
|
||||
prevCommitSha := ""
|
||||
if len(reflogCommits)-1 >= reflogCommitIdx+1 {
|
||||
prevCommitSha = reflogCommits[reflogCommitIdx+1].Sha
|
||||
}
|
||||
|
||||
if rebaseFinishCommitSha == "" {
|
||||
if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit undo\]`); ok {
|
||||
counter++
|
||||
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit redo\]`); ok {
|
||||
counter--
|
||||
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(abort\)|^rebase -i \(finish\)`); ok {
|
||||
rebaseFinishCommitSha = reflogCommit.Sha
|
||||
} else if ok, match := utils.FindStringSubmatch(reflogCommit.Name, `^checkout: moving from ([\S]+) to ([\S]+)`); ok {
|
||||
action = &reflogAction{kind: CHECKOUT, from: match[1], to: match[2]}
|
||||
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^commit|^reset: moving to|^pull`); ok {
|
||||
action = &reflogAction{kind: COMMIT, from: prevCommitSha, to: reflogCommit.Sha}
|
||||
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(start\)`); ok {
|
||||
// if we're here then we must be currently inside an interactive rebase
|
||||
action = &reflogAction{kind: CURRENT_REBASE, from: prevCommitSha}
|
||||
}
|
||||
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(start\)`); ok {
|
||||
action = &reflogAction{kind: REBASE, from: prevCommitSha, to: rebaseFinishCommitSha}
|
||||
rebaseFinishCommitSha = ""
|
||||
}
|
||||
|
||||
if action != nil {
|
||||
if action.kind != CURRENT_REBASE && action.from == action.to {
|
||||
// if we're going from one place to the same place we'll ignore the action.
|
||||
continue
|
||||
}
|
||||
ok, err := onUserAction(counter, *action)
|
||||
if ok {
|
||||
return err
|
||||
}
|
||||
counter--
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type hardResetOptions struct {
|
||||
WaitingStatus string
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
// only to be used in the undo flow for now (does an autostash)
|
||||
func (self *UndoController) hardResetWithAutoStash(commitSha string, options hardResetOptions) error {
|
||||
reset := func() error {
|
||||
if err := self.refHelper.ResetToRef(commitSha, "hard", options.EnvVars); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we have any modified tracked files we need to ask the user if they want us to stash for them
|
||||
dirtyWorkingTree := self.workingTreeHelper.IsWorkingTreeDirty()
|
||||
if dirtyWorkingTree {
|
||||
// offer to autostash changes
|
||||
return self.c.Ask(popup.AskOpts{
|
||||
Title: self.c.Tr.AutoStashTitle,
|
||||
Prompt: self.c.Tr.AutoStashPrompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(options.WaitingStatus, func() error {
|
||||
if err := self.git.Stash.Save(self.c.Tr.StashPrefix + commitSha); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
if err := reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := self.git.Stash.Pop(0)
|
||||
if err := self.c.Refresh(types.RefreshOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(options.WaitingStatus, func() error {
|
||||
return reset()
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user