diff --git a/docs/Config.md b/docs/Config.md index 34e89c130..e3890f9f6 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -252,21 +252,21 @@ keybinding: ```yaml os: - openCommand: 'start "" {{filename}}' + open: 'start "" {{filename}}' ``` ### Linux ```yaml os: - openCommand: 'xdg-open {{filename}} >/dev/null' + open: 'xdg-open {{filename}} >/dev/null' ``` ### OSX ```yaml os: - openCommand: 'open {{filename}}' + open: 'open {{filename}}' ``` ### Configuring File Editing @@ -285,9 +285,9 @@ os: editPreset: 'vscode' ``` -Supported presets are `vim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`, and -`xcode`. In many cases lazygit will be able to guess the right preset from your -$(git config core.editor), or an environment variable such as $VISUAL or $EDITOR. +Supported presets are `vim`, `nvim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`, +`kakoune` and `xcode`. In many cases lazygit will be able to guess the right preset +from your $(git config core.editor), or an environment variable such as $VISUAL or $EDITOR. If for some reason you are not happy with the default commands from a preset, or there simply is no preset for your editor, you can customize the commands by diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index 50c599a66..e7a2d766a 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -181,12 +181,18 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract debug = "TRUE" } + emptyArg := " --empty=keep" + if self.version.IsOlderThan(2, 26, 0) { + emptyArg = "" + } + rebaseMergesArg := " --rebase-merges" if self.version.IsOlderThan(2, 22, 0) { rebaseMergesArg = "" } - cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash%s %s", - rebaseMergesArg, opts.baseShaOrRoot) + + cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty%s --no-autosquash%s %s", + emptyArg, rebaseMergesArg, opts.baseShaOrRoot) self.Log.WithField("command", cmdStr).Debug("RunCommand") cmdObj := self.cmd.New(cmdStr) diff --git a/pkg/commands/git_commands/rebase_test.go b/pkg/commands/git_commands/rebase_test.go index 1ef22ff5d..f865468fb 100644 --- a/pkg/commands/git_commands/rebase_test.go +++ b/pkg/commands/git_commands/rebase_test.go @@ -16,37 +16,60 @@ import ( func TestRebaseRebaseBranch(t *testing.T) { type scenario struct { - testName string - arg string - runner *oscommands.FakeCmdObjRunner - test func(error) + testName string + arg string + gitVersion *GitVersion + runner *oscommands.FakeCmdObjRunner + test func(error) } scenarios := []scenario{ { - testName: "successful rebase", - arg: "master", + testName: "successful rebase", + arg: "master", + gitVersion: &GitVersion{2, 26, 0, ""}, runner: oscommands.NewFakeRunner(t). - Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", nil), + Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges master`, "", nil), test: func(err error) { assert.NoError(t, err) }, }, { - testName: "unsuccessful rebase", - arg: "master", + testName: "unsuccessful rebase", + arg: "master", + gitVersion: &GitVersion{2, 26, 0, ""}, runner: oscommands.NewFakeRunner(t). - Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", errors.New("error")), + Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges master`, "", errors.New("error")), test: func(err error) { assert.Error(t, err) }, }, + { + testName: "successful rebase (< 2.26.0)", + arg: "master", + gitVersion: &GitVersion{2, 25, 5, ""}, + runner: oscommands.NewFakeRunner(t). + Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash --rebase-merges master`, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, + { + testName: "successful rebase (< 2.22.0)", + arg: "master", + gitVersion: &GitVersion{2, 21, 9, ""}, + runner: oscommands.NewFakeRunner(t). + Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash master`, "", nil), + test: func(err error) { + assert.NoError(t, err) + }, + }, } for _, s := range scenarios { s := s t.Run(s.testName, func(t *testing.T) { - instance := buildRebaseCommands(commonDeps{runner: s.runner}) + instance := buildRebaseCommands(commonDeps{runner: s.runner, gitVersion: s.gitVersion}) s.test(instance.RebaseBranch(s.arg)) }) } @@ -126,7 +149,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) { commitIndex: 0, fileName: "test999.txt", runner: oscommands.NewFakeRunner(t). - Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash abcdef`, "", nil). + Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges abcdef`, "", nil). Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil). Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil). Expect(`git commit --amend --no-edit --allow-empty`, "", nil). @@ -143,8 +166,9 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) { s := s t.Run(s.testName, func(t *testing.T) { instance := buildRebaseCommands(commonDeps{ - runner: s.runner, - gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses), + runner: s.runner, + gitVersion: &GitVersion{2, 26, 0, ""}, + gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses), }) s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName)) diff --git a/pkg/config/editor_presets.go b/pkg/config/editor_presets.go index d461112d0..76b9d5996 100644 --- a/pkg/config/editor_presets.go +++ b/pkg/config/editor_presets.go @@ -35,6 +35,7 @@ type editPreset struct { editInTerminal bool } +// IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset { presets := map[string]*editPreset{ "vi": standardTerminalEditorPreset("vi"), diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index dfed29c44..5a9eb9214 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -80,6 +80,10 @@ func (self *guiCommon) ActivateContext(context types.Context) error { return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{}) } +func (self *guiCommon) ActivateContext(context types.Context) error { + return self.gui.activateContext(context, types.OnFocusOpts{}) +} + func (self *guiCommon) GetAppState() *config.AppState { return self.gui.Config.GetAppState() } diff --git a/pkg/integration/components/test.go b/pkg/integration/components/test.go index 847781c8e..f4838ad86 100644 --- a/pkg/integration/components/test.go +++ b/pkg/integration/components/test.go @@ -60,7 +60,7 @@ type GitVersionRestriction struct { } // Verifies the version is at least the given version (inclusive) -func From(version string) GitVersionRestriction { +func AtLeast(version string) GitVersionRestriction { return GitVersionRestriction{from: version} } diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index 062382c2d..d15f86b0c 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -96,18 +96,18 @@ func TestGitVersionRestriction(t *testing.T) { expectedShouldRun bool }{ { - testName: "From, current is newer", - gitVersion: From("2.24.9"), + testName: "AtLeast, current is newer", + gitVersion: AtLeast("2.24.9"), expectedShouldRun: true, }, { - testName: "From, current is same", - gitVersion: From("2.25.0"), + testName: "AtLeast, current is same", + gitVersion: AtLeast("2.25.0"), expectedShouldRun: true, }, { - testName: "From, current is older", - gitVersion: From("2.26.0"), + testName: "AtLeast, current is older", + gitVersion: AtLeast("2.26.0"), expectedShouldRun: false, }, { diff --git a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go index ace5bed40..e8d1e170a 100644 --- a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go +++ b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go @@ -9,7 +9,7 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{ Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file", ExtraCmdArgs: "", Skip: false, - GitVersion: From("2.38.0"), + GitVersion: AtLeast("2.38.0"), SetupConfig: func(config *config.AppConfig) {}, SetupRepo: func(shell *Shell) { shell. diff --git a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go index cb42ce989..6321891a7 100644 --- a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go +++ b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go @@ -9,7 +9,7 @@ var DropTodoCommitWithUpdateRefShowBranchHeads = NewIntegrationTest(NewIntegrati Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file (with experimentalShowBranchHeads on)", ExtraCmdArgs: "", Skip: false, - GitVersion: From("2.38.0"), + GitVersion: AtLeast("2.38.0"), SetupConfig: func(config *config.AppConfig) { config.UserConfig.Gui.ExperimentalShowBranchHeads = true }, diff --git a/pkg/integration/tests/patch_building/move_to_earlier_commit.go b/pkg/integration/tests/patch_building/move_to_earlier_commit.go index 5803737f0..98bf6fa05 100644 --- a/pkg/integration/tests/patch_building/move_to_earlier_commit.go +++ b/pkg/integration/tests/patch_building/move_to_earlier_commit.go @@ -9,6 +9,7 @@ var MoveToEarlierCommit = NewIntegrationTest(NewIntegrationTestArgs{ Description: "Move a patch from a commit to an earlier commit", ExtraCmdArgs: "", Skip: false, + GitVersion: AtLeast("2.26.0"), SetupConfig: func(config *config.AppConfig) {}, SetupRepo: func(shell *Shell) { shell.CreateDir("dir") diff --git a/pkg/integration/tests/patch_building/move_to_earlier_commit_no_keep_empty.go b/pkg/integration/tests/patch_building/move_to_earlier_commit_no_keep_empty.go new file mode 100644 index 000000000..a44ba3438 --- /dev/null +++ b/pkg/integration/tests/patch_building/move_to_earlier_commit_no_keep_empty.go @@ -0,0 +1,77 @@ +package patch_building + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var MoveToEarlierCommitNoKeepEmpty = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Move a patch from a commit to an earlier commit, for older git versions that don't keep the empty commit", + ExtraCmdArgs: "", + Skip: false, + GitVersion: Before("2.26.0"), + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateDir("dir") + shell.CreateFileAndAdd("dir/file1", "file1 content") + shell.CreateFileAndAdd("dir/file2", "file2 content") + shell.Commit("first commit") + + shell.CreateFileAndAdd("unrelated-file", "") + shell.Commit("destination commit") + + shell.UpdateFileAndAdd("dir/file1", "file1 content with old changes") + shell.DeleteFileAndAdd("dir/file2") + shell.CreateFileAndAdd("dir/file3", "file3 content") + shell.Commit("commit to move from") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("commit to move from").IsSelected(), + Contains("destination commit"), + Contains("first commit"), + ). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Contains("dir").IsSelected(), + Contains(" M file1"), + Contains(" D file2"), + Contains(" A file3"), + ). + PressPrimaryAction(). + PressEscape() + + t.Views().Information().Content(Contains("building patch")) + + t.Views().Commits(). + IsFocused(). + SelectNextItem() + + t.Common().SelectPatchOption(Contains("move patch to selected commit")) + + t.Views().Commits(). + IsFocused(). + Lines( + Contains("destination commit"), + Contains("first commit").IsSelected(), + ). + SelectPreviousItem(). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Contains("dir").IsSelected(), + Contains(" M file1"), + Contains(" D file2"), + Contains(" A file3"), + Contains("A unrelated-file"), + ). + PressEscape() + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index b2cb12e14..fe6604ac1 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -117,6 +117,7 @@ var tests = []*components.IntegrationTest{ patch_building.ApplyInReverseWithConflict, patch_building.CopyPatchToClipboard, patch_building.MoveToEarlierCommit, + patch_building.MoveToEarlierCommitNoKeepEmpty, patch_building.MoveToIndex, patch_building.MoveToIndexPartOfAdjacentAddedLines, patch_building.MoveToIndexPartial, diff --git a/pkg/utils/yaml_utils/yaml_utils.go b/pkg/utils/yaml_utils/yaml_utils.go new file mode 100644 index 000000000..9ed7ae875 --- /dev/null +++ b/pkg/utils/yaml_utils/yaml_utils.go @@ -0,0 +1,54 @@ +package yaml_utils + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// takes a yaml document in bytes, a path to a key, and a value to set. The value must be a scalar. +func UpdateYaml(yamlBytes []byte, path []string, value string) ([]byte, error) { + // Parse the YAML file. + var node yaml.Node + err := yaml.Unmarshal(yamlBytes, &node) + if err != nil { + return nil, fmt.Errorf("failed to parse YAML: %w", err) + } + + body := node.Content[0] + + updateYamlNode(body, path, value) + + // Convert the updated YAML node back to YAML bytes. + updatedYAMLBytes, err := yaml.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to convert YAML node to bytes: %w", err) + } + + return updatedYAMLBytes, nil +} + +// Recursive function to update the YAML node. +func updateYamlNode(node *yaml.Node, path []string, value string) { + if len(path) == 0 { + node.Value = value + return + } + + key := path[0] + for i := 0; i < len(node.Content)-1; i += 2 { + if node.Content[i].Value == key { + updateYamlNode(node.Content[i+1], path[1:], value) + return + } + } + + // if the key doesn't exist, we'll add it + node.Content = append(node.Content, &yaml.Node{ + Kind: yaml.ScalarNode, + Value: key, + }, &yaml.Node{ + Kind: yaml.ScalarNode, + Value: value, + }) +} diff --git a/pkg/utils/yaml_utils/yaml_utils_test.go b/pkg/utils/yaml_utils/yaml_utils_test.go new file mode 100644 index 000000000..4bdfeb432 --- /dev/null +++ b/pkg/utils/yaml_utils/yaml_utils_test.go @@ -0,0 +1,64 @@ +package yaml_utils + +import "testing" + +func TestUpdateYaml(t *testing.T) { + tests := []struct { + name string + in string + path []string + value string + expectedOut string + expectedErr string + }{ + { + name: "update value", + in: "foo: bar\n", + path: []string{"foo"}, + value: "baz", + expectedOut: "foo: baz\n", + expectedErr: "", + }, + { + name: "add new key and value", + in: "foo: bar\n", + path: []string{"foo2"}, + value: "baz", + expectedOut: "foo: bar\nfoo2: baz\n", + expectedErr: "", + }, + { + name: "preserve inline comment", + in: "foo: bar # my comment\n", + path: []string{"foo2"}, + value: "baz", + expectedOut: "foo: bar # my comment\nfoo2: baz\n", + expectedErr: "", + }, + { + name: "nested update", + in: "foo:\n bar: baz\n", + path: []string{"foo", "bar"}, + value: "qux", + // indentation is not preserved. See https://github.com/go-yaml/yaml/issues/899 + expectedOut: "foo:\n bar: qux\n", + expectedErr: "", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + out, err := UpdateYaml([]byte(test.in), test.path, test.value) + if test.expectedErr != "" { + if err == nil { + t.Errorf("expected error %q but got none", test.expectedErr) + } + } else if err != nil { + t.Errorf("unexpected error: %v", err) + } else if string(out) != test.expectedOut { + t.Errorf("expected %q but got %q", test.expectedOut, string(out)) + } + }) + } +}