1
0
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:
nullishamy 2022-08-01 03:14:49 +01:00 committed by GitHub
commit 9987e65c35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 928 additions and 805 deletions

View File

@ -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.

File diff suppressed because one or more lines are too long

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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) {

View File

@ -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)
}

View 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,
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -62,6 +62,7 @@ func (gui *Gui) resetControllers() {
model,
gui.State.Contexts,
gui.State.Modes,
&gui.Mutexes,
)
syncController := controllers.NewSyncController(

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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(),
)

View File

@ -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 {

View File

@ -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",

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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",

View File

@ -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 {

View File

@ -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
View 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()
}

View File

@ -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{},

View File

@ -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))...)
}
}

View File

@ -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

View File

@ -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, "/")
}

View File

@ -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),
}
}

View File

@ -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)

View File

@ -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{})
})
}

View File

@ -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
}

View File

@ -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",

View File

@ -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)
}
}
}
}

View File

@ -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
View File

@ -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