mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-08-10 22:42:00 +02:00
Print migration changes to the console when migrating config file (#4548)
- **PR Description** This might be useful to see in general (users will normally only see it after they quit lazygit again, but still). But it is especially useful when writing back the config file fails; some users have their config file in a read-only location, so we had reports of lazygit no longer starting up when migration was necessary. #4210 was supposed to improve this a bit, but it didn't tell users what changes need to be made to the config file. Now we tell them, and users can then make these changes manually if they want. We do this only at startup, when the GUI hasn't started yet. This is probably good enough, because it is much less likely that writing back a migrated repo-local config fails because it is not writeable. Example output: ``` The user config file /Users/stk/Library/Application Support/lazygit/config.yml must be migrated. Attempting to do this automatically. The following changes were made: - Renamed 'gui.windowSize' to 'screenMode' - Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.confirmInEditor' - Changed 'stream: true' to 'output: log' in custom command Config file saved successfully to /Users/stk/Library/Application Support/lazygit/config.yml ``` The branch also contains a lot of code cleanups. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [ ] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [ ] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
This commit is contained in:
2
go.mod
2
go.mod
@@ -13,7 +13,7 @@ require (
|
|||||||
github.com/go-errors/errors v1.5.1
|
github.com/go-errors/errors v1.5.1
|
||||||
github.com/gookit/color v1.4.2
|
github.com/gookit/color v1.4.2
|
||||||
github.com/integrii/flaggy v1.4.0
|
github.com/integrii/flaggy v1.4.0
|
||||||
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918
|
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
||||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20250421160159-82c9aaeba2b9
|
github.com/jesseduffield/gocui v0.3.1-0.20250421160159-82c9aaeba2b9
|
||||||
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
|
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
|
||||||
|
4
go.sum
4
go.sum
@@ -190,8 +190,8 @@ github.com/invopop/jsonschema v0.10.0 h1:c1ktzNLBun3LyQQhyty5WE3lulbOdIIyOVlkmDL
|
|||||||
github.com/invopop/jsonschema v0.10.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
github.com/invopop/jsonschema v0.10.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918 h1:meoUDZGF6jZAbhW5IBwj92mTqGmrOn+Cuu0jM7/aUcs=
|
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2PaiisXAC5sOjDPfMArSnbswDObtCssx+xn28edX4=
|
||||||
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
|
||||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
|
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
|
||||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
|
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20250421160159-82c9aaeba2b9 h1:k23sCKHCNpAvwJP8Yr16CBUItuarmUHBGH7FaAm2glc=
|
github.com/jesseduffield/gocui v0.3.1-0.20250421160159-82c9aaeba2b9 h1:k23sCKHCNpAvwJP8Yr16CBUItuarmUHBGH7FaAm2glc=
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
|
"github.com/jesseduffield/generics/orderedset"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils/yaml_utils"
|
"github.com/jesseduffield/lazygit/pkg/utils/yaml_utils"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -96,7 +97,7 @@ func NewAppConfig(
|
|||||||
configFiles = []*ConfigFile{configFile}
|
configFiles = []*ConfigFile{configFile}
|
||||||
}
|
}
|
||||||
|
|
||||||
userConfig, err := loadUserConfigWithDefaults(configFiles)
|
userConfig, err := loadUserConfigWithDefaults(configFiles, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -145,11 +146,11 @@ func findOrCreateConfigDir() (string, error) {
|
|||||||
return folder, os.MkdirAll(folder, 0o755)
|
return folder, os.MkdirAll(folder, 0o755)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserConfigWithDefaults(configFiles []*ConfigFile) (*UserConfig, error) {
|
func loadUserConfigWithDefaults(configFiles []*ConfigFile, isGuiInitialized bool) (*UserConfig, error) {
|
||||||
return loadUserConfig(configFiles, GetDefaultConfig())
|
return loadUserConfig(configFiles, GetDefaultConfig(), isGuiInitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, error) {
|
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig, isGuiInitialized bool) (*UserConfig, error) {
|
||||||
for _, configFile := range configFiles {
|
for _, configFile := range configFiles {
|
||||||
path := configFile.Path
|
path := configFile.Path
|
||||||
statInfo, err := os.Stat(path)
|
statInfo, err := os.Stat(path)
|
||||||
@@ -194,7 +195,7 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err = migrateUserConfig(path, content)
|
content, err = migrateUserConfig(path, content, isGuiInitialized)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -215,41 +216,64 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
|
|||||||
return base, nil
|
return base, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChangesSet = orderedset.OrderedSet[string]
|
||||||
|
|
||||||
|
func NewChangesSet() *ChangesSet {
|
||||||
|
return orderedset.New[string]()
|
||||||
|
}
|
||||||
|
|
||||||
// Do any backward-compatibility migrations of things that have changed in the
|
// Do any backward-compatibility migrations of things that have changed in the
|
||||||
// config over time; examples are renaming a key to a better name, moving a key
|
// config over time; examples are renaming a key to a better name, moving a key
|
||||||
// from one container to another, or changing the type of a key (e.g. from bool
|
// from one container to another, or changing the type of a key (e.g. from bool
|
||||||
// to an enum).
|
// to an enum).
|
||||||
func migrateUserConfig(path string, content []byte) ([]byte, error) {
|
func migrateUserConfig(path string, content []byte, isGuiInitialized bool) ([]byte, error) {
|
||||||
changedContent, err := computeMigratedConfig(path, content)
|
changes := NewChangesSet()
|
||||||
|
|
||||||
|
changedContent, didChange, err := computeMigratedConfig(path, content, changes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write config back if changed
|
// Nothing to do if config didn't change
|
||||||
if string(changedContent) != string(content) {
|
if !didChange {
|
||||||
fmt.Println("Provided user config is deprecated but auto-fixable. Attempting to write fixed version back to file...")
|
return content, nil
|
||||||
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
|
|
||||||
return nil, fmt.Errorf("While attempting to write back fixed user config to %s, an error occurred: %s", path, err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Success. New config written to %s\n", path)
|
|
||||||
return changedContent, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return content, nil
|
changesText := "The following changes were made:\n\n"
|
||||||
|
changesText += strings.Join(lo.Map(changes.ToSliceFromOldest(), func(change string, _ int) string {
|
||||||
|
return fmt.Sprintf("- %s\n", change)
|
||||||
|
}), "")
|
||||||
|
|
||||||
|
// Write config back
|
||||||
|
if !isGuiInitialized {
|
||||||
|
fmt.Printf("The user config file %s must be migrated. Attempting to do this automatically.\n", path)
|
||||||
|
fmt.Println(changesText)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
|
||||||
|
errorMsg := fmt.Sprintf("While attempting to write back migrated user config to %s, an error occurred: %s", path, err)
|
||||||
|
if isGuiInitialized {
|
||||||
|
errorMsg += "\n\n" + changesText
|
||||||
|
}
|
||||||
|
return nil, errors.New(errorMsg)
|
||||||
|
}
|
||||||
|
if !isGuiInitialized {
|
||||||
|
fmt.Printf("Config file saved successfully to %s\n", path)
|
||||||
|
}
|
||||||
|
return changedContent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// A pure function helper for testing purposes
|
// A pure function helper for testing purposes
|
||||||
func computeMigratedConfig(path string, content []byte) ([]byte, error) {
|
func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]byte, bool, error) {
|
||||||
var err error
|
var err error
|
||||||
var rootNode yaml.Node
|
var rootNode yaml.Node
|
||||||
err = yaml.Unmarshal(content, &rootNode)
|
err = yaml.Unmarshal(content, &rootNode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse YAML: %w", err)
|
return nil, false, fmt.Errorf("failed to parse YAML: %w", err)
|
||||||
}
|
}
|
||||||
var originalCopy yaml.Node
|
var originalCopy yaml.Node
|
||||||
err = yaml.Unmarshal(content, &originalCopy)
|
err = yaml.Unmarshal(content, &originalCopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse YAML, but only the second time!?!? How did that happen: %w", err)
|
return nil, false, fmt.Errorf("failed to parse YAML, but only the second time!?!? How did that happen: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pathsToReplace := []struct {
|
pathsToReplace := []struct {
|
||||||
@@ -262,60 +286,64 @@ func computeMigratedConfig(path string, content []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, pathToReplace := range pathsToReplace {
|
for _, pathToReplace := range pathsToReplace {
|
||||||
err := yaml_utils.RenameYamlKey(&rootNode, pathToReplace.oldPath, pathToReplace.newName)
|
err, didReplace := yaml_utils.RenameYamlKey(&rootNode, pathToReplace.oldPath, pathToReplace.newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %s", path, strings.Join(pathToReplace.oldPath, "."), err)
|
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %s", path, strings.Join(pathToReplace.oldPath, "."), err)
|
||||||
|
}
|
||||||
|
if didReplace {
|
||||||
|
changes.Add(fmt.Sprintf("Renamed '%s' to '%s'", strings.Join(pathToReplace.oldPath, "."), pathToReplace.newName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = changeNullKeybindingsToDisabled(&rootNode)
|
err = changeNullKeybindingsToDisabled(&rootNode, changes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = changeElementToSequence(&rootNode, []string{"git", "commitPrefix"})
|
err = changeElementToSequence(&rootNode, []string{"git", "commitPrefix"}, changes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = changeCommitPrefixesMap(&rootNode)
|
err = changeCommitPrefixesMap(&rootNode, changes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = changeCustomCommandStreamAndOutputToOutputEnum(&rootNode)
|
err = changeCustomCommandStreamAndOutputToOutputEnum(&rootNode, changes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = migrateAllBranchesLogCmd(&rootNode)
|
err = migrateAllBranchesLogCmd(&rootNode, changes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add more migrations here...
|
// Add more migrations here...
|
||||||
|
|
||||||
if !reflect.DeepEqual(rootNode, originalCopy) {
|
if reflect.DeepEqual(rootNode, originalCopy) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
newContent, err := yaml_utils.YamlMarshal(&rootNode)
|
newContent, err := yaml_utils.YamlMarshal(&rootNode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to remarsal!\n %w", err)
|
return nil, false, fmt.Errorf("Failed to remarsal!\n %w", err)
|
||||||
}
|
|
||||||
return newContent, nil
|
|
||||||
} else {
|
|
||||||
return content, nil
|
|
||||||
}
|
}
|
||||||
|
return newContent, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeNullKeybindingsToDisabled(rootNode *yaml.Node) error {
|
func changeNullKeybindingsToDisabled(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||||
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
|
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
|
||||||
if strings.HasPrefix(path, "keybinding.") && node.Kind == yaml.ScalarNode && node.Tag == "!!null" {
|
if strings.HasPrefix(path, "keybinding.") && node.Kind == yaml.ScalarNode && node.Tag == "!!null" {
|
||||||
node.Value = "<disabled>"
|
node.Value = "<disabled>"
|
||||||
node.Tag = "!!str"
|
node.Tag = "!!str"
|
||||||
|
changes.Add(fmt.Sprintf("Changed 'null' to '<disabled>' for keybinding '%s'", path))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeElementToSequence(rootNode *yaml.Node, path []string) error {
|
func changeElementToSequence(rootNode *yaml.Node, path []string, changes *ChangesSet) error {
|
||||||
return yaml_utils.TransformNode(rootNode, path, func(node *yaml.Node) error {
|
return yaml_utils.TransformNode(rootNode, path, func(node *yaml.Node) error {
|
||||||
if node.Kind == yaml.MappingNode {
|
if node.Kind == yaml.MappingNode {
|
||||||
nodeContentCopy := node.Content
|
nodeContentCopy := node.Content
|
||||||
@@ -327,13 +355,15 @@ func changeElementToSequence(rootNode *yaml.Node, path []string) error {
|
|||||||
Content: nodeContentCopy,
|
Content: nodeContentCopy,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
changes.Add(fmt.Sprintf("Changed '%s' to an array of strings", strings.Join(path, ".")))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeCommitPrefixesMap(rootNode *yaml.Node) error {
|
func changeCommitPrefixesMap(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||||
return yaml_utils.TransformNode(rootNode, []string{"git", "commitPrefixes"}, func(prefixesNode *yaml.Node) error {
|
return yaml_utils.TransformNode(rootNode, []string{"git", "commitPrefixes"}, func(prefixesNode *yaml.Node) error {
|
||||||
if prefixesNode.Kind == yaml.MappingNode {
|
if prefixesNode.Kind == yaml.MappingNode {
|
||||||
for _, contentNode := range prefixesNode.Content {
|
for _, contentNode := range prefixesNode.Content {
|
||||||
@@ -346,6 +376,7 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
|
|||||||
Kind: yaml.MappingNode,
|
Kind: yaml.MappingNode,
|
||||||
Content: nodeContentCopy,
|
Content: nodeContentCopy,
|
||||||
}}
|
}}
|
||||||
|
changes.Add("Changed 'git.commitPrefixes' elements to arrays of strings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,7 +384,7 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node) error {
|
func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||||
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
|
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
|
||||||
// We are being lazy here and rely on the fact that the only mapping
|
// We are being lazy here and rely on the fact that the only mapping
|
||||||
// nodes in the tree under customCommands are actual custom commands. If
|
// nodes in the tree under customCommands are actual custom commands. If
|
||||||
@@ -364,16 +395,25 @@ func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node) error {
|
|||||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "subprocess"); streamKey != nil {
|
if streamKey, streamValue := yaml_utils.RemoveKey(node, "subprocess"); streamKey != nil {
|
||||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" {
|
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" {
|
||||||
output = "terminal"
|
output = "terminal"
|
||||||
|
changes.Add("Changed 'subprocess: true' to 'output: terminal' in custom command")
|
||||||
|
} else {
|
||||||
|
changes.Add("Deleted redundant 'subprocess: false' in custom command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "stream"); streamKey != nil {
|
if streamKey, streamValue := yaml_utils.RemoveKey(node, "stream"); streamKey != nil {
|
||||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
||||||
output = "log"
|
output = "log"
|
||||||
|
changes.Add("Changed 'stream: true' to 'output: log' in custom command")
|
||||||
|
} else {
|
||||||
|
changes.Add(fmt.Sprintf("Deleted redundant 'stream: %v' property in custom command", streamValue.Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "showOutput"); streamKey != nil {
|
if streamKey, streamValue := yaml_utils.RemoveKey(node, "showOutput"); streamKey != nil {
|
||||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
||||||
|
changes.Add("Changed 'showOutput: true' to 'output: popup' in custom command")
|
||||||
output = "popup"
|
output = "popup"
|
||||||
|
} else {
|
||||||
|
changes.Add(fmt.Sprintf("Deleted redundant 'showOutput: %v' property in custom command", streamValue.Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if output != "" {
|
if output != "" {
|
||||||
@@ -397,7 +437,7 @@ func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node) error {
|
|||||||
// a single element at `allBranchesLogCmd` and the sequence at `allBranchesLogCmds`.
|
// a single element at `allBranchesLogCmd` and the sequence at `allBranchesLogCmds`.
|
||||||
// Some users have explicitly set `allBranchesLogCmd` to be an empty string in order
|
// Some users have explicitly set `allBranchesLogCmd` to be an empty string in order
|
||||||
// to remove it, so in that case we just delete the element, and add nothing to the list
|
// to remove it, so in that case we just delete the element, and add nothing to the list
|
||||||
func migrateAllBranchesLogCmd(rootNode *yaml.Node) error {
|
func migrateAllBranchesLogCmd(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||||
return yaml_utils.TransformNode(rootNode, []string{"git"}, func(gitNode *yaml.Node) error {
|
return yaml_utils.TransformNode(rootNode, []string{"git"}, func(gitNode *yaml.Node) error {
|
||||||
cmdKeyNode, cmdValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmd")
|
cmdKeyNode, cmdValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmd")
|
||||||
// Nothing to do if they do not have the deprecated item
|
// Nothing to do if they do not have the deprecated item
|
||||||
@@ -406,6 +446,7 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdsKeyNode, cmdsValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmds")
|
cmdsKeyNode, cmdsValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmds")
|
||||||
|
var change string
|
||||||
if cmdsKeyNode == nil {
|
if cmdsKeyNode == nil {
|
||||||
// Create empty sequence node and attach it onto the root git node
|
// Create empty sequence node and attach it onto the root git node
|
||||||
// We will later populate it with the individual allBranchesLogCmd record
|
// We will later populate it with the individual allBranchesLogCmd record
|
||||||
@@ -415,17 +456,24 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node) error {
|
|||||||
cmdsKeyNode,
|
cmdsKeyNode,
|
||||||
cmdsValueNode,
|
cmdsValueNode,
|
||||||
)
|
)
|
||||||
} else if cmdsValueNode.Kind != yaml.SequenceNode {
|
change = "Created git.allBranchesLogCmds array containing value of git.allBranchesLogCmd"
|
||||||
|
} else {
|
||||||
|
if cmdsValueNode.Kind != yaml.SequenceNode {
|
||||||
return errors.New("You should have an allBranchesLogCmds defined as a sequence!")
|
return errors.New("You should have an allBranchesLogCmds defined as a sequence!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
change = "Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array"
|
||||||
|
}
|
||||||
|
|
||||||
if cmdValueNode.Value != "" {
|
if cmdValueNode.Value != "" {
|
||||||
// Prepending the individual element to make it show up first in the list, which was prior behavior
|
// Prepending the individual element to make it show up first in the list, which was prior behavior
|
||||||
cmdsValueNode.Content = utils.Prepend(cmdsValueNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: cmdValueNode.Value})
|
cmdsValueNode.Content = utils.Prepend(cmdsValueNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: cmdValueNode.Value})
|
||||||
|
changes.Add(change)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out the existing allBranchesLogCmd, now that we have migrated it into the list
|
// Clear out the existing allBranchesLogCmd, now that we have migrated it into the list
|
||||||
_, _ = yaml_utils.RemoveKey(gitNode, "allBranchesLogCmd")
|
_, _ = yaml_utils.RemoveKey(gitNode, "allBranchesLogCmd")
|
||||||
|
changes.Add("Removed obsolete git.allBranchesLogCmd")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -471,7 +519,7 @@ func (c *AppConfig) GetUserConfigDir() string {
|
|||||||
|
|
||||||
func (c *AppConfig) ReloadUserConfigForRepo(repoConfigFiles []*ConfigFile) error {
|
func (c *AppConfig) ReloadUserConfigForRepo(repoConfigFiles []*ConfigFile) error {
|
||||||
configFiles := append(c.globalUserConfigFiles, repoConfigFiles...)
|
configFiles := append(c.globalUserConfigFiles, repoConfigFiles...)
|
||||||
userConfig, err := loadUserConfigWithDefaults(configFiles)
|
userConfig, err := loadUserConfigWithDefaults(configFiles, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -496,7 +544,7 @@ func (c *AppConfig) ReloadChangedUserConfigFiles() (error, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
userConfig, err := loadUserConfigWithDefaults(c.userConfigFiles)
|
userConfig, err := loadUserConfigWithDefaults(c.userConfigFiles, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, false
|
return err, false
|
||||||
}
|
}
|
||||||
|
@@ -6,17 +6,165 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMigrationOfRenamedKeys(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
expectedDidChange bool
|
||||||
|
expectedChanges []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty String",
|
||||||
|
input: "",
|
||||||
|
expectedDidChange: false,
|
||||||
|
expectedChanges: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No rename needed",
|
||||||
|
input: `foo:
|
||||||
|
bar: 5
|
||||||
|
`,
|
||||||
|
expectedDidChange: false,
|
||||||
|
expectedChanges: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rename one",
|
||||||
|
input: `gui:
|
||||||
|
skipUnstageLineWarning: true
|
||||||
|
`,
|
||||||
|
expected: `gui:
|
||||||
|
skipDiscardChangeWarning: true
|
||||||
|
`,
|
||||||
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Renamed 'gui.skipUnstageLineWarning' to 'skipDiscardChangeWarning'"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rename several",
|
||||||
|
input: `gui:
|
||||||
|
windowSize: half
|
||||||
|
skipUnstageLineWarning: true
|
||||||
|
keybinding:
|
||||||
|
universal:
|
||||||
|
executeCustomCommand: a
|
||||||
|
`,
|
||||||
|
expected: `gui:
|
||||||
|
screenMode: half
|
||||||
|
skipDiscardChangeWarning: true
|
||||||
|
keybinding:
|
||||||
|
universal:
|
||||||
|
executeShellCommand: a
|
||||||
|
`,
|
||||||
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Renamed 'gui.skipUnstageLineWarning' to 'skipDiscardChangeWarning'",
|
||||||
|
"Renamed 'keybinding.universal.executeCustomCommand' to 'executeShellCommand'",
|
||||||
|
"Renamed 'gui.windowSize' to 'screenMode'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
|
changes := NewChangesSet()
|
||||||
|
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, s.expectedDidChange, didChange)
|
||||||
|
if didChange {
|
||||||
|
assert.Equal(t, s.expected, string(actual))
|
||||||
|
}
|
||||||
|
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateNullKeybindingsToDisabled(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
expectedDidChange bool
|
||||||
|
expectedChanges []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty String",
|
||||||
|
input: "",
|
||||||
|
expectedDidChange: false,
|
||||||
|
expectedChanges: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No change needed",
|
||||||
|
input: `keybinding:
|
||||||
|
universal:
|
||||||
|
quit: q
|
||||||
|
`,
|
||||||
|
expectedDidChange: false,
|
||||||
|
expectedChanges: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Change one",
|
||||||
|
input: `keybinding:
|
||||||
|
universal:
|
||||||
|
quit: null
|
||||||
|
`,
|
||||||
|
expected: `keybinding:
|
||||||
|
universal:
|
||||||
|
quit: <disabled>
|
||||||
|
`,
|
||||||
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.quit'"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Change several",
|
||||||
|
input: `keybinding:
|
||||||
|
universal:
|
||||||
|
quit: null
|
||||||
|
return: <esc>
|
||||||
|
new: null
|
||||||
|
`,
|
||||||
|
expected: `keybinding:
|
||||||
|
universal:
|
||||||
|
quit: <disabled>
|
||||||
|
return: <esc>
|
||||||
|
new: <disabled>
|
||||||
|
`,
|
||||||
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.quit'",
|
||||||
|
"Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.new'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
|
changes := NewChangesSet()
|
||||||
|
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, s.expectedDidChange, didChange)
|
||||||
|
if didChange {
|
||||||
|
assert.Equal(t, s.expected, string(actual))
|
||||||
|
}
|
||||||
|
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCommitPrefixMigrations(t *testing.T) {
|
func TestCommitPrefixMigrations(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
expected string
|
expected string
|
||||||
|
expectedDidChange bool
|
||||||
|
expectedChanges []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Empty String",
|
name: "Empty String",
|
||||||
input: "",
|
input: "",
|
||||||
expected: "",
|
expectedDidChange: false,
|
||||||
}, {
|
expectedChanges: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Single CommitPrefix Rename",
|
name: "Single CommitPrefix Rename",
|
||||||
input: `git:
|
input: `git:
|
||||||
commitPrefix:
|
commitPrefix:
|
||||||
@@ -28,7 +176,10 @@ func TestCommitPrefixMigrations(t *testing.T) {
|
|||||||
- pattern: "^\\w+-\\w+.*"
|
- pattern: "^\\w+-\\w+.*"
|
||||||
replace: '[JIRA $0] '
|
replace: '[JIRA $0] '
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Changed 'git.commitPrefix' to an array of strings"},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Complicated CommitPrefixes Rename",
|
name: "Complicated CommitPrefixes Rename",
|
||||||
input: `git:
|
input: `git:
|
||||||
commitPrefixes:
|
commitPrefixes:
|
||||||
@@ -48,13 +199,16 @@ func TestCommitPrefixMigrations(t *testing.T) {
|
|||||||
- pattern: "^foo.bar*"
|
- pattern: "^foo.bar*"
|
||||||
replace: '[FUN $0] '
|
replace: '[FUN $0] '
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Changed 'git.commitPrefixes' elements to arrays of strings"},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Incomplete Configuration",
|
name: "Incomplete Configuration",
|
||||||
input: "git:",
|
input: "git:",
|
||||||
expected: "git:",
|
expectedDidChange: false,
|
||||||
}, {
|
expectedChanges: []string{},
|
||||||
// This test intentionally uses non-standard indentation to test that the migration
|
},
|
||||||
// does not change the input.
|
{
|
||||||
name: "No changes made when already migrated",
|
name: "No changes made when already migrated",
|
||||||
input: `
|
input: `
|
||||||
git:
|
git:
|
||||||
@@ -65,25 +219,21 @@ git:
|
|||||||
foo:
|
foo:
|
||||||
- pattern: "^\\w+-\\w+.*"
|
- pattern: "^\\w+-\\w+.*"
|
||||||
replace: '[JIRA $0] '`,
|
replace: '[JIRA $0] '`,
|
||||||
expected: `
|
expectedDidChange: false,
|
||||||
git:
|
expectedChanges: []string{},
|
||||||
commitPrefix:
|
|
||||||
- pattern: "Hello World"
|
|
||||||
replace: "Goodbye"
|
|
||||||
commitPrefixes:
|
|
||||||
foo:
|
|
||||||
- pattern: "^\\w+-\\w+.*"
|
|
||||||
replace: '[JIRA $0] '`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
t.Run(s.name, func(t *testing.T) {
|
t.Run(s.name, func(t *testing.T) {
|
||||||
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
|
changes := NewChangesSet()
|
||||||
if err != nil {
|
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||||
t.Error(err)
|
assert.NoError(t, err)
|
||||||
}
|
assert.Equal(t, s.expectedDidChange, didChange)
|
||||||
|
if didChange {
|
||||||
assert.Equal(t, s.expected, string(actual))
|
assert.Equal(t, s.expected, string(actual))
|
||||||
|
}
|
||||||
|
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,12 +243,16 @@ func TestCustomCommandsOutputMigration(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
expected string
|
expected string
|
||||||
|
expectedDidChange bool
|
||||||
|
expectedChanges []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Empty String",
|
name: "Empty String",
|
||||||
input: "",
|
input: "",
|
||||||
expected: "",
|
expectedDidChange: false,
|
||||||
}, {
|
expectedChanges: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Convert subprocess to output=terminal",
|
name: "Convert subprocess to output=terminal",
|
||||||
input: `customCommands:
|
input: `customCommands:
|
||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
@@ -108,7 +262,10 @@ func TestCustomCommandsOutputMigration(t *testing.T) {
|
|||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
output: terminal
|
output: terminal
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Changed 'subprocess: true' to 'output: terminal' in custom command"},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Convert stream to output=log",
|
name: "Convert stream to output=log",
|
||||||
input: `customCommands:
|
input: `customCommands:
|
||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
@@ -118,7 +275,10 @@ func TestCustomCommandsOutputMigration(t *testing.T) {
|
|||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
output: log
|
output: log
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Changed 'stream: true' to 'output: log' in custom command"},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Convert showOutput to output=popup",
|
name: "Convert showOutput to output=popup",
|
||||||
input: `customCommands:
|
input: `customCommands:
|
||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
@@ -128,7 +288,10 @@ func TestCustomCommandsOutputMigration(t *testing.T) {
|
|||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
output: popup
|
output: popup
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Changed 'showOutput: true' to 'output: popup' in custom command"},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Subprocess wins over the other two",
|
name: "Subprocess wins over the other two",
|
||||||
input: `customCommands:
|
input: `customCommands:
|
||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
@@ -140,7 +303,14 @@ func TestCustomCommandsOutputMigration(t *testing.T) {
|
|||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
output: terminal
|
output: terminal
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Changed 'subprocess: true' to 'output: terminal' in custom command",
|
||||||
|
"Deleted redundant 'stream: true' property in custom command",
|
||||||
|
"Deleted redundant 'showOutput: true' property in custom command",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Stream wins over showOutput",
|
name: "Stream wins over showOutput",
|
||||||
input: `customCommands:
|
input: `customCommands:
|
||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
@@ -151,7 +321,13 @@ func TestCustomCommandsOutputMigration(t *testing.T) {
|
|||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
output: log
|
output: log
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Changed 'stream: true' to 'output: log' in custom command",
|
||||||
|
"Deleted redundant 'showOutput: true' property in custom command",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Explicitly setting to false doesn't create an output=none key",
|
name: "Explicitly setting to false doesn't create an output=none key",
|
||||||
input: `customCommands:
|
input: `customCommands:
|
||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
@@ -162,14 +338,25 @@ func TestCustomCommandsOutputMigration(t *testing.T) {
|
|||||||
expected: `customCommands:
|
expected: `customCommands:
|
||||||
- command: echo 'hello'
|
- command: echo 'hello'
|
||||||
`,
|
`,
|
||||||
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Deleted redundant 'subprocess: false' in custom command",
|
||||||
|
"Deleted redundant 'stream: false' property in custom command",
|
||||||
|
"Deleted redundant 'showOutput: false' property in custom command",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
t.Run(s.name, func(t *testing.T) {
|
t.Run(s.name, func(t *testing.T) {
|
||||||
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
|
changes := NewChangesSet()
|
||||||
|
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, s.expectedDidChange, didChange)
|
||||||
|
if didChange {
|
||||||
assert.Equal(t, s.expected, string(actual))
|
assert.Equal(t, s.expected, string(actual))
|
||||||
|
}
|
||||||
|
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -778,7 +965,8 @@ keybinding:
|
|||||||
|
|
||||||
func BenchmarkMigrationOnLargeConfiguration(b *testing.B) {
|
func BenchmarkMigrationOnLargeConfiguration(b *testing.B) {
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
_, _ = computeMigratedConfig("path doesn't matter", largeConfiguration)
|
changes := NewChangesSet()
|
||||||
|
_, _, _ = computeMigratedConfig("path doesn't matter", largeConfiguration, changes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,12 +975,16 @@ func TestAllBranchesLogCmdMigrations(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
expected string
|
expected string
|
||||||
|
expectedDidChange bool
|
||||||
|
expectedChanges []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Incomplete Configuration Passes uneventfully",
|
name: "Incomplete Configuration Passes uneventfully",
|
||||||
input: "git:",
|
input: "git:",
|
||||||
expected: "git:",
|
expectedDidChange: false,
|
||||||
}, {
|
expectedChanges: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Single Cmd with no Cmds",
|
name: "Single Cmd with no Cmds",
|
||||||
input: `git:
|
input: `git:
|
||||||
allBranchesLogCmd: git log --graph --oneline
|
allBranchesLogCmd: git log --graph --oneline
|
||||||
@@ -801,7 +993,13 @@ func TestAllBranchesLogCmdMigrations(t *testing.T) {
|
|||||||
allBranchesLogCmds:
|
allBranchesLogCmds:
|
||||||
- git log --graph --oneline
|
- git log --graph --oneline
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Created git.allBranchesLogCmds array containing value of git.allBranchesLogCmd",
|
||||||
|
"Removed obsolete git.allBranchesLogCmd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Cmd with one existing Cmds",
|
name: "Cmd with one existing Cmds",
|
||||||
input: `git:
|
input: `git:
|
||||||
allBranchesLogCmd: git log --graph --oneline
|
allBranchesLogCmd: git log --graph --oneline
|
||||||
@@ -813,17 +1011,22 @@ func TestAllBranchesLogCmdMigrations(t *testing.T) {
|
|||||||
- git log --graph --oneline
|
- git log --graph --oneline
|
||||||
- git log --graph --oneline --pretty
|
- git log --graph --oneline --pretty
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array",
|
||||||
|
"Removed obsolete git.allBranchesLogCmd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Only Cmds set have no changes",
|
name: "Only Cmds set have no changes",
|
||||||
input: `git:
|
input: `git:
|
||||||
allBranchesLogCmds:
|
allBranchesLogCmds:
|
||||||
- git log
|
- git log
|
||||||
`,
|
`,
|
||||||
expected: `git:
|
expected: "",
|
||||||
allBranchesLogCmds:
|
expectedChanges: []string{},
|
||||||
- git log
|
},
|
||||||
`,
|
{
|
||||||
}, {
|
|
||||||
name: "Removes Empty Cmd When at end of yaml",
|
name: "Removes Empty Cmd When at end of yaml",
|
||||||
input: `git:
|
input: `git:
|
||||||
allBranchesLogCmds:
|
allBranchesLogCmds:
|
||||||
@@ -834,7 +1037,10 @@ func TestAllBranchesLogCmdMigrations(t *testing.T) {
|
|||||||
allBranchesLogCmds:
|
allBranchesLogCmds:
|
||||||
- git log --graph --oneline
|
- git log --graph --oneline
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Removed obsolete git.allBranchesLogCmd"},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Migrates when sequence defined inline",
|
name: "Migrates when sequence defined inline",
|
||||||
input: `git:
|
input: `git:
|
||||||
allBranchesLogCmds: [foo, bar]
|
allBranchesLogCmds: [foo, bar]
|
||||||
@@ -843,7 +1049,13 @@ func TestAllBranchesLogCmdMigrations(t *testing.T) {
|
|||||||
expected: `git:
|
expected: `git:
|
||||||
allBranchesLogCmds: [baz, foo, bar]
|
allBranchesLogCmds: [baz, foo, bar]
|
||||||
`,
|
`,
|
||||||
}, {
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{
|
||||||
|
"Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array",
|
||||||
|
"Removed obsolete git.allBranchesLogCmd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
name: "Removes Empty Cmd With Keys Afterwards",
|
name: "Removes Empty Cmd With Keys Afterwards",
|
||||||
input: `git:
|
input: `git:
|
||||||
allBranchesLogCmds:
|
allBranchesLogCmds:
|
||||||
@@ -856,14 +1068,21 @@ func TestAllBranchesLogCmdMigrations(t *testing.T) {
|
|||||||
- git log --graph --oneline
|
- git log --graph --oneline
|
||||||
foo: bar
|
foo: bar
|
||||||
`,
|
`,
|
||||||
|
expectedDidChange: true,
|
||||||
|
expectedChanges: []string{"Removed obsolete git.allBranchesLogCmd"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
t.Run(s.name, func(t *testing.T) {
|
t.Run(s.name, func(t *testing.T) {
|
||||||
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
|
changes := NewChangesSet()
|
||||||
|
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, s.expectedDidChange, didChange)
|
||||||
|
if didChange {
|
||||||
assert.Equal(t, s.expected, string(actual))
|
assert.Equal(t, s.expected, string(actual))
|
||||||
|
}
|
||||||
|
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -65,41 +65,37 @@ func transformNode(node *yaml.Node, path []string, transform func(node *yaml.Nod
|
|||||||
|
|
||||||
// Takes the root node of a yaml document, a path to a key, and a new name for the key.
|
// Takes the root node of a yaml document, a path to a key, and a new name for the key.
|
||||||
// Will rename the key to the new name if it exists, and do nothing otherwise.
|
// Will rename the key to the new name if it exists, and do nothing otherwise.
|
||||||
func RenameYamlKey(rootNode *yaml.Node, path []string, newKey string) error {
|
func RenameYamlKey(rootNode *yaml.Node, path []string, newKey string) (error, bool) {
|
||||||
// Empty document: nothing to do.
|
// Empty document: nothing to do.
|
||||||
if len(rootNode.Content) == 0 {
|
if len(rootNode.Content) == 0 {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
body := rootNode.Content[0]
|
body := rootNode.Content[0]
|
||||||
|
|
||||||
if err := renameYamlKey(body, path, newKey); err != nil {
|
return renameYamlKey(body, path, newKey)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursive function to rename the YAML key.
|
// Recursive function to rename the YAML key.
|
||||||
func renameYamlKey(node *yaml.Node, path []string, newKey string) error {
|
func renameYamlKey(node *yaml.Node, path []string, newKey string) (error, bool) {
|
||||||
if node.Kind != yaml.MappingNode {
|
if node.Kind != yaml.MappingNode {
|
||||||
return errors.New("yaml node in path is not a dictionary")
|
return errors.New("yaml node in path is not a dictionary"), false
|
||||||
}
|
}
|
||||||
|
|
||||||
keyNode, valueNode := LookupKey(node, path[0])
|
keyNode, valueNode := LookupKey(node, path[0])
|
||||||
if keyNode == nil {
|
if keyNode == nil {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// end of path reached: rename key
|
// end of path reached: rename key
|
||||||
if len(path) == 1 {
|
if len(path) == 1 {
|
||||||
// Check that new key doesn't exist yet
|
// Check that new key doesn't exist yet
|
||||||
if newKeyNode, _ := LookupKey(node, newKey); newKeyNode != nil {
|
if newKeyNode, _ := LookupKey(node, newKey); newKeyNode != nil {
|
||||||
return fmt.Errorf("new key `%s' already exists", newKey)
|
return fmt.Errorf("new key `%s' already exists", newKey), false
|
||||||
}
|
}
|
||||||
|
|
||||||
keyNode.Value = newKey
|
keyNode.Value = newKey
|
||||||
return nil
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return renameYamlKey(valueNode, path[1:], newKey)
|
return renameYamlKey(valueNode, path[1:], newKey)
|
||||||
|
@@ -15,6 +15,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path []string
|
path []string
|
||||||
newKey string
|
newKey string
|
||||||
expectedOut string
|
expectedOut string
|
||||||
|
expectedDidRename bool
|
||||||
expectedErr string
|
expectedErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -23,6 +24,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path: []string{"foo"},
|
path: []string{"foo"},
|
||||||
newKey: "bar",
|
newKey: "bar",
|
||||||
expectedOut: "bar: 5\n",
|
expectedOut: "bar: 5\n",
|
||||||
|
expectedDidRename: true,
|
||||||
expectedErr: "",
|
expectedErr: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -31,6 +33,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path: []string{"foo", "bar"},
|
path: []string{"foo", "bar"},
|
||||||
newKey: "baz",
|
newKey: "baz",
|
||||||
expectedOut: "foo:\n baz: 5\n",
|
expectedOut: "foo:\n baz: 5\n",
|
||||||
|
expectedDidRename: true,
|
||||||
expectedErr: "",
|
expectedErr: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -39,6 +42,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path: []string{"foo"},
|
path: []string{"foo"},
|
||||||
newKey: "qux",
|
newKey: "qux",
|
||||||
expectedOut: "qux:\n bar: 5\n",
|
expectedOut: "qux:\n bar: 5\n",
|
||||||
|
expectedDidRename: true,
|
||||||
expectedErr: "",
|
expectedErr: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -47,6 +51,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path: []string{"nonExistingKey"},
|
path: []string{"nonExistingKey"},
|
||||||
newKey: "qux",
|
newKey: "qux",
|
||||||
expectedOut: "foo:\n bar: 5\n",
|
expectedOut: "foo:\n bar: 5\n",
|
||||||
|
expectedDidRename: false,
|
||||||
expectedErr: "",
|
expectedErr: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path: []string{"foo"},
|
path: []string{"foo"},
|
||||||
newKey: "bar",
|
newKey: "bar",
|
||||||
expectedOut: "42\n",
|
expectedOut: "42\n",
|
||||||
|
expectedDidRename: false,
|
||||||
expectedErr: "yaml node in path is not a dictionary",
|
expectedErr: "yaml node in path is not a dictionary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -65,6 +71,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path: []string{"foo", "bar", "baz"},
|
path: []string{"foo", "bar", "baz"},
|
||||||
newKey: "qux",
|
newKey: "qux",
|
||||||
expectedOut: "foo:\n bar: [1, 2, 3]\n",
|
expectedOut: "foo:\n bar: [1, 2, 3]\n",
|
||||||
|
expectedDidRename: false,
|
||||||
expectedErr: "yaml node in path is not a dictionary",
|
expectedErr: "yaml node in path is not a dictionary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,6 +80,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
path: []string{"foo"},
|
path: []string{"foo"},
|
||||||
newKey: "bar",
|
newKey: "bar",
|
||||||
expectedOut: "foo: 5\nbar: 7\n",
|
expectedOut: "foo: 5\nbar: 7\n",
|
||||||
|
expectedDidRename: false,
|
||||||
expectedErr: "new key `bar' already exists",
|
expectedErr: "new key `bar' already exists",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -80,7 +88,7 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
node := unmarshalForTest(t, test.in)
|
node := unmarshalForTest(t, test.in)
|
||||||
actualErr := RenameYamlKey(&node, test.path, test.newKey)
|
actualErr, didRename := RenameYamlKey(&node, test.path, test.newKey)
|
||||||
if test.expectedErr == "" {
|
if test.expectedErr == "" {
|
||||||
assert.NoError(t, actualErr)
|
assert.NoError(t, actualErr)
|
||||||
} else {
|
} else {
|
||||||
@@ -89,6 +97,8 @@ func TestRenameYamlKey(t *testing.T) {
|
|||||||
out := marshalForTest(t, &node)
|
out := marshalForTest(t, &node)
|
||||||
|
|
||||||
assert.Equal(t, test.expectedOut, out)
|
assert.Equal(t, test.expectedOut, out)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expectedDidRename, didRename)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
vendor/github.com/jesseduffield/generics/maps/maps.go
generated
vendored
4
vendor/github.com/jesseduffield/generics/maps/maps.go
generated
vendored
@@ -19,7 +19,7 @@ func Values[Key comparable, Value any](m map[Key]Value) []Value {
|
|||||||
func TransformValues[Key comparable, Value any, NewValue any](
|
func TransformValues[Key comparable, Value any, NewValue any](
|
||||||
m map[Key]Value, fn func(Value) NewValue,
|
m map[Key]Value, fn func(Value) NewValue,
|
||||||
) map[Key]NewValue {
|
) map[Key]NewValue {
|
||||||
output := make(map[Key]NewValue)
|
output := make(map[Key]NewValue, len(m))
|
||||||
for key, value := range m {
|
for key, value := range m {
|
||||||
output[key] = fn(value)
|
output[key] = fn(value)
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ func TransformValues[Key comparable, Value any, NewValue any](
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TransformKeys[Key comparable, Value any, NewKey comparable](m map[Key]Value, fn func(Key) NewKey) map[NewKey]Value {
|
func TransformKeys[Key comparable, Value any, NewKey comparable](m map[Key]Value, fn func(Key) NewKey) map[NewKey]Value {
|
||||||
output := make(map[NewKey]Value)
|
output := make(map[NewKey]Value, len(m))
|
||||||
for key, value := range m {
|
for key, value := range m {
|
||||||
output[fn(key)] = value
|
output[fn(key)] = value
|
||||||
}
|
}
|
||||||
|
65
vendor/github.com/jesseduffield/generics/orderedset/orderedset.go
generated
vendored
Normal file
65
vendor/github.com/jesseduffield/generics/orderedset/orderedset.go
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package orderedset
|
||||||
|
|
||||||
|
import (
|
||||||
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderedSet[T comparable] struct {
|
||||||
|
om *orderedmap.OrderedMap[T, bool]
|
||||||
|
}
|
||||||
|
|
||||||
|
func New[T comparable]() *OrderedSet[T] {
|
||||||
|
return &OrderedSet[T]{om: orderedmap.New[T, bool]()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromSlice[T comparable](slice []T) *OrderedSet[T] {
|
||||||
|
result := &OrderedSet[T]{om: orderedmap.New[T, bool](len(slice))}
|
||||||
|
result.Add(slice...)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OrderedSet[T]) Add(values ...T) {
|
||||||
|
for _, value := range values {
|
||||||
|
os.om.Set(value, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OrderedSet[T]) Remove(value T) {
|
||||||
|
os.om.Delete(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OrderedSet[T]) RemoveSlice(slice []T) {
|
||||||
|
for _, value := range slice {
|
||||||
|
os.Remove(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OrderedSet[T]) Includes(value T) bool {
|
||||||
|
return os.om.Value(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OrderedSet[T]) Len() int {
|
||||||
|
return os.om.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OrderedSet[T]) ToSliceFromOldest() []T {
|
||||||
|
// TODO: can be simplified to
|
||||||
|
// return os.om.KeysFromOldest()
|
||||||
|
// when we update to a newer version of go-ordered-map
|
||||||
|
result := make([]T, 0, os.Len())
|
||||||
|
for pair := os.om.Oldest(); pair != nil; pair = pair.Next() {
|
||||||
|
result = append(result, pair.Key)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OrderedSet[T]) ToSliceFromNewest() []T {
|
||||||
|
// TODO: can be simplified to
|
||||||
|
// return os.om.KeysFromNewest()
|
||||||
|
// when we update to a newer version of go-ordered-map
|
||||||
|
result := make([]T, 0, os.Len())
|
||||||
|
for pair := os.om.Newest(); pair != nil; pair = pair.Prev() {
|
||||||
|
result = append(result, pair.Key)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
9
vendor/github.com/jesseduffield/generics/set/set.go
generated
vendored
9
vendor/github.com/jesseduffield/generics/set/set.go
generated
vendored
@@ -11,12 +11,9 @@ func New[T comparable]() *Set[T] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewFromSlice[T comparable](slice []T) *Set[T] {
|
func NewFromSlice[T comparable](slice []T) *Set[T] {
|
||||||
hashMap := make(map[T]bool)
|
result := &Set[T]{hashMap: make(map[T]bool, len(slice))}
|
||||||
for _, value := range slice {
|
result.Add(slice...)
|
||||||
hashMap[value] = true
|
return result
|
||||||
}
|
|
||||||
|
|
||||||
return &Set[T]{hashMap: hashMap}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set[T]) Add(values ...T) {
|
func (s *Set[T]) Add(values ...T) {
|
||||||
|
3
vendor/modules.txt
vendored
3
vendor/modules.txt
vendored
@@ -174,9 +174,10 @@ github.com/integrii/flaggy
|
|||||||
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
||||||
## explicit
|
## explicit
|
||||||
github.com/jbenet/go-context/io
|
github.com/jbenet/go-context/io
|
||||||
# github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918
|
# github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/jesseduffield/generics/maps
|
github.com/jesseduffield/generics/maps
|
||||||
|
github.com/jesseduffield/generics/orderedset
|
||||||
github.com/jesseduffield/generics/set
|
github.com/jesseduffield/generics/set
|
||||||
# github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
# github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
||||||
## explicit; go 1.23.0
|
## explicit; go 1.23.0
|
||||||
|
Reference in New Issue
Block a user