1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-13 11:50:28 +02:00

Merge branch 'master' into refactor-better-encapsulation

This commit is contained in:
Jesse Duffield 2023-05-11 19:00:01 +10:00
commit 2b30085dba
14 changed files with 263 additions and 31 deletions

View File

@ -252,21 +252,21 @@ keybinding:
```yaml ```yaml
os: os:
openCommand: 'start "" {{filename}}' open: 'start "" {{filename}}'
``` ```
### Linux ### Linux
```yaml ```yaml
os: os:
openCommand: 'xdg-open {{filename}} >/dev/null' open: 'xdg-open {{filename}} >/dev/null'
``` ```
### OSX ### OSX
```yaml ```yaml
os: os:
openCommand: 'open {{filename}}' open: 'open {{filename}}'
``` ```
### Configuring File Editing ### Configuring File Editing
@ -285,9 +285,9 @@ os:
editPreset: 'vscode' editPreset: 'vscode'
``` ```
Supported presets are `vim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`, and Supported presets are `vim`, `nvim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`,
`xcode`. In many cases lazygit will be able to guess the right preset from your `kakoune` and `xcode`. In many cases lazygit will be able to guess the right preset
$(git config core.editor), or an environment variable such as $VISUAL or $EDITOR. 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 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 there simply is no preset for your editor, you can customize the commands by

View File

@ -181,12 +181,18 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract
debug = "TRUE" debug = "TRUE"
} }
emptyArg := " --empty=keep"
if self.version.IsOlderThan(2, 26, 0) {
emptyArg = ""
}
rebaseMergesArg := " --rebase-merges" rebaseMergesArg := " --rebase-merges"
if self.version.IsOlderThan(2, 22, 0) { if self.version.IsOlderThan(2, 22, 0) {
rebaseMergesArg = "" 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") self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr) cmdObj := self.cmd.New(cmdStr)

View File

@ -16,37 +16,60 @@ import (
func TestRebaseRebaseBranch(t *testing.T) { func TestRebaseRebaseBranch(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
arg string arg string
runner *oscommands.FakeCmdObjRunner gitVersion *GitVersion
test func(error) runner *oscommands.FakeCmdObjRunner
test func(error)
} }
scenarios := []scenario{ scenarios := []scenario{
{ {
testName: "successful rebase", testName: "successful rebase",
arg: "master", arg: "master",
gitVersion: &GitVersion{2, 26, 0, ""},
runner: oscommands.NewFakeRunner(t). 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) { test: func(err error) {
assert.NoError(t, err) assert.NoError(t, err)
}, },
}, },
{ {
testName: "unsuccessful rebase", testName: "unsuccessful rebase",
arg: "master", arg: "master",
gitVersion: &GitVersion{2, 26, 0, ""},
runner: oscommands.NewFakeRunner(t). 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) { test: func(err error) {
assert.Error(t, err) 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 { for _, s := range scenarios {
s := s s := s
t.Run(s.testName, func(t *testing.T) { 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)) s.test(instance.RebaseBranch(s.arg))
}) })
} }
@ -126,7 +149,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
commitIndex: 0, commitIndex: 0,
fileName: "test999.txt", fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t). 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 cat-file -e HEAD^:"test999.txt"`, "", nil).
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil). Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
Expect(`git commit --amend --no-edit --allow-empty`, "", nil). Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
@ -143,8 +166,9 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
s := s s := s
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
instance := buildRebaseCommands(commonDeps{ instance := buildRebaseCommands(commonDeps{
runner: s.runner, runner: s.runner,
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses), gitVersion: &GitVersion{2, 26, 0, ""},
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
}) })
s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName)) s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))

View File

@ -35,6 +35,7 @@ type editPreset struct {
editInTerminal bool 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 { func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
presets := map[string]*editPreset{ presets := map[string]*editPreset{
"vi": standardTerminalEditorPreset("vi"), "vi": standardTerminalEditorPreset("vi"),

View File

@ -80,6 +80,10 @@ func (self *guiCommon) ActivateContext(context types.Context) error {
return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{}) 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 { func (self *guiCommon) GetAppState() *config.AppState {
return self.gui.Config.GetAppState() return self.gui.Config.GetAppState()
} }

View File

@ -60,7 +60,7 @@ type GitVersionRestriction struct {
} }
// Verifies the version is at least the given version (inclusive) // Verifies the version is at least the given version (inclusive)
func From(version string) GitVersionRestriction { func AtLeast(version string) GitVersionRestriction {
return GitVersionRestriction{from: version} return GitVersionRestriction{from: version}
} }

View File

@ -96,18 +96,18 @@ func TestGitVersionRestriction(t *testing.T) {
expectedShouldRun bool expectedShouldRun bool
}{ }{
{ {
testName: "From, current is newer", testName: "AtLeast, current is newer",
gitVersion: From("2.24.9"), gitVersion: AtLeast("2.24.9"),
expectedShouldRun: true, expectedShouldRun: true,
}, },
{ {
testName: "From, current is same", testName: "AtLeast, current is same",
gitVersion: From("2.25.0"), gitVersion: AtLeast("2.25.0"),
expectedShouldRun: true, expectedShouldRun: true,
}, },
{ {
testName: "From, current is older", testName: "AtLeast, current is older",
gitVersion: From("2.26.0"), gitVersion: AtLeast("2.26.0"),
expectedShouldRun: false, expectedShouldRun: false,
}, },
{ {

View File

@ -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", Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file",
ExtraCmdArgs: "", ExtraCmdArgs: "",
Skip: false, Skip: false,
GitVersion: From("2.38.0"), GitVersion: AtLeast("2.38.0"),
SetupConfig: func(config *config.AppConfig) {}, SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) { SetupRepo: func(shell *Shell) {
shell. shell.

View File

@ -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)", Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file (with experimentalShowBranchHeads on)",
ExtraCmdArgs: "", ExtraCmdArgs: "",
Skip: false, Skip: false,
GitVersion: From("2.38.0"), GitVersion: AtLeast("2.38.0"),
SetupConfig: func(config *config.AppConfig) { SetupConfig: func(config *config.AppConfig) {
config.UserConfig.Gui.ExperimentalShowBranchHeads = true config.UserConfig.Gui.ExperimentalShowBranchHeads = true
}, },

View File

@ -9,6 +9,7 @@ var MoveToEarlierCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to an earlier commit", Description: "Move a patch from a commit to an earlier commit",
ExtraCmdArgs: "", ExtraCmdArgs: "",
Skip: false, Skip: false,
GitVersion: AtLeast("2.26.0"),
SetupConfig: func(config *config.AppConfig) {}, SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) { SetupRepo: func(shell *Shell) {
shell.CreateDir("dir") shell.CreateDir("dir")

View File

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

View File

@ -117,6 +117,7 @@ var tests = []*components.IntegrationTest{
patch_building.ApplyInReverseWithConflict, patch_building.ApplyInReverseWithConflict,
patch_building.CopyPatchToClipboard, patch_building.CopyPatchToClipboard,
patch_building.MoveToEarlierCommit, patch_building.MoveToEarlierCommit,
patch_building.MoveToEarlierCommitNoKeepEmpty,
patch_building.MoveToIndex, patch_building.MoveToIndex,
patch_building.MoveToIndexPartOfAdjacentAddedLines, patch_building.MoveToIndexPartOfAdjacentAddedLines,
patch_building.MoveToIndexPartial, patch_building.MoveToIndexPartial,

View File

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

View File

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