From cfaf4b29d063d675ee32ce0690e357d6d587e199 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 6 Mar 2024 18:41:27 +0100 Subject: [PATCH 01/19] Don't strike out reserved keys in menus It seems to cause more confusion than it helps. --- pkg/gui/context/menu_context.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 7f8656fe7b9..2158d5c7ac6 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -92,21 +92,8 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { return displayStrings } - // These keys are used for general navigation so we'll strike them out to - // avoid confusion - reservedKeys := []string{ - self.c.UserConfig.Keybinding.Universal.Confirm, - self.c.UserConfig.Keybinding.Universal.Select, - self.c.UserConfig.Keybinding.Universal.Return, - self.c.UserConfig.Keybinding.Universal.StartSearch, - } keyLabel := keybindings.LabelFromKey(item.Key) - keyStyle := style.FgCyan - if lo.Contains(reservedKeys, keyLabel) { - keyStyle = style.FgDefault.SetStrikethrough() - } - - displayStrings = utils.Prepend(displayStrings, keyStyle.Sprint(keyLabel)) + displayStrings = utils.Prepend(displayStrings, style.FgCyan.Sprint(keyLabel)) return displayStrings }) } From 85a6a42bff05c38d0485ff40ce064145604c661b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 10 Feb 2024 19:42:30 +0100 Subject: [PATCH 02/19] Extend submodule tests to use a submodule where name and path are different In most real-world scenarios, name and path are usually the same. They don't have to be though, and it's important to make sure we use the right one when passing arguments to git commands, so change the tests to have different name and path. --- pkg/integration/components/git.go | 11 +++++++++++ pkg/integration/components/shell.go | 4 ++-- pkg/integration/tests/submodule/enter.go | 12 ++++++++---- pkg/integration/tests/submodule/remove.go | 12 ++++++------ pkg/integration/tests/submodule/reset.go | 22 ++++++++++++---------- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/pkg/integration/components/git.go b/pkg/integration/components/git.go index ed327b3ede2..1e2975be25a 100644 --- a/pkg/integration/components/git.go +++ b/pkg/integration/components/git.go @@ -2,7 +2,10 @@ package components import ( "fmt" + "log" "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" ) type Git struct { @@ -44,3 +47,11 @@ func (self *Git) expect(cmdArgs []string, condition func(string) (bool, string)) return self } + +func (self *Git) Version() *git_commands.GitVersion { + version, err := getGitVersion() + if err != nil { + log.Fatalf("Could not get git version: %v", err) + } + return version +} diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index 48ff3fdf734..60c62791826 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -345,9 +345,9 @@ func (self *Shell) CloneIntoRemote(name string) *Shell { return self } -func (self *Shell) CloneIntoSubmodule(submoduleName string) *Shell { +func (self *Shell) CloneIntoSubmodule(submoduleName string, submodulePath string) *Shell { self.Clone("other_repo") - self.RunCommand([]string{"git", "submodule", "add", "../other_repo", submoduleName}) + self.RunCommand([]string{"git", "submodule", "add", "--name", submoduleName, "../other_repo", submodulePath}) return self } diff --git a/pkg/integration/tests/submodule/enter.go b/pkg/integration/tests/submodule/enter.go index 7b055c2b66e..29e983b7f38 100644 --- a/pkg/integration/tests/submodule/enter.go +++ b/pkg/integration/tests/submodule/enter.go @@ -20,7 +20,7 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{ }, SetupRepo: func(shell *Shell) { shell.EmptyCommit("first commit") - shell.CloneIntoSubmodule("my_submodule") + shell.CloneIntoSubmodule("my_submodule_name", "my_submodule_path") shell.GitAddAll() shell.Commit("add submodule") }, @@ -29,14 +29,18 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Status().Content(Contains("repo")) } assertInSubmodule := func() { - t.Views().Status().Content(Contains("my_submodule")) + if t.Git().Version().IsAtLeast(2, 22, 0) { + t.Views().Status().Content(Contains("my_submodule_path(my_submodule_name)")) + } else { + t.Views().Status().Content(Contains("my_submodule_path")) + } } assertInParentRepo() t.Views().Submodules().Focus(). Lines( - Contains("my_submodule").IsSelected(), + Contains("my_submodule_name").IsSelected(), ). // enter the submodule PressEnter() @@ -60,7 +64,7 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Files().Focus(). Lines( - MatchesRegexp(` M.*my_submodule \(submodule\)`).IsSelected(), + MatchesRegexp(` M.*my_submodule_path \(submodule\)`).IsSelected(), ). Tap(func() { // main view also shows the new commit when we're looking at the submodule within the files view diff --git a/pkg/integration/tests/submodule/remove.go b/pkg/integration/tests/submodule/remove.go index 3d85d4d5c32..886eb62944a 100644 --- a/pkg/integration/tests/submodule/remove.go +++ b/pkg/integration/tests/submodule/remove.go @@ -12,20 +12,20 @@ var Remove = NewIntegrationTest(NewIntegrationTestArgs{ SetupConfig: func(config *config.AppConfig) {}, SetupRepo: func(shell *Shell) { shell.EmptyCommit("first commit") - shell.CloneIntoSubmodule("my_submodule") + shell.CloneIntoSubmodule("my_submodule_name", "my_submodule_path") shell.GitAddAll() shell.Commit("add submodule") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { t.Views().Submodules().Focus(). Lines( - Contains("my_submodule").IsSelected(), + Contains("my_submodule_name").IsSelected(), ). Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup().Confirmation(). Title(Equals("Remove submodule")). - Content(Equals("Are you sure you want to remove submodule 'my_submodule' and its corresponding directory? This is irreversible.")). + Content(Equals("Are you sure you want to remove submodule 'my_submodule_name' and its corresponding directory? This is irreversible.")). Confirm() }). IsEmpty() @@ -33,12 +33,12 @@ var Remove = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Files().Focus(). Lines( MatchesRegexp(`M.*\.gitmodules`).IsSelected(), - MatchesRegexp(`D.*my_submodule`), + MatchesRegexp(`D.*my_submodule_path`), ) t.Views().Main().Content( - Contains("-[submodule \"my_submodule\"]"). - Contains("- path = my_submodule"). + Contains("-[submodule \"my_submodule_name\"]"). + Contains("- path = my_submodule_path"). Contains("- url = ../other_repo"), ) }, diff --git a/pkg/integration/tests/submodule/reset.go b/pkg/integration/tests/submodule/reset.go index fba91bee84e..a723561fc9f 100644 --- a/pkg/integration/tests/submodule/reset.go +++ b/pkg/integration/tests/submodule/reset.go @@ -20,7 +20,7 @@ var Reset = NewIntegrationTest(NewIntegrationTestArgs{ }, SetupRepo: func(shell *Shell) { shell.EmptyCommit("first commit") - shell.CloneIntoSubmodule("my_submodule") + shell.CloneIntoSubmodule("my_submodule_name", "my_submodule_path") shell.GitAddAll() shell.Commit("add submodule") @@ -31,22 +31,24 @@ var Reset = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Status().Content(Contains("repo")) } assertInSubmodule := func() { - t.Views().Status().Content(Contains("my_submodule")) + if t.Git().Version().IsAtLeast(2, 22, 0) { + t.Views().Status().Content(Contains("my_submodule_path(my_submodule_name)")) + } else { + t.Views().Status().Content(Contains("my_submodule_path")) + } } assertInParentRepo() t.Views().Submodules().Focus(). Lines( - Contains("my_submodule").IsSelected(), + Contains("my_submodule_name").IsSelected(), ). // enter the submodule PressEnter() assertInSubmodule() - t.Views().Status().Content(Contains("my_submodule")) - t.Views().Files().IsFocused(). Press("e"). Tap(func() { @@ -65,18 +67,18 @@ var Reset = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Submodules().IsFocused() - t.Views().Main().Content(Contains("Submodule my_submodule contains modified content")) + t.Views().Main().Content(Contains("Submodule my_submodule_path contains modified content")) t.Views().Files().Focus(). Lines( - MatchesRegexp(` M.*my_submodule \(submodule\)`), + MatchesRegexp(` M.*my_submodule_path \(submodule\)`), Contains("other_file").IsSelected(), ). // Verify we can't use range select on submodules Press(keys.Universal.ToggleRangeSelect). SelectPreviousItem(). Lines( - MatchesRegexp(` M.*my_submodule \(submodule\)`).IsSelected(), + MatchesRegexp(` M.*my_submodule_path \(submodule\)`).IsSelected(), Contains("other_file").IsSelected(), ). Press(keys.Universal.Remove). @@ -85,13 +87,13 @@ var Reset = NewIntegrationTest(NewIntegrationTestArgs{ }). Press(keys.Universal.ToggleRangeSelect). Lines( - MatchesRegexp(` M.*my_submodule \(submodule\)`).IsSelected(), + MatchesRegexp(` M.*my_submodule_path \(submodule\)`).IsSelected(), Contains("other_file"), ). Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup().Menu(). - Title(Equals("my_submodule")). + Title(Equals("my_submodule_path")). Select(Contains("Stash uncommitted submodule changes and update")). Confirm() }). From ddcd9163018e264321046023d49ab176f71733c7 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 10 Feb 2024 19:44:43 +0100 Subject: [PATCH 03/19] Add test to check that the git dir for a deleted submodule was removed The test shows that it actually doesn't work when the submodule name is different from its path. We'll fix this in the next commit. --- pkg/integration/tests/submodule/remove.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/integration/tests/submodule/remove.go b/pkg/integration/tests/submodule/remove.go index 886eb62944a..f4f1cd04ffa 100644 --- a/pkg/integration/tests/submodule/remove.go +++ b/pkg/integration/tests/submodule/remove.go @@ -17,6 +17,9 @@ var Remove = NewIntegrationTest(NewIntegrationTestArgs{ shell.Commit("add submodule") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { + gitDirSubmodulePath := ".git/modules/my_submodule_name" + t.FileSystem().PathPresent(gitDirSubmodulePath) + t.Views().Submodules().Focus(). Lines( Contains("my_submodule_name").IsSelected(), @@ -41,5 +44,10 @@ var Remove = NewIntegrationTest(NewIntegrationTestArgs{ Contains("- path = my_submodule_path"). Contains("- url = ../other_repo"), ) + + /* EXPECTED: + t.FileSystem().PathNotPresent(gitDirSubmodulePath) + ACTUAL: */ + t.FileSystem().PathPresent(gitDirSubmodulePath) }, }) From ea87912a740464d1c69f4acad9c79efe6588d532 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 10 Feb 2024 19:47:36 +0100 Subject: [PATCH 04/19] Fix deleting submodule where name and path are different --- pkg/commands/git_commands/submodule.go | 2 +- pkg/integration/tests/submodule/remove.go | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/commands/git_commands/submodule.go b/pkg/commands/git_commands/submodule.go index d9d1ccd20b6..3f69ce87542 100644 --- a/pkg/commands/git_commands/submodule.go +++ b/pkg/commands/git_commands/submodule.go @@ -141,7 +141,7 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error { // We may in fact want to use the repo's git dir path but git docs say not to // mix submodules and worktrees anyway. - return os.RemoveAll(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "modules", submodule.Path)) + return os.RemoveAll(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "modules", submodule.Name)) } func (self *SubmoduleCommands) Add(name string, path string, url string) error { diff --git a/pkg/integration/tests/submodule/remove.go b/pkg/integration/tests/submodule/remove.go index f4f1cd04ffa..22fb83f3006 100644 --- a/pkg/integration/tests/submodule/remove.go +++ b/pkg/integration/tests/submodule/remove.go @@ -45,9 +45,6 @@ var Remove = NewIntegrationTest(NewIntegrationTestArgs{ Contains("- url = ../other_repo"), ) - /* EXPECTED: t.FileSystem().PathNotPresent(gitDirSubmodulePath) - ACTUAL: */ - t.FileSystem().PathPresent(gitDirSubmodulePath) }, }) From db4f12929ec242182e482ee5ab829d463cda6ea5 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 10 Feb 2024 18:07:42 +0100 Subject: [PATCH 05/19] Pass entire submodule to UpdateUrl instead of name and path separately This will make the next commit slightly simpler. --- pkg/commands/git_commands/submodule.go | 6 +++--- pkg/gui/controllers/submodules_controller.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/commands/git_commands/submodule.go b/pkg/commands/git_commands/submodule.go index 3f69ce87542..b105d57ea4f 100644 --- a/pkg/commands/git_commands/submodule.go +++ b/pkg/commands/git_commands/submodule.go @@ -158,10 +158,10 @@ func (self *SubmoduleCommands) Add(name string, path string, url string) error { return self.cmd.New(cmdArgs).Run() } -func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error { +func (self *SubmoduleCommands) UpdateUrl(submodule *models.SubmoduleConfig, newUrl string) error { setUrlCmdStr := NewGitCmd("config"). Arg( - "--file", ".gitmodules", "submodule."+name+".url", newUrl, + "--file", ".gitmodules", "submodule."+submodule.Name+".url", newUrl, ). ToArgv() @@ -170,7 +170,7 @@ func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string return err } - syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", path). + syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", submodule.Path). ToArgv() if err := self.cmd.New(syncCmdStr).Run(); err != nil { diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index 3cf3b5bf537..13496ce73c9 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -183,7 +183,7 @@ func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) err HandleConfirm: func(newUrl string) error { return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleUrlStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.UpdateSubmoduleUrl) - err := self.c.Git().Submodule.UpdateUrl(submodule.Name, submodule.Path, newUrl) + err := self.c.Git().Submodule.UpdateUrl(submodule, newUrl) if err != nil { _ = self.c.Error(err) } From 3b723282cbe98063523e22f9dd71000d20dc5e20 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 4 Feb 2024 22:42:58 +0100 Subject: [PATCH 06/19] Show all submodules recursively --- .../git_commands/git_command_builder.go | 8 +++ pkg/commands/git_commands/submodule.go | 65 +++++++++++++++---- pkg/commands/git_commands/working_tree.go | 2 +- pkg/commands/models/submodule_config.go | 31 ++++++++- pkg/gui/context/submodules_context.go | 2 +- pkg/gui/controllers/helpers/refresh_helper.go | 2 +- pkg/gui/controllers/helpers/repos_helper.go | 2 +- pkg/gui/controllers/submodules_controller.go | 8 +-- pkg/gui/presentation/submodules.go | 12 +++- .../tests/submodule/enter_nested.go | 52 +++++++++++++++ .../tests/submodule/remove_nested.go | 56 ++++++++++++++++ pkg/integration/tests/submodule/shared.go | 39 +++++++++++ pkg/integration/tests/test_list.go | 2 + 13 files changed, 260 insertions(+), 21 deletions(-) create mode 100644 pkg/integration/tests/submodule/enter_nested.go create mode 100644 pkg/integration/tests/submodule/remove_nested.go create mode 100644 pkg/integration/tests/submodule/shared.go diff --git a/pkg/commands/git_commands/git_command_builder.go b/pkg/commands/git_commands/git_command_builder.go index 4aa35be5f79..b6fe573641c 100644 --- a/pkg/commands/git_commands/git_command_builder.go +++ b/pkg/commands/git_commands/git_command_builder.go @@ -60,6 +60,14 @@ func (self *GitCommandBuilder) Dir(path string) *GitCommandBuilder { return self } +func (self *GitCommandBuilder) DirIf(condition bool, path string) *GitCommandBuilder { + if condition { + return self.Dir(path) + } + + return self +} + // Note, you may prefer to use the Dir method instead of this one func (self *GitCommandBuilder) Worktree(path string) *GitCommandBuilder { // worktree arg comes before the command diff --git a/pkg/commands/git_commands/submodule.go b/pkg/commands/git_commands/submodule.go index b105d57ea4f..40a0d35098f 100644 --- a/pkg/commands/git_commands/submodule.go +++ b/pkg/commands/git_commands/submodule.go @@ -26,8 +26,12 @@ func NewSubmoduleCommands(gitCommon *GitCommon) *SubmoduleCommands { } } -func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) { - file, err := os.Open(".gitmodules") +func (self *SubmoduleCommands) GetConfigs(parentModule *models.SubmoduleConfig) ([]*models.SubmoduleConfig, error) { + gitModulesPath := ".gitmodules" + if parentModule != nil { + gitModulesPath = filepath.Join(parentModule.FullPath(), gitModulesPath) + } + file, err := os.Open(gitModulesPath) if err != nil { if os.IsNotExist(err) { return nil, nil @@ -51,21 +55,27 @@ func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) { } configs := []*models.SubmoduleConfig{} + lastConfigIdx := -1 for scanner.Scan() { line := scanner.Text() if name, ok := firstMatch(line, `\[submodule "(.*)"\]`); ok { - configs = append(configs, &models.SubmoduleConfig{Name: name}) + configs = append(configs, &models.SubmoduleConfig{ + Name: name, ParentModule: parentModule, + }) + lastConfigIdx = len(configs) - 1 continue } - if len(configs) > 0 { - lastConfig := configs[len(configs)-1] - + if lastConfigIdx != -1 { if path, ok := firstMatch(line, `\s*path\s*=\s*(.*)\s*`); ok { - lastConfig.Path = path + configs[lastConfigIdx].Path = path + nestedConfigs, err := self.GetConfigs(configs[lastConfigIdx]) + if err == nil { + configs = append(configs, nestedConfigs...) + } } else if url, ok := firstMatch(line, `\s*url\s*=\s*(.*)\s*`); ok { - lastConfig.Url = url + configs[lastConfigIdx].Url = url } } } @@ -77,12 +87,12 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error { // if the path does not exist then it hasn't yet been initialized so we'll swallow the error // because the intention here is to have no dirty worktree state if _, err := os.Stat(submodule.Path); os.IsNotExist(err) { - self.Log.Infof("submodule path %s does not exist, returning", submodule.Path) + self.Log.Infof("submodule path %s does not exist, returning", submodule.FullPath()) return nil } cmdArgs := NewGitCmd("stash"). - Dir(submodule.Path). + Dir(submodule.FullPath()). Arg("--include-untracked"). ToArgv() @@ -90,8 +100,13 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error { } func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error { + parentDir := "" + if submodule.ParentModule != nil { + parentDir = submodule.ParentModule.FullPath() + } cmdArgs := NewGitCmd("submodule"). Arg("update", "--init", "--force", "--", submodule.Path). + DirIf(parentDir != "", parentDir). ToArgv() return self.cmd.New(cmdArgs).Run() @@ -107,6 +122,20 @@ func (self *SubmoduleCommands) UpdateAll() error { func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error { // based on https://gist.github.com/myusuf3/7f645819ded92bda6677 + if submodule.ParentModule != nil { + wd, err := os.Getwd() + if err != nil { + return err + } + + err = os.Chdir(submodule.ParentModule.FullPath()) + if err != nil { + return err + } + + defer func() { _ = os.Chdir(wd) }() + } + if err := self.cmd.New( NewGitCmd("submodule"). Arg("deinit", "--force", "--", submodule.Path).ToArgv(), @@ -141,7 +170,7 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error { // We may in fact want to use the repo's git dir path but git docs say not to // mix submodules and worktrees anyway. - return os.RemoveAll(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "modules", submodule.Name)) + return os.RemoveAll(submodule.GitDirPath(self.repoPaths.repoGitDirPath)) } func (self *SubmoduleCommands) Add(name string, path string, url string) error { @@ -159,6 +188,20 @@ func (self *SubmoduleCommands) Add(name string, path string, url string) error { } func (self *SubmoduleCommands) UpdateUrl(submodule *models.SubmoduleConfig, newUrl string) error { + if submodule.ParentModule != nil { + wd, err := os.Getwd() + if err != nil { + return err + } + + err = os.Chdir(submodule.ParentModule.FullPath()) + if err != nil { + return err + } + + defer func() { _ = os.Chdir(wd) }() + } + setUrlCmdStr := NewGitCmd("config"). Arg( "--file", ".gitmodules", "submodule."+submodule.Name+".url", newUrl, diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go index 2bb82578d52..99665d7cca2 100644 --- a/pkg/commands/git_commands/working_tree.go +++ b/pkg/commands/git_commands/working_tree.go @@ -343,7 +343,7 @@ func (self *WorkingTreeCommands) RemoveUntrackedFiles() error { // ResetAndClean removes all unstaged changes and removes all untracked files func (self *WorkingTreeCommands) ResetAndClean() error { - submoduleConfigs, err := self.submodule.GetConfigs() + submoduleConfigs, err := self.submodule.GetConfigs(nil) if err != nil { return err } diff --git a/pkg/commands/models/submodule_config.go b/pkg/commands/models/submodule_config.go index f525769217d..7df0d131abf 100644 --- a/pkg/commands/models/submodule_config.go +++ b/pkg/commands/models/submodule_config.go @@ -1,15 +1,35 @@ package models +import "path/filepath" + type SubmoduleConfig struct { Name string Path string Url string + + ParentModule *SubmoduleConfig // nil if top-level } -func (r *SubmoduleConfig) RefName() string { +func (r *SubmoduleConfig) FullName() string { + if r.ParentModule != nil { + return r.ParentModule.FullName() + "/" + r.Name + } + return r.Name } +func (r *SubmoduleConfig) FullPath() string { + if r.ParentModule != nil { + return r.ParentModule.FullPath() + "/" + r.Path + } + + return r.Path +} + +func (r *SubmoduleConfig) RefName() string { + return r.FullName() +} + func (r *SubmoduleConfig) ID() string { return r.RefName() } @@ -17,3 +37,12 @@ func (r *SubmoduleConfig) ID() string { func (r *SubmoduleConfig) Description() string { return r.RefName() } + +func (r *SubmoduleConfig) GitDirPath(repoGitDirPath string) string { + parentPath := repoGitDirPath + if r.ParentModule != nil { + parentPath = r.ParentModule.GitDirPath(repoGitDirPath) + } + + return filepath.Join(parentPath, "modules", r.Name) +} diff --git a/pkg/gui/context/submodules_context.go b/pkg/gui/context/submodules_context.go index aff8f64ab19..dbd12077a27 100644 --- a/pkg/gui/context/submodules_context.go +++ b/pkg/gui/context/submodules_context.go @@ -17,7 +17,7 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext { viewModel := NewFilteredListViewModel( func() []*models.SubmoduleConfig { return c.Model().Submodules }, func(submodule *models.SubmoduleConfig) []string { - return []string{submodule.Name} + return []string{submodule.FullName()} }, nil, ) diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index da43c47bb68..fd0d11881d6 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -415,7 +415,7 @@ func (self *RefreshHelper) refreshTags() error { } func (self *RefreshHelper) refreshStateSubmoduleConfigs() error { - configs, err := self.c.Git().Submodule.GetConfigs() + configs, err := self.c.Git().Submodule.GetConfigs(nil) if err != nil { return err } diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go index 59d45e0c195..c4a00cb7334 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -48,7 +48,7 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error } self.c.State().GetRepoPathStack().Push(wd) - return self.DispatchSwitchToRepo(submodule.Path, context.NO_CONTEXT) + return self.DispatchSwitchToRepo(submodule.FullPath(), context.NO_CONTEXT) } func (self *ReposHelper) getCurrentBranch(path string) string { diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index 13496ce73c9..dde1a1f46d2 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -116,8 +116,8 @@ func (self *SubmodulesController) GetOnRenderToMain() func() error { } else { prefix := fmt.Sprintf( "Name: %s\nPath: %s\nUrl: %s\n\n", - style.FgGreen.Sprint(submodule.Name), - style.FgYellow.Sprint(submodule.Path), + style.FgGreen.Sprint(submodule.FullName()), + style.FgYellow.Sprint(submodule.FullPath()), style.FgCyan.Sprint(submodule.Url), ) @@ -178,7 +178,7 @@ func (self *SubmodulesController) add() error { func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) error { return self.c.Prompt(types.PromptOpts{ - Title: fmt.Sprintf(self.c.Tr.UpdateSubmoduleUrl, submodule.Name), + Title: fmt.Sprintf(self.c.Tr.UpdateSubmoduleUrl, submodule.FullName()), InitialContent: submodule.Url, HandleConfirm: func(newUrl string) error { return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleUrlStatus, func(gocui.Task) error { @@ -272,7 +272,7 @@ func (self *SubmodulesController) update(submodule *models.SubmoduleConfig) erro func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) error { return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.RemoveSubmodule, - Prompt: fmt.Sprintf(self.c.Tr.RemoveSubmodulePrompt, submodule.Name), + Prompt: fmt.Sprintf(self.c.Tr.RemoveSubmodulePrompt, submodule.FullName()), HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.RemoveSubmodule) if err := self.c.Git().Submodule.Delete(submodule); err != nil { diff --git a/pkg/gui/presentation/submodules.go b/pkg/gui/presentation/submodules.go index e580ee1f6db..72c6bfc081a 100644 --- a/pkg/gui/presentation/submodules.go +++ b/pkg/gui/presentation/submodules.go @@ -13,5 +13,15 @@ func GetSubmoduleListDisplayStrings(submodules []*models.SubmoduleConfig) [][]st } func getSubmoduleDisplayStrings(s *models.SubmoduleConfig) []string { - return []string{theme.DefaultTextColor.Sprint(s.Name)} + name := s.Name + if s.ParentModule != nil { + indentation := "" + for p := s.ParentModule; p != nil; p = p.ParentModule { + indentation += " " + } + + name = indentation + "- " + s.Name + } + + return []string{theme.DefaultTextColor.Sprint(name)} } diff --git a/pkg/integration/tests/submodule/enter_nested.go b/pkg/integration/tests/submodule/enter_nested.go new file mode 100644 index 00000000000..172dfbfaeca --- /dev/null +++ b/pkg/integration/tests/submodule/enter_nested.go @@ -0,0 +1,52 @@ +package submodule + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var EnterNested = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Enter a nested submodule", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + setupNestedSubmodules(shell) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Submodules().Focus(). + Lines( + Equals("outerSubName").IsSelected(), + Equals(" - innerSubName"), + ). + Tap(func() { + t.Views().Main().ContainsLines( + Contains("Name: outerSubName"), + Contains("Path: modules/outerSubPath"), + Contains("Url: ../outerSubmodule"), + ) + }). + SelectNextItem(). + Tap(func() { + t.Views().Main().ContainsLines( + Contains("Name: outerSubName/innerSubName"), + Contains("Path: modules/outerSubPath/modules/innerSubPath"), + Contains("Url: ../innerSubmodule"), + ) + }). + // enter the nested submodule + PressEnter() + + if t.Git().Version().IsAtLeast(2, 22, 0) { + t.Views().Status().Content(Contains("innerSubPath(innerSubName)")) + } else { + t.Views().Status().Content(Contains("innerSubPath")) + } + t.Views().Commits().ContainsLines( + Contains("initial inner commit"), + ) + + t.Views().Files().PressEscape() + t.Views().Status().Content(Contains("repo")) + }, +}) diff --git a/pkg/integration/tests/submodule/remove_nested.go b/pkg/integration/tests/submodule/remove_nested.go new file mode 100644 index 00000000000..ae32c0907a1 --- /dev/null +++ b/pkg/integration/tests/submodule/remove_nested.go @@ -0,0 +1,56 @@ +package submodule + +import ( + "path/filepath" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var RemoveNested = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Remove a nested submodule", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + setupNestedSubmodules(shell) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + gitDirSubmodulePath, _ := filepath.Abs(".git/modules/outerSubName/modules/innerSubName") + t.FileSystem().PathPresent(gitDirSubmodulePath) + + t.Views().Submodules().Focus(). + Lines( + Equals("outerSubName").IsSelected(), + Equals(" - innerSubName"), + ). + SelectNextItem(). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Remove submodule")). + Content(Equals("Are you sure you want to remove submodule 'outerSubName/innerSubName' and its corresponding directory? This is irreversible.")). + Confirm() + }). + Lines( + Equals("outerSubName").IsSelected(), + ). + Press(keys.Universal.GoInto) + + t.Views().Files().IsFocused(). + Lines( + Contains("modules").IsSelected(), + MatchesRegexp(`D.*innerSubPath`), + MatchesRegexp(`M.*\.gitmodules`), + ). + NavigateToLine(Contains(".gitmodules")) + + t.Views().Main().Content( + Contains("-[submodule \"innerSubName\"]"). + Contains("- path = modules/innerSubPath"). + Contains("- url = ../innerSubmodule"), + ) + + t.FileSystem().PathNotPresent(gitDirSubmodulePath) + }, +}) diff --git a/pkg/integration/tests/submodule/shared.go b/pkg/integration/tests/submodule/shared.go new file mode 100644 index 00000000000..43e0144abbc --- /dev/null +++ b/pkg/integration/tests/submodule/shared.go @@ -0,0 +1,39 @@ +package submodule + +import ( + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +func setupNestedSubmodules(shell *Shell) { + // we're going to have a directory structure like this: + // project + // - repo/modules/outerSubName/modules/innerSubName/ + // + shell.CreateFileAndAdd("rootFile", "rootStuff") + shell.Commit("initial repo commit") + + shell.Chdir("..") + shell.CreateDir("innerSubmodule") + shell.Chdir("innerSubmodule") + shell.Init() + shell.CreateFileAndAdd("inner", "inner") + shell.Commit("initial inner commit") + + shell.Chdir("..") + shell.CreateDir("outerSubmodule") + shell.Chdir("outerSubmodule") + shell.Init() + shell.CreateFileAndAdd("outer", "outer") + shell.Commit("initial outer commit") + shell.CreateDir("modules") + // the git config (-c) parameter below is required + // to let git create a file-protocol/path submodule + shell.RunCommand([]string{"git", "-c", "protocol.file.allow=always", "submodule", "add", "--name", "innerSubName", "../innerSubmodule", "modules/innerSubPath"}) + shell.Commit("add dependency as innerSubmodule") + + shell.Chdir("../repo") + shell.CreateDir("modules") + shell.RunCommand([]string{"git", "-c", "protocol.file.allow=always", "submodule", "add", "--name", "outerSubName", "../outerSubmodule", "modules/outerSubPath"}) + shell.Commit("add dependency as outerSubmodule") + shell.RunCommand([]string{"git", "-c", "protocol.file.allow=always", "submodule", "update", "--init", "--recursive"}) +} diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index e26a0731f2b..531fce5d98d 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -245,7 +245,9 @@ var tests = []*components.IntegrationTest{ stash.StashUnstaged, submodule.Add, submodule.Enter, + submodule.EnterNested, submodule.Remove, + submodule.RemoveNested, submodule.Reset, sync.FetchPrune, sync.FetchWhenSortedByDate, From 40232440b7249b8e214265cbb13a9c84bf5e2d05 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 8 Mar 2024 21:19:10 +0100 Subject: [PATCH 07/19] Support setting a range of commits to "edit" outside of a rebase It starts a rebase on the bottom-most commit of the range, and sets all the selected commits to "edit" (skipping merge commits, because they can't be edited). --- .../controllers/local_commits_controller.go | 31 ++++++----- .../edit_range_select_outside_rebase.go | 51 +++++++++++++++++++ .../mid_rebase_range_select.go | 21 -------- pkg/integration/tests/test_list.go | 1 + 4 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 pkg/integration/tests/interactive_rebase/edit_range_select_outside_rebase.go diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 68a0ea742c8..d290192e45f 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -115,7 +115,6 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ { Key: opts.GetKey(editCommitKey), Handler: self.withItems(self.edit), - // TODO: have disabled reason ensure that if we're not rebasing, we only select one commit GetDisabledReason: self.require( self.itemRangeSelected(self.midRebaseCommandEnabled), ), @@ -457,15 +456,7 @@ func (self *LocalCommitsController) edit(selectedCommits []*models.Commit) error return self.updateTodos(todo.Edit, selectedCommits) } - // TODO: support range select here (start a rebase and set the selected commits - // to 'edit' in the todo file) - if len(selectedCommits) > 1 { - return self.c.ErrorMsg(self.c.Tr.RangeSelectNotSupported) - } - - selectedCommit := selectedCommits[0] - - return self.startInteractiveRebaseWithEdit(selectedCommit) + return self.startInteractiveRebaseWithEdit(selectedCommits) } func (self *LocalCommitsController) quickStartInteractiveRebase() error { @@ -474,11 +465,11 @@ func (self *LocalCommitsController) quickStartInteractiveRebase() error { return self.c.Error(err) } - return self.startInteractiveRebaseWithEdit(commitToEdit) + return self.startInteractiveRebaseWithEdit([]*models.Commit{commitToEdit}) } func (self *LocalCommitsController) startInteractiveRebaseWithEdit( - commitToEdit *models.Commit, + commitsToEdit []*models.Commit, ) error { return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.EditCommit) @@ -486,10 +477,24 @@ func (self *LocalCommitsController) startInteractiveRebaseWithEdit( commits := self.c.Model().Commits selectedSha := commits[selectedIdx].Sha rangeStartSha := commits[rangeStartIdx].Sha - err := self.c.Git().Rebase.EditRebase(commitToEdit.Sha) + err := self.c.Git().Rebase.EditRebase(commitsToEdit[len(commitsToEdit)-1].Sha) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( err, types.RefreshOptions{Mode: types.BLOCK_UI, Then: func() { + todos := make([]*models.Commit, 0, len(commitsToEdit)-1) + for _, c := range commitsToEdit[:len(commitsToEdit)-1] { + // Merge commits can't be set to "edit", so just skip them + if !c.IsMerge() { + todos = append(todos, &models.Commit{Sha: c.Sha, Action: todo.Pick}) + } + } + if len(todos) > 0 { + err := self.updateTodos(todo.Edit, todos) + if err != nil { + _ = self.c.Error(err) + } + } + // We need to select the same commit range again because after starting a rebase, // new lines can be added for update-ref commands in the TODO file, due to // stacked branches. So the selected commits may be in different positions in the list. diff --git a/pkg/integration/tests/interactive_rebase/edit_range_select_outside_rebase.go b/pkg/integration/tests/interactive_rebase/edit_range_select_outside_rebase.go new file mode 100644 index 00000000000..4ff2a8c831a --- /dev/null +++ b/pkg/integration/tests/interactive_rebase/edit_range_select_outside_rebase.go @@ -0,0 +1,51 @@ +package interactive_rebase + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var EditRangeSelectOutsideRebase = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Select a range of commits to edit outside of a rebase", + ExtraCmdArgs: []string{}, + Skip: false, + GitVersion: AtLeast("2.22.0"), // first version that supports the --rebase-merges option + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shared.CreateMergeCommit(shell) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + TopLines( + Contains("Merge branch 'second-change-branch' into first-change-branch").IsSelected(), + ). + Press(keys.Universal.RangeSelectDown). + Press(keys.Universal.RangeSelectDown). + Press(keys.Universal.RangeSelectDown). + Press(keys.Universal.RangeSelectDown). + Press(keys.Universal.RangeSelectDown). + Lines( + Contains("CI ⏣─╮ Merge branch 'second-change-branch' into first-change-branch").IsSelected(), + Contains("CI │ ◯ * second-change-branch unrelated change").IsSelected(), + Contains("CI │ ◯ second change").IsSelected(), + Contains("CI ◯ │ first change").IsSelected(), + Contains("CI ◯─╯ * original").IsSelected(), + Contains("CI ◯ three").IsSelected(), + Contains("CI ◯ two"), + Contains("CI ◯ one"), + ). + Press(keys.Universal.Edit). + Lines( + Contains("merge CI Merge branch 'second-change-branch' into first-change-branch").IsSelected(), + Contains("edit CI first change").IsSelected(), + Contains("edit CI * second-change-branch unrelated change").IsSelected(), + Contains("edit CI second change").IsSelected(), + Contains("edit CI * original").IsSelected(), + Contains(" CI ◯ <-- YOU ARE HERE --- three").IsSelected(), + Contains(" CI ◯ two"), + Contains(" CI ◯ one"), + ) + }, +}) diff --git a/pkg/integration/tests/interactive_rebase/mid_rebase_range_select.go b/pkg/integration/tests/interactive_rebase/mid_rebase_range_select.go index 6dcb12ea6bc..c94b6329573 100644 --- a/pkg/integration/tests/interactive_rebase/mid_rebase_range_select.go +++ b/pkg/integration/tests/interactive_rebase/mid_rebase_range_select.go @@ -20,27 +20,6 @@ var MidRebaseRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ TopLines( Contains("commit 10").IsSelected(), ). - NavigateToLine(Contains("commit 07")). - Press(keys.Universal.RangeSelectDown). - TopLines( - Contains("commit 10"), - Contains("commit 09"), - Contains("commit 08"), - Contains("commit 07").IsSelected(), - Contains("commit 06").IsSelected(), - Contains("commit 05"), - Contains("commit 04"), - ). - // Verify we can't perform an edit on multiple commits (it's not supported - // yet) - Press(keys.Universal.Edit). - Tap(func() { - // This ought to be a toast but I'm too lazy to implement that right now. - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Contains("Action does not support range selection, please select a single item")). - Confirm() - }). NavigateToLine(Contains("commit 05")). // Start a rebase Press(keys.Universal.Edit). diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 531fce5d98d..d0525fa596b 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -167,6 +167,7 @@ var tests = []*components.IntegrationTest{ interactive_rebase.DropWithCustomCommentChar, interactive_rebase.EditFirstCommit, interactive_rebase.EditNonTodoCommitDuringRebase, + interactive_rebase.EditRangeSelectOutsideRebase, interactive_rebase.EditTheConflCommit, interactive_rebase.FixupFirstCommit, interactive_rebase.FixupSecondCommit, From 314efe25394349c3aeb602b826bcbe9560599660 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 22 Aug 2023 16:59:09 +0200 Subject: [PATCH 08/19] Add test for creating a fixup commit and squashing fixups We have such a test already (squash_fixups_above_first_commit.go), but it can't be used for what we want to check here, because it uses the first commit, and we can't move down from there. So create a new one that basically does the same thing, but for a commit in the middle. The focus of this new test is to check how the selection behaves; as you can see, there is a problem both when creating a fixup and when squashing fixups. We'll address these separately in the next commits. --- .../interactive_rebase/squash_fixups_above.go | 58 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 2 files changed, 59 insertions(+) create mode 100644 pkg/integration/tests/interactive_rebase/squash_fixups_above.go diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_above.go b/pkg/integration/tests/interactive_rebase/squash_fixups_above.go new file mode 100644 index 00000000000..9af6594efeb --- /dev/null +++ b/pkg/integration/tests/interactive_rebase/squash_fixups_above.go @@ -0,0 +1,58 @@ +package interactive_rebase + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var SquashFixupsAbove = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Squashes all fixups above a commit and checks that the selected line stays correct.", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + CreateNCommits(3). + CreateFileAndAdd("fixup-file", "fixup content") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("commit 03"), + Contains("commit 02"), + Contains("commit 01"), + ). + NavigateToLine(Contains("commit 02")). + Press(keys.Commits.CreateFixupCommit). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Create fixup commit")). + Content(Contains("Are you sure you want to create a fixup! commit for commit")). + Confirm() + }). + Lines( + Contains("fixup! commit 02"), + Contains("commit 03").IsSelected(), // wrong, we want the next line + Contains("commit 02"), + Contains("commit 01"), + ). + SelectNextItem(). + Press(keys.Commits.SquashAboveCommits). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Apply fixup commits")). + Select(Contains("Above the selected commit")). + Confirm() + }). + Lines( + Contains("commit 03"), + Contains("commit 02"), + Contains("commit 01").IsSelected(), // wrong, we want the previous line + ). + SelectPreviousItem() + + t.Views().Main(). + Content(Contains("fixup content")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index d0525fa596b..402a40acf22 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -188,6 +188,7 @@ var tests = []*components.IntegrationTest{ interactive_rebase.RewordYouAreHereCommitWithEditor, interactive_rebase.SquashDownFirstCommit, interactive_rebase.SquashDownSecondCommit, + interactive_rebase.SquashFixupsAbove, interactive_rebase.SquashFixupsAboveFirstCommit, interactive_rebase.SquashFixupsInCurrentBranch, interactive_rebase.SwapInRebaseWithConflict, From dfb45ba893200f09cdeceef281de65169757059c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 31 Jan 2024 12:16:55 +0100 Subject: [PATCH 09/19] Extend squash_fixups_in_current_branch test to check the selection This shows a problem with the wrong commit being selected after squashing. --- .../interactive_rebase/squash_fixups_in_current_branch.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go b/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go index 63681053329..75bfbf159b3 100644 --- a/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go +++ b/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go @@ -27,10 +27,12 @@ var SquashFixupsInCurrentBranch = NewIntegrationTest(NewIntegrationTestArgs{ Run: func(t *TestDriver, keys config.KeybindingConfig) { t.Views().Commits(). Focus(). + SelectNextItem(). + SelectNextItem(). Lines( Contains("fixup! commit 01"), Contains("commit 02"), - Contains("commit 01"), + Contains("commit 01").IsSelected(), Contains("fixup! master commit"), Contains("master commit"), ). @@ -44,7 +46,7 @@ var SquashFixupsInCurrentBranch = NewIntegrationTest(NewIntegrationTestArgs{ Lines( Contains("commit 02"), Contains("commit 01"), - Contains("fixup! master commit"), + Contains("fixup! master commit").IsSelected(), // wrong, we want the previous line Contains("master commit"), ). NavigateToLine(Contains("commit 01")) From 3e3b902228189255d7411413f0ef7deef71ef6ac Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 22 Aug 2023 16:35:30 +0200 Subject: [PATCH 10/19] Move selection down by one after creating a fixup commit --- pkg/gui/controllers/local_commits_controller.go | 11 +++++++---- pkg/i18n/english.go | 2 ++ .../tests/interactive_rebase/squash_fixups_above.go | 5 ++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index d290192e45f..0c55ffa441c 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -811,11 +811,14 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err HandleConfirm: func() error { return self.c.Helpers().WorkingTree.WithEnsureCommitableFiles(func() error { self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit) - if err := self.c.Git().Commit.CreateFixupCommit(commit.Sha); err != nil { - return self.c.Error(err) - } + return self.c.WithWaitingStatusSync(self.c.Tr.CreatingFixupCommitStatus, func() error { + if err := self.c.Git().Commit.CreateFixupCommit(commit.Sha); err != nil { + return self.c.Error(err) + } - return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + self.context().MoveSelectedLine(1) + return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC}) + }) }) }, }) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index c050338345c..b1d0e01ebd2 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -341,6 +341,7 @@ type TranslationSet struct { CheckingOutStatus string CommittingStatus string RevertingStatus string + CreatingFixupCommitStatus string CommitFiles string SubCommitsDynamicTitle string CommitFilesDynamicTitle string @@ -1289,6 +1290,7 @@ func EnglishTranslationSet() TranslationSet { CheckingOutStatus: "Checking out", CommittingStatus: "Committing", RevertingStatus: "Reverting", + CreatingFixupCommitStatus: "Creating fixup commit", CommitFiles: "Commit files", SubCommitsDynamicTitle: "Commits (%s)", CommitFilesDynamicTitle: "Diff files (%s)", diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_above.go b/pkg/integration/tests/interactive_rebase/squash_fixups_above.go index 9af6594efeb..b412f93ed5e 100644 --- a/pkg/integration/tests/interactive_rebase/squash_fixups_above.go +++ b/pkg/integration/tests/interactive_rebase/squash_fixups_above.go @@ -33,11 +33,10 @@ var SquashFixupsAbove = NewIntegrationTest(NewIntegrationTestArgs{ }). Lines( Contains("fixup! commit 02"), - Contains("commit 03").IsSelected(), // wrong, we want the next line - Contains("commit 02"), + Contains("commit 03"), + Contains("commit 02").IsSelected(), Contains("commit 01"), ). - SelectNextItem(). Press(keys.Commits.SquashAboveCommits). Tap(func() { t.ExpectPopup().Menu(). From c6d20c876ecb11b2476eb9b173734f6af3aff06c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 31 Jan 2024 12:02:24 +0100 Subject: [PATCH 11/19] Extract common code to a helper method This should arguably have been done in b133318b40 already; it's becoming more important now because we're going to extend the common code with more logic in the next commit. --- pkg/gui/controllers/local_commits_controller.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 0c55ffa441c..e6b8bc80114 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -847,11 +847,7 @@ func (self *LocalCommitsController) squashFixupCommits() error { } func (self *LocalCommitsController) squashAllFixupsAboveSelectedCommit(commit *models.Commit) error { - return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error { - self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits) - err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit) - return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) - }) + return self.squashFixupsImpl(commit) } func (self *LocalCommitsController) squashAllFixupsInCurrentBranch() error { @@ -860,6 +856,10 @@ func (self *LocalCommitsController) squashAllFixupsInCurrentBranch() error { return self.c.Error(err) } + return self.squashFixupsImpl(commit) +} + +func (self *LocalCommitsController) squashFixupsImpl(commit *models.Commit) error { return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits) err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit) From bb26979420fa3338ebd89ac124322f611c26a9c6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 22 Aug 2023 15:47:32 +0200 Subject: [PATCH 12/19] Keep the same line selected after squashing fixup commits This uses a bit of a heuristic that is hopefully correct most of the time. --- .../controllers/local_commits_controller.go | 73 +++++++-- .../local_commits_controller_test.go | 141 ++++++++++++++++++ .../interactive_rebase/squash_fixups_above.go | 7 +- .../squash_fixups_in_current_branch.go | 7 +- 4 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 pkg/gui/controllers/local_commits_controller_test.go diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index e6b8bc80114..ef6b5be8023 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -2,6 +2,7 @@ package controllers import ( "fmt" + "strings" "github.com/fsmiamoto/git-todo-parser/todo" "github.com/go-errors/errors" @@ -847,37 +848,89 @@ func (self *LocalCommitsController) squashFixupCommits() error { } func (self *LocalCommitsController) squashAllFixupsAboveSelectedCommit(commit *models.Commit) error { - return self.squashFixupsImpl(commit) + return self.squashFixupsImpl(commit, self.context().GetSelectedLineIdx()) } func (self *LocalCommitsController) squashAllFixupsInCurrentBranch() error { - commit, err := self.findCommitForSquashFixupsInCurrentBranch() + commit, rebaseStartIdx, err := self.findCommitForSquashFixupsInCurrentBranch() if err != nil { return self.c.Error(err) } - return self.squashFixupsImpl(commit) + return self.squashFixupsImpl(commit, rebaseStartIdx) } -func (self *LocalCommitsController) squashFixupsImpl(commit *models.Commit) error { - return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error { +func (self *LocalCommitsController) squashFixupsImpl(commit *models.Commit, rebaseStartIdx int) error { + selectionOffset := countSquashableCommitsAbove(self.c.Model().Commits, self.context().GetSelectedLineIdx(), rebaseStartIdx) + return self.c.WithWaitingStatusSync(self.c.Tr.SquashingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits) err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit) - return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + self.context().MoveSelectedLine(-selectionOffset) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( + err, types.RefreshOptions{Mode: types.SYNC}) }) } -func (self *LocalCommitsController) findCommitForSquashFixupsInCurrentBranch() (*models.Commit, error) { +func (self *LocalCommitsController) findCommitForSquashFixupsInCurrentBranch() (*models.Commit, int, error) { commits := self.c.Model().Commits _, index, ok := lo.FindIndexOf(commits, func(c *models.Commit) bool { return c.IsMerge() || c.Status == models.StatusMerged }) if !ok || index == 0 { - return nil, errors.New(self.c.Tr.CannotSquashCommitsInCurrentBranch) + return nil, -1, errors.New(self.c.Tr.CannotSquashCommitsInCurrentBranch) + } + + return commits[index-1], index - 1, nil +} + +// Anticipate how many commits above the selectedIdx are going to get squashed +// by the SquashAllAboveFixupCommits call, so that we can adjust the selection +// afterwards. Let's hope we're matching git's behavior correctly here. +func countSquashableCommitsAbove(commits []*models.Commit, selectedIdx int, rebaseStartIdx int) int { + result := 0 + + // For each commit _above_ the selection, ... + for i, commit := range commits[0:selectedIdx] { + // ... see if it is a fixup commit, and get the base subject it applies to + if baseSubject, isFixup := isFixupCommit(commit.Name); isFixup { + // Then, for each commit after the fixup, up to and including the + // rebase start commit, see if we find the base commit + for _, baseCommit := range commits[i+1 : rebaseStartIdx+1] { + if strings.HasPrefix(baseCommit.Name, baseSubject) { + result++ + } + } + } + } + return result +} + +// Check whether the given subject line is the subject of a fixup commit, and +// returns (trimmedSubject, true) if so (where trimmedSubject is the subject +// with all fixup prefixes removed), or (subject, false) if not. +func isFixupCommit(subject string) (string, bool) { + prefixes := []string{"fixup! ", "squash! ", "amend! "} + trimPrefix := func(s string) (string, bool) { + for _, prefix := range prefixes { + if strings.HasPrefix(s, prefix) { + return strings.TrimPrefix(s, prefix), true + } + } + return s, false + } + + if subject, wasTrimmed := trimPrefix(subject); wasTrimmed { + for { + // handle repeated prefixes like "fixup! amend! fixup! Subject" + if subject, wasTrimmed = trimPrefix(subject); !wasTrimmed { + break + } + } + return subject, true } - return commits[index-1], nil + return subject, false } func (self *LocalCommitsController) createTag(commit *models.Commit) error { @@ -1070,7 +1123,7 @@ func (self *LocalCommitsController) canFindCommitForQuickStart() *types.Disabled } func (self *LocalCommitsController) canFindCommitForSquashFixupsInCurrentBranch() *types.DisabledReason { - if _, err := self.findCommitForSquashFixupsInCurrentBranch(); err != nil { + if _, _, err := self.findCommitForSquashFixupsInCurrentBranch(); err != nil { return &types.DisabledReason{Text: err.Error()} } diff --git a/pkg/gui/controllers/local_commits_controller_test.go b/pkg/gui/controllers/local_commits_controller_test.go new file mode 100644 index 00000000000..d425821d777 --- /dev/null +++ b/pkg/gui/controllers/local_commits_controller_test.go @@ -0,0 +1,141 @@ +package controllers + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func Test_countSquashableCommitsAbove(t *testing.T) { + scenarios := []struct { + name string + commits []*models.Commit + selectedIdx int + rebaseStartIdx int + expectedResult int + }{ + { + name: "no squashable commits", + commits: []*models.Commit{ + {Name: "abc"}, + {Name: "def"}, + {Name: "ghi"}, + }, + selectedIdx: 2, + rebaseStartIdx: 2, + expectedResult: 0, + }, + { + name: "some squashable commits, including for the selected commit", + commits: []*models.Commit{ + {Name: "fixup! def"}, + {Name: "fixup! ghi"}, + {Name: "abc"}, + {Name: "def"}, + {Name: "ghi"}, + }, + selectedIdx: 4, + rebaseStartIdx: 4, + expectedResult: 2, + }, + { + name: "base commit is below rebase start", + commits: []*models.Commit{ + {Name: "fixup! def"}, + {Name: "abc"}, + {Name: "def"}, + }, + selectedIdx: 1, + rebaseStartIdx: 1, + expectedResult: 0, + }, + { + name: "base commit does not exist at all", + commits: []*models.Commit{ + {Name: "fixup! xyz"}, + {Name: "abc"}, + {Name: "def"}, + }, + selectedIdx: 2, + rebaseStartIdx: 2, + expectedResult: 0, + }, + { + name: "selected commit is in the middle of fixups", + commits: []*models.Commit{ + {Name: "fixup! def"}, + {Name: "abc"}, + {Name: "fixup! ghi"}, + {Name: "def"}, + {Name: "ghi"}, + }, + selectedIdx: 1, + rebaseStartIdx: 4, + expectedResult: 1, + }, + { + name: "selected commit is after rebase start", + commits: []*models.Commit{ + {Name: "fixup! def"}, + {Name: "abc"}, + {Name: "def"}, + {Name: "ghi"}, + }, + selectedIdx: 3, + rebaseStartIdx: 2, + expectedResult: 1, + }, + } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + assert.Equal(t, s.expectedResult, countSquashableCommitsAbove(s.commits, s.selectedIdx, s.rebaseStartIdx)) + }) + } +} + +func Test_isFixupCommit(t *testing.T) { + scenarios := []struct { + subject string + expectedTrimmedSubject string + expectedIsFixup bool + }{ + { + subject: "Bla", + expectedTrimmedSubject: "Bla", + expectedIsFixup: false, + }, + { + subject: "fixup Bla", + expectedTrimmedSubject: "fixup Bla", + expectedIsFixup: false, + }, + { + subject: "fixup! Bla", + expectedTrimmedSubject: "Bla", + expectedIsFixup: true, + }, + { + subject: "fixup! fixup! Bla", + expectedTrimmedSubject: "Bla", + expectedIsFixup: true, + }, + { + subject: "amend! squash! Bla", + expectedTrimmedSubject: "Bla", + expectedIsFixup: true, + }, + { + subject: "fixup!", + expectedTrimmedSubject: "fixup!", + expectedIsFixup: false, + }, + } + for _, s := range scenarios { + t.Run(s.subject, func(t *testing.T) { + trimmedSubject, isFixupCommit := isFixupCommit(s.subject) + assert.Equal(t, s.expectedTrimmedSubject, trimmedSubject) + assert.Equal(t, s.expectedIsFixup, isFixupCommit) + }) + } +} diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_above.go b/pkg/integration/tests/interactive_rebase/squash_fixups_above.go index b412f93ed5e..e87addce00a 100644 --- a/pkg/integration/tests/interactive_rebase/squash_fixups_above.go +++ b/pkg/integration/tests/interactive_rebase/squash_fixups_above.go @@ -46,10 +46,9 @@ var SquashFixupsAbove = NewIntegrationTest(NewIntegrationTestArgs{ }). Lines( Contains("commit 03"), - Contains("commit 02"), - Contains("commit 01").IsSelected(), // wrong, we want the previous line - ). - SelectPreviousItem() + Contains("commit 02").IsSelected(), + Contains("commit 01"), + ) t.Views().Main(). Content(Contains("fixup content")) diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go b/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go index 75bfbf159b3..c6721d829e4 100644 --- a/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go +++ b/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go @@ -45,11 +45,10 @@ var SquashFixupsInCurrentBranch = NewIntegrationTest(NewIntegrationTestArgs{ }). Lines( Contains("commit 02"), - Contains("commit 01"), - Contains("fixup! master commit").IsSelected(), // wrong, we want the previous line + Contains("commit 01").IsSelected(), + Contains("fixup! master commit"), Contains("master commit"), - ). - NavigateToLine(Contains("commit 01")) + ) t.Views().Main(). Content(Contains("fixup content")) From 99ad6005e82e49e84121121feb4255e09dbb8673 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 9 Mar 2024 09:53:25 +0100 Subject: [PATCH 13/19] Bump gocui --- go.mod | 8 +- go.sum | 14 +- .../gdamore/tcell/v2/console_win.go | 6 +- .../jesseduffield/gocui/text_area.go | 168 ++++++++++++++++-- vendor/golang.org/x/sys/unix/aliases.go | 2 +- .../x/sys/unix/syscall_darwin_libSystem.go | 2 +- .../golang.org/x/sys/unix/syscall_freebsd.go | 12 +- vendor/golang.org/x/sys/unix/syscall_linux.go | 99 +++++++++++ .../golang.org/x/sys/unix/zsyscall_linux.go | 10 ++ vendor/golang.org/x/sys/unix/ztypes_linux.go | 60 +++++++ vendor/modules.txt | 8 +- 11 files changed, 351 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index bc039bc578e..58ad19c3c58 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,14 @@ require ( github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/creack/pty v1.1.11 github.com/fsmiamoto/git-todo-parser v0.0.5 - github.com/gdamore/tcell/v2 v2.7.3 + github.com/gdamore/tcell/v2 v2.7.4 github.com/go-errors/errors v1.5.1 github.com/gookit/color v1.4.2 github.com/imdario/mergo v0.3.11 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.20221018185014-fdd53fef665d - github.com/jesseduffield/gocui v0.3.1-0.20240303173746-f2b0f1f68dd8 + github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e @@ -74,8 +74,8 @@ require ( github.com/xanzy/ssh-agent v0.2.1 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 84e171721ce..0e728ff52f0 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/fsmiamoto/git-todo-parser v0.0.5/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLI github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.7.3 h1:YLQlOj5F0hSlKy5TJvlych29+WTcJzbElnLYwx8gvdg= -github.com/gdamore/tcell/v2 v2.7.3/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= +github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= +github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= @@ -187,8 +187,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.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE= github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o= -github.com/jesseduffield/gocui v0.3.1-0.20240303173746-f2b0f1f68dd8 h1:DGyAjpaAnxDuKO4MEoFjifhkUV7sU6znMR9eRfjjvn0= -github.com/jesseduffield/gocui v0.3.1-0.20240303173746-f2b0f1f68dd8/go.mod h1:lLLfxEGyIvvkzzpHdKkfgIVFmxqEejeACxKMVxSHLeM= +github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de h1:2ww1SWgakihE8hFxZ7L3agVeGpA6qwW5vdnhFUXKMQo= +github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8= 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/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY= @@ -470,13 +470,15 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/vendor/github.com/gdamore/tcell/v2/console_win.go b/vendor/github.com/gdamore/tcell/v2/console_win.go index 92ae4e5c284..e2652509eed 100644 --- a/vendor/github.com/gdamore/tcell/v2/console_win.go +++ b/vendor/github.com/gdamore/tcell/v2/console_win.go @@ -341,12 +341,12 @@ func (s *cScreen) disengage() { } } else if !s.disableAlt { s.clearScreen(StyleDefault, s.vten) + s.setCursorPos(0, 0, false) } + s.setCursorInfo(&s.ocursor) + s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y)) s.setInMode(s.oimode) s.setOutMode(s.oomode) - s.setBufferSize(int(s.oscreen.size.x), int(s.oscreen.size.y)) - s.setCursorPos(0, 0, false) - s.setCursorInfo(&s.ocursor) _, _, _ = procSetConsoleTextAttribute.Call( uintptr(s.out), uintptr(s.mapStyle(StyleDefault))) diff --git a/vendor/github.com/jesseduffield/gocui/text_area.go b/vendor/github.com/jesseduffield/gocui/text_area.go index ca4809107ab..ebd6a6bfa6a 100644 --- a/vendor/github.com/jesseduffield/gocui/text_area.go +++ b/vendor/github.com/jesseduffield/gocui/text_area.go @@ -11,11 +11,59 @@ const ( WORD_SEPARATORS = "*?_+-.[]~=/&;!#$%^(){}<>" ) +type CursorMapping struct { + Orig int + Wrapped int +} + type TextArea struct { - content []rune - cursor int - overwrite bool - clipboard string + content []rune + wrappedContent []rune + cursorMapping []CursorMapping + cursor int + overwrite bool + clipboard string + AutoWrap bool + AutoWrapWidth int +} + +func AutoWrapContent(content []rune, autoWrapWidth int) ([]rune, []CursorMapping) { + estimatedNumberOfSoftLineBreaks := len(content) / autoWrapWidth + cursorMapping := make([]CursorMapping, 0, estimatedNumberOfSoftLineBreaks) + wrappedContent := make([]rune, 0, len(content)+estimatedNumberOfSoftLineBreaks) + startOfLine := 0 + indexOfLastWhitespace := -1 + + for currentPos, r := range content { + if r == '\n' { + wrappedContent = append(wrappedContent, content[startOfLine:currentPos+1]...) + startOfLine = currentPos + 1 + indexOfLastWhitespace = -1 + } else { + if r == ' ' { + indexOfLastWhitespace = currentPos + 1 + } else if currentPos-startOfLine >= autoWrapWidth && indexOfLastWhitespace >= 0 { + wrapAt := indexOfLastWhitespace + wrappedContent = append(wrappedContent, content[startOfLine:wrapAt]...) + wrappedContent = append(wrappedContent, '\n') + cursorMapping = append(cursorMapping, CursorMapping{wrapAt, len(wrappedContent)}) + startOfLine = wrapAt + indexOfLastWhitespace = -1 + } + } + } + + wrappedContent = append(wrappedContent, content[startOfLine:]...) + + return wrappedContent, cursorMapping +} + +func (self *TextArea) autoWrapContent() { + if self.AutoWrap { + self.wrappedContent, self.cursorMapping = AutoWrapContent(self.content, self.AutoWrapWidth) + } else { + self.wrappedContent, self.cursorMapping = self.content, []CursorMapping{} + } } func (self *TextArea) TypeRune(r rune) { @@ -27,6 +75,7 @@ func (self *TextArea) TypeRune(r rune) { append([]rune{r}, self.content[self.cursor:]...)..., ) } + self.autoWrapContent() self.cursor++ } @@ -37,6 +86,7 @@ func (self *TextArea) BackSpaceChar() { } self.content = append(self.content[:self.cursor-1], self.content[self.cursor:]...) + self.autoWrapContent() self.cursor-- } @@ -46,6 +96,7 @@ func (self *TextArea) DeleteChar() { } self.content = append(self.content[:self.cursor], self.content[self.cursor+1:]...) + self.autoWrapContent() } func (self *TextArea) MoveCursorLeft() { @@ -123,6 +174,10 @@ func (self *TextArea) MoveCursorDown() { } func (self *TextArea) GetContent() string { + return string(self.wrappedContent) +} + +func (self *TextArea) GetUnwrappedContent() string { return string(self.content) } @@ -144,14 +199,24 @@ func (self *TextArea) DeleteToStartOfLine() { self.content = append(self.content[:self.cursor-1], self.content[self.cursor:]...) self.cursor-- + self.autoWrapContent() return } + // otherwise, if we're at a soft line start, skip left past the soft line + // break, so we'll end up deleting the previous line. This seems like the + // only reasonable behavior in this case, as you can't delete just the soft + // line break. + if self.atSoftLineStart() { + self.cursor-- + } + // otherwise, you delete everything up to the start of the current line, without // deleting the newline character newlineIndex := self.closestNewlineOnLeft() self.clipboard = string(self.content[newlineIndex+1 : self.cursor]) self.content = append(self.content[:newlineIndex+1], self.content[self.cursor:]...) + self.autoWrapContent() self.cursor = newlineIndex + 1 } @@ -159,18 +224,30 @@ func (self *TextArea) DeleteToEndOfLine() { if self.atEnd() { return } + + // if we're at the end of the line, delete just the newline character if self.atLineEnd() { self.content = append(self.content[:self.cursor], self.content[self.cursor+1:]...) + self.autoWrapContent() return } + // otherwise, if we're at a soft line end, skip right past the soft line + // break, so we'll end up deleting the next line. This seems like the + // only reasonable behavior in this case, as you can't delete just the soft + // line break. + if self.atSoftLineEnd() { + self.cursor++ + } + lineEndIndex := self.closestNewlineOnRight() self.clipboard = string(self.content[self.cursor:lineEndIndex]) self.content = append(self.content[:self.cursor], self.content[lineEndIndex:]...) + self.autoWrapContent() } func (self *TextArea) GoToStartOfLine() { - if self.atLineStart() { + if self.atSoftLineStart() { return } @@ -181,15 +258,21 @@ func (self *TextArea) GoToStartOfLine() { } func (self *TextArea) closestNewlineOnLeft() int { + wrappedCursor := self.origCursorToWrappedCursor(self.cursor) + newlineIndex := -1 - for i, r := range self.content[0:self.cursor] { + for i, r := range self.wrappedContent[0:wrappedCursor] { if r == '\n' { newlineIndex = i } } - return newlineIndex + unwrappedNewlineIndex := self.wrappedCursorToOrigCursor(newlineIndex) + if unwrappedNewlineIndex >= 0 && self.content[unwrappedNewlineIndex] != '\n' { + unwrappedNewlineIndex-- + } + return unwrappedNewlineIndex } func (self *TextArea) GoToEndOfLine() { @@ -198,12 +281,22 @@ func (self *TextArea) GoToEndOfLine() { } self.cursor = self.closestNewlineOnRight() + + // If the end of line is a soft line break, we need to move left by one so + // that we end up at the last whitespace before the line break. Otherwise + // we'd be at the start of the next line, since the newline character + // doesn't really exist in the real content. + if self.cursor < len(self.content) && self.content[self.cursor] != '\n' { + self.cursor-- + } } func (self *TextArea) closestNewlineOnRight() int { - for i, r := range self.content[self.cursor:] { + wrappedCursor := self.origCursorToWrappedCursor(self.cursor) + + for i, r := range self.wrappedContent[wrappedCursor:] { if r == '\n' { - return self.cursor + i + return self.wrappedCursorToOrigCursor(wrappedCursor + i) } } @@ -215,11 +308,23 @@ func (self *TextArea) atLineStart() bool { (len(self.content) > self.cursor-1 && self.content[self.cursor-1] == '\n') } +func (self *TextArea) atSoftLineStart() bool { + wrappedCursor := self.origCursorToWrappedCursor(self.cursor) + return wrappedCursor == 0 || + (len(self.wrappedContent) > wrappedCursor-1 && self.wrappedContent[wrappedCursor-1] == '\n') +} + func (self *TextArea) atLineEnd() bool { return self.atEnd() || (len(self.content) > self.cursor && self.content[self.cursor] == '\n') } +func (self *TextArea) atSoftLineEnd() bool { + wrappedCursor := self.origCursorToWrappedCursor(self.cursor) + return wrappedCursor == len(self.wrappedContent) || + (len(self.wrappedContent) > wrappedCursor+1 && self.wrappedContent[wrappedCursor+1] == '\n') +} + func (self *TextArea) BackSpaceWord() { if self.cursor == 0 { return @@ -246,16 +351,50 @@ func (self *TextArea) BackSpaceWord() { self.clipboard = string(self.content[self.cursor:right]) self.content = append(self.content[:self.cursor], self.content[right:]...) + self.autoWrapContent() } func (self *TextArea) Yank() { self.TypeString(self.clipboard) } +func origCursorToWrappedCursor(origCursor int, cursorMapping []CursorMapping) int { + prevMapping := CursorMapping{0, 0} + for _, mapping := range cursorMapping { + if origCursor < mapping.Orig { + break + } + prevMapping = mapping + } + + return origCursor + prevMapping.Wrapped - prevMapping.Orig +} + +func (self *TextArea) origCursorToWrappedCursor(origCursor int) int { + return origCursorToWrappedCursor(origCursor, self.cursorMapping) +} + +func wrappedCursorToOrigCursor(wrappedCursor int, cursorMapping []CursorMapping) int { + prevMapping := CursorMapping{0, 0} + for _, mapping := range cursorMapping { + if wrappedCursor < mapping.Wrapped { + break + } + prevMapping = mapping + } + + return wrappedCursor + prevMapping.Orig - prevMapping.Wrapped +} + +func (self *TextArea) wrappedCursorToOrigCursor(wrappedCursor int) int { + return wrappedCursorToOrigCursor(wrappedCursor, self.cursorMapping) +} + func (self *TextArea) GetCursorXY() (int, int) { cursorX := 0 cursorY := 0 - for _, r := range self.content[0:self.cursor] { + wrappedCursor := self.origCursorToWrappedCursor(self.cursor) + for _, r := range self.wrappedContent[0:wrappedCursor] { if r == '\n' { cursorY++ cursorX = 0 @@ -278,15 +417,15 @@ func (self *TextArea) SetCursor2D(x int, y int) { } newCursor := 0 - for _, r := range self.content { + for _, r := range self.wrappedContent { if x <= 0 && y == 0 { - self.cursor = newCursor + self.cursor = self.wrappedCursorToOrigCursor(newCursor) return } if r == '\n' { if y == 0 { - self.cursor = newCursor + self.cursor = self.wrappedCursorToOrigCursor(newCursor) return } y-- @@ -304,11 +443,12 @@ func (self *TextArea) SetCursor2D(x int, y int) { return } - self.cursor = newCursor + self.cursor = self.wrappedCursorToOrigCursor(newCursor) } func (self *TextArea) Clear() { self.content = []rune{} + self.wrappedContent = []rune{} self.cursor = 0 } diff --git a/vendor/golang.org/x/sys/unix/aliases.go b/vendor/golang.org/x/sys/unix/aliases.go index e7d3df4bd36..b0e41985750 100644 --- a/vendor/golang.org/x/sys/unix/aliases.go +++ b/vendor/golang.org/x/sys/unix/aliases.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos) && go1.9 +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos package unix diff --git a/vendor/golang.org/x/sys/unix/syscall_darwin_libSystem.go b/vendor/golang.org/x/sys/unix/syscall_darwin_libSystem.go index 16dc6993799..2f0fa76e4f6 100644 --- a/vendor/golang.org/x/sys/unix/syscall_darwin_libSystem.go +++ b/vendor/golang.org/x/sys/unix/syscall_darwin_libSystem.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build darwin && go1.12 +//go:build darwin package unix diff --git a/vendor/golang.org/x/sys/unix/syscall_freebsd.go b/vendor/golang.org/x/sys/unix/syscall_freebsd.go index 64d1bb4dba5..2b57e0f73bb 100644 --- a/vendor/golang.org/x/sys/unix/syscall_freebsd.go +++ b/vendor/golang.org/x/sys/unix/syscall_freebsd.go @@ -13,6 +13,7 @@ package unix import ( + "errors" "sync" "unsafe" ) @@ -169,25 +170,26 @@ func Getfsstat(buf []Statfs_t, flags int) (n int, err error) { func Uname(uname *Utsname) error { mib := []_C_int{CTL_KERN, KERN_OSTYPE} n := unsafe.Sizeof(uname.Sysname) - if err := sysctl(mib, &uname.Sysname[0], &n, nil, 0); err != nil { + // Suppress ENOMEM errors to be compatible with the C library __xuname() implementation. + if err := sysctl(mib, &uname.Sysname[0], &n, nil, 0); err != nil && !errors.Is(err, ENOMEM) { return err } mib = []_C_int{CTL_KERN, KERN_HOSTNAME} n = unsafe.Sizeof(uname.Nodename) - if err := sysctl(mib, &uname.Nodename[0], &n, nil, 0); err != nil { + if err := sysctl(mib, &uname.Nodename[0], &n, nil, 0); err != nil && !errors.Is(err, ENOMEM) { return err } mib = []_C_int{CTL_KERN, KERN_OSRELEASE} n = unsafe.Sizeof(uname.Release) - if err := sysctl(mib, &uname.Release[0], &n, nil, 0); err != nil { + if err := sysctl(mib, &uname.Release[0], &n, nil, 0); err != nil && !errors.Is(err, ENOMEM) { return err } mib = []_C_int{CTL_KERN, KERN_VERSION} n = unsafe.Sizeof(uname.Version) - if err := sysctl(mib, &uname.Version[0], &n, nil, 0); err != nil { + if err := sysctl(mib, &uname.Version[0], &n, nil, 0); err != nil && !errors.Is(err, ENOMEM) { return err } @@ -205,7 +207,7 @@ func Uname(uname *Utsname) error { mib = []_C_int{CTL_HW, HW_MACHINE} n = unsafe.Sizeof(uname.Machine) - if err := sysctl(mib, &uname.Machine[0], &n, nil, 0); err != nil { + if err := sysctl(mib, &uname.Machine[0], &n, nil, 0); err != nil && !errors.Is(err, ENOMEM) { return err } diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go index 0f85e29e621..5682e2628ad 100644 --- a/vendor/golang.org/x/sys/unix/syscall_linux.go +++ b/vendor/golang.org/x/sys/unix/syscall_linux.go @@ -1849,6 +1849,105 @@ func Dup2(oldfd, newfd int) error { //sys Fsmount(fd int, flags int, mountAttrs int) (fsfd int, err error) //sys Fsopen(fsName string, flags int) (fd int, err error) //sys Fspick(dirfd int, pathName string, flags int) (fd int, err error) + +//sys fsconfig(fd int, cmd uint, key *byte, value *byte, aux int) (err error) + +func fsconfigCommon(fd int, cmd uint, key string, value *byte, aux int) (err error) { + var keyp *byte + if keyp, err = BytePtrFromString(key); err != nil { + return + } + return fsconfig(fd, cmd, keyp, value, aux) +} + +// FsconfigSetFlag is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_SET_FLAG. +// +// fd is the filesystem context to act upon. +// key the parameter key to set. +func FsconfigSetFlag(fd int, key string) (err error) { + return fsconfigCommon(fd, FSCONFIG_SET_FLAG, key, nil, 0) +} + +// FsconfigSetString is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_SET_STRING. +// +// fd is the filesystem context to act upon. +// key the parameter key to set. +// value is the parameter value to set. +func FsconfigSetString(fd int, key string, value string) (err error) { + var valuep *byte + if valuep, err = BytePtrFromString(value); err != nil { + return + } + return fsconfigCommon(fd, FSCONFIG_SET_STRING, key, valuep, 0) +} + +// FsconfigSetBinary is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_SET_BINARY. +// +// fd is the filesystem context to act upon. +// key the parameter key to set. +// value is the parameter value to set. +func FsconfigSetBinary(fd int, key string, value []byte) (err error) { + if len(value) == 0 { + return EINVAL + } + return fsconfigCommon(fd, FSCONFIG_SET_BINARY, key, &value[0], len(value)) +} + +// FsconfigSetPath is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_SET_PATH. +// +// fd is the filesystem context to act upon. +// key the parameter key to set. +// path is a non-empty path for specified key. +// atfd is a file descriptor at which to start lookup from or AT_FDCWD. +func FsconfigSetPath(fd int, key string, path string, atfd int) (err error) { + var valuep *byte + if valuep, err = BytePtrFromString(path); err != nil { + return + } + return fsconfigCommon(fd, FSCONFIG_SET_PATH, key, valuep, atfd) +} + +// FsconfigSetPathEmpty is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_SET_PATH_EMPTY. The same as +// FconfigSetPath but with AT_PATH_EMPTY implied. +func FsconfigSetPathEmpty(fd int, key string, path string, atfd int) (err error) { + var valuep *byte + if valuep, err = BytePtrFromString(path); err != nil { + return + } + return fsconfigCommon(fd, FSCONFIG_SET_PATH_EMPTY, key, valuep, atfd) +} + +// FsconfigSetFd is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_SET_FD. +// +// fd is the filesystem context to act upon. +// key the parameter key to set. +// value is a file descriptor to be assigned to specified key. +func FsconfigSetFd(fd int, key string, value int) (err error) { + return fsconfigCommon(fd, FSCONFIG_SET_FD, key, nil, value) +} + +// FsconfigCreate is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_CMD_CREATE. +// +// fd is the filesystem context to act upon. +func FsconfigCreate(fd int) (err error) { + return fsconfig(fd, FSCONFIG_CMD_CREATE, nil, nil, 0) +} + +// FsconfigReconfigure is equivalent to fsconfig(2) called +// with cmd == FSCONFIG_CMD_RECONFIGURE. +// +// fd is the filesystem context to act upon. +func FsconfigReconfigure(fd int) (err error) { + return fsconfig(fd, FSCONFIG_CMD_RECONFIGURE, nil, nil, 0) +} + //sys Getdents(fd int, buf []byte) (n int, err error) = SYS_GETDENTS64 //sysnb Getpgid(pid int) (pgid int, err error) diff --git a/vendor/golang.org/x/sys/unix/zsyscall_linux.go b/vendor/golang.org/x/sys/unix/zsyscall_linux.go index 1488d27128c..87d8612a1dc 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_linux.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_linux.go @@ -906,6 +906,16 @@ func Fspick(dirfd int, pathName string, flags int) (fd int, err error) { // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fsconfig(fd int, cmd uint, key *byte, value *byte, aux int) (err error) { + _, _, e1 := Syscall6(SYS_FSCONFIG, uintptr(fd), uintptr(cmd), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(value)), uintptr(aux), 0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func Getdents(fd int, buf []byte) (n int, err error) { var _p0 unsafe.Pointer if len(buf) > 0 { diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index dc0c955eecd..eff6bcdef81 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -836,6 +836,15 @@ const ( FSPICK_EMPTY_PATH = 0x8 FSMOUNT_CLOEXEC = 0x1 + + FSCONFIG_SET_FLAG = 0x0 + FSCONFIG_SET_STRING = 0x1 + FSCONFIG_SET_BINARY = 0x2 + FSCONFIG_SET_PATH = 0x3 + FSCONFIG_SET_PATH_EMPTY = 0x4 + FSCONFIG_SET_FD = 0x5 + FSCONFIG_CMD_CREATE = 0x6 + FSCONFIG_CMD_RECONFIGURE = 0x7 ) type OpenHow struct { @@ -1550,6 +1559,7 @@ const ( IFLA_DEVLINK_PORT = 0x3e IFLA_GSO_IPV4_MAX_SIZE = 0x3f IFLA_GRO_IPV4_MAX_SIZE = 0x40 + IFLA_DPLL_PIN = 0x41 IFLA_PROTO_DOWN_REASON_UNSPEC = 0x0 IFLA_PROTO_DOWN_REASON_MASK = 0x1 IFLA_PROTO_DOWN_REASON_VALUE = 0x2 @@ -1565,6 +1575,7 @@ const ( IFLA_INET6_ICMP6STATS = 0x6 IFLA_INET6_TOKEN = 0x7 IFLA_INET6_ADDR_GEN_MODE = 0x8 + IFLA_INET6_RA_MTU = 0x9 IFLA_BR_UNSPEC = 0x0 IFLA_BR_FORWARD_DELAY = 0x1 IFLA_BR_HELLO_TIME = 0x2 @@ -1612,6 +1623,9 @@ const ( IFLA_BR_MCAST_MLD_VERSION = 0x2c IFLA_BR_VLAN_STATS_PER_PORT = 0x2d IFLA_BR_MULTI_BOOLOPT = 0x2e + IFLA_BR_MCAST_QUERIER_STATE = 0x2f + IFLA_BR_FDB_N_LEARNED = 0x30 + IFLA_BR_FDB_MAX_LEARNED = 0x31 IFLA_BRPORT_UNSPEC = 0x0 IFLA_BRPORT_STATE = 0x1 IFLA_BRPORT_PRIORITY = 0x2 @@ -1649,6 +1663,14 @@ const ( IFLA_BRPORT_BACKUP_PORT = 0x22 IFLA_BRPORT_MRP_RING_OPEN = 0x23 IFLA_BRPORT_MRP_IN_OPEN = 0x24 + IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT = 0x25 + IFLA_BRPORT_MCAST_EHT_HOSTS_CNT = 0x26 + IFLA_BRPORT_LOCKED = 0x27 + IFLA_BRPORT_MAB = 0x28 + IFLA_BRPORT_MCAST_N_GROUPS = 0x29 + IFLA_BRPORT_MCAST_MAX_GROUPS = 0x2a + IFLA_BRPORT_NEIGH_VLAN_SUPPRESS = 0x2b + IFLA_BRPORT_BACKUP_NHID = 0x2c IFLA_INFO_UNSPEC = 0x0 IFLA_INFO_KIND = 0x1 IFLA_INFO_DATA = 0x2 @@ -1670,6 +1692,9 @@ const ( IFLA_MACVLAN_MACADDR = 0x4 IFLA_MACVLAN_MACADDR_DATA = 0x5 IFLA_MACVLAN_MACADDR_COUNT = 0x6 + IFLA_MACVLAN_BC_QUEUE_LEN = 0x7 + IFLA_MACVLAN_BC_QUEUE_LEN_USED = 0x8 + IFLA_MACVLAN_BC_CUTOFF = 0x9 IFLA_VRF_UNSPEC = 0x0 IFLA_VRF_TABLE = 0x1 IFLA_VRF_PORT_UNSPEC = 0x0 @@ -1693,9 +1718,22 @@ const ( IFLA_XFRM_UNSPEC = 0x0 IFLA_XFRM_LINK = 0x1 IFLA_XFRM_IF_ID = 0x2 + IFLA_XFRM_COLLECT_METADATA = 0x3 IFLA_IPVLAN_UNSPEC = 0x0 IFLA_IPVLAN_MODE = 0x1 IFLA_IPVLAN_FLAGS = 0x2 + NETKIT_NEXT = -0x1 + NETKIT_PASS = 0x0 + NETKIT_DROP = 0x2 + NETKIT_REDIRECT = 0x7 + NETKIT_L2 = 0x0 + NETKIT_L3 = 0x1 + IFLA_NETKIT_UNSPEC = 0x0 + IFLA_NETKIT_PEER_INFO = 0x1 + IFLA_NETKIT_PRIMARY = 0x2 + IFLA_NETKIT_POLICY = 0x3 + IFLA_NETKIT_PEER_POLICY = 0x4 + IFLA_NETKIT_MODE = 0x5 IFLA_VXLAN_UNSPEC = 0x0 IFLA_VXLAN_ID = 0x1 IFLA_VXLAN_GROUP = 0x2 @@ -1726,6 +1764,8 @@ const ( IFLA_VXLAN_GPE = 0x1b IFLA_VXLAN_TTL_INHERIT = 0x1c IFLA_VXLAN_DF = 0x1d + IFLA_VXLAN_VNIFILTER = 0x1e + IFLA_VXLAN_LOCALBYPASS = 0x1f IFLA_GENEVE_UNSPEC = 0x0 IFLA_GENEVE_ID = 0x1 IFLA_GENEVE_REMOTE = 0x2 @@ -1740,6 +1780,7 @@ const ( IFLA_GENEVE_LABEL = 0xb IFLA_GENEVE_TTL_INHERIT = 0xc IFLA_GENEVE_DF = 0xd + IFLA_GENEVE_INNER_PROTO_INHERIT = 0xe IFLA_BAREUDP_UNSPEC = 0x0 IFLA_BAREUDP_PORT = 0x1 IFLA_BAREUDP_ETHERTYPE = 0x2 @@ -1752,6 +1793,8 @@ const ( IFLA_GTP_FD1 = 0x2 IFLA_GTP_PDP_HASHSIZE = 0x3 IFLA_GTP_ROLE = 0x4 + IFLA_GTP_CREATE_SOCKETS = 0x5 + IFLA_GTP_RESTART_COUNT = 0x6 IFLA_BOND_UNSPEC = 0x0 IFLA_BOND_MODE = 0x1 IFLA_BOND_ACTIVE_SLAVE = 0x2 @@ -1781,6 +1824,9 @@ const ( IFLA_BOND_AD_ACTOR_SYSTEM = 0x1a IFLA_BOND_TLB_DYNAMIC_LB = 0x1b IFLA_BOND_PEER_NOTIF_DELAY = 0x1c + IFLA_BOND_AD_LACP_ACTIVE = 0x1d + IFLA_BOND_MISSED_MAX = 0x1e + IFLA_BOND_NS_IP6_TARGET = 0x1f IFLA_BOND_AD_INFO_UNSPEC = 0x0 IFLA_BOND_AD_INFO_AGGREGATOR = 0x1 IFLA_BOND_AD_INFO_NUM_PORTS = 0x2 @@ -1796,6 +1842,7 @@ const ( IFLA_BOND_SLAVE_AD_AGGREGATOR_ID = 0x6 IFLA_BOND_SLAVE_AD_ACTOR_OPER_PORT_STATE = 0x7 IFLA_BOND_SLAVE_AD_PARTNER_OPER_PORT_STATE = 0x8 + IFLA_BOND_SLAVE_PRIO = 0x9 IFLA_VF_INFO_UNSPEC = 0x0 IFLA_VF_INFO = 0x1 IFLA_VF_UNSPEC = 0x0 @@ -1854,8 +1901,16 @@ const ( IFLA_STATS_LINK_XSTATS_SLAVE = 0x3 IFLA_STATS_LINK_OFFLOAD_XSTATS = 0x4 IFLA_STATS_AF_SPEC = 0x5 + IFLA_STATS_GETSET_UNSPEC = 0x0 + IFLA_STATS_GET_FILTERS = 0x1 + IFLA_STATS_SET_OFFLOAD_XSTATS_L3_STATS = 0x2 IFLA_OFFLOAD_XSTATS_UNSPEC = 0x0 IFLA_OFFLOAD_XSTATS_CPU_HIT = 0x1 + IFLA_OFFLOAD_XSTATS_HW_S_INFO = 0x2 + IFLA_OFFLOAD_XSTATS_L3_STATS = 0x3 + IFLA_OFFLOAD_XSTATS_HW_S_INFO_UNSPEC = 0x0 + IFLA_OFFLOAD_XSTATS_HW_S_INFO_REQUEST = 0x1 + IFLA_OFFLOAD_XSTATS_HW_S_INFO_USED = 0x2 IFLA_XDP_UNSPEC = 0x0 IFLA_XDP_FD = 0x1 IFLA_XDP_ATTACHED = 0x2 @@ -1885,6 +1940,11 @@ const ( IFLA_RMNET_UNSPEC = 0x0 IFLA_RMNET_MUX_ID = 0x1 IFLA_RMNET_FLAGS = 0x2 + IFLA_MCTP_UNSPEC = 0x0 + IFLA_MCTP_NET = 0x1 + IFLA_DSA_UNSPEC = 0x0 + IFLA_DSA_CONDUIT = 0x1 + IFLA_DSA_MASTER = 0x1 ) const ( diff --git a/vendor/modules.txt b/vendor/modules.txt index 56662374198..a02efe4f549 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -40,7 +40,7 @@ github.com/fsmiamoto/git-todo-parser/todo # github.com/gdamore/encoding v1.0.0 ## explicit; go 1.9 github.com/gdamore/encoding -# github.com/gdamore/tcell/v2 v2.7.3 +# github.com/gdamore/tcell/v2 v2.7.4 ## explicit; go 1.12 github.com/gdamore/tcell/v2 github.com/gdamore/tcell/v2/terminfo @@ -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.20240303173746-f2b0f1f68dd8 +# github.com/jesseduffield/gocui v0.3.1-0.20240309085756-86e0d5a312de ## explicit; go 1.12 github.com/jesseduffield/gocui # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 @@ -313,13 +313,13 @@ golang.org/x/exp/slices golang.org/x/net/context golang.org/x/net/internal/socks golang.org/x/net/proxy -# golang.org/x/sys v0.17.0 +# golang.org/x/sys v0.18.0 ## explicit; go 1.18 golang.org/x/sys/cpu golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows -# golang.org/x/term v0.17.0 +# golang.org/x/term v0.18.0 ## explicit; go 1.18 golang.org/x/term # golang.org/x/text v0.14.0 From cede0214009888e32027beb212b63e30e77d29e7 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 19 Nov 2023 16:39:01 +0100 Subject: [PATCH 14/19] Add config for soft-wrapping the commit message body --- docs/Config.md | 2 ++ pkg/config/user_config.go | 8 +++++++- pkg/gui/views.go | 2 ++ schema/config.json | 10 ++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/Config.md b/docs/Config.md index 5b686fc1978..4b26c5f844d 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -92,6 +92,8 @@ git: useConfig: false commit: signOff: false + autoWrapCommitMessage: true # automatic WYSIWYG wrapping of the commit message as you type + autoWrapWidth: 72 # if autoWrapCommitMessage is true, the width to wrap to merging: # only applicable to unix users manualCommit: false diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 9cb758259e3..6a4efe78a89 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -236,6 +236,10 @@ type PagingConfig struct { type CommitConfig struct { // If true, pass '--signoff' flag when committing SignOff bool `yaml:"signOff"` + // Automatic WYSIWYG wrapping of the commit message as you type + AutoWrapCommitMessage bool `yaml:"autoWrapCommitMessage"` + // If autoWrapCommitMessage is true, the width to wrap to + AutoWrapWidth int `yaml:"autoWrapWidth"` } type MergingConfig struct { @@ -658,7 +662,9 @@ func GetDefaultConfig() *UserConfig { ExternalDiffCommand: "", }, Commit: CommitConfig{ - SignOff: false, + SignOff: false, + AutoWrapCommitMessage: true, + AutoWrapWidth: 72, }, Merging: MergingConfig{ ManualCommit: false, diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 13caa9c7f29..9fd775764d3 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -168,6 +168,8 @@ func (gui *Gui) createAllViews() error { gui.Views.CommitDescription.FgColor = theme.GocuiDefaultTextColor gui.Views.CommitDescription.Editable = true gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor) + gui.Views.CommitDescription.TextArea.AutoWrap = gui.c.UserConfig.Git.Commit.AutoWrapCommitMessage + gui.Views.CommitDescription.TextArea.AutoWrapWidth = gui.c.UserConfig.Git.Commit.AutoWrapWidth gui.Views.Confirmation.Visible = false gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.promptEditor) diff --git a/schema/config.json b/schema/config.json index b9538130200..44d8e0cb477 100644 --- a/schema/config.json +++ b/schema/config.json @@ -405,6 +405,16 @@ "signOff": { "type": "boolean", "description": "If true, pass '--signoff' flag when committing" + }, + "autoWrapCommitMessage": { + "type": "boolean", + "description": "Automatic WYSIWYG wrapping of the commit message as you type", + "default": true + }, + "autoWrapWidth": { + "type": "integer", + "description": "If autoWrapCommitMessage is true, the width to wrap to", + "default": 72 } }, "additionalProperties": false, From 379a6f1922aaa9f2eb7ec27a8818fbacd1db0057 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 20 Dec 2023 13:01:41 +0100 Subject: [PATCH 15/19] Save and restore the unwrapped description When preserving the commit message (when cancelling a commit), and later restoring it, use the unwrapped description. --- pkg/gui/controllers.go | 4 +++ .../controllers/commit_message_controller.go | 2 +- pkg/gui/controllers/helpers/commits_helper.go | 29 ++++++++++--------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 30caae930ea..1dbf9b7d7d8 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -38,10 +38,14 @@ func (gui *Gui) resetHelpersAndControllers() { getCommitDescription := func() string { return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent()) } + getUnwrappedCommitDescription := func() string { + return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetUnwrappedContent()) + } commitsHelper := helpers.NewCommitsHelper(helperCommon, getCommitSummary, setCommitSummary, getCommitDescription, + getUnwrappedCommitDescription, setCommitDescription, ) diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index c52a8038f01..ef95dffe07c 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -100,7 +100,7 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error { self.c.Helpers().Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage()) return nil } else if currentIndex == context.NoCommitIndex { - self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndDescription()) + self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndUnwrappedDescription()) } validCommit, err := self.setCommitMessageAtIndex(newIndex) diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index 8691518cd55..cd90790a339 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -16,10 +16,11 @@ type ICommitsHelper interface { type CommitsHelper struct { c *HelperCommon - getCommitSummary func() string - setCommitSummary func(string) - getCommitDescription func() string - setCommitDescription func(string) + getCommitSummary func() string + setCommitSummary func(string) + getCommitDescription func() string + getUnwrappedCommitDescription func() string + setCommitDescription func(string) } var _ ICommitsHelper = &CommitsHelper{} @@ -29,14 +30,16 @@ func NewCommitsHelper( getCommitSummary func() string, setCommitSummary func(string), getCommitDescription func() string, + getUnwrappedCommitDescription func() string, setCommitDescription func(string), ) *CommitsHelper { return &CommitsHelper{ - c: c, - getCommitSummary: getCommitSummary, - setCommitSummary: setCommitSummary, - getCommitDescription: getCommitDescription, - setCommitDescription: setCommitDescription, + c: c, + getCommitSummary: getCommitSummary, + setCommitSummary: setCommitSummary, + getCommitDescription: getCommitDescription, + getUnwrappedCommitDescription: getUnwrappedCommitDescription, + setCommitDescription: setCommitDescription, } } @@ -53,11 +56,11 @@ func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) { self.c.Contexts().CommitMessage.RenderCommitLength() } -func (self *CommitsHelper) JoinCommitMessageAndDescription() string { - if len(self.getCommitDescription()) == 0 { +func (self *CommitsHelper) JoinCommitMessageAndUnwrappedDescription() string { + if len(self.getUnwrappedCommitDescription()) == 0 { return self.getCommitSummary() } - return self.getCommitSummary() + "\n" + self.getCommitDescription() + return self.getCommitSummary() + "\n" + self.getUnwrappedCommitDescription() } func (self *CommitsHelper) SwitchToEditor() error { @@ -154,7 +157,7 @@ func (self *CommitsHelper) HandleCommitConfirm() error { func (self *CommitsHelper) CloseCommitMessagePanel() error { if self.c.Contexts().CommitMessage.GetPreserveMessage() { - message := self.JoinCommitMessageAndDescription() + message := self.JoinCommitMessageAndUnwrappedDescription() self.c.Contexts().CommitMessage.SetPreservedMessage(message) } else { From 944d82028faed05b3b53e630c9fcf0c4747891e0 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 20 Dec 2023 18:27:01 +0100 Subject: [PATCH 16/19] Replace DOS linefeeds with Unix line feeds when loading a commit message I have seen some commit messages that contain CRLF instead of just LF; I'm not sure if these were created by a broken git client, but they exist, so we need to deal with them. Editing them when rewording a commit sort of works, but is a little strange; the \r characters are invisble, so you need an extra arrow key press to skip over them. In the next commit we are going to add more logic related to line breaks, and it is getting confused by the \r, so it is becoming more important to fix this. The easiest fix is to normalize the line endings right after loading. --- pkg/commands/git_commands/commit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 12806dafc32..960fab81193 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -142,7 +142,7 @@ func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) { ToArgv() message, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() - return strings.TrimSpace(message), err + return strings.ReplaceAll(strings.TrimSpace(message), "\r\n", "\n"), err } func (self *CommitCommands) GetCommitSubject(commitSha string) (string, error) { From 41a68f7c4afcda3479b13bf8bd3e63b4906054b8 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 20 Dec 2023 19:17:42 +0100 Subject: [PATCH 17/19] Remove hard line breaks when rewording commits ... and when recalling a commit message from an old commit by pressing up-arrow. This is necessary because committing turns our soft line breaks into real ones, but when rewording we want to turn them back into soft ones again, so that it's possible to insert words at the beginning of a paragraph and have everything rewrap nicely. This is only a best effort; the algorithm only removes those hard line breaks that can be removed without changing the way the message looks. This works well when the previous commit message was wrapped at the same width, which for most users should be the most common case; but if it wasn't, the result is not great. Specifically, if the old wrap width was smaller, some hard line breaks just won't be removed; if it was wider though, you'll get an unpleasant comb effect with alternating long and short lines. In such a case it's best to switch to the editor and use whatever wrapping features you have there (e.g. alt-Q). --- .../controllers/commit_message_controller.go | 4 ++ pkg/gui/controllers/helpers/commits_helper.go | 26 ++++++++++++ .../helpers/commits_helper_test.go | 41 +++++++++++++++++++ .../controllers/local_commits_controller.go | 4 +- 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 pkg/gui/controllers/helpers/commits_helper_test.go diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index ef95dffe07c..756b240e69a 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -3,6 +3,7 @@ package controllers import ( "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -119,6 +120,9 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e } return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) } + if self.c.UserConfig.Git.Commit.AutoWrapCommitMessage { + commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig.Git.Commit.AutoWrapWidth) + } self.c.Helpers().Commits.UpdateCommitPanelView(commitMessage) return true, nil } diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index cd90790a339..0801d57421f 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" ) @@ -63,6 +64,31 @@ func (self *CommitsHelper) JoinCommitMessageAndUnwrappedDescription() string { return self.getCommitSummary() + "\n" + self.getUnwrappedCommitDescription() } +func TryRemoveHardLineBreaks(message string, autoWrapWidth int) string { + messageRunes := []rune(message) + lastHardLineStart := 0 + for i, r := range messageRunes { + if r == '\n' { + // Try to make this a soft linebreak by turning it into a space, and + // checking whether it still wraps to the same result then. + messageRunes[i] = ' ' + + _, cursorMapping := gocui.AutoWrapContent(messageRunes[lastHardLineStart:], autoWrapWidth) + + // Look at the cursorMapping to check whether auto-wrapping inserted + // a line break. If it did, there will be a cursorMapping entry with + // Orig pointing to the position after the inserted line break. + if len(cursorMapping) == 0 || cursorMapping[0].Orig != i-lastHardLineStart+1 { + // It didn't, so change it back to a newline + messageRunes[i] = '\n' + } + lastHardLineStart = i + 1 + } + } + + return string(messageRunes) +} + func (self *CommitsHelper) SwitchToEditor() error { if !self.c.Contexts().CommitMessage.CanSwitchToEditor() { return nil diff --git a/pkg/gui/controllers/helpers/commits_helper_test.go b/pkg/gui/controllers/helpers/commits_helper_test.go new file mode 100644 index 00000000000..6197c3916ce --- /dev/null +++ b/pkg/gui/controllers/helpers/commits_helper_test.go @@ -0,0 +1,41 @@ +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTryRemoveHardLineBreaks(t *testing.T) { + scenarios := []struct { + name string + message string + autoWrapWidth int + expectedResult string + }{ + { + name: "empty", + message: "", + autoWrapWidth: 7, + expectedResult: "", + }, + { + name: "all line breaks are needed", + message: "abc\ndef\n\nxyz", + autoWrapWidth: 7, + expectedResult: "abc\ndef\n\nxyz", + }, + { + name: "some can be unwrapped", + message: "123\nabc def\nghi jkl\nmno\n456\n", + autoWrapWidth: 7, + expectedResult: "123\nabc def ghi jkl mno\n456\n", + }, + } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + actualResult := TryRemoveHardLineBreaks(s.message, s.autoWrapWidth) + assert.Equal(t, s.expectedResult, actualResult) + }) + } +} diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index ef6b5be8023..8c99d758629 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -354,7 +354,9 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error { if err != nil { return self.c.Error(err) } - + if self.c.UserConfig.Git.Commit.AutoWrapCommitMessage { + commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig.Git.Commit.AutoWrapWidth) + } return self.c.Helpers().Commits.OpenCommitMessagePanel( &helpers.OpenCommitMessagePanelOpts{ CommitIndex: self.context().GetSelectedLineIdx(), From d1f8c450995ace6b1b0e352ba10dcc0653da4030 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 20 Jan 2024 17:55:25 +0100 Subject: [PATCH 18/19] Add integration test --- .../commit_description_panel_driver.go | 10 ++++ .../tests/commit/auto_wrap_message.go | 59 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 3 files changed, 70 insertions(+) create mode 100644 pkg/integration/tests/commit/auto_wrap_message.go diff --git a/pkg/integration/components/commit_description_panel_driver.go b/pkg/integration/components/commit_description_panel_driver.go index 0c4b2cfbb08..eddc53533f8 100644 --- a/pkg/integration/components/commit_description_panel_driver.go +++ b/pkg/integration/components/commit_description_panel_driver.go @@ -31,6 +31,16 @@ func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDr return self } +func (self *CommitDescriptionPanelDriver) GoToBeginning() *CommitDescriptionPanelDriver { + numLines := len(self.getViewDriver().getView().BufferLines()) + for i := 0; i < numLines; i++ { + self.t.pressFast("") + } + + self.t.pressFast("") + return self +} + func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver { self.getViewDriver().Title(expected) diff --git a/pkg/integration/tests/commit/auto_wrap_message.go b/pkg/integration/tests/commit/auto_wrap_message.go new file mode 100644 index 00000000000..477bfae62fe --- /dev/null +++ b/pkg/integration/tests/commit/auto_wrap_message.go @@ -0,0 +1,59 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var AutoWrapMessage = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Commit, and test how the commit message body is auto-wrapped", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + // Use a ridiculously small width so that we don't have to use so much test data + config.UserConfig.Git.Commit.AutoWrapWidth = 20 + }, + SetupRepo: func(shell *Shell) { + shell.CreateFile("file", "file content") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() + + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). // stage file + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel(). + Type("subject"). + SwitchToDescription(). + Type("Lorem ipsum dolor sit amet, consectetur adipiscing elit."). + // See how it automatically inserted line feeds to wrap the text: + Content(Equals("Lorem ipsum dolor \nsit amet, \nconsectetur \nadipiscing elit.")). + SwitchToSummary(). + Confirm() + + t.Views().Commits(). + Lines( + Contains("subject"), + ). + Focus(). + Tap(func() { + t.Views().Main().Content(Contains( + "subject\n \n Lorem ipsum dolor\n sit amet,\n consectetur\n adipiscing elit.")) + }). + Press(keys.Commits.RenameCommit) + + // Test that when rewording, the hard line breaks are turned back into + // soft ones, so that we can insert text at the beginning and have the + // paragraph reflow nicely. + t.ExpectPopup().CommitMessagePanel(). + InitialText(Equals("subject")). + SwitchToDescription(). + Content(Equals("Lorem ipsum dolor \nsit amet, \nconsectetur \nadipiscing elit.")). + GoToBeginning(). + Type("More text. "). + Content(Equals("More text. Lorem \nipsum dolor sit \namet, consectetur \nadipiscing elit.")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 402a40acf22..8c2891d471a 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -65,6 +65,7 @@ var tests = []*components.IntegrationTest{ cherry_pick.CherryPickRange, commit.AddCoAuthor, commit.Amend, + commit.AutoWrapMessage, commit.Commit, commit.CommitMultiline, commit.CommitSwitchToEditor, From 272e41929c165636144526108bd7d919cc870c57 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 9 Mar 2024 20:48:33 +1100 Subject: [PATCH 19/19] Update sponsors in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index adf754e697e..4d2885e106d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ A simple terminal UI for git commands

