mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-20 05:19:24 +02:00
Merge branch 'master' into feat/detect-bare-repo
This commit is contained in:
commit
9987e65c35
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -30,3 +30,5 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Note:** please try updating to the latest version or [manually building](https://github.com/jesseduffield/lazygit/#manual) the latest `master` to see if the issue still occurs.
|
||||
|
@ -77,6 +77,7 @@ For a given custom command, here are the allowed fields:
|
||||
| loadingText | text to display while waiting for command to finish | no |
|
||||
| description | text to display in the keybindings menu that appears when you press 'x' | no |
|
||||
| stream | whether you want to stream the command's output to the Command Log panel | no |
|
||||
| showOutput | whether you want to show the command's output in a gui prompt | no |
|
||||
|
||||
### Contexts
|
||||
|
||||
@ -142,6 +143,7 @@ SelectedLocalCommit
|
||||
SelectedReflogCommit
|
||||
SelectedSubCommit
|
||||
SelectedFile
|
||||
SelectedPath
|
||||
SelectedLocalBranch
|
||||
SelectedRemoteBranch
|
||||
SelectedRemote
|
||||
@ -159,7 +161,7 @@ If your custom keybinding collides with an inbuilt keybinding that is defined fo
|
||||
|
||||
### Debugging
|
||||
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `subprocess: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
|
||||
|
||||
### More Examples
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -17,7 +17,7 @@ require (
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220723050330-1f853fadb335
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
|
4
go.sum
4
go.sum
@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21taUpXiIbFkjjIIcn0E5GKwp5UFRuXaOiGQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220723050330-1f853fadb335 h1:36XGBaxzg5umrZO99Ir7Y7zpJPlOzOq8rDZUuTUrCvI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220723050330-1f853fadb335/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
|
||||
|
@ -148,7 +148,7 @@ func isGitVersionValid(versionStr string) bool {
|
||||
|
||||
func isDirectoryAGitRepository(dir string) (bool, error) {
|
||||
info, err := os.Stat(filepath.Join(dir, ".git"))
|
||||
return info != nil && info.IsDir(), err
|
||||
return info != nil, err
|
||||
}
|
||||
|
||||
func isBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type FileLoaderConfig interface {
|
||||
@ -54,28 +53,15 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
|
||||
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
change := status.Change
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := change[1:2]
|
||||
untracked := lo.Contains([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := lo.Contains([]string{" ", "U", "?"}, stagedChange)
|
||||
hasInlineMergeConflicts := lo.Contains([]string{"UU", "AA"}, change)
|
||||
hasMergeConflicts := hasInlineMergeConflicts || lo.Contains([]string{"DD", "AU", "UA", "UD", "DU"}, change)
|
||||
|
||||
file := &models.File{
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: self.getFileType(status.Name),
|
||||
ShortStatus: change,
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
Type: self.getFileType(status.Name),
|
||||
}
|
||||
|
||||
models.SetStatusFields(file, status.Change)
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// File : A file from git status
|
||||
@ -90,3 +91,48 @@ func (f *File) GetPath() string {
|
||||
func (f *File) GetPreviousPath() string {
|
||||
return f.PreviousName
|
||||
}
|
||||
|
||||
type StatusFields struct {
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Deleted bool
|
||||
Added bool
|
||||
HasMergeConflicts bool
|
||||
HasInlineMergeConflicts bool
|
||||
ShortStatus string
|
||||
}
|
||||
|
||||
func SetStatusFields(file *File, shortStatus string) {
|
||||
derived := deriveStatusFields(shortStatus)
|
||||
|
||||
file.HasStagedChanges = derived.HasStagedChanges
|
||||
file.HasUnstagedChanges = derived.HasUnstagedChanges
|
||||
file.Tracked = derived.Tracked
|
||||
file.Deleted = derived.Deleted
|
||||
file.Added = derived.Added
|
||||
file.HasMergeConflicts = derived.HasMergeConflicts
|
||||
file.HasInlineMergeConflicts = derived.HasInlineMergeConflicts
|
||||
file.ShortStatus = derived.ShortStatus
|
||||
}
|
||||
|
||||
// shortStatus is something like '??' or 'A '
|
||||
func deriveStatusFields(shortStatus string) StatusFields {
|
||||
stagedChange := shortStatus[0:1]
|
||||
unstagedChange := shortStatus[1:2]
|
||||
tracked := !lo.Contains([]string{"??", "A ", "AM"}, shortStatus)
|
||||
hasStagedChanges := !lo.Contains([]string{" ", "U", "?"}, stagedChange)
|
||||
hasInlineMergeConflicts := lo.Contains([]string{"UU", "AA"}, shortStatus)
|
||||
hasMergeConflicts := hasInlineMergeConflicts || lo.Contains([]string{"DD", "AU", "UA", "UD", "DU"}, shortStatus)
|
||||
|
||||
return StatusFields{
|
||||
HasStagedChanges: hasStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: tracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || !tracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
ShortStatus: shortStatus,
|
||||
}
|
||||
}
|
||||
|
@ -310,6 +310,7 @@ type CustomCommand struct {
|
||||
LoadingText string `yaml:"loadingText"`
|
||||
Description string `yaml:"description"`
|
||||
Stream bool `yaml:"stream"`
|
||||
ShowOutput bool `yaml:"showOutput"`
|
||||
}
|
||||
|
||||
type CustomCommandPrompt struct {
|
||||
|
@ -123,29 +123,24 @@ func (gui *Gui) getConfirmationPanelWidth() int {
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareConfirmationPanel(
|
||||
title,
|
||||
prompt string,
|
||||
hasLoader bool,
|
||||
findSuggestionsFunc func(string) []*types.Suggestion,
|
||||
editable bool,
|
||||
mask bool,
|
||||
opts types.ConfirmOpts,
|
||||
) error {
|
||||
gui.Views.Confirmation.HasLoader = hasLoader
|
||||
if hasLoader {
|
||||
gui.Views.Confirmation.HasLoader = opts.HasLoader
|
||||
if opts.HasLoader {
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
gui.Views.Confirmation.Title = title
|
||||
gui.Views.Confirmation.Title = opts.Title
|
||||
// for now we do not support wrapping in our editor
|
||||
gui.Views.Confirmation.Wrap = !editable
|
||||
gui.Views.Confirmation.Wrap = !opts.Editable
|
||||
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Confirmation.Mask = runeForMask(mask)
|
||||
gui.Views.Confirmation.Mask = runeForMask(opts.Mask)
|
||||
|
||||
gui.findSuggestions = findSuggestionsFunc
|
||||
if findSuggestionsFunc != nil {
|
||||
gui.findSuggestions = opts.FindSuggestionsFunc
|
||||
if opts.FindSuggestionsFunc != nil {
|
||||
suggestionsView := gui.Views.Suggestions
|
||||
suggestionsView.Wrap = false
|
||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.setSuggestions(findSuggestionsFunc(""))
|
||||
gui.setSuggestions(opts.FindSuggestionsFunc(""))
|
||||
suggestionsView.Visible = true
|
||||
suggestionsView.Title = fmt.Sprintf(gui.c.Tr.SuggestionsTitle, gui.c.UserConfig.Keybinding.Universal.TogglePanel)
|
||||
}
|
||||
@ -177,13 +172,14 @@ func (gui *Gui) createPopupPanel(opts types.CreatePopupPanelOpts) error {
|
||||
gui.clearConfirmationViewKeyBindings()
|
||||
|
||||
err := gui.prepareConfirmationPanel(
|
||||
opts.Title,
|
||||
opts.Prompt,
|
||||
opts.HasLoader,
|
||||
opts.FindSuggestionsFunc,
|
||||
opts.Editable,
|
||||
opts.Mask,
|
||||
)
|
||||
types.ConfirmOpts{
|
||||
Title: opts.Title,
|
||||
Prompt: opts.Prompt,
|
||||
HasLoader: opts.HasLoader,
|
||||
FindSuggestionsFunc: opts.FindSuggestionsFunc,
|
||||
Editable: opts.Editable,
|
||||
Mask: opts.Mask,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -359,7 +359,7 @@ func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||
newView := gui.g.CurrentView()
|
||||
// for now we don't consider losing focus to a popup panel as actually losing focus
|
||||
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
|
||||
if err := gui.onViewFocusLost(previousView, newView); err != nil {
|
||||
if err := gui.onViewFocusLost(previousView); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -369,7 +369,7 @@ func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error {
|
||||
func (gui *Gui) onViewFocusLost(oldView *gocui.View) error {
|
||||
if oldView == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
@ -87,7 +86,7 @@ func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]str
|
||||
})
|
||||
|
||||
return slices.Map(self.menuItems, func(item *types.MenuItem) []string {
|
||||
displayStrings := getItemDisplayStrings(item)
|
||||
displayStrings := item.LabelColumns
|
||||
if showKeys {
|
||||
displayStrings = slices.Prepend(displayStrings, style.FgCyan.Sprint(keybindings.GetKeyDisplay(item.Key)))
|
||||
}
|
||||
@ -95,18 +94,6 @@ func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]str
|
||||
})
|
||||
}
|
||||
|
||||
func getItemDisplayStrings(item *types.MenuItem) []string {
|
||||
if item.LabelColumns != nil {
|
||||
return item.LabelColumns
|
||||
}
|
||||
|
||||
styledStr := item.Label
|
||||
if item.OpensMenu {
|
||||
styledStr = presentation.OpensMenuStyle(styledStr)
|
||||
}
|
||||
return []string{styledStr}
|
||||
}
|
||||
|
||||
func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
basicBindings := self.ListContextTrait.GetKeybindings(opts)
|
||||
menuItemsWithKeys := slices.Filter(self.menuItems, func(item *types.MenuItem) bool {
|
||||
|
@ -62,6 +62,7 @@ func (gui *Gui) resetControllers() {
|
||||
model,
|
||||
gui.State.Contexts,
|
||||
gui.State.Modes,
|
||||
&gui.Mutexes,
|
||||
)
|
||||
|
||||
syncController := controllers.NewSyncController(
|
||||
|
@ -165,7 +165,7 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode)
|
||||
|
||||
// if there is any file that hasn't been fully added we'll fully add everything,
|
||||
// otherwise we'll remove everything
|
||||
adding := node.AnyFile(func(file *models.CommitFile) bool {
|
||||
adding := node.SomeFile(func(file *models.CommitFile) bool {
|
||||
return self.git.Patch.PatchManager.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE
|
||||
})
|
||||
|
||||
@ -203,8 +203,7 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode)
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) toggleAllForPatch(_ *filetree.CommitFileNode) error {
|
||||
// not a fan of type assertions but this will be fixed very soon thanks to generics
|
||||
root := self.context().CommitFileTreeViewModel.Tree().(*filetree.CommitFileNode)
|
||||
root := self.context().CommitFileTreeViewModel.GetRoot()
|
||||
return self.toggleForPatch(root)
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ type controllerCommon struct {
|
||||
model *types.Model
|
||||
contexts *context.ContextTree
|
||||
modes *types.Modes
|
||||
mutexes *types.Mutexes
|
||||
}
|
||||
|
||||
func NewControllerCommon(
|
||||
@ -26,6 +27,7 @@ func NewControllerCommon(
|
||||
model *types.Model,
|
||||
contexts *context.ContextTree,
|
||||
modes *types.Modes,
|
||||
mutexes *types.Mutexes,
|
||||
) *controllerCommon {
|
||||
return &controllerCommon{
|
||||
c: c,
|
||||
@ -35,5 +37,6 @@ func NewControllerCommon(
|
||||
model: model,
|
||||
contexts: contexts,
|
||||
modes: modes,
|
||||
mutexes: mutexes,
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Files.ToggleStagedAll),
|
||||
Handler: self.stageAll,
|
||||
Handler: self.toggleStagedAll,
|
||||
Description: self.c.Tr.LcToggleStagedAll,
|
||||
},
|
||||
{
|
||||
@ -167,8 +167,87 @@ func (self *FilesController) GetOnClick() func() error {
|
||||
return self.checkSelectedFileNode(self.press)
|
||||
}
|
||||
|
||||
func (self *FilesController) press(node *filetree.FileNode) error {
|
||||
if node.IsLeaf() {
|
||||
// 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
|
||||
for _, modelFile := range self.model.Files {
|
||||
if modelFile.Name == f.Name {
|
||||
if optimisticChangeFn(modelFile) {
|
||||
rerender = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rerender {
|
||||
if err := self.c.PostRefreshUpdate(self.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.
|
||||
self.mutexes.RefreshingFilesMutex.Lock()
|
||||
defer self.mutexes.RefreshingFilesMutex.Unlock()
|
||||
|
||||
if node.IsFile() {
|
||||
file := node.File
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
@ -177,11 +256,21 @@ func (self *FilesController) press(node *filetree.FileNode) error {
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
self.c.LogAction(self.c.Tr.Actions.StageFile)
|
||||
|
||||
if err := self.optimisticChange(node, self.optimisticStage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.optimisticChange(node, self.optimisticUnstage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
@ -195,19 +284,37 @@ func (self *FilesController) press(node *filetree.FileNode) error {
|
||||
|
||||
if node.GetHasUnstagedChanges() {
|
||||
self.c.LogAction(self.c.Tr.Actions.StageFile)
|
||||
|
||||
if err := self.optimisticChange(node, self.optimisticStage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.optimisticChange(node, self.optimisticUnstage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// pretty sure it doesn't matter that we're always passing true here
|
||||
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 nil
|
||||
}
|
||||
|
||||
func (self *FilesController) press(node *filetree.FileNode) error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -273,33 +380,53 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
|
||||
return self.c.PushContext(self.contexts.Staging, opts)
|
||||
}
|
||||
|
||||
func (self *FilesController) allFilesStaged() bool {
|
||||
for _, file := range self.model.Files {
|
||||
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 {
|
||||
func (self *FilesController) toggleStagedAll() error {
|
||||
if err := self.toggleStagedAllWithLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.contexts.Files.HandleFocus()
|
||||
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.context().HandleFocus()
|
||||
}
|
||||
|
||||
func (self *FilesController) toggleStagedAllWithLock() error {
|
||||
self.mutexes.RefreshingFilesMutex.Lock()
|
||||
defer self.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
|
||||
}
|
||||
|
||||
if err := self.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
|
||||
}
|
||||
|
||||
if err := self.git.WorkingTree.UnstageAll(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FilesController) unstageFiles(node *filetree.FileNode) error {
|
||||
|
@ -49,7 +49,7 @@ func (self *UpstreamHelper) ParseUpstream(upstream string) (string, string, erro
|
||||
return upstreamRemote, upstreamBranch, nil
|
||||
}
|
||||
|
||||
func (self *UpstreamHelper) promptForUpstream(currentBranch *models.Branch, initialContent string, onConfirm func(string) error) error {
|
||||
func (self *UpstreamHelper) promptForUpstream(initialContent string, onConfirm func(string) error) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.EnterUpstream,
|
||||
InitialContent: initialContent,
|
||||
@ -62,11 +62,11 @@ func (self *UpstreamHelper) PromptForUpstreamWithInitialContent(currentBranch *m
|
||||
suggestedRemote := self.GetSuggestedRemote()
|
||||
initialContent := suggestedRemote + " " + currentBranch.Name
|
||||
|
||||
return self.promptForUpstream(currentBranch, initialContent, onConfirm)
|
||||
return self.promptForUpstream(initialContent, onConfirm)
|
||||
}
|
||||
|
||||
func (self *UpstreamHelper) PromptForUpstreamWithoutInitialContent(currentBranch *models.Branch, onConfirm func(string) error) error {
|
||||
return self.promptForUpstream(currentBranch, "", onConfirm)
|
||||
func (self *UpstreamHelper) PromptForUpstreamWithoutInitialContent(_ *models.Branch, onConfirm func(string) error) error {
|
||||
return self.promptForUpstream("", onConfirm)
|
||||
}
|
||||
|
||||
func (self *UpstreamHelper) GetSuggestedRemote() string {
|
||||
|
@ -227,11 +227,11 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
|
||||
applied, err := self.handleMidRebaseCommand("reword", commit)
|
||||
midRebase, err := self.handleMidRebaseCommand("reword", commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if applied {
|
||||
if midRebase {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -240,6 +240,11 @@ func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
|
||||
Prompt: self.c.Tr.RewordInEditorPrompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
|
||||
|
||||
if self.context().GetSelectedLineIdx() == 0 {
|
||||
return self.c.RunSubprocessAndRefresh(self.os.Cmd.New("git commit --allow-empty --amend --only"))
|
||||
}
|
||||
|
||||
subProcess, err := self.git.Rebase.RewordCommitInEditor(
|
||||
self.model.Commits, self.context().GetSelectedLineIdx(),
|
||||
)
|
||||
|
@ -7,10 +7,10 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
root := &FileNode{}
|
||||
func BuildTreeFromFiles(files []*models.File) *Node[models.File] {
|
||||
root := &Node[models.File]{}
|
||||
|
||||
var curr *FileNode
|
||||
var curr *Node[models.File]
|
||||
for _, file := range files {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
@ -30,7 +30,7 @@ func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &FileNode{
|
||||
newChild := &Node[models.File]{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
@ -46,17 +46,17 @@ func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
|
||||
rootAux := BuildTreeFromCommitFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
return &CommitFileNode{Children: sortedFiles}
|
||||
return &Node[models.CommitFile]{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
root := &CommitFileNode{}
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
|
||||
root := &Node[models.CommitFile]{}
|
||||
|
||||
var curr *CommitFileNode
|
||||
var curr *Node[models.CommitFile]
|
||||
for _, file := range files {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
@ -77,7 +77,7 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &CommitFileNode{
|
||||
newChild := &Node[models.CommitFile]{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
@ -93,7 +93,7 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
|
||||
func BuildFlatTreeFromFiles(files []*models.File) *Node[models.File] {
|
||||
rootAux := BuildTreeFromFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
@ -128,7 +128,7 @@ func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
|
||||
return false
|
||||
})
|
||||
|
||||
return &FileNode{Children: sortedFiles}
|
||||
return &Node[models.File]{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func split(str string) []string {
|
||||
|
@ -11,14 +11,14 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
Children: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -31,12 +31,12 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -60,12 +60,12 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1/dir3",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
@ -75,7 +75,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
@ -96,9 +96,9 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
@ -124,11 +124,11 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
// it is a little strange that we're not bubbling up our merge conflict
|
||||
// here but we are technically still in in tree mode and that's the rule
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
@ -159,14 +159,14 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
Children: []*Node[models.File]{},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -179,9 +179,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -205,9 +205,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -231,9 +231,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
@ -273,9 +273,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Tracked: true,
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "c1", HasMergeConflicts: true},
|
||||
Path: "c1",
|
||||
@ -318,14 +318,14 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
expected *Node[models.CommitFile]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
Children: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -338,12 +338,12 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -367,12 +367,12 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
Path: "dir1/dir3",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
@ -382,7 +382,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
@ -403,9 +403,9 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
@ -432,14 +432,14 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
expected *Node[models.CommitFile]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
Children: []*Node[models.CommitFile]{},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -452,9 +452,9 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -478,9 +478,9 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
@ -504,9 +504,9 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
expected: &Node[models.CommitFile]{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
|
@ -1,166 +1,21 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
||||
// CommitFileNode wraps a node and provides some commit-file-specific methods for it.
|
||||
type CommitFileNode struct {
|
||||
Children []*CommitFileNode
|
||||
File *models.CommitFile
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
*Node[models.CommitFile]
|
||||
}
|
||||
|
||||
var (
|
||||
_ INode = &CommitFileNode{}
|
||||
_ types.ListItem = &CommitFileNode{}
|
||||
)
|
||||
|
||||
func (s *CommitFileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
func (s *CommitFileNode) IsNil() bool {
|
||||
return s == nil
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetChildren() []INode {
|
||||
return slices.Map(s.Children, func(child *CommitFileNode) INode {
|
||||
return child
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetChildren(children []INode) {
|
||||
castChildren := slices.Map(children, func(child INode) *CommitFileNode {
|
||||
return child.(*CommitFileNode)
|
||||
})
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *CommitFileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) ForEachFile(cb func(*models.CommitFile) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Any(test func(node *CommitFileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Every(test func(node *CommitFileNode) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) EveryFile(test func(file *models.CommitFile) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
|
||||
return castNode.File == nil || test(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *CommitFileNode) Flatten(collapsedPaths *CollapsedPaths) []*CommitFileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
|
||||
return slices.Map(results, func(result INode) *CommitFileNode {
|
||||
return result.(*CommitFileNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths *CollapsedPaths) *CommitFileNode {
|
||||
func NewCommitFileNode(node *Node[models.CommitFile]) *CommitFileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.(*CommitFileNode)
|
||||
return &CommitFileNode{Node: node}
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetIndexForPath(path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) Size(collapsedPaths *CollapsedPaths) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetLeaves() []*CommitFileNode {
|
||||
leaves := getLeaves(s)
|
||||
|
||||
return slices.Map(leaves, func(leaf INode) *CommitFileNode {
|
||||
return leaf.(*CommitFileNode)
|
||||
})
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool {
|
||||
return s.Any(func(node *CommitFileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) NameAtDepth(depth int) string {
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
return name
|
||||
// returns the underlying node, without any commit-file-specific methods attached
|
||||
func (self *CommitFileNode) Raw() *Node[models.CommitFile] {
|
||||
return self.Node
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICommitFileTree interface {
|
||||
ITree
|
||||
ITree[models.CommitFile]
|
||||
|
||||
Get(index int) *CommitFileNode
|
||||
GetFile(path string) *models.CommitFile
|
||||
GetAllItems() []*CommitFileNode
|
||||
GetAllFiles() []*models.CommitFile
|
||||
GetRoot() *CommitFileNode
|
||||
}
|
||||
|
||||
type CommitFileTree struct {
|
||||
getFiles func() []*models.CommitFile
|
||||
tree *CommitFileNode
|
||||
tree *Node[models.CommitFile]
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
collapsedPaths *CollapsedPaths
|
||||
@ -44,7 +46,7 @@ func (self *CommitFileTree) ToggleShowTree() {
|
||||
|
||||
func (self *CommitFileTree) Get(index int) *CommitFileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
|
||||
return NewCommitFileNode(self.tree.GetNodeAtIndex(index+1, self.collapsedPaths)) // ignoring root
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) GetIndexForPath(path string) (int, bool) {
|
||||
@ -57,7 +59,10 @@ func (self *CommitFileTree) GetAllItems() []*CommitFileNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
|
||||
// ignoring root
|
||||
return slices.Map(self.tree.Flatten(self.collapsedPaths)[1:], func(node *Node[models.CommitFile]) *CommitFileNode {
|
||||
return NewCommitFileNode(node)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) Len() int {
|
||||
@ -84,8 +89,8 @@ func (self *CommitFileTree) ToggleCollapsed(path string) {
|
||||
self.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) Tree() INode {
|
||||
return self.tree
|
||||
func (self *CommitFileTree) GetRoot() *CommitFileNode {
|
||||
return NewCommitFileNode(self.tree)
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) CollapsedPaths() *CollapsedPaths {
|
||||
|
@ -1,199 +1,47 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
||||
// FileNode wraps a node and provides some file-specific methods for it.
|
||||
type FileNode struct {
|
||||
Children []*FileNode
|
||||
File *models.File
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
*Node[models.File]
|
||||
}
|
||||
|
||||
var (
|
||||
_ INode = &FileNode{}
|
||||
_ types.ListItem = &FileNode{}
|
||||
)
|
||||
var _ models.IFile = &FileNode{}
|
||||
|
||||
func (s *FileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *FileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
// interfaces values whose concrete value is nil are not themselves nil
|
||||
// hence the existence of this method
|
||||
func (s *FileNode) IsNil() bool {
|
||||
return s == nil
|
||||
}
|
||||
|
||||
func (s *FileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *FileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *FileNode) GetPreviousPath() string {
|
||||
if s.File != nil {
|
||||
return s.File.GetPreviousPath()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *FileNode) GetChildren() []INode {
|
||||
return slices.Map(s.Children, func(child *FileNode) INode {
|
||||
return child
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) SetChildren(children []INode) {
|
||||
castChildren := slices.Map(children, func(child INode) *FileNode {
|
||||
return child.(*FileNode)
|
||||
})
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *FileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *FileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *FileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *FileNode) ForEachFile(cb func(*models.File) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*FileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) Any(test func(node *FileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*FileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *FileNode) Flatten(collapsedPaths *CollapsedPaths) []*FileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
return slices.Map(results, func(result INode) *FileNode {
|
||||
return result.(*FileNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths *CollapsedPaths) *FileNode {
|
||||
func NewFileNode(node *Node[models.File]) *FileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
return &FileNode{Node: node}
|
||||
}
|
||||
|
||||
// returns the underlying node, without any file-specific methods attached
|
||||
func (self *FileNode) Raw() *Node[models.File] {
|
||||
return self.Node
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasUnstagedChanges() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasStagedChanges() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetHasInlineMergeConflicts() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetIsTracked() bool {
|
||||
return self.SomeFile(func(file *models.File) bool { return file.Tracked })
|
||||
}
|
||||
|
||||
func (self *FileNode) GetPreviousPath() string {
|
||||
if self.File == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return result.(*FileNode)
|
||||
}
|
||||
|
||||
func (node *FileNode) GetIndexForPath(path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *FileNode) Size(collapsedPaths *CollapsedPaths) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *FileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
func (node *FileNode) GetFilePathsMatching(test func(*models.File) bool) []string {
|
||||
return getPathsMatching(node, func(n INode) bool {
|
||||
castNode := n.(*FileNode)
|
||||
if castNode.File == nil {
|
||||
return false
|
||||
}
|
||||
return test(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) GetLeaves() []*FileNode {
|
||||
leaves := getLeaves(s)
|
||||
|
||||
return slices.Map(leaves, func(leaf INode) *FileNode {
|
||||
return leaf.(*FileNode)
|
||||
})
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *FileNode) GetHasUnstagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasStagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasInlineMergeConflicts() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetIsTracked() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.Tracked })
|
||||
}
|
||||
|
||||
func (s *FileNode) AnyFile(test func(file *models.File) bool) bool {
|
||||
return s.Any(func(node *FileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) NameAtDepth(depth int) string {
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if s.File != nil && s.File.IsRename() {
|
||||
splitPrevName := split(s.File.PreviousName)
|
||||
|
||||
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 := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return prevName + " → " + name
|
||||
}
|
||||
|
||||
return name
|
||||
return self.File.PreviousName
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
func TestCompress(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
expected *FileNode
|
||||
root *Node[models.File]
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
@ -20,27 +20,27 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
root: &FileNode{
|
||||
root: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
root: &FileNode{
|
||||
root: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
@ -49,7 +49,7 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
@ -62,10 +62,10 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir3",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir3/dir3-1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir3/dir3-1/file5",
|
||||
@ -80,12 +80,12 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
expected: &Node[models.File]{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
@ -94,7 +94,7 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
@ -108,7 +108,7 @@ func TestCompress(t *testing.T) {
|
||||
{
|
||||
Path: "dir3/dir3-1",
|
||||
CompressionLevel: 1,
|
||||
Children: []*FileNode{
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir3/dir3-1/file5",
|
||||
|
@ -18,7 +18,7 @@ const (
|
||||
DisplayConflicted
|
||||
)
|
||||
|
||||
type ITree interface {
|
||||
type ITree[T any] interface {
|
||||
InTreeMode() bool
|
||||
ExpandToPath(path string)
|
||||
ToggleShowTree()
|
||||
@ -27,12 +27,11 @@ type ITree interface {
|
||||
SetTree()
|
||||
IsCollapsed(path string) bool
|
||||
ToggleCollapsed(path string)
|
||||
Tree() INode
|
||||
CollapsedPaths() *CollapsedPaths
|
||||
}
|
||||
|
||||
type IFileTree interface {
|
||||
ITree
|
||||
ITree[models.File]
|
||||
|
||||
FilterFiles(test func(*models.File) bool) []*models.File
|
||||
SetFilter(filter FileTreeDisplayFilter)
|
||||
@ -41,17 +40,20 @@ type IFileTree interface {
|
||||
GetAllItems() []*FileNode
|
||||
GetAllFiles() []*models.File
|
||||
GetFilter() FileTreeDisplayFilter
|
||||
GetRoot() *FileNode
|
||||
}
|
||||
|
||||
type FileTree struct {
|
||||
getFiles func() []*models.File
|
||||
tree *FileNode
|
||||
tree *Node[models.File]
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
filter FileTreeDisplayFilter
|
||||
collapsedPaths *CollapsedPaths
|
||||
}
|
||||
|
||||
var _ IFileTree = &FileTree{}
|
||||
|
||||
func NewFileTree(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTree {
|
||||
return &FileTree{
|
||||
getFiles: getFiles,
|
||||
@ -101,7 +103,7 @@ func (self *FileTree) ToggleShowTree() {
|
||||
|
||||
func (self *FileTree) Get(index int) *FileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
|
||||
return NewFileNode(self.tree.GetNodeAtIndex(index+1, self.collapsedPaths)) // ignoring root
|
||||
}
|
||||
|
||||
func (self *FileTree) GetFile(path string) *models.File {
|
||||
@ -127,7 +129,10 @@ func (self *FileTree) GetAllItems() []*FileNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
|
||||
// ignoring root
|
||||
return slices.Map(self.tree.Flatten(self.collapsedPaths)[1:], func(node *Node[models.File]) *FileNode {
|
||||
return NewFileNode(node)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FileTree) Len() int {
|
||||
@ -155,8 +160,12 @@ func (self *FileTree) ToggleCollapsed(path string) {
|
||||
self.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (self *FileTree) Tree() INode {
|
||||
return self.tree
|
||||
func (self *FileTree) Tree() *FileNode {
|
||||
return NewFileNode(self.tree)
|
||||
}
|
||||
|
||||
func (self *FileTree) GetRoot() *FileNode {
|
||||
return NewFileNode(self.tree)
|
||||
}
|
||||
|
||||
func (self *FileTree) CollapsedPaths() *CollapsedPaths {
|
||||
|
@ -1,206 +0,0 @@
|
||||
package filetree
|
||||
|
||||
import "github.com/jesseduffield/generics/slices"
|
||||
|
||||
type INode interface {
|
||||
IsNil() bool
|
||||
IsLeaf() bool
|
||||
GetPath() string
|
||||
GetChildren() []INode
|
||||
SetChildren([]INode)
|
||||
GetCompressionLevel() int
|
||||
SetCompressionLevel(int)
|
||||
}
|
||||
|
||||
func sortNode(node INode) {
|
||||
sortChildren(node)
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
sortNode(child)
|
||||
}
|
||||
}
|
||||
|
||||
func sortChildren(node INode) {
|
||||
if node.IsLeaf() {
|
||||
return
|
||||
}
|
||||
|
||||
sortedChildren := slices.Clone(node.GetChildren())
|
||||
|
||||
slices.SortFunc(sortedChildren, func(a, b INode) bool {
|
||||
if !a.IsLeaf() && b.IsLeaf() {
|
||||
return true
|
||||
}
|
||||
if a.IsLeaf() && !b.IsLeaf() {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.GetPath() < b.GetPath()
|
||||
})
|
||||
|
||||
// TODO: think about making this in-place
|
||||
node.SetChildren(sortedChildren)
|
||||
}
|
||||
|
||||
func forEachLeaf(node INode, cb func(INode) error) error {
|
||||
if node.IsLeaf() {
|
||||
if err := cb(node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if err := forEachLeaf(child, cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func any(node INode, test func(INode) bool) bool {
|
||||
if test(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if any(child, test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func every(node INode, test func(INode) bool) bool {
|
||||
if !test(node) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if !every(child, test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func flatten(node INode, collapsedPaths *CollapsedPaths) []INode {
|
||||
result := []INode{}
|
||||
result = append(result, node)
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
result = append(result, flatten(child, collapsedPaths)...)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getNodeAtIndex(node INode, index int, collapsedPaths *CollapsedPaths) INode {
|
||||
foundNode, _ := getNodeAtIndexAux(node, index, collapsedPaths)
|
||||
|
||||
return foundNode
|
||||
}
|
||||
|
||||
func getNodeAtIndexAux(node INode, index int, collapsedPaths *CollapsedPaths) (INode, int) {
|
||||
offset := 1
|
||||
|
||||
if index == 0 {
|
||||
return node, offset
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
foundNode, offsetChange := getNodeAtIndexAux(child, index-offset, collapsedPaths)
|
||||
offset += offsetChange
|
||||
if foundNode != nil {
|
||||
return foundNode, offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, offset
|
||||
}
|
||||
|
||||
func getIndexForPath(node INode, path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
offset := 0
|
||||
|
||||
if node.GetPath() == path {
|
||||
return offset, true
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
offsetChange, found := getIndexForPath(child, path, collapsedPaths)
|
||||
offset += offsetChange + 1
|
||||
if found {
|
||||
return offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset, false
|
||||
}
|
||||
|
||||
func size(node INode, collapsedPaths *CollapsedPaths) int {
|
||||
output := 1
|
||||
|
||||
if !collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
for _, child := range node.GetChildren() {
|
||||
output += size(child, collapsedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func compressAux(node INode) INode {
|
||||
if node.IsLeaf() {
|
||||
return node
|
||||
}
|
||||
|
||||
children := node.GetChildren()
|
||||
for i := range children {
|
||||
grandchildren := children[i].GetChildren()
|
||||
for len(grandchildren) == 1 && !grandchildren[0].IsLeaf() {
|
||||
grandchildren[0].SetCompressionLevel(children[i].GetCompressionLevel() + 1)
|
||||
children[i] = grandchildren[0]
|
||||
grandchildren = children[i].GetChildren()
|
||||
}
|
||||
}
|
||||
|
||||
for i := range children {
|
||||
children[i] = compressAux(children[i])
|
||||
}
|
||||
|
||||
node.SetChildren(children)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func getPathsMatching(node INode, test func(INode) bool) []string {
|
||||
paths := []string{}
|
||||
|
||||
if test(node) {
|
||||
paths = append(paths, node.GetPath())
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
paths = append(paths, getPathsMatching(child, test)...)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func getLeaves(node INode) []INode {
|
||||
if node.IsLeaf() {
|
||||
return []INode{node}
|
||||
}
|
||||
|
||||
return slices.FlatMap(node.GetChildren(), func(child INode) []INode {
|
||||
return getLeaves(child)
|
||||
})
|
||||
}
|
301
pkg/gui/filetree/node.go
Normal file
301
pkg/gui/filetree/node.go
Normal file
@ -0,0 +1,301 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// Represents a file or directory in a file tree.
|
||||
type Node[T any] struct {
|
||||
// File will be nil if the node is a directory.
|
||||
File *T
|
||||
|
||||
// If the node is a directory, Children contains the contents of the directory,
|
||||
// otherwise it's nil.
|
||||
Children []*Node[T]
|
||||
|
||||
// path of the file/directory
|
||||
Path string
|
||||
|
||||
// rather than render a tree as:
|
||||
// a/
|
||||
// b/
|
||||
// file.blah
|
||||
//
|
||||
// we instead render it as:
|
||||
// a/b/
|
||||
// file.blah
|
||||
// This saves vertical space. The CompressionLevel of a node is equal to the
|
||||
// number of times a 'compression' like the above has happened, where two
|
||||
// nodes are squished into one.
|
||||
CompressionLevel int
|
||||
}
|
||||
|
||||
var _ types.ListItem = &Node[models.File]{}
|
||||
|
||||
func (self *Node[T]) IsFile() bool {
|
||||
return self.File != nil
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetPath() string {
|
||||
return self.Path
|
||||
}
|
||||
|
||||
func (self *Node[T]) Sort() {
|
||||
self.SortChildren()
|
||||
|
||||
for _, child := range self.Children {
|
||||
child.Sort()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Node[T]) ForEachFile(cb func(*T) error) error {
|
||||
if self.IsFile() {
|
||||
if err := cb(self.File); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
if err := child.ForEachFile(cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node[T]) SortChildren() {
|
||||
if self.IsFile() {
|
||||
return
|
||||
}
|
||||
|
||||
children := slices.Clone(self.Children)
|
||||
|
||||
slices.SortFunc(children, func(a, b *Node[T]) bool {
|
||||
if !a.IsFile() && b.IsFile() {
|
||||
return true
|
||||
}
|
||||
if a.IsFile() && !b.IsFile() {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.GetPath() < b.GetPath()
|
||||
})
|
||||
|
||||
// TODO: think about making this in-place
|
||||
self.Children = children
|
||||
}
|
||||
|
||||
func (self *Node[T]) Some(test func(*Node[T]) bool) bool {
|
||||
if test(self) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
if child.Some(test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Node[T]) SomeFile(test func(*T) bool) bool {
|
||||
if self.IsFile() {
|
||||
if test(self.File) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
for _, child := range self.Children {
|
||||
if child.SomeFile(test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Node[T]) Every(test func(*Node[T]) bool) bool {
|
||||
if !test(self) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
if !child.Every(test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *Node[T]) EveryFile(test func(*T) bool) bool {
|
||||
if self.IsFile() {
|
||||
if !test(self.File) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
for _, child := range self.Children {
|
||||
if !child.EveryFile(test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *Node[T]) Flatten(collapsedPaths *CollapsedPaths) []*Node[T] {
|
||||
result := []*Node[T]{self}
|
||||
|
||||
if len(self.Children) > 0 && !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
result = append(result, slices.FlatMap(self.Children, func(child *Node[T]) []*Node[T] {
|
||||
return child.Flatten(collapsedPaths)
|
||||
})...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetNodeAtIndex(index int, collapsedPaths *CollapsedPaths) *Node[T] {
|
||||
if self == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
node, _ := self.getNodeAtIndexAux(index, collapsedPaths)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (self *Node[T]) getNodeAtIndexAux(index int, collapsedPaths *CollapsedPaths) (*Node[T], int) {
|
||||
offset := 1
|
||||
|
||||
if index == 0 {
|
||||
return self, offset
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
for _, child := range self.Children {
|
||||
foundNode, offsetChange := child.getNodeAtIndexAux(index-offset, collapsedPaths)
|
||||
offset += offsetChange
|
||||
if foundNode != nil {
|
||||
return foundNode, offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, offset
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetIndexForPath(path string, collapsedPaths *CollapsedPaths) (int, bool) {
|
||||
offset := 0
|
||||
|
||||
if self.GetPath() == path {
|
||||
return offset, true
|
||||
}
|
||||
|
||||
if !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
for _, child := range self.Children {
|
||||
offsetChange, found := child.GetIndexForPath(path, collapsedPaths)
|
||||
offset += offsetChange + 1
|
||||
if found {
|
||||
return offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset, false
|
||||
}
|
||||
|
||||
func (self *Node[T]) Size(collapsedPaths *CollapsedPaths) int {
|
||||
if self == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
output := 1
|
||||
|
||||
if !collapsedPaths.IsCollapsed(self.GetPath()) {
|
||||
for _, child := range self.Children {
|
||||
output += child.Size(collapsedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (self *Node[T]) Compress() {
|
||||
if self == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.compressAux()
|
||||
}
|
||||
|
||||
func (self *Node[T]) compressAux() *Node[T] {
|
||||
if self.IsFile() {
|
||||
return self
|
||||
}
|
||||
|
||||
children := self.Children
|
||||
for i := range children {
|
||||
grandchildren := children[i].Children
|
||||
for len(grandchildren) == 1 && !grandchildren[0].IsFile() {
|
||||
grandchildren[0].CompressionLevel = children[i].CompressionLevel + 1
|
||||
children[i] = grandchildren[0]
|
||||
grandchildren = children[i].Children
|
||||
}
|
||||
}
|
||||
|
||||
for i := range children {
|
||||
children[i] = children[i].compressAux()
|
||||
}
|
||||
|
||||
self.Children = children
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetPathsMatching(test func(*Node[T]) bool) []string {
|
||||
paths := []string{}
|
||||
|
||||
if test(self) {
|
||||
paths = append(paths, self.GetPath())
|
||||
}
|
||||
|
||||
for _, child := range self.Children {
|
||||
paths = append(paths, child.GetPathsMatching(test)...)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetFilePathsMatching(test func(*T) bool) []string {
|
||||
matchingFileNodes := slices.Filter(self.GetLeaves(), func(node *Node[T]) bool {
|
||||
return test(node.File)
|
||||
})
|
||||
|
||||
return slices.Map(matchingFileNodes, func(node *Node[T]) string {
|
||||
return node.GetPath()
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Node[T]) GetLeaves() []*Node[T] {
|
||||
if self.IsFile() {
|
||||
return []*Node[T]{self}
|
||||
}
|
||||
|
||||
return slices.FlatMap(self.Children, func(child *Node[T]) []*Node[T] {
|
||||
return child.GetLeaves()
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Node[T]) ID() string {
|
||||
return self.GetPath()
|
||||
}
|
||||
|
||||
func (self *Node[T]) Description() string {
|
||||
return self.GetPath()
|
||||
}
|
@ -97,7 +97,7 @@ type Gui struct {
|
||||
// recent repo with the recent repos popup showing
|
||||
showRecentRepos bool
|
||||
|
||||
Mutexes guiMutexes
|
||||
Mutexes types.Mutexes
|
||||
|
||||
// findSuggestions will take a string that the user has typed into a prompt
|
||||
// and return a slice of suggestions which match that string.
|
||||
@ -237,18 +237,6 @@ const (
|
||||
COMPLETE
|
||||
)
|
||||
|
||||
// if you add a new mutex here be sure to instantiate it. We're using pointers to
|
||||
// mutexes so that we can pass the mutexes to controllers.
|
||||
type guiMutexes struct {
|
||||
RefreshingFilesMutex *sync.Mutex
|
||||
RefreshingStatusMutex *sync.Mutex
|
||||
SyncMutex *sync.Mutex
|
||||
LocalCommitsMutex *sync.Mutex
|
||||
LineByLinePanelMutex *sync.Mutex
|
||||
SubprocessMutex *sync.Mutex
|
||||
PopupMutex *sync.Mutex
|
||||
}
|
||||
|
||||
func (gui *Gui) onNewRepo(startArgs types.StartArgs, reuseState bool) error {
|
||||
var err error
|
||||
gui.git, err = commands.NewGitCommand(
|
||||
@ -418,7 +406,7 @@ func NewGui(
|
||||
// but now we do it via state. So we need to still support the config for the
|
||||
// sake of backwards compatibility. We're making use of short circuiting here
|
||||
ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog,
|
||||
Mutexes: guiMutexes{
|
||||
Mutexes: types.Mutexes{
|
||||
RefreshingFilesMutex: &sync.Mutex{},
|
||||
RefreshingStatusMutex: &sync.Mutex{},
|
||||
SyncMutex: &sync.Mutex{},
|
||||
|
@ -1,11 +1,12 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) getMenuOptions() map[string]string {
|
||||
@ -30,9 +31,25 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
|
||||
})
|
||||
}
|
||||
|
||||
maxColumnSize := 1
|
||||
|
||||
for _, item := range opts.Items {
|
||||
if item.OpensMenu && item.LabelColumns != nil {
|
||||
return errors.New("Message for the developer of this app: you've set opensMenu with displaystrings on the menu panel. Bad developer!. Apologies, user")
|
||||
if item.LabelColumns == nil {
|
||||
item.LabelColumns = []string{item.Label}
|
||||
}
|
||||
|
||||
if item.OpensMenu {
|
||||
item.LabelColumns[0] = presentation.OpensMenuStyle(item.LabelColumns[0])
|
||||
}
|
||||
|
||||
maxColumnSize = utils.Max(maxColumnSize, len(item.LabelColumns))
|
||||
}
|
||||
|
||||
for _, item := range opts.Items {
|
||||
if len(item.LabelColumns) < maxColumnSize {
|
||||
// we require that each item has the same number of columns so we're padding out with blank strings
|
||||
// if this item has too few
|
||||
item.LabelColumns = append(item.LabelColumns, make([]string, maxColumnSize-len(item.LabelColumns))...)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
@ -50,21 +49,14 @@ func uniqueBindings(bindings []*types.Binding) []*types.Binding {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) displayDescription(binding *types.Binding) string {
|
||||
if binding.OpensMenu {
|
||||
return presentation.OpensMenuStyle(binding.Description)
|
||||
}
|
||||
|
||||
return binding.Description
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateOptionsMenu() error {
|
||||
context := gui.currentContext()
|
||||
bindings := gui.getBindings(context)
|
||||
|
||||
menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
Label: gui.displayDescription(binding),
|
||||
OpensMenu: binding.OpensMenu,
|
||||
Label: binding.Description,
|
||||
OnPress: func() error {
|
||||
if binding.Key == nil {
|
||||
return nil
|
||||
|
@ -30,9 +30,10 @@ func RenderFileTree(
|
||||
diffName string,
|
||||
submoduleConfigs []*models.SubmoduleConfig,
|
||||
) []string {
|
||||
return renderAux(tree.Tree(), tree.CollapsedPaths(), "", -1, func(n filetree.INode, depth int) string {
|
||||
castN := n.(*filetree.FileNode)
|
||||
return getFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
return renderAux(tree.GetRoot().Raw(), tree.CollapsedPaths(), "", -1, func(node *filetree.Node[models.File], depth int) string {
|
||||
fileNode := filetree.NewFileNode(node)
|
||||
|
||||
return getFileLine(fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), fileNameAtDepth(node, depth), diffName, submoduleConfigs, node.File)
|
||||
})
|
||||
}
|
||||
|
||||
@ -41,19 +42,17 @@ func RenderCommitFileTree(
|
||||
diffName string,
|
||||
patchManager *patch.PatchManager,
|
||||
) []string {
|
||||
return renderAux(tree.Tree(), tree.CollapsedPaths(), "", -1, func(n filetree.INode, depth int) string {
|
||||
castN := n.(*filetree.CommitFileNode)
|
||||
|
||||
return renderAux(tree.GetRoot().Raw(), tree.CollapsedPaths(), "", -1, func(node *filetree.Node[models.CommitFile], depth int) string {
|
||||
// This is a little convoluted because we're dealing with either a leaf or a non-leaf.
|
||||
// But this code actually applies to both. If it's a leaf, the status will just
|
||||
// be whatever status it is, but if it's a non-leaf it will determine its status
|
||||
// based on the leaves of that subtree
|
||||
var status patch.PatchStatus
|
||||
if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
if node.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, tree.GetRef().RefName()) == patch.WHOLE
|
||||
}) {
|
||||
status = patch.WHOLE
|
||||
} else if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
} else if node.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, tree.GetRef().RefName()) == patch.UNSELECTED
|
||||
}) {
|
||||
status = patch.UNSELECTED
|
||||
@ -61,37 +60,37 @@ func RenderCommitFileTree(
|
||||
status = patch.PART
|
||||
}
|
||||
|
||||
return getCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
return getCommitFileLine(commitFileNameAtDepth(node, depth), diffName, node.File, status)
|
||||
})
|
||||
}
|
||||
|
||||
func renderAux(
|
||||
s filetree.INode,
|
||||
func renderAux[T any](
|
||||
node *filetree.Node[T],
|
||||
collapsedPaths *filetree.CollapsedPaths,
|
||||
prefix string,
|
||||
depth int,
|
||||
renderLine func(filetree.INode, int) string,
|
||||
renderLine func(*filetree.Node[T], int) string,
|
||||
) []string {
|
||||
if s == nil || s.IsNil() {
|
||||
if node == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
isRoot := depth == -1
|
||||
|
||||
if s.IsLeaf() {
|
||||
if node.IsFile() {
|
||||
if isRoot {
|
||||
return []string{}
|
||||
}
|
||||
return []string{prefix + renderLine(s, depth)}
|
||||
return []string{prefix + renderLine(node, depth)}
|
||||
}
|
||||
|
||||
if collapsedPaths.IsCollapsed(s.GetPath()) {
|
||||
return []string{prefix + COLLAPSED_ARROW + " " + renderLine(s, depth)}
|
||||
if collapsedPaths.IsCollapsed(node.GetPath()) {
|
||||
return []string{prefix + COLLAPSED_ARROW + " " + renderLine(node, depth)}
|
||||
}
|
||||
|
||||
arr := []string{}
|
||||
if !isRoot {
|
||||
arr = append(arr, prefix+EXPANDED_ARROW+" "+renderLine(s, depth))
|
||||
arr = append(arr, prefix+EXPANDED_ARROW+" "+renderLine(node, depth))
|
||||
}
|
||||
|
||||
newPrefix := prefix
|
||||
@ -101,8 +100,8 @@ func renderAux(
|
||||
newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
|
||||
}
|
||||
|
||||
for i, child := range s.GetChildren() {
|
||||
isLast := i == len(s.GetChildren())-1
|
||||
for i, child := range node.Children {
|
||||
isLast := i == len(node.Children)-1
|
||||
|
||||
var childPrefix string
|
||||
if isRoot {
|
||||
@ -113,7 +112,7 @@ func renderAux(
|
||||
childPrefix = newPrefix + INNER_ITEM
|
||||
}
|
||||
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+node.CompressionLevel, renderLine)...)
|
||||
}
|
||||
|
||||
return arr
|
||||
@ -220,3 +219,39 @@ func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
func fileNameAtDepth(node *filetree.Node[models.File], depth int) string {
|
||||
splitName := split(node.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if node.File != nil && node.File.IsRename() {
|
||||
splitPrevName := split(node.File.PreviousName)
|
||||
|
||||
prevName := node.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 := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return prevName + " → " + name
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func commitFileNameAtDepth(node *filetree.Node[models.CommitFile], depth int) string {
|
||||
splitName := split(node.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func split(str string) []string {
|
||||
return strings.Split(str, "/")
|
||||
}
|
||||
|
||||
func join(strs []string) string {
|
||||
return strings.Join(strs, "/")
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func GetReflogCommitListDisplayStrings(commits []*models.Commit, fullDescription bool, cherryPickedCommitShaSet *set.Set[string], diffName string, timeFormat string, parseEmoji bool) [][]string {
|
||||
var displayFunc func(*models.Commit, string, bool, bool, bool) []string
|
||||
var displayFunc func(*models.Commit, reflogCommitDisplayAttributes) []string
|
||||
if fullDescription {
|
||||
displayFunc = getFullDescriptionDisplayStringsForReflogCommit
|
||||
} else {
|
||||
@ -21,7 +21,13 @@ func GetReflogCommitListDisplayStrings(commits []*models.Commit, fullDescription
|
||||
return slices.Map(commits, func(commit *models.Commit) []string {
|
||||
diffed := commit.Sha == diffName
|
||||
cherryPicked := cherryPickedCommitShaSet.Includes(commit.Sha)
|
||||
return displayFunc(commit, timeFormat, cherryPicked, diffed, parseEmoji)
|
||||
return displayFunc(commit,
|
||||
reflogCommitDisplayAttributes{
|
||||
cherryPicked: cherryPicked,
|
||||
diffed: diffed,
|
||||
parseEmoji: parseEmoji,
|
||||
timeFormat: timeFormat,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -38,27 +44,34 @@ func reflogShaColor(cherryPicked, diffed bool) style.TextStyle {
|
||||
return shaColor
|
||||
}
|
||||
|
||||
func getFullDescriptionDisplayStringsForReflogCommit(c *models.Commit, timeFormat string, cherryPicked, diffed, parseEmoji bool) []string {
|
||||
type reflogCommitDisplayAttributes struct {
|
||||
cherryPicked bool
|
||||
diffed bool
|
||||
parseEmoji bool
|
||||
timeFormat string
|
||||
}
|
||||
|
||||
func getFullDescriptionDisplayStringsForReflogCommit(c *models.Commit, attrs reflogCommitDisplayAttributes) []string {
|
||||
name := c.Name
|
||||
if parseEmoji {
|
||||
if attrs.parseEmoji {
|
||||
name = emoji.Sprint(name)
|
||||
}
|
||||
|
||||
return []string{
|
||||
reflogShaColor(cherryPicked, diffed).Sprint(c.ShortSha()),
|
||||
style.FgMagenta.Sprint(utils.UnixToDate(c.UnixTimestamp, timeFormat)),
|
||||
reflogShaColor(attrs.cherryPicked, attrs.diffed).Sprint(c.ShortSha()),
|
||||
style.FgMagenta.Sprint(utils.UnixToDate(c.UnixTimestamp, attrs.timeFormat)),
|
||||
theme.DefaultTextColor.Sprint(name),
|
||||
}
|
||||
}
|
||||
|
||||
func getDisplayStringsForReflogCommit(c *models.Commit, timeFormat string, cherryPicked, diffed, parseEmoji bool) []string {
|
||||
func getDisplayStringsForReflogCommit(c *models.Commit, attrs reflogCommitDisplayAttributes) []string {
|
||||
name := c.Name
|
||||
if parseEmoji {
|
||||
if attrs.parseEmoji {
|
||||
name = emoji.Sprint(name)
|
||||
}
|
||||
|
||||
return []string{
|
||||
reflogShaColor(cherryPicked, diffed).Sprint(c.ShortSha()),
|
||||
reflogShaColor(attrs.cherryPicked, attrs.diffed).Sprint(c.ShortSha()),
|
||||
theme.DefaultTextColor.Sprint(name),
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,92 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateRecentReposMenu() error {
|
||||
recentRepoPaths := gui.c.GetAppState().RecentRepos
|
||||
func (gui *Gui) getCurrentBranch(path string) string {
|
||||
readHeadFile := func(path string) (string, error) {
|
||||
headFile, err := ioutil.ReadFile(filepath.Join(path, "HEAD"))
|
||||
if err == nil {
|
||||
content := strings.TrimSpace(string(headFile))
|
||||
refsPrefix := "ref: refs/heads/"
|
||||
branchDisplay := ""
|
||||
if strings.HasPrefix(content, refsPrefix) {
|
||||
// is a branch
|
||||
branchDisplay = strings.TrimPrefix(content, refsPrefix)
|
||||
} else {
|
||||
// detached HEAD state, displaying short SHA
|
||||
branchDisplay = utils.ShortSha(content)
|
||||
}
|
||||
return branchDisplay, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
gitDirPath := filepath.Join(path, ".git")
|
||||
|
||||
if gitDir, err := os.Stat(gitDirPath); err == nil {
|
||||
if gitDir.IsDir() {
|
||||
// ordinary repo
|
||||
if branch, err := readHeadFile(gitDirPath); err == nil {
|
||||
return branch
|
||||
}
|
||||
} else {
|
||||
// worktree
|
||||
if worktreeGitDir, err := ioutil.ReadFile(gitDirPath); err == nil {
|
||||
content := strings.TrimSpace(string(worktreeGitDir))
|
||||
worktreePath := strings.TrimPrefix(content, "gitdir: ")
|
||||
if branch, err := readHeadFile(worktreePath); err == nil {
|
||||
return branch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.c.Tr.LcBranchUnknown
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateRecentReposMenu() error {
|
||||
// we skip the first one because we're currently in it
|
||||
recentRepoPaths := gui.c.GetAppState().RecentRepos[1:]
|
||||
|
||||
currentBranches := sync.Map{}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(recentRepoPaths))
|
||||
|
||||
for _, path := range recentRepoPaths {
|
||||
go func(path string) {
|
||||
defer wg.Done()
|
||||
currentBranches.Store(path, gui.getCurrentBranch(path))
|
||||
}(path)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem {
|
||||
branchName, _ := currentBranches.Load(path)
|
||||
if icons.IsIconEnabled() {
|
||||
branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName)
|
||||
}
|
||||
|
||||
// we won't show the current repo hence the -1
|
||||
menuItems := slices.Map(recentRepoPaths[1:], func(path string) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{
|
||||
filepath.Base(path),
|
||||
style.FgCyan.Sprint(branchName),
|
||||
style.FgMagenta.Sprint(path),
|
||||
},
|
||||
OnPress: func() error {
|
||||
@ -110,7 +178,7 @@ func newRecentReposList(recentRepos []string, currentRepo string) (bool, []strin
|
||||
newRepos := []string{currentRepo}
|
||||
for _, repo := range recentRepos {
|
||||
if repo != currentRepo {
|
||||
if _, err := os.Stat(repo); err != nil {
|
||||
if _, err := os.Stat(filepath.Join(repo, ".git")); err != nil {
|
||||
continue
|
||||
}
|
||||
newRepos = append(newRepos, repo)
|
||||
|
@ -1,6 +1,8 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
@ -187,10 +189,20 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
||||
if customCommand.Stream {
|
||||
cmdObj.StreamOutput()
|
||||
}
|
||||
err := cmdObj.Run()
|
||||
output, err := cmdObj.RunWithOutput()
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
if customCommand.ShowOutput {
|
||||
if strings.TrimSpace(output) == "" {
|
||||
output = self.c.Tr.EmptyOutput
|
||||
}
|
||||
if err = self.c.Alert(cmdStr, output); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{})
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{})
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
@ -96,6 +98,10 @@ type ConfirmOpts struct {
|
||||
HandleConfirm func() error
|
||||
HandleClose func() error
|
||||
HandlersManageFocus bool
|
||||
HasLoader bool
|
||||
FindSuggestionsFunc func(string) []*Suggestion
|
||||
Editable bool
|
||||
Mask bool
|
||||
}
|
||||
|
||||
type PromptOpts struct {
|
||||
@ -152,3 +158,15 @@ type Model struct {
|
||||
// for displaying suggestions while typing in a file name
|
||||
FilesTrie *patricia.Trie
|
||||
}
|
||||
|
||||
// if you add a new mutex here be sure to instantiate it. We're using pointers to
|
||||
// mutexes so that we can pass the mutexes to controllers.
|
||||
type Mutexes struct {
|
||||
RefreshingFilesMutex *sync.Mutex
|
||||
RefreshingStatusMutex *sync.Mutex
|
||||
SyncMutex *sync.Mutex
|
||||
LocalCommitsMutex *sync.Mutex
|
||||
LineByLinePanelMutex *sync.Mutex
|
||||
SubprocessMutex *sync.Mutex
|
||||
PopupMutex *sync.Mutex
|
||||
}
|
||||
|
@ -409,6 +409,7 @@ type TranslationSet struct {
|
||||
NoFilesStagedPrompt string
|
||||
BranchNotFoundTitle string
|
||||
BranchNotFoundPrompt string
|
||||
LcBranchUnknown string
|
||||
UnstageLinesTitle string
|
||||
UnstageLinesPrompt string
|
||||
LcCreateNewBranchFromCommit string
|
||||
@ -501,6 +502,7 @@ type TranslationSet struct {
|
||||
UpstreamGone string
|
||||
NukeDescription string
|
||||
DiscardStagedChangesDescription string
|
||||
EmptyOutput string
|
||||
Actions Actions
|
||||
Bisect Bisect
|
||||
}
|
||||
@ -1045,6 +1047,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
NoFilesStagedPrompt: "You have not staged any files. Commit all files?",
|
||||
BranchNotFoundTitle: "Branch not found",
|
||||
BranchNotFoundPrompt: "Branch not found. Create a new branch named",
|
||||
LcBranchUnknown: "branch unknown",
|
||||
UnstageLinesTitle: "Unstage lines",
|
||||
UnstageLinesPrompt: "Are you sure you want to delete the selected lines (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipUnstageLineWarning' to true",
|
||||
LcCreateNewBranchFromCommit: "create new branch off of commit",
|
||||
@ -1136,6 +1139,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
UpstreamGone: "(upstream gone)",
|
||||
NukeDescription: "If you want to make all the changes in the worktree go away, this is the way to do it. If there are dirty submodule changes this will stash those changes in the submodule(s).",
|
||||
DiscardStagedChangesDescription: "This will create a new stash entry containing only staged files and then drop it, so that the working tree is left with only unstaged changes",
|
||||
EmptyOutput: "<empty output>",
|
||||
Actions: Actions{
|
||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||
CheckoutCommit: "Checkout commit",
|
||||
|
8
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
8
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
@ -1158,9 +1158,11 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
if len(v.Tabs) > 0 {
|
||||
tabIndex := v.GetClickedTabIndex(mx - v.x0)
|
||||
|
||||
for _, binding := range g.tabClickBindings {
|
||||
if binding.viewName == v.Name() {
|
||||
return binding.handler(tabIndex)
|
||||
if tabIndex >= 0 {
|
||||
for _, binding := range g.tabClickBindings {
|
||||
if binding.viewName == v.Name() {
|
||||
return binding.handler(tabIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
15
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
@ -1214,15 +1214,22 @@ func (v *View) GetClickedTabIndex(x int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
charIndex := 0
|
||||
charX := 1
|
||||
if x <= charX {
|
||||
return -1
|
||||
}
|
||||
for i, tab := range v.Tabs {
|
||||
charIndex += len(tab + " - ")
|
||||
if x < charIndex {
|
||||
charX += runewidth.StringWidth(tab)
|
||||
if x <= charX {
|
||||
return i
|
||||
}
|
||||
charX += runewidth.StringWidth(" - ")
|
||||
if x <= charX {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
return -1
|
||||
}
|
||||
|
||||
func (v *View) SelectedLineIdx() int {
|
||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20220723050330-1f853fadb335
|
||||
## explicit; go 1.12
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
|
Loading…
x
Reference in New Issue
Block a user