From e1f41b653c8a84b551500b5df472de46c87379cb Mon Sep 17 00:00:00 2001
From: sportshead <32637656+sportshead@users.noreply.github.com>
Date: Thu, 28 Jul 2022 18:35:58 +0800
Subject: [PATCH 01/23] Add showOutput option to custom commands (#1163)
---
pkg/config/user_config.go | 1 +
pkg/gui/services/custom_commands/handler_creator.go | 5 ++++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 59bfcc042..804a5bee0 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -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 {
diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go
index 3dd9a0517..b5c2b9efd 100644
--- a/pkg/gui/services/custom_commands/handler_creator.go
+++ b/pkg/gui/services/custom_commands/handler_creator.go
@@ -187,10 +187,13 @@ 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 {
+ return self.c.Alert(cmdStr, output)
+ }
return self.c.Refresh(types.RefreshOptions{})
})
}
From 168fbe0a6c25390a6982536254cfb0ff68a06b4a Mon Sep 17 00:00:00 2001
From: sportshead <32637656+sportshead@users.noreply.github.com>
Date: Thu, 28 Jul 2022 18:57:16 +0800
Subject: [PATCH 02/23] Add showOutput option to docs
---
docs/Custom_Command_Keybindings.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md
index 47b9d8f97..ac61c1282 100644
--- a/docs/Custom_Command_Keybindings.md
+++ b/docs/Custom_Command_Keybindings.md
@@ -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
@@ -159,7 +160,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
From 017d44ff171889359375326c938c870f2e2f83e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Thu, 28 Jul 2022 21:17:15 +0200
Subject: [PATCH 03/23] Update bug_report.md
Add the `please try updating` tidbit to the bug report because that's one of the most common answers (and it cuts down debugging time)
---
.github/ISSUE_TEMPLATE/bug_report.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 845b8ab35..cf0d253c7 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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 the latest `master` to see if the issue still occurs.
From 83dfc3b28b0ee3a38859ef461b0b7e5d19dcb000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Fri, 29 Jul 2022 06:53:49 +0200
Subject: [PATCH 04/23] Update gocui
---
go.mod | 2 +-
go.sum | 4 ++--
vendor/github.com/jesseduffield/gocui/gui.go | 8 +++++---
vendor/github.com/jesseduffield/gocui/view.go | 15 +++++++++++----
vendor/modules.txt | 2 +-
5 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/go.mod b/go.mod
index 10de81c9d..04613ee04 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index 99cb31fd9..a034850f2 100644
--- a/go.sum
+++ b/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=
diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go
index 17150c477..563d18dcc 100644
--- a/vendor/github.com/jesseduffield/gocui/gui.go
+++ b/vendor/github.com/jesseduffield/gocui/gui.go
@@ -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)
+ }
}
}
}
diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go
index 8051cce72..5735c2fc4 100644
--- a/vendor/github.com/jesseduffield/gocui/view.go
+++ b/vendor/github.com/jesseduffield/gocui/view.go
@@ -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 {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 07d49b414..96043b723 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -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
From 1f482e585e6eccc4c56d7bbaf34cdaeee62ba8a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sat, 30 Jul 2022 08:10:29 +0200
Subject: [PATCH 05/23] Fix github linter errors
---
pkg/gui/confirmation_panel.go | 38 +++++++++----------
pkg/gui/context.go | 4 +-
.../controllers/helpers/upstream_helper.go | 8 ++--
pkg/gui/presentation/reflog_commits.go | 31 ++++++++++-----
pkg/gui/types/common.go | 4 ++
5 files changed, 49 insertions(+), 36 deletions(-)
diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go
index 5d8a13bb7..19f8745a8 100644
--- a/pkg/gui/confirmation_panel.go
+++ b/pkg/gui/confirmation_panel.go
@@ -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
}
diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index b83aa71c9..d2a99d84b 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -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
}
diff --git a/pkg/gui/controllers/helpers/upstream_helper.go b/pkg/gui/controllers/helpers/upstream_helper.go
index a3ece704e..a2d8e8ae2 100644
--- a/pkg/gui/controllers/helpers/upstream_helper.go
+++ b/pkg/gui/controllers/helpers/upstream_helper.go
@@ -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 {
diff --git a/pkg/gui/presentation/reflog_commits.go b/pkg/gui/presentation/reflog_commits.go
index 15cd2cb74..5a49f0374 100644
--- a/pkg/gui/presentation/reflog_commits.go
+++ b/pkg/gui/presentation/reflog_commits.go
@@ -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),
}
}
diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go
index 9272317f9..72bba0b6c 100644
--- a/pkg/gui/types/common.go
+++ b/pkg/gui/types/common.go
@@ -96,6 +96,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 {
From 61e14c46b2a8e5852efef5706900eaddbf642fd7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sat, 30 Jul 2022 08:25:40 +0200
Subject: [PATCH 06/23] Add SelectedPath to the list of placeholder values
---
docs/Custom_Command_Keybindings.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md
index 47b9d8f97..50f244c0c 100644
--- a/docs/Custom_Command_Keybindings.md
+++ b/docs/Custom_Command_Keybindings.md
@@ -142,6 +142,7 @@ SelectedLocalCommit
SelectedReflogCommit
SelectedSubCommit
SelectedFile
+SelectedPath
SelectedLocalBranch
SelectedRemoteBranch
SelectedRemote
From 1f97ddd9b1d6e3ab18fea4803b8659c000abd5d5 Mon Sep 17 00:00:00 2001
From: README-bot
Date: Sat, 30 Jul 2022 06:38:57 +0000
Subject: [PATCH 07/23] Updated README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index bde1cec41..70e8de392 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ A simple terminal UI for git commands, written in Go with the [gocui](https://gi
-















































+
















































## Elevator Pitch
From cc377b5c498260ed9f0444c7a7d28c35f884d1d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Fri, 29 Jul 2022 07:50:01 +0200
Subject: [PATCH 08/23] Fix rewording a merge commit with editor
---
pkg/gui/controllers/local_commits_controller.go | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go
index 1d1420a97..6539a13af 100644
--- a/pkg/gui/controllers/local_commits_controller.go
+++ b/pkg/gui/controllers/local_commits_controller.go
@@ -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(),
)
From 085904d209c0c5d28425e498625ce59edc60aaeb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sat, 30 Jul 2022 08:46:53 +0200
Subject: [PATCH 09/23] Add link to the installation instructions
---
.github/ISSUE_TEMPLATE/bug_report.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index cf0d253c7..8956e8abc 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -31,4 +31,4 @@ 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 the latest `master` to see if the issue still occurs.
+**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.
From ab5a8091f5d8768406f32260711131ccbcedb195 Mon Sep 17 00:00:00 2001
From: Jesse Duffield
Date: Sat, 30 Jul 2022 20:27:51 +1000
Subject: [PATCH 10/23] add better support for OpensMenu option when creating a
menu
---
pkg/gui/context/menu_context.go | 15 +--------------
pkg/gui/menu_panel.go | 23 ++++++++++++++++++++---
pkg/gui/options_menu_panel.go | 12 ++----------
3 files changed, 23 insertions(+), 27 deletions(-)
diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go
index b705adcee..489f412f0 100644
--- a/pkg/gui/context/menu_context.go
+++ b/pkg/gui/context/menu_context.go
@@ -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 {
diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go
index 59dd6b3a5..34dd1c615 100644
--- a/pkg/gui/menu_panel.go
+++ b/pkg/gui/menu_panel.go
@@ -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))...)
}
}
diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go
index aee88d866..ee07f2f08 100644
--- a/pkg/gui/options_menu_panel.go
+++ b/pkg/gui/options_menu_panel.go
@@ -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
From 9c6239df3dda06b94ba70787871ca074f651745f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Fri, 17 Jun 2022 21:20:37 +0200
Subject: [PATCH 11/23] Worktrees should not be filtered out
But non-git directories should
---
pkg/app/app.go | 2 +-
pkg/gui/recent_repos_panel.go | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/app/app.go b/pkg/app/app.go
index 9a7b1ffcc..418c1406e 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -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 (app *App) setupRepo() (bool, error) {
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index 605ae79d3..71354aac1 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -110,7 +110,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)
From e6e4513f45689f907fc8143b0ac5183ae1032bd3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Fri, 17 Jun 2022 21:10:19 +0200
Subject: [PATCH 12/23] Show active branch for recent repo
Split recent repo menu into three columns
---
pkg/gui/recent_repos_panel.go | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index 71354aac1..94161030d 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -1,24 +1,42 @@
package gui
import (
+ "fmt"
"os"
"path/filepath"
+ "strings"
"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"
)
-func (gui *Gui) handleCreateRecentReposMenu() error {
- recentRepoPaths := gui.c.GetAppState().RecentRepos
+func (gui *Gui) getCurrentBranch(path string) string {
+ if branch, err := gui.os.Cmd.New(
+ fmt.Sprintf("git -C %s rev-parse --abbrev-ref HEAD", gui.os.Quote(path)),
+ ).DontLog().RunWithOutput(); err == nil {
+ return strings.Trim(branch, "\n")
+ }
+ return ""
+}
+
+func (gui *Gui) handleCreateRecentReposMenu() error {
+ // we skip the first one because we're currently in it
+ recentRepoPaths := gui.c.GetAppState().RecentRepos[1:]
+
+ menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem {
+ branchName, _ := currentBranches.Load(path)
+ if icons.IsIconEnabled() {
+ branchName = icons.BRANCH_ICON + " " + 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 {
From 7c09ce38719ddf44e775783a7dfea0f3e6d4071c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Fri, 29 Jul 2022 22:53:05 +0200
Subject: [PATCH 13/23] Parallelize fetching current branch
---
pkg/gui/recent_repos_panel.go | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index 94161030d..636c01b1e 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
+ "sync"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands"
@@ -27,10 +28,24 @@ 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 + " " + branchName
+ branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName)
}
return &types.MenuItem{
From 25ddac0d8fc227836f67f9a9402a32456d3ab1ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sat, 30 Jul 2022 17:28:07 +0200
Subject: [PATCH 14/23] Gotta go fast
---
pkg/gui/recent_repos_panel.go | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index 636c01b1e..6d7eacb8b 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -2,6 +2,7 @@ package gui
import (
"fmt"
+ "io/ioutil"
"os"
"path/filepath"
"strings"
@@ -16,12 +17,14 @@ import (
)
func (gui *Gui) getCurrentBranch(path string) string {
- if branch, err := gui.os.Cmd.New(
- fmt.Sprintf("git -C %s rev-parse --abbrev-ref HEAD", gui.os.Quote(path)),
- ).DontLog().RunWithOutput(); err == nil {
- return strings.Trim(branch, "\n")
+ if headFile, err := ioutil.ReadFile(fmt.Sprintf("%s/.git/HEAD", path)); err == nil {
+ content := strings.TrimSpace(string(headFile))
+ branch := strings.TrimPrefix(content, "ref: refs/heads/")
+ return branch
}
- return ""
+ // worktrees don't have `.git/HEAD`
+ // and detached HEAD repos have only a hash in `.git/HEAD`
+ return "HEAD"
}
func (gui *Gui) handleCreateRecentReposMenu() error {
From 966733240c0a6b2ca2233aa260966d53915254e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sat, 30 Jul 2022 19:02:19 +0200
Subject: [PATCH 15/23] Refactor a bit, enable worktrees
---
pkg/gui/recent_repos_panel.go | 35 +++++++++++++++++++++++++++++------
1 file changed, 29 insertions(+), 6 deletions(-)
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index 6d7eacb8b..bfc363e76 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -17,13 +17,36 @@ import (
)
func (gui *Gui) getCurrentBranch(path string) string {
- if headFile, err := ioutil.ReadFile(fmt.Sprintf("%s/.git/HEAD", path)); err == nil {
- content := strings.TrimSpace(string(headFile))
- branch := strings.TrimPrefix(content, "ref: refs/heads/")
- return branch
+ readHeadFile := func(path string) (string, error) {
+ headFile, err := ioutil.ReadFile(fmt.Sprintf("%s%cHEAD", path, os.PathSeparator))
+ if err == nil {
+ content := strings.TrimSpace(string(headFile))
+ branch := strings.TrimPrefix(content, "ref: refs/heads/")
+ return branch, nil
+ }
+ return "", err
}
- // worktrees don't have `.git/HEAD`
- // and detached HEAD repos have only a hash in `.git/HEAD`
+
+ gitDirPath := fmt.Sprintf("%s%c.git", path, os.PathSeparator)
+
+ 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 "HEAD"
}
From f2880ecb463e9165977edcbee19e3013f56fc005 Mon Sep 17 00:00:00 2001
From: sportshead <32637656+sportshead@users.noreply.github.com>
Date: Fri, 29 Jul 2022 14:12:36 +0800
Subject: [PATCH 16/23] Add empty output message and refreshing to showOutput
---
pkg/gui/services/custom_commands/handler_creator.go | 11 ++++++++++-
pkg/i18n/english.go | 2 ++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go
index b5c2b9efd..6ac9fb733 100644
--- a/pkg/gui/services/custom_commands/handler_creator.go
+++ b/pkg/gui/services/custom_commands/handler_creator.go
@@ -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"
@@ -191,8 +193,15 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
if err != nil {
return self.c.Error(err)
}
+
if customCommand.ShowOutput {
- return self.c.Alert(cmdStr, output)
+ 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{})
})
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index af5a8a99b..be771d813 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -501,6 +501,7 @@ type TranslationSet struct {
UpstreamGone string
NukeDescription string
DiscardStagedChangesDescription string
+ EmptyOutput string
Actions Actions
Bisect Bisect
}
@@ -1136,6 +1137,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: "",
Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",
From 7077ea428f17a4a04de88884512022381bf4db7b Mon Sep 17 00:00:00 2001
From: Jesse Duffield
Date: Sun, 31 Jul 2022 13:52:56 +1000
Subject: [PATCH 17/23] add optimistic rendering for staging and unstaging
files
---
pkg/commands/loaders/files.go | 26 +---
pkg/commands/models/file.go | 46 ++++++
pkg/gui/controllers.go | 1 +
pkg/gui/controllers/common.go | 3 +
pkg/gui/controllers/files_controller.go | 183 ++++++++++++++++++++----
pkg/gui/filetree/file_tree.go | 5 +
pkg/gui/gui.go | 16 +--
pkg/gui/types/common.go | 14 ++
8 files changed, 232 insertions(+), 62 deletions(-)
diff --git a/pkg/commands/loaders/files.go b/pkg/commands/loaders/files.go
index 825cad595..db37da935 100644
--- a/pkg/commands/loaders/files.go
+++ b/pkg/commands/loaders/files.go
@@ -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)
}
diff --git a/pkg/commands/models/file.go b/pkg/commands/models/file.go
index bfb40ee53..88e5a14e1 100644
--- a/pkg/commands/models/file.go
+++ b/pkg/commands/models/file.go
@@ -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]
+ untracked := lo.Contains([]string{"??", "A ", "AM"}, shortStatus)
+ hasNoStagedChanges := 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: !hasNoStagedChanges,
+ HasUnstagedChanges: unstagedChange != " ",
+ Tracked: !untracked,
+ Deleted: unstagedChange == "D" || stagedChange == "D",
+ Added: unstagedChange == "A" || untracked,
+ HasMergeConflicts: hasMergeConflicts,
+ HasInlineMergeConflicts: hasInlineMergeConflicts,
+ ShortStatus: shortStatus,
+ }
+}
diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go
index 2c851739d..dcb3377cf 100644
--- a/pkg/gui/controllers.go
+++ b/pkg/gui/controllers.go
@@ -62,6 +62,7 @@ func (gui *Gui) resetControllers() {
model,
gui.State.Contexts,
gui.State.Modes,
+ &gui.Mutexes,
)
syncController := controllers.NewSyncController(
diff --git a/pkg/gui/controllers/common.go b/pkg/gui/controllers/common.go
index 55ba4b176..12a3788fd 100644
--- a/pkg/gui/controllers/common.go
+++ b/pkg/gui/controllers/common.go
@@ -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,
}
}
diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go
index 4aed5cf0a..220891a4d 100644
--- a/pkg/gui/controllers/files_controller.go
+++ b/pkg/gui/controllers/files_controller.go
@@ -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,7 +167,86 @@ func (self *FilesController) GetOnClick() func() error {
return self.checkSelectedFileNode(self.press)
}
-func (self *FilesController) press(node *filetree.FileNode) error {
+// 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.IsLeaf() {
file := node.File
@@ -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 {
diff --git a/pkg/gui/filetree/file_tree.go b/pkg/gui/filetree/file_tree.go
index d4bb8e596..17dafaa5d 100644
--- a/pkg/gui/filetree/file_tree.go
+++ b/pkg/gui/filetree/file_tree.go
@@ -41,6 +41,7 @@ type IFileTree interface {
GetAllItems() []*FileNode
GetAllFiles() []*models.File
GetFilter() FileTreeDisplayFilter
+ GetRoot() *FileNode
}
type FileTree struct {
@@ -159,6 +160,10 @@ func (self *FileTree) Tree() INode {
return self.tree
}
+func (self *FileTree) GetRoot() *FileNode {
+ return self.tree
+}
+
func (self *FileTree) CollapsedPaths() *CollapsedPaths {
return self.collapsedPaths
}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 531a43035..04b53e074 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -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{},
diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go
index 72bba0b6c..21808705a 100644
--- a/pkg/gui/types/common.go
+++ b/pkg/gui/types/common.go
@@ -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"
@@ -156,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
+}
From a9051650461b40948a5fb3739be4b6ade3de1b5c Mon Sep 17 00:00:00 2001
From: Jesse Duffield
Date: Sun, 31 Jul 2022 16:11:39 +1000
Subject: [PATCH 18/23] remove double negatives
---
pkg/commands/models/file.go | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/pkg/commands/models/file.go b/pkg/commands/models/file.go
index 88e5a14e1..4589f91fa 100644
--- a/pkg/commands/models/file.go
+++ b/pkg/commands/models/file.go
@@ -120,17 +120,17 @@ func SetStatusFields(file *File, shortStatus string) {
func deriveStatusFields(shortStatus string) StatusFields {
stagedChange := shortStatus[0:1]
unstagedChange := shortStatus[1:2]
- untracked := lo.Contains([]string{"??", "A ", "AM"}, shortStatus)
- hasNoStagedChanges := lo.Contains([]string{" ", "U", "?"}, stagedChange)
+ 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: !hasNoStagedChanges,
+ HasStagedChanges: hasStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
- Tracked: !untracked,
+ Tracked: tracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
- Added: unstagedChange == "A" || untracked,
+ Added: unstagedChange == "A" || !tracked,
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
ShortStatus: shortStatus,
From bb64fe43097b43081ab70b1dd32008ca529817a2 Mon Sep 17 00:00:00 2001
From: README-bot
Date: Sun, 31 Jul 2022 06:20:16 +0000
Subject: [PATCH 19/23] Updated README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 70e8de392..a7c3ec9e3 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ A simple terminal UI for git commands, written in Go with the [gocui](https://gi
-
















































+















































## Elevator Pitch
From 767ef3166106d32bb60bf580f553dfa220d12391 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sun, 31 Jul 2022 08:43:31 +0200
Subject: [PATCH 20/23] Use filepath.Join instead of manual concat
---
pkg/gui/recent_repos_panel.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index bfc363e76..fc2c38262 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -18,7 +18,7 @@ import (
func (gui *Gui) getCurrentBranch(path string) string {
readHeadFile := func(path string) (string, error) {
- headFile, err := ioutil.ReadFile(fmt.Sprintf("%s%cHEAD", path, os.PathSeparator))
+ headFile, err := ioutil.ReadFile(filepath.Join(path, "HEAD"))
if err == nil {
content := strings.TrimSpace(string(headFile))
branch := strings.TrimPrefix(content, "ref: refs/heads/")
@@ -27,7 +27,7 @@ func (gui *Gui) getCurrentBranch(path string) string {
return "", err
}
- gitDirPath := fmt.Sprintf("%s%c.git", path, os.PathSeparator)
+ gitDirPath := filepath.Join(path, ".git")
if gitDir, err := os.Stat(gitDirPath); err == nil {
if gitDir.IsDir() {
From 37bdbd9a21617be17a07e89f165492734f9dfa7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sun, 31 Jul 2022 08:44:42 +0200
Subject: [PATCH 21/23] Display short SHA when in `detached HEAD` state
---
pkg/gui/recent_repos_panel.go | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index fc2c38262..3ccf77bcd 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -14,6 +14,7 @@ import (
"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) getCurrentBranch(path string) string {
@@ -21,8 +22,16 @@ func (gui *Gui) getCurrentBranch(path string) string {
headFile, err := ioutil.ReadFile(filepath.Join(path, "HEAD"))
if err == nil {
content := strings.TrimSpace(string(headFile))
- branch := strings.TrimPrefix(content, "ref: refs/heads/")
- return branch, nil
+ 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
}
@@ -47,7 +56,7 @@ func (gui *Gui) getCurrentBranch(path string) string {
}
}
- return "HEAD"
+ return "unknown branch"
}
func (gui *Gui) handleCreateRecentReposMenu() error {
From 44de380c2b8d7633a3c9c078f032c5822e830598 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luka=20Marku=C5=A1i=C4=87?=
Date: Sun, 31 Jul 2022 08:57:57 +0200
Subject: [PATCH 22/23] Add i18n for unknown branch
---
pkg/gui/recent_repos_panel.go | 2 +-
pkg/i18n/english.go | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go
index 3ccf77bcd..3b817fbca 100644
--- a/pkg/gui/recent_repos_panel.go
+++ b/pkg/gui/recent_repos_panel.go
@@ -56,7 +56,7 @@ func (gui *Gui) getCurrentBranch(path string) string {
}
}
- return "unknown branch"
+ return gui.c.Tr.LcBranchUnknown
}
func (gui *Gui) handleCreateRecentReposMenu() error {
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index af5a8a99b..18d64016b 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -409,6 +409,7 @@ type TranslationSet struct {
NoFilesStagedPrompt string
BranchNotFoundTitle string
BranchNotFoundPrompt string
+ LcBranchUnknown string
UnstageLinesTitle string
UnstageLinesPrompt string
LcCreateNewBranchFromCommit string
@@ -1045,6 +1046,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",
From 682be18507ecf202116100547cb5049f471af664 Mon Sep 17 00:00:00 2001
From: Jesse Duffield
Date: Sun, 31 Jul 2022 19:10:16 +1000
Subject: [PATCH 23/23] refactor to use generics for file nodes
use less generic names
---
.../controllers/commits_files_controller.go | 5 +-
pkg/gui/controllers/files_controller.go | 2 +-
pkg/gui/filetree/build_tree.go | 24 +-
pkg/gui/filetree/build_tree_test.go | 92 +++---
pkg/gui/filetree/commit_file_node.go | 161 +---------
pkg/gui/filetree/commit_file_tree.go | 17 +-
pkg/gui/filetree/file_node.go | 218 ++-----------
pkg/gui/filetree/file_node_test.go | 34 +-
pkg/gui/filetree/file_tree.go | 22 +-
pkg/gui/filetree/inode.go | 206 ------------
pkg/gui/filetree/node.go | 301 ++++++++++++++++++
pkg/gui/presentation/files.go | 77 +++--
12 files changed, 500 insertions(+), 659 deletions(-)
delete mode 100644 pkg/gui/filetree/inode.go
create mode 100644 pkg/gui/filetree/node.go
diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go
index 20e78997e..5beecba02 100644
--- a/pkg/gui/controllers/commits_files_controller.go
+++ b/pkg/gui/controllers/commits_files_controller.go
@@ -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)
}
diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go
index 220891a4d..5304d0d81 100644
--- a/pkg/gui/controllers/files_controller.go
+++ b/pkg/gui/controllers/files_controller.go
@@ -247,7 +247,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
self.mutexes.RefreshingFilesMutex.Lock()
defer self.mutexes.RefreshingFilesMutex.Unlock()
- if node.IsLeaf() {
+ if node.IsFile() {
file := node.File
if file.HasInlineMergeConflicts {
diff --git a/pkg/gui/filetree/build_tree.go b/pkg/gui/filetree/build_tree.go
index 36034d02d..c7c465e28 100644
--- a/pkg/gui/filetree/build_tree.go
+++ b/pkg/gui/filetree/build_tree.go
@@ -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 {
diff --git a/pkg/gui/filetree/build_tree_test.go b/pkg/gui/filetree/build_tree_test.go
index c486ddfa5..ac36be9af 100644
--- a/pkg/gui/filetree/build_tree_test.go
+++ b/pkg/gui/filetree/build_tree_test.go
@@ -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",
diff --git a/pkg/gui/filetree/commit_file_node.go b/pkg/gui/filetree/commit_file_node.go
index ad794c0c2..067eee682 100644
--- a/pkg/gui/filetree/commit_file_node.go
+++ b/pkg/gui/filetree/commit_file_node.go
@@ -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
}
diff --git a/pkg/gui/filetree/commit_file_tree.go b/pkg/gui/filetree/commit_file_tree.go
index e539c9dea..862db26f1 100644
--- a/pkg/gui/filetree/commit_file_tree.go
+++ b/pkg/gui/filetree/commit_file_tree.go
@@ -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 {
diff --git a/pkg/gui/filetree/file_node.go b/pkg/gui/filetree/file_node.go
index 53f87c71b..092841b52 100644
--- a/pkg/gui/filetree/file_node.go
+++ b/pkg/gui/filetree/file_node.go
@@ -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
}
diff --git a/pkg/gui/filetree/file_node_test.go b/pkg/gui/filetree/file_node_test.go
index c7649bd16..a3b2b9aee 100644
--- a/pkg/gui/filetree/file_node_test.go
+++ b/pkg/gui/filetree/file_node_test.go
@@ -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",
diff --git a/pkg/gui/filetree/file_tree.go b/pkg/gui/filetree/file_tree.go
index 17dafaa5d..950bf24be 100644
--- a/pkg/gui/filetree/file_tree.go
+++ b/pkg/gui/filetree/file_tree.go
@@ -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)
@@ -46,13 +45,15 @@ type IFileTree interface {
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,
@@ -102,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 {
@@ -128,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 {
@@ -156,12 +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 self.tree
+ return NewFileNode(self.tree)
}
func (self *FileTree) CollapsedPaths() *CollapsedPaths {
diff --git a/pkg/gui/filetree/inode.go b/pkg/gui/filetree/inode.go
deleted file mode 100644
index d59315b28..000000000
--- a/pkg/gui/filetree/inode.go
+++ /dev/null
@@ -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)
- })
-}
diff --git a/pkg/gui/filetree/node.go b/pkg/gui/filetree/node.go
new file mode 100644
index 000000000..8de655b37
--- /dev/null
+++ b/pkg/gui/filetree/node.go
@@ -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()
+}
diff --git a/pkg/gui/presentation/files.go b/pkg/gui/presentation/files.go
index cb4d19cec..394b39f73 100644
--- a/pkg/gui/presentation/files.go
+++ b/pkg/gui/presentation/files.go
@@ -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, "/")
+}