1
0
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:
Stefan Haller
2025-05-21 08:54:55 +02:00
committed by GitHub
10 changed files with 519 additions and 183 deletions

2
go.mod
View File

@@ -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
View File

@@ -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=

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}

View File

@@ -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
View File

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