-Mark LussierDean HerbertPeter BjorklundReilly WoodOliver GüntherPawan DhananjayBartłomiej DachDavid KarlssonCarsten GehlingCEUKAkos PutzXeteraHolden LucasChau TranmatejciktheAverageDev (Luca Tumedei)Zach FullerIvan ZaitsevNicholas CloudLightQuantumAliaksandr StelmachonakBurgy BenjaminJoe KlemmerTobias LütkeBen BeaumontHollyJames SantucciJeff ForcierMaciej T. NowakMichael HuggettFarzad MajidfayyazYuryAndreas KurthBraden SteffaniakJordan GillardSebastianGeorge SpanosBen HuntAlex BradnerFrantisek StankoRobert Clancy (Robbo)Andy SlezakMartin KockChristian RackersederMichal KolasinskiIllarion KoperskiJesse AlamaCodacyBrettJan HeijmansKevin Nowaldsem pruijsOmar Luq Esron SilvaEthan LiChristopher McAvaneyDaniel RBBrian MacAskillRaheel JunaidGeoffrey van WykMaxiMark FeinsteinJean-Luc Geeringnbr +Mark LussierDean HerbertPeter BjorklundReilly WoodOliver GüntherPawan DhananjayBartłomiej DachDavid KarlssonCarsten GehlingCEUKAkos PutzXeteraHolden LucasChau TranmatejciktheAverageDev (Luca Tumedei)Zach FullerIvan ZaitsevNicholas CloudLightQuantumAliaksandr StelmachonakBurgy BenjaminJoe KlemmerTobias LütkeBen BeaumontHollyJames SantucciJeff ForcierMaciej T. NowakMichael HuggettFarzad MajidfayyazYuryAndreas KurthBraden SteffaniakJordan GillardSebastianGeorge SpanosBen HuntAlex BradnerFrantisek StankoRobert Clancy (Robbo)Andy SlezakMartin KockIllarion KoperskiJesse AlamaDaniel KokottCodacyBrettJan HeijmansKevin Nowaldsem pruijsOmar Luq Ethan LiBrian MacAskillRaheel JunaidMaxinbrDon DavisFrancesco PiraJan ZenknerthedevdadVictor AremuRo Santalla

## Elevator Pitch