diff options
Diffstat (limited to 'pkg/gui/controllers')
-rw-r--r-- | pkg/gui/controllers/bisect_controller.go | 273 | ||||
-rw-r--r-- | pkg/gui/controllers/controller_common.go | 10 | ||||
-rw-r--r-- | pkg/gui/controllers/files_controller.go | 737 | ||||
-rw-r--r-- | pkg/gui/controllers/local_commits_controller.go | 783 | ||||
-rw-r--r-- | pkg/gui/controllers/menu_controller.go | 70 | ||||
-rw-r--r-- | pkg/gui/controllers/remotes_controller.go | 204 | ||||
-rw-r--r-- | pkg/gui/controllers/submodules_controller.go | 60 | ||||
-rw-r--r-- | pkg/gui/controllers/sync_controller.go | 253 | ||||
-rw-r--r-- | pkg/gui/controllers/tags_controller.go | 229 | ||||
-rw-r--r-- | pkg/gui/controllers/types.go | 53 | ||||
-rw-r--r-- | pkg/gui/controllers/undo_controller.go | 266 |
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 |