summaryrefslogtreecommitdiffstats
path: root/pkg/gui/controllers
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-01-16 14:46:53 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-03-17 19:13:40 +1100
commit1dd7307fde033dae5fececac15810a99e26c3d91 (patch)
tree4e851c9e3229a6fe3b4191f6311d05d7a9142960 /pkg/gui/controllers
parenta90b6efded49abcfa2516db794d7875b0396f558 (diff)
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
Diffstat (limited to 'pkg/gui/controllers')
-rw-r--r--pkg/gui/controllers/bisect_controller.go273
-rw-r--r--pkg/gui/controllers/controller_common.go10
-rw-r--r--pkg/gui/controllers/files_controller.go737
-rw-r--r--pkg/gui/controllers/local_commits_controller.go783
-rw-r--r--pkg/gui/controllers/menu_controller.go70
-rw-r--r--pkg/gui/controllers/remotes_controller.go204
-rw-r--r--pkg/gui/controllers/submodules_controller.go60
-rw-r--r--pkg/gui/controllers/sync_controller.go253
-rw-r--r--pkg/gui/controllers/tags_controller.go229
-rw-r--r--pkg/gui/controllers/types.go53
-rw-r--r--pkg/gui/controllers/undo_controller.go266
11 files changed, 2908 insertions, 30 deletions
diff --git a/pkg/gui/controllers/bisect_controller.go b/pkg/gui/controllers/bisect_controller.go
new file mode 100644
index 000000000..674e79f76
--- /dev/null
+++ b/pkg/gui/controllers/bisect_controller.go
@@ -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
+}
diff --git a/pkg/gui/controllers/controller_common.go b/pkg/gui/controllers/controller_common.go
new file mode 100644
index 000000000..013439945
--- /dev/null
+++ b/pkg/gui/controllers/controller_common.go
@@ -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
+}
diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go
new file mode 100644
index 000000000..10d378f9f
--- /dev/null
+++ b/pkg/gui/controllers/files_controller.go
@@ -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)
+}
+