summaryrefslogtreecommitdiffstats
path: root/pkg/gui/controllers
diff options
context:
space:
mode:
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)
+}
+
+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.PostRe