diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2021-03-21 08:41:06 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2021-03-30 21:57:00 +1100 |
commit | da6fe01eca531635c09627c60bd38d49bb092906 (patch) | |
tree | 79ecf551adaee34fccabaa60f3d083c7792a25a0 | |
parent | c27cea6f30c35328a24bb4fb7db4f002ab544ad3 (diff) |
allow toggling on/off file tree mode
-rw-r--r-- | docs/Config.md | 1 | ||||
-rw-r--r-- | pkg/commands/files.go | 24 | ||||
-rw-r--r-- | pkg/commands/git_test.go | 4 | ||||
-rw-r--r-- | pkg/commands/loading_files.go | 17 | ||||
-rw-r--r-- | pkg/commands/models/file.go | 15 | ||||
-rw-r--r-- | pkg/commands/models/status_line_node.go | 49 | ||||
-rw-r--r-- | pkg/commands/stash_entries.go | 2 | ||||
-rw-r--r-- | pkg/config/user_config.go | 2 | ||||
-rw-r--r-- | pkg/gui/files_panel.go | 37 | ||||
-rw-r--r-- | pkg/gui/keybindings.go | 7 | ||||
-rw-r--r-- | pkg/gui/status_line_manager.go | 35 | ||||
-rw-r--r-- | pkg/gui/status_tree.go | 36 | ||||
-rw-r--r-- | pkg/gui/submodules_panel.go | 2 | ||||
-rw-r--r-- | pkg/i18n/english.go | 2 |
14 files changed, 175 insertions, 58 deletions
diff --git a/docs/Config.md b/docs/Config.md index 08742bcdb..ac30d95aa 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -133,6 +133,7 @@ Default path for the config file: toggleStagedAll: 'a' # stage/unstage all viewResetOptions: 'D' fetch: 'f' + toggleTreeView: '`' branches: createPullRequest: 'o' checkoutBranchByName: 'c' diff --git a/pkg/commands/files.go b/pkg/commands/files.go index 8244408cc..163e006e1 100644 --- a/pkg/commands/files.go +++ b/pkg/commands/files.go @@ -5,7 +5,6 @@ import ( "os" "os/exec" "path/filepath" - "strings" "time" "github.com/go-errors/errors" @@ -21,9 +20,7 @@ func (c *GitCommand) CatFile(fileName string) (string, error) { // StageFile stages a file func (c *GitCommand) StageFile(fileName string) error { - // renamed files look like "file1 -> file2" - fileNames := strings.Split(fileName, models.RENAME_SEPARATOR) - return c.OSCommand.RunCommand("git add -- %s", c.OSCommand.Quote(fileNames[len(fileNames)-1])) + return c.OSCommand.RunCommand("git add -- %s", c.OSCommand.Quote(fileName)) } // StageAll stages all files @@ -37,14 +34,14 @@ func (c *GitCommand) UnstageAll() error { } // UnStageFile unstages a file -func (c *GitCommand) UnStageFile(fileName string, tracked bool) error { +// we accept an array of filenames for the cases where a file has been renamed i.e. +// we accept the current name and the previous name +func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error { command := "git rm --cached --force -- %s" - if tracked { + if reset { command = "git reset HEAD -- %s" } - // renamed files look like "file1 -> file2" - fileNames := strings.Split(fileName, models.RENAME_SEPARATOR) for _, name := range fileNames { if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil { return err @@ -59,20 +56,19 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil return nil, nil, errors.New("Expected renamed file") } - // we've got a file that represents a rename from one file to another. Unfortunately - // our File abstraction fails to consider this case, so here we will refetch + // we've got a file that represents a rename from one file to another. Here we will refetch // all files, passing the --no-renames flag and then recursively call the function - // again for the before file and after file. At some point we should fix the abstraction itself + // again for the before file and after file. - split := strings.Split(file.Name, models.RENAME_SEPARATOR) filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true}) var beforeFile *models.File var afterFile *models.File for _, f := range filesWithoutRenames { - if f.Name == split[0] { + if f.Name == file.PreviousName { beforeFile = f } - if f.Name == split[1] { + + if f.Name == file.Name { afterFile = f } } diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index 9eea0c34c..e0955932f 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -1056,7 +1056,7 @@ func TestGitCommandUnstageFile(t *testing.T) { testName string command func(string, ...string) *exec.Cmd test func(error) - tracked bool + reset bool } scenarios := []scenario{ @@ -1092,7 +1092,7 @@ func TestGitCommandUnstageFile(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := NewDummyGitCommand() gitCmd.OSCommand.Command = s.command - s.test(gitCmd.UnStageFile("test.txt", s.tracked)) + s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset)) }) } } diff --git a/pkg/commands/loading_files.go b/pkg/commands/loading_files.go index 411c0251a..ba3244999 100644 --- a/pkg/commands/loading_files.go +++ b/pkg/commands/loading_files.go @@ -8,6 +8,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +const RENAME_SEPARATOR = " -> " + // GetStatusFiles git status files type GetStatusFileOptions struct { NoRenames bool @@ -37,14 +39,21 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File { change := statusString[0:2] stagedChange := change[0:1] unstagedChange := statusString[1:2] - filename := statusString[3:] + name := statusString[3:] untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change) hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange) hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change) hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change) + previousName := "" + if strings.Contains(name, RENAME_SEPARATOR) { + split := strings.Split(name, RENAME_SEPARATOR) + name = split[1] + previousName = split[0] + } file := &models.File{ - Name: filename, + Name: name, + PreviousName: previousName, DisplayString: statusString, HasStagedChanges: !hasNoStagedChanges, HasUnstagedChanges: unstagedChange != " ", @@ -53,7 +62,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File { Added: unstagedChange == "A" || untracked, HasMergeConflicts: hasMergeConflicts, HasInlineMergeConflicts: hasInlineMergeConflicts, - Type: c.OSCommand.FileType(filename), + Type: c.OSCommand.FileType(name), ShortStatus: change, } files = append(files, file) @@ -85,7 +94,7 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) { original := splitLines[i] if strings.HasPrefix(original, "R ") { next := splitLines[i+1] - updated := "R " + next + models.RENAME_SEPARATOR + strings.TrimPrefix(original, "R ") + updated := "R " + next + RENAME_SEPARATOR + strings.TrimPrefix(original, "R ") splitLines[i] = updated splitLines = append(splitLines[0:i+1], splitLines[i+2:]...) } diff --git a/pkg/commands/models/file.go b/pkg/commands/models/file.go index 32e90f718..3e28ca46f 100644 --- a/pkg/commands/models/file.go +++ b/pkg/commands/models/file.go @@ -1,8 +1,6 @@ package models import ( - "strings" - "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -10,6 +8,7 @@ import ( // duplicating this for now type File struct { Name string + PreviousName string HasStagedChanges bool HasUnstagedChanges bool Tracked bool @@ -25,12 +24,16 @@ type File struct { const RENAME_SEPARATOR = " -> " func (f *File) IsRename() bool { - return strings.Contains(f.Name, RENAME_SEPARATOR) + return f.PreviousName != "" } // Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename func (f *File) Names() []string { - return strings.Split(f.Name, RENAME_SEPARATOR) + result := []string{f.Name} + if f.PreviousName != "" { + result = append(result, f.PreviousName) + } + return result } // returns true if the file names are the same or if a a file rename includes the filename of the other @@ -73,6 +76,6 @@ func (f *File) GetIsTracked() bool { } func (f *File) GetPath() string { - names := f.Names() - return names[len(names)-1] + // TODO: remove concept of name; just use path + return f.Name } diff --git a/pkg/commands/models/status_line_node.go b/pkg/commands/models/status_line_node.go index 3962e0c42..84dc638ba 100644 --- a/pkg/commands/models/status_line_node.go +++ b/pkg/commands/models/status_line_node.go @@ -2,7 +2,10 @@ package models import ( "fmt" + "os" + "path/filepath" "sort" + "strings" ) type StatusLineNode struct { @@ -67,6 +70,30 @@ func (s *StatusLineNode) getNodeAtIndexAux(index int, collapsedPaths map[string] return nil, offset } +func (s *StatusLineNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) { + return s.getIndexForPathAux(path, collapsedPaths) +} + +func (s *StatusLineNode) getIndexForPathAux(path string, collapsedPaths map[string]bool) (int, bool) { + offset := 0 + + if s.Path == path { + return offset, true + } + + if !collapsedPaths[s.GetPath()] { + for _, child := range s.Children { + offsetChange, found := child.getIndexForPathAux(path, collapsedPaths) + offset += offsetChange + 1 + if found { + return offset, true + } + } + } + + return offset, false +} + func (s *StatusLineNode) IsLeaf() bool { return len(s.Children) == 0 } @@ -161,7 +188,6 @@ func (s *StatusLineNode) compressAux() *StatusLineNode { for i := range s.Children { for s.Children[i].HasExactlyOneChild() { grandchild := s.Children[i].Children[0] - grandchild.Name = fmt.Sprintf("%s/%s", s.Children[i].Name, grandchild.Name) s.Children[i] = grandchild } } @@ -215,3 +241,24 @@ func (s *StatusLineNode) ForEachFile(cb func(*File) error) error { return nil } + +func (s *StatusLineNode) NameAtDepth(depth int) string { + splitName := strings.Split(s.Name, string(os.PathSeparator)) + name := filepath.Join(splitName[depth:]...) + + if s.File != nil && s.File.IsRename() { + splitPrevName := strings.Split(s.File.PreviousName, string(os.PathSeparator)) + + prevName := s.File.PreviousName + // if the file has just been renamed inside the same directory, we can shave off + // the prefix for the previous path too. Otherwise we'll keep it unchanged + sameParentDir := filepath.Join(splitName[0:depth]...) == filepath.Join(splitPrevName[0:depth]...) + if sameParentDir { + prevName = filepath.Join(splitPrevName[depth:]...) + } + + return fmt.Sprintf("%s%s%s", prevName, " -> ", name) + } + + return name +} diff --git a/pkg/commands/stash_entries.go b/pkg/commands/stash_entries.go index 1bc957130..4fad9ffab 100644 --- a/pkg/commands/stash_entries.go +++ b/pkg/commands/stash_entries.go @@ -48,7 +48,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error { files := c.GetStatusFiles(GetStatusFileOptions{}) for _, file := range files { if file.ShortStatus == "AD" { - if err := c.UnStageFile(file.Name, false); err != nil { + if err := c.UnStageFile(file.Names(), false); err != nil { return err } } diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 9d77dbdee..c8093943e 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -175,6 +175,7 @@ type KeybindingFilesConfig struct { ToggleStagedAll string `yaml:"toggleStagedAll"` ViewResetOptions string `yaml:"viewResetOptions"` Fetch string `yaml:"fetch"` + ToggleTreeView string `yaml:"toggleTreeView"` } type KeybindingBranchesConfig struct { @@ -398,6 +399,7 @@ func GetDefaultConfig() *UserConfig { ToggleStagedAll: "a", ViewResetOptions: "D", Fetch: "f", + ToggleTreeView: "`", }, Branches: KeybindingBranchesConfig{ CopyPullRequestURL: "<c-y>", diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 30ce0a51f..854be0263 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -239,7 +239,7 @@ func (gui *Gui) handleFilePress() error { return gui.surfaceError(err) } } else { - if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil { + if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { return gui.surfaceError(err) } } @@ -250,7 +250,7 @@ func (gui *Gui) handleFilePress() error { } } else { // pretty sure it doesn't matter that we're always passing true here - if err := gui.GitCommand.UnStageFile(node.Path, true); err != nil { + if err := gui.GitCommand.UnStageFile([]string{node.Path}, true); err != nil { return gui.surfaceError(err) } } @@ -307,7 +307,7 @@ func (gui *Gui) handleIgnoreFile() error { unstageFiles := func() error { return node.ForEachFile(func(file *models.File) error { if file.HasStagedChanges { - if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil { + if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { return err } } @@ -526,8 +526,8 @@ func (gui *Gui) refreshStateFiles() error { prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx // get files to stage - noRenames := gui.State.StatusLineManager.TreeMode - files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{NoRenames: noRenames}) + // noRenames := gui.State.StatusLineManager.TreeMode + files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{}) gui.State.StatusLineManager.SetFiles( gui.GitCommand.MergeStatusFiles(gui.State.StatusLineManager.GetAllFiles(), files, selectedFile), ) @@ -800,3 +800,30 @@ func (gui *Gui) handleToggleDirCollapsed() error { return nil } + +func (gui *Gui) handleToggleFileTreeView() error { + // get path of currently selected file + node := gui.getSelectedStatusNode() + path := "" + if node != nil { + path = node.Path + } + gui.State.StatusLineManager.TreeMode = !gui.State.StatusLineManager.TreeMode + gui.State.StatusLineManager.SetTree() + + // find that same node in the new format and move the cursor to it + if path != "" { + index, found := gui.State.StatusLineManager.GetIndexForPath(path) + if found { + gui.filesListContext().GetPanelState().SetSelectedLineIdx(index) + } + } + + if gui.getFilesView().Context == FILES_CONTEXT_KEY { + if err := gui.Contexts.Files.Context.HandleRender(); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index fc6107a96..e9924f55e 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -506,6 +506,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { OpensMenu: true, }, { + ViewName: "files", + Contexts: []string{FILES_CONTEXT_KEY}, + Key: gui.getKey(config.Files.ToggleTreeView), + Handler: gui.wrappedHandler(gui.handleToggleFileTreeView), + Description: gui.Tr.LcToggleTreeView, + }, + { ViewName: "branches", Contexts: []string{LOCAL_BRANCHES_CONTEXT_KEY}, Key: gui.getKey(config.Universal.Select), diff --git a/pkg/gui/status_line_manager.go b/pkg/gui/status_line_manager.go index ba07c16b4..ebf123ef2 100644 --- a/pkg/gui/status_line_manager.go +++ b/pkg/gui/status_line_manager.go @@ -2,6 +2,7 @@ package gui import ( "fmt" + "sort" "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" @@ -24,23 +25,19 @@ func NewStatusLineManager(files []*models.File, log *logrus.Entry) *StatusLineMa return &StatusLineManager{ Files: files, Log: log, - TreeMode: true, // always true for now + TreeMode: false, // always true for now CollapsedPaths: map[string]bool{}, } } func (m *StatusLineManager) GetItemAtIndex(index int) *models.StatusLineNode { - if m.TreeMode { - // need to traverse the three depth first until we get to the index. - return m.Tree.GetNodeAtIndex(index+1, m.CollapsedPaths) // ignoring root - } - - m.Log.Warn(index) - if index > len(m.Files)-1 { - return nil - } + // need to traverse the three depth first until we get to the index. + return m.Tree.GetNodeAtIndex(index+1, m.CollapsedPaths) // ignoring root +} - return &models.StatusLineNode{File: m.Files[index]} +func (m *StatusLineManager) GetIndexForPath(path string) (int, bool) { + index, found := m.Tree.GetIndexForPath(path, m.CollapsedPaths) + return index - 1, found } func (m *StatusLineManager) GetAllItems() []*models.StatusLineNode { @@ -57,7 +54,19 @@ func (m *StatusLineManager) GetAllFiles() []*models.File { func (m *StatusLineManager) SetFiles(files []*models.File) { m.Files = files - m.Tree = GetTreeFromStatusFiles(files) + sort.SliceStable(m.Files, func(i, j int) bool { + return m.Files[i].Name < m.Files[j].Name + }) + + m.SetTree() +} + +func (m *StatusLineManager) SetTree() { + if m.TreeMode { + m.Tree = GetTreeFromStatusFiles(m.Files, m.Log) + } else { + m.Tree = GetFlatTreeFromStatusFiles(m.Files) + } } func (m *StatusLineManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string { @@ -84,7 +93,7 @@ func (m *StatusLineManager) renderAux(s *models.StatusLineNode, prefix string, d } getLine := func() string { - return prefix + presentation.GetStatusNodeLine(s.GetHasUnstagedChanges(), s.GetHasStagedChanges(), s.Name, diffName, submoduleConfigs, s.File) + return prefix + presentation.GetStatusNodeLine(s.GetHasUnstagedChanges(), s.GetHasStagedChanges(), s.NameAtDepth(depth), diffName, submoduleConfigs, s.File) } if s.IsLeaf() { diff --git a/pkg/gui/status_tree.go b/pkg/gui/status_tree.go index 193d34fd8..6ff489566 100644 --- a/pkg/gui/status_tree.go +++ b/pkg/gui/status_tree.go @@ -3,38 +3,39 @@ package gui import ( "os" "path/filepath" - "sort" "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/sirupsen/logrus" ) -func GetTreeFromStatusFiles(files []*models.File) *models.StatusLineNode { +func GetTreeFromStatusFiles(files []*models.File, log *logrus.Entry) *models.StatusLineNode { root := &models.StatusLineNode{} - sort.SliceStable(files, func(i, j int) bool { - return files[i].Name < files[j].Name - }) - var curr *models.StatusLineNode for _, file := range files { split := strings.Split(file.Name, string(os.PathSeparator)) curr = root outer: - for i, dir := range split { + for i := range split { var setFile *models.File - if i == len(split)-1 { + isFile := i == len(split)-1 + if isFile { setFile = file } + + path := filepath.Join(split[:i+1]...) + for _, existingChild := range curr.Children { - if existingChild.Name == dir { + if existingChild.Path == path { curr = existingChild continue outer } } + newChild := &models.StatusLineNode{ - Name: dir, - Path: filepath.Join(split[:i+1]...), + Name: path, // TODO: Remove concept of name + Path: path, File: setFile, } curr.Children = append(curr.Children, newChild) @@ -48,3 +49,16 @@ func GetTreeFromStatusFiles(files []*models.File) *models.StatusLineNode { return root } + +func GetFlatTreeFromStatusFiles(files []*models.File) *models.StatusLineNode { + root := &models.StatusLineNode{} + for _, file := range files { + root.Children = append(root.Children, &models.StatusLineNode{ + Name: file.Name, + Path: file.GetPath(), + File: file, + }) + } + + return root +} diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go index d4b76b7dd..c9ce7168a 100644 --- a/pkg/gui/submodules_panel.go +++ b/pkg/gui/submodules_panel.go @@ -110,7 +110,7 @@ func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error { file := gui.fileForSubmodule(submodule) if file != nil { - if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil { + if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil { return gui.surfaceError(err) } } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 4862f80ae..6d7601144 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -47,6 +47,7 @@ type TranslationSet struct { LcDelete string LcToggleStaged string LcToggleStagedAll string + LcToggleTreeView string LcRefresh string LcPush string LcPull string @@ -605,6 +606,7 @@ func englishTranslationSet() TranslationSet { LcDelete: "delete", LcToggleStaged: "toggle staged", LcToggleStagedAll: "stage/unstage all", + LcToggleTreeView: "toggle file tree view", LcRefresh: "refresh", LcPush: "push", LcPull: "pull", |