1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-14 11:23:09 +02:00
lazygit/pkg/gui/controllers/files_controller.go

823 lines
23 KiB
Go
Raw Normal View History

package controllers
import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type FilesController struct {
2023-02-09 12:45:14 +02:00
baseController // nolint: unused
2023-03-23 09:47:29 +02:00
c *ControllerCommon
}
var _ types.IController = &FilesController{}
func NewFilesController(
2023-03-23 09:47:29 +02:00
common *ControllerCommon,
) *FilesController {
return &FilesController{
c: common,
}
}
2022-02-05 01:31:07 +02:00
func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelectedFileNode(self.press),
Description: self.c.Tr.ToggleStaged,
},
{
2022-02-27 07:22:04 +02:00
Key: opts.GetKey(opts.Config.Files.OpenStatusFilter),
Handler: self.handleStatusFilterPressed,
Description: self.c.Tr.FileFilter,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.CommitChanges),
2023-03-23 09:47:29 +02:00
Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
Description: self.c.Tr.CommitChanges,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.CommitChangesWithoutHook),
2023-03-23 09:47:29 +02:00
Handler: self.c.Helpers().WorkingTree.HandleWIPCommitPress,
Description: self.c.Tr.CommitChangesWithoutHook,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.AmendLastCommit),
Handler: self.handleAmendCommitPress,
Description: self.c.Tr.AmendLastCommit,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.CommitChangesWithEditor),
2023-03-23 09:47:29 +02:00
Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress,
Description: self.c.Tr.CommitChangesWithEditor,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Universal.Edit),
2022-01-23 05:40:28 +02:00
Handler: self.checkSelectedFileNode(self.edit),
Description: self.c.Tr.EditFile,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Universal.OpenFile),
Handler: self.Open,
Description: self.c.Tr.OpenFile,
},
{
2022-11-30 10:36:35 +02:00
Key: opts.GetKey(opts.Config.Files.IgnoreFile),
Handler: self.checkSelectedFileNode(self.ignoreOrExcludeMenu),
Description: self.c.Tr.Actions.IgnoreExcludeFile,
2022-08-11 14:18:19 +02:00
OpensMenu: true,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.RefreshFiles),
Handler: self.refresh,
Description: self.c.Tr.RefreshFiles,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.StashAllChanges),
Handler: self.stash,
Description: self.c.Tr.StashAllChanges,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.ViewStashOptions),
Handler: self.createStashMenu,
Description: self.c.Tr.ViewStashOptions,
OpensMenu: true,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.ToggleStagedAll),
Handler: self.toggleStagedAll,
Description: self.c.Tr.ToggleStagedAll,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.enter,
Description: self.c.Tr.FileEnter,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.createResetToUpstreamMenu,
Description: self.c.Tr.ViewResetToUpstreamOptions,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Files.ViewResetOptions),
Handler: self.createResetMenu,
Description: self.c.Tr.ViewResetOptions,
OpensMenu: true,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.ToggleTreeView),
Handler: self.toggleTreeView,
Description: self.c.Tr.ToggleTreeView,
},
{
2022-02-05 01:31:07 +02:00
Key: opts.GetKey(opts.Config.Files.OpenMergeTool),
2023-03-23 09:47:29 +02:00
Handler: self.c.Helpers().WorkingTree.OpenMergeTool,
Description: self.c.Tr.OpenMergeTool,
},
2022-02-06 06:54:26 +02:00
{
Key: opts.GetKey(opts.Config.Files.Fetch),
Handler: self.fetch,
Description: self.c.Tr.Fetch,
2022-02-06 06:54:26 +02:00
},
}
}
2022-02-05 05:42:56 +02:00
func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
2022-02-27 02:42:22 +02:00
ViewName: "main",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "patchBuilding",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "mergeConflicts",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
2022-02-05 05:42:56 +02:00
},
{
2022-02-27 02:42:22 +02:00
ViewName: "secondary",
Key: gocui.MouseLeft,
Handler: self.onClickSecondary,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "patchBuildingSecondary",
Key: gocui.MouseLeft,
Handler: self.onClickSecondary,
FocusedView: self.context().GetViewName(),
2022-02-05 05:42:56 +02:00
},
}
}
func (self *FilesController) GetOnRenderToMain() func() error {
return func() error {
2023-03-23 09:47:29 +02:00
return self.c.Helpers().Diff.WithDiffModeCheck(func() error {
node := self.context().GetSelected()
if node == nil {
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.DiffTitle,
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Task: types.NewRenderStringTask(self.c.Tr.NoChangedFiles),
},
})
}
if node.File != nil && node.File.HasInlineMergeConflicts {
2023-03-23 09:47:29 +02:00
hasConflicts, err := self.c.Helpers().MergeConflicts.SetMergeState(node.GetPath())
if err != nil {
return err
}
if hasConflicts {
2023-03-23 09:47:29 +02:00
return self.c.Helpers().MergeConflicts.Render(false)
}
}
2023-03-23 09:47:29 +02:00
self.c.Helpers().MergeConflicts.ResetMergeState()
pair := self.c.MainViewPairs().Normal
if node.File != nil {
pair = self.c.MainViewPairs().Staging
}
split := self.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
mainShowsStaged := !split && node.GetHasStagedChanges()
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, self.c.GetAppState().IgnoreWhitespaceInDiffView)
title := self.c.Tr.UnstagedChanges
if mainShowsStaged {
title = self.c.Tr.StagedChanges
}
refreshOpts := types.RefreshMainOpts{
Pair: pair,
Main: &types.ViewUpdateOpts{
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Title: title,
},
}
if split {
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, self.c.GetAppState().IgnoreWhitespaceInDiffView)
title := self.c.Tr.StagedChanges
if mainShowsStaged {
title = self.c.Tr.UnstagedChanges
}
refreshOpts.Secondary = &types.ViewUpdateOpts{
Title: title,
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
}
}
return self.c.RenderToMainViews(refreshOpts)
})
}
}
2022-02-27 02:42:22 +02:00
func (self *FilesController) GetOnClick() func() error {
return self.checkSelectedFileNode(self.press)
}
// if we are dealing with a status for which there is no key in this map,
// then we won't optimistically render: we'll just let `git status` tell
// us what the new status is.
// There are no doubt more entries that could be added to these two maps.
var stageStatusMap = map[string]string{
"??": "A ",
" M": "M ",
"MM": "M ",
" D": "D ",
" A": "A ",
"AM": "A ",
"MD": "D ",
}
var unstageStatusMap = map[string]string{
"A ": "??",
"M ": " M",
"D ": " D",
}
func (self *FilesController) optimisticStage(file *models.File) bool {
newShortStatus, ok := stageStatusMap[file.ShortStatus]
if !ok {
return false
}
models.SetStatusFields(file, newShortStatus)
return true
}
func (self *FilesController) optimisticUnstage(file *models.File) bool {
newShortStatus, ok := unstageStatusMap[file.ShortStatus]
if !ok {
return false
}
models.SetStatusFields(file, newShortStatus)
return true
}
// Running a git add command followed by a git status command can take some time (e.g. 200ms).
// Given how often users stage/unstage files in Lazygit, we're adding some
// optimistic rendering to make things feel faster. When we go to stage
// a file, we'll first update that file's status in-memory, then re-render
// the files panel. Then we'll immediately do a proper git status call
// so that if the optimistic rendering got something wrong, it's quickly
// corrected.
func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisticChangeFn func(*models.File) bool) error {
rerender := false
err := node.ForEachFile(func(f *models.File) error {
// can't act on the file itself: we need to update the original model file
2023-03-23 04:04:57 +02:00
for _, modelFile := range self.c.Model().Files {
if modelFile.Name == f.Name {
if optimisticChangeFn(modelFile) {
rerender = true
}
break
}
}
return nil
})
if err != nil {
return err
}
if rerender {
2023-03-23 04:04:57 +02:00
if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
return err
}
}
return nil
}
func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
// Obtaining this lock because optimistic rendering requires us to mutate
// the files in our model.
2023-03-23 04:04:57 +02:00
self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
if node.IsFile() {
file := node.File
if file.HasUnstagedChanges {
self.c.LogAction(self.c.Tr.Actions.StageFile)
if err := self.optimisticChange(node, self.optimisticStage); err != nil {
return err
}
2023-03-23 04:04:57 +02:00
if err := self.c.Git().WorkingTree.StageFile(file.Name); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
if err := self.optimisticChange(node, self.optimisticUnstage); err != nil {
return err
}
2023-03-23 04:04:57 +02:00
if err := self.c.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.optimisticChange(node, self.optimisticStage); err != nil {
return err
}
2023-03-23 04:04:57 +02:00
if err := self.c.Git().WorkingTree.StageFile(node.Path); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
if err := self.optimisticChange(node, self.optimisticUnstage); err != nil {
return err
}
// pretty sure it doesn't matter that we're always passing true here
2023-03-23 04:04:57 +02:00
if err := self.c.Git().WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
return self.c.Error(err)
}
}
}
return nil
}
func (self *FilesController) press(node *filetree.FileNode) error {
if node.IsFile() && node.File.HasInlineMergeConflicts {
return self.switchToMerge()
}
if err := self.pressWithLock(node); err != nil {
return err
}
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC}); err != nil {
return err
}
return self.context().HandleFocus(types.OnFocusOpts{})
}
func (self *FilesController) checkSelectedFileNode(callback func(*filetree.FileNode) error) func() error {
return func() error {
2022-03-19 00:31:52 +02:00
node := self.context().GetSelected()
if node == nil {
return nil
}
return callback(node)
}
}
func (self *FilesController) Context() types.Context {
2022-02-06 06:54:26 +02:00
return self.context()
}
func (self *FilesController) context() *context.WorkingTreeContext {
2023-03-23 04:04:57 +02:00
return self.c.Contexts().Files
}
func (self *FilesController) getSelectedFile() *models.File {
2022-03-19 00:31:52 +02:00
node := self.context().GetSelected()
if node == nil {
return nil
}
return node.File
}
func (self *FilesController) enter() error {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1})
}
func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
2022-03-19 00:31:52 +02:00
node := self.context().GetSelected()
if node == nil {
return nil
}
if node.File == nil {
return self.handleToggleDirCollapsed()
}
file := node.File
2023-03-23 04:04:57 +02:00
submoduleConfigs := self.c.Model().Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
2023-03-23 09:47:29 +02:00
return self.c.Helpers().Repos.EnterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {
return self.switchToMerge()
}
if file.HasMergeConflicts {
return self.c.ErrorMsg(self.c.Tr.FileStagingRequirements)
}
2023-03-23 04:04:57 +02:00
return self.c.PushContext(self.c.Contexts().Staging, opts)
}
func (self *FilesController) toggleStagedAll() error {
if err := self.toggleStagedAllWithLock(); err != nil {
return err
}
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC}); err != nil {
return err
}
return self.context().HandleFocus(types.OnFocusOpts{})
}
func (self *FilesController) toggleStagedAllWithLock() error {
2023-03-23 04:04:57 +02:00
self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
root := self.context().FileTreeViewModel.GetRoot()
// 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 root.GetHasInlineMergeConflicts() {
return self.c.ErrorMsg(self.c.Tr.ErrStageDirWithInlineMergeConflicts)
}
if root.GetHasUnstagedChanges() {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
if err := self.optimisticChange(root, self.optimisticStage); err != nil {
return err
}
2023-03-23 04:04:57 +02:00
if err := self.c.Git().WorkingTree.StageAll(); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageAllFiles)
if err := self.optimisticChange(root, self.optimisticUnstage); err != nil {
return err
}
2023-03-23 04:04:57 +02:00
if err := self.c.Git().WorkingTree.UnstageAll(); err != nil {
return self.c.Error(err)
}
}
return nil
}
func (self *FilesController) unstageFiles(node *filetree.FileNode) error {
return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges {
2023-03-23 04:04:57 +02:00
if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return err
}
}
return nil
})
}
func (self *FilesController) ignoreOrExcludeTracked(node *filetree.FileNode, trAction string, f func(string) error) error {
self.c.LogAction(trAction)
// not 100% sure if this is necessary but I'll assume it is
if err := self.unstageFiles(node); err != nil {
return err
}
2023-03-23 04:04:57 +02:00
if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err
}
if err := f(node.GetPath()); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) ignoreOrExcludeUntracked(node *filetree.FileNode, trAction string, f func(string) error) error {
self.c.LogAction(trAction)
if err := f(node.GetPath()); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) ignoreOrExcludeFile(node *filetree.FileNode, trText string, trPrompt string, trAction string, f func(string) error) error {
if node.GetIsTracked() {
return self.c.Confirm(types.ConfirmOpts{
Title: trText,
Prompt: trPrompt,
HandleConfirm: func() error {
return self.ignoreOrExcludeTracked(node, trAction, f)
},
})
}
return self.ignoreOrExcludeUntracked(node, trAction, f)
}
func (self *FilesController) ignore(node *filetree.FileNode) error {
if node.GetPath() == ".gitignore" {
return self.c.ErrorMsg(self.c.Tr.Actions.IgnoreFileErr)
}
err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.IgnoreExcludeFile, self.c.Git().WorkingTree.Ignore)
if err != nil {
return err
}
return nil
}
func (self *FilesController) exclude(node *filetree.FileNode) error {
if node.GetPath() == ".git/info/exclude" {
return self.c.ErrorMsg(self.c.Tr.Actions.ExcludeFileErr)
}
if node.GetPath() == ".gitignore" {
return self.c.ErrorMsg(self.c.Tr.Actions.ExcludeGitIgnoreErr)
}
2023-03-23 04:04:57 +02:00
err := self.ignoreOrExcludeFile(node, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.c.Git().WorkingTree.Exclude)
if err != nil {
return err
}
return nil
}
func (self *FilesController) ignoreOrExcludeMenu(node *filetree.FileNode) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.IgnoreExcludeFile,
Items: []*types.MenuItem{
{
LabelColumns: []string{self.c.Tr.IgnoreFile},
OnPress: func() error {
if err := self.ignore(node); err != nil {
return self.c.Error(err)
}
return nil
},
Key: 'i',
},
{
LabelColumns: []string{self.c.Tr.ExcludeFile},
OnPress: func() error {
if err := self.exclude(node); err != nil {
return self.c.Error(err)
}
return nil
},
Key: 'e',
},
},
})
}
func (self *FilesController) refresh() error {
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) handleAmendCommitPress() error {
2023-03-23 04:04:57 +02:00
if len(self.c.Model().Files) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
}
2023-03-23 09:47:29 +02:00
if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
return self.c.Helpers().WorkingTree.PromptToStageAllAndRetry(self.handleAmendCommitPress)
}
2023-03-23 04:04:57 +02:00
if len(self.c.Model().Commits) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend)
}
2023-03-23 09:47:29 +02:00
return self.c.Helpers().AmendHelper.AmendHead()
}
func (self *FilesController) handleStatusFilterPressed() error {
2022-01-29 10:09:20 +02:00
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.FilteringMenuTitle,
2022-01-29 10:09:20 +02:00
Items: []*types.MenuItem{
{
Label: self.c.Tr.FilterStagedFiles,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayStaged)
},
},
{
Label: self.c.Tr.FilterUnstagedFiles,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayUnstaged)
},
},
{
Label: self.c.Tr.ResetFilter,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayAll)
},
},
},
})
}
func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
2023-05-27 11:58:48 +02:00
self.context().FileTreeViewModel.SetStatusFilter(filter)
2022-02-06 06:54:26 +02:00
return self.c.PostRefreshUpdate(self.context())
}
2022-01-23 05:40:28 +02:00
func (self *FilesController) edit(node *filetree.FileNode) error {
if node.File == nil {
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory)
}
2023-03-23 09:47:29 +02:00
return self.c.Helpers().Files.EditFile(node.GetPath())
}
func (self *FilesController) Open() error {
2022-03-19 00:31:52 +02:00
node := self.context().GetSelected()
if node == nil {
return nil
}
2023-03-23 09:47:29 +02:00
return self.c.Helpers().Files.OpenFile(node.GetPath())
}
func (self *FilesController) switchToMerge() error {
file := self.getSelectedFile()
if file == nil {
return nil
}
2023-03-23 09:47:29 +02:00
return self.c.Helpers().MergeConflicts.SwitchToMerge(file.Name)
}
func (self *FilesController) createStashMenu() error {
2022-01-29 10:09:20 +02:00
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.StashOptions,
2022-01-29 10:09:20 +02:00
Items: []*types.MenuItem{
{
Label: self.c.Tr.StashAllChanges,
OnPress: func() error {
2023-03-23 09:47:29 +02:00
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
}
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
},
Key: 'a',
},
{
Label: self.c.Tr.StashAllChangesKeepIndex,
OnPress: func() error {
2023-03-23 09:47:29 +02:00
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
}
2022-04-14 21:45:55 +02:00
// if there are no staged files it behaves the same as Stash.Save
2023-03-23 04:04:57 +02:00
return self.handleStashSave(self.c.Git().Stash.StashAndKeepIndex, self.c.Tr.Actions.StashAllChangesKeepIndex)
},
Key: 'i',
},
2022-06-04 04:24:39 +02:00
{
Label: self.c.Tr.StashIncludeUntrackedChanges,
2022-06-04 04:24:39 +02:00
OnPress: func() error {
2023-03-23 04:04:57 +02:00
return self.handleStashSave(self.c.Git().Stash.StashIncludeUntrackedChanges, self.c.Tr.Actions.StashIncludeUntrackedChanges)
2022-06-04 04:24:39 +02:00
},
Key: 'U',
},
2022-04-14 21:45:55 +02:00
{
Label: self.c.Tr.StashStagedChanges,
2022-04-14 21:45:55 +02:00
OnPress: func() error {
// there must be something in staging otherwise the current implementation mucks the stash up
2023-03-23 09:47:29 +02:00
if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
2022-04-14 21:45:55 +02:00
return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash)
}
2023-03-23 04:04:57 +02:00
return self.handleStashSave(self.c.Git().Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges)
2022-04-14 21:45:55 +02:00
},
Key: 's',
},
{
Label: self.c.Tr.StashUnstagedChanges,
OnPress: func() error {
2023-03-23 09:47:29 +02:00
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
}
2023-03-23 09:47:29 +02:00
if self.c.Helpers().WorkingTree.AnyStagedFiles() {
2023-03-23 04:04:57 +02:00
return self.handleStashSave(self.c.Git().Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges)
2022-04-14 21:45:55 +02:00
}
// ordinary stash
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashUnstagedChanges)
},
Key: 'u',
},
},
})
}
func (self *FilesController) stash() error {
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
}
func (self *FilesController) createResetToUpstreamMenu() error {
2023-03-23 09:47:29 +02:00
return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}")
}
func (self *FilesController) handleToggleDirCollapsed() error {
2022-03-19 00:31:52 +02:00
node := self.context().GetSelected()
if node == nil {
return nil
}
2022-02-06 06:54:26 +02:00
self.context().FileTreeViewModel.ToggleCollapsed(node.GetPath())
2023-03-23 04:04:57 +02:00
if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
self.c.Log.Error(err)
}
return nil
}
func (self *FilesController) toggleTreeView() error {
2022-02-06 06:54:26 +02:00
self.context().FileTreeViewModel.ToggleShowTree()
2022-02-06 06:54:26 +02:00
return self.c.PostRefreshUpdate(self.context())
}
func (self *FilesController) handleStashSave(stashFunc func(message string) error, action string) error {
2022-01-29 10:09:20 +02:00
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.StashChanges,
HandleConfirm: func(stashComment string) error {
2022-03-27 08:41:07 +02:00
self.c.LogAction(action)
if err := stashFunc(stashComment); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}})
},
})
}
2022-02-05 05:42:56 +02:00
func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
2022-02-05 05:42:56 +02:00
}
func (self *FilesController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "secondary", ClickedViewLineIdx: opts.Y})
2022-02-05 05:42:56 +02:00
}
2022-02-06 06:54:26 +02:00
func (self *FilesController) fetch() error {
return self.c.WithLoaderPanel(self.c.Tr.FetchWait, func(task gocui.Task) error {
Use first class task objects instead of global counter The global counter approach is easy to understand but it's brittle and depends on implicit behaviour that is not very discoverable. With a global counter, if any goroutine accidentally decrements the counter twice, we'll think lazygit is idle when it's actually busy. Likewise if a goroutine accidentally increments the counter twice we'll think lazygit is busy when it's actually idle. With the new approach we have a map of tasks where each task can either be busy or not. We create a new task and add it to the map when we spawn a worker goroutine (among other things) and we remove it once the task is done. The task can also be paused and continued for situations where we switch back and forth between running a program and asking for user input. In order for this to work with `git push` (and other commands that require credentials) we need to obtain the task from gocui when we create the worker goroutine, and then pass it along to the commands package to pause/continue the task as required. This is MUCH more discoverable than the old approach which just decremented and incremented the global counter from within the commands package, but it's at the cost of expanding some function signatures (arguably a good thing). Likewise, whenever you want to call WithWaitingStatus or WithLoaderPanel the callback will now have access to the task for pausing/ continuing. We only need to actually make use of this functionality in a couple of places so it's a high price to pay, but I don't know if I want to introduce a WithWaitingStatusTask and WithLoaderPanelTask function (open to suggestions).
2023-07-09 03:32:27 +02:00
if err := self.fetchAux(task); err != nil {
2022-02-06 06:54:26 +02:00
_ = self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
}
func (self *FilesController) fetchAux(task gocui.Task) (err error) {
2022-02-06 06:54:26 +02:00
self.c.LogAction("Fetch")
Use first class task objects instead of global counter The global counter approach is easy to understand but it's brittle and depends on implicit behaviour that is not very discoverable. With a global counter, if any goroutine accidentally decrements the counter twice, we'll think lazygit is idle when it's actually busy. Likewise if a goroutine accidentally increments the counter twice we'll think lazygit is busy when it's actually idle. With the new approach we have a map of tasks where each task can either be busy or not. We create a new task and add it to the map when we spawn a worker goroutine (among other things) and we remove it once the task is done. The task can also be paused and continued for situations where we switch back and forth between running a program and asking for user input. In order for this to work with `git push` (and other commands that require credentials) we need to obtain the task from gocui when we create the worker goroutine, and then pass it along to the commands package to pause/continue the task as required. This is MUCH more discoverable than the old approach which just decremented and incremented the global counter from within the commands package, but it's at the cost of expanding some function signatures (arguably a good thing). Likewise, whenever you want to call WithWaitingStatus or WithLoaderPanel the callback will now have access to the task for pausing/ continuing. We only need to actually make use of this functionality in a couple of places so it's a high price to pay, but I don't know if I want to introduce a WithWaitingStatusTask and WithLoaderPanelTask function (open to suggestions).
2023-07-09 03:32:27 +02:00
err = self.c.Git().Sync.Fetch(task)
2022-02-06 06:54:26 +02:00
if err != nil && strings.Contains(err.Error(), "exit status 128") {
_ = self.c.ErrorMsg(self.c.Tr.PassUnameWrong)
}
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
return err
}