diff --git a/assets/issues/issue-669/between/from.yml b/assets/issues/issue-669/between/from.yml new file mode 100644 index 00000000..616d0abe --- /dev/null +++ b/assets/issues/issue-669/between/from.yml @@ -0,0 +1,4 @@ +--- +z_key: 1 +a_key: 2 +m_key: 3 diff --git a/assets/issues/issue-669/between/to.yml b/assets/issues/issue-669/between/to.yml new file mode 100644 index 00000000..136132d3 --- /dev/null +++ b/assets/issues/issue-669/between/to.yml @@ -0,0 +1,4 @@ +--- +a_key: 2 +m_key: 4 +z_key: 1 diff --git a/assets/issues/issue-669/input.yml b/assets/issues/issue-669/input.yml new file mode 100644 index 00000000..f31b9cde --- /dev/null +++ b/assets/issues/issue-669/input.yml @@ -0,0 +1,13 @@ +--- +z_key: value_z +a_key: value_a +m_key: value_m +nested: + z_nested: 1 + a_nested: 2 + m_nested: 3 +list: + - z_item: 1 + a_item: 2 + - a_item: 3 + z_item: 4 diff --git a/internal/cmd/between.go b/internal/cmd/between.go index f0d6f813..09e38363 100644 --- a/internal/cmd/between.go +++ b/internal/cmd/between.go @@ -38,6 +38,7 @@ var betweenCmdUsageTemplate string type betweenCmdOptions struct { swap bool translateListToDocuments bool + sort bool chroot string chrootFrom string chrootTo string @@ -70,6 +71,17 @@ types are: YAML (http://yaml.org/) and JSON (http://json.org/). return fmt.Errorf("failed to load input files: %w", err) } + // If the --sort flag is set, sort mapping keys in both input files + // to ensure consistent key ordering before comparison. + if betweenCmdSettings.sort { + for i := range from.Documents { + sortYAMLKeys(from.Documents[i]) + } + for i := range to.Documents { + sortYAMLKeys(to.Documents[i]) + } + } + // If the main change root flag is set, this (re-)sets the individual change roots of the two input files if betweenCmdSettings.chroot != "" { betweenCmdSettings.chrootFrom = betweenCmdSettings.chroot @@ -133,6 +145,7 @@ func init() { var groups = []*pflag.FlagSet{ flagSet("Input Documents Handling", func(fs *pflag.FlagSet) { fs.BoolVar(&betweenCmdSettings.swap, "swap", false, "Swap 'from' and 'to' for comparison") + fs.BoolVar(&betweenCmdSettings.sort, "sort", false, "Sort YAML map keys alphabetically before comparison to eliminate false order-change entries") fs.StringVar(&betweenCmdSettings.chroot, "chroot", "", "change the root level of the input file to another point in the document") fs.StringVar(&betweenCmdSettings.chrootFrom, "chroot-of-from", "", "only change the root level of the from input file") fs.StringVar(&betweenCmdSettings.chrootTo, "chroot-of-to", "", "only change the root level of the to input file") diff --git a/internal/cmd/cmds_test.go b/internal/cmd/cmds_test.go index 8ac3acc6..0a29abb4 100644 --- a/internal/cmd/cmds_test.go +++ b/internal/cmd/cmds_test.go @@ -562,5 +562,20 @@ foo: bar _, err := dyff("last-applied", kubeYAML) Expect(err).To(HaveOccurred()) }) + + It("should sort keys and avoid false order changes with --sort flag", func() { + out, err := dyff("between", "--omit-header", "--sort", + assets("issues", "issue-669", "between", "from.yml"), + assets("issues", "issue-669", "between", "to.yml"), + ) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(BeEquivalentTo(` +m_key + ± value change + - 3 + + 4 + +`)) + }) }) }) diff --git a/internal/cmd/common.go b/internal/cmd/common.go index be1e54a6..ed03ae0a 100644 --- a/internal/cmd/common.go +++ b/internal/cmd/common.go @@ -27,6 +27,7 @@ import ( "fmt" "io" "os" + "sort" "strings" "github.com/caarlos0/env/v11" @@ -135,6 +136,7 @@ func reportOptionsFlags() []*pflag.FlagSet { type OutputWriter struct { PlainMode bool Restructure bool + SortKeys bool OmitIndentHelper bool EnforceDocumentStartMarker bool OutputStyle string @@ -190,6 +192,10 @@ func (w *OutputWriter) write(writer io.Writer, filename string) error { ytbx.RestructureObject(document) } + if w.SortKeys { + sortYAMLKeys(document) + } + switch { case w.PlainMode && w.OutputStyle == "json": outputProcessor := neat.NewOutputProcessorWithDefaults() @@ -349,3 +355,44 @@ func writeReport(cmd *cobra.Command, report dyff.Report) error { return nil } + +// sortYAMLKeys recursively sorts mapping node keys alphabetically in place. +// MappingNode.Content is a flat [key, val, key, val, ...] slice; this function +// reorders pairs so that keys are in ascending alphabetical order. +func sortYAMLKeys(node *yamlv3.Node) { + switch node.Kind { + case yamlv3.DocumentNode: + for _, content := range node.Content { + sortYAMLKeys(content) + } + + case yamlv3.MappingNode: + // Collect key-value pairs. + type pair struct { + key *yamlv3.Node + value *yamlv3.Node + } + pairs := make([]pair, 0, len(node.Content)/2) + for i := 0; i < len(node.Content); i += 2 { + pairs = append(pairs, pair{key: node.Content[i], value: node.Content[i+1]}) + } + // Sort by key name. + sort.SliceStable(pairs, func(i, j int) bool { + return pairs[i].key.Value < pairs[j].key.Value + }) + // Rebuild Content slice. + for i, p := range pairs { + node.Content[i*2] = p.key + node.Content[i*2+1] = p.value + } + // Recurse into values. + for _, p := range pairs { + sortYAMLKeys(p.value) + } + + case yamlv3.SequenceNode: + for i := range node.Content { + sortYAMLKeys(node.Content[i]) + } + } +} diff --git a/internal/cmd/yaml.go b/internal/cmd/yaml.go index 33ec6cda..cd014a51 100644 --- a/internal/cmd/yaml.go +++ b/internal/cmd/yaml.go @@ -31,6 +31,7 @@ import ( type yamlCmdOptions struct { restructure bool + sort bool inplace bool plainMode bool @@ -55,6 +56,7 @@ Converts input document into YAML format while preserving the order of all keys. OutputStyle: "yaml", PlainMode: yamlCmdSettings.plainMode, Restructure: yamlCmdSettings.restructure, + SortKeys: yamlCmdSettings.sort, OmitIndentHelper: yamlCmdSettings.omitIndentHelper, EnforceDocumentStartMarker: yamlCmdSettings.enforceDocumentStartMarker, } @@ -91,6 +93,7 @@ func init() { yamlCmd.Flags().BoolVarP(&yamlCmdSettings.plainMode, "plain", "p", false, "output in plain style without any highlighting") yamlCmd.Flags().BoolVarP(&yamlCmdSettings.restructure, "restructure", "r", false, "restructure map keys in reasonable order") + yamlCmd.Flags().BoolVarP(&yamlCmdSettings.sort, "sort", "s", false, "sort map keys alphabetically") yamlCmd.Flags().BoolVarP(&yamlCmdSettings.inplace, "in-place", "i", false, "overwrite input file with output of this command") yamlCmd.Flags().BoolVarP(&yamlCmdSettings.omitIndentHelper, "omit-indent-helper", "O", false, "omit indent helper lines in highlighted output") diff --git a/internal/cmd/yaml_test.go b/internal/cmd/yaml_test.go index 8a7fa977..6ecae623 100644 --- a/internal/cmd/yaml_test.go +++ b/internal/cmd/yaml_test.go @@ -129,6 +129,24 @@ Bar: Foo --- name: three foobar: foobar +`)) + }) + + It("should sort keys alphabetically with --sort flag", func() { + out, err := dyff("yaml", "--plain", "--sort", assets("issues", "issue-669", "input.yml")) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(BeEquivalentTo(`a_key: value_a +list: + - a_item: 2 + z_item: 1 + - a_item: 3 + z_item: 4 +m_key: value_m +nested: + a_nested: 2 + m_nested: 3 + z_nested: 1 +z_key: value_z `)) }) })