Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package dyff for openSUSE:Factory checked in at 2026-03-02 17:39:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dyff (Old) and /work/SRC/openSUSE:Factory/.dyff.new.29461 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dyff" Mon Mar 2 17:39:45 2026 rev:18 rq:1335705 version:1.11.2 Changes: -------- --- /work/SRC/openSUSE:Factory/dyff/dyff.changes 2026-02-25 21:11:26.572328959 +0100 +++ /work/SRC/openSUSE:Factory/.dyff.new.29461/dyff.changes 2026-03-02 17:39:51.942201330 +0100 @@ -1,0 +2,17 @@ +Mon Mar 02 06:43:50 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.11.2: + * Fix incorrect diff for mixing types in a list by @HeavyWombat + in #613 +- Update to version 1.11.1: + * Fix grab issue with non-standard identifier by @HeavyWombat in + #612 +- Update to version 1.11.0: + * Tidy up go.mod by @HeavyWombat in #608 + * build(deps): bump goreleaser/goreleaser-action from 6 to 7 by + @dependabot[bot] in #609 + * build(deps): bump golang.org/x/net from 0.50.0 to 0.51.0 by + @dependabot[bot] in #610 + * Introduce format strings feature by @HeavyWombat in #611 + +------------------------------------------------------------------- Old: ---- dyff-1.10.5.obscpio New: ---- dyff-1.11.2.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dyff.spec ++++++ --- /var/tmp/diff_new_pack.1uavjC/_old 2026-03-02 17:39:52.922242235 +0100 +++ /var/tmp/diff_new_pack.1uavjC/_new 2026-03-02 17:39:52.926242402 +0100 @@ -17,14 +17,14 @@ Name: dyff -Version: 1.10.5 +Version: 1.11.2 Release: 0 Summary: Diff tool for YAML files, and sometimes JSON License: MIT URL: https://github.com/homeport/dyff Source: dyff-%{version}.tar.gz Source1: vendor.tar.gz -BuildRequires: go >= 1.23 +BuildRequires: golang(API) >= 1.25 %description A diff tool for YAML files, and sometimes JSON. ++++++ _service ++++++ --- /var/tmp/diff_new_pack.1uavjC/_old 2026-03-02 17:39:52.962243905 +0100 +++ /var/tmp/diff_new_pack.1uavjC/_new 2026-03-02 17:39:52.978244572 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/homeport/dyff</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v1.10.5</param> + <param name="revision">v1.11.2</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.1uavjC/_old 2026-03-02 17:39:52.998245407 +0100 +++ /var/tmp/diff_new_pack.1uavjC/_new 2026-03-02 17:39:53.002245574 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/homeport/dyff</param> - <param name="changesrevision">0c7ba9461bf06f54f9527d9945c3f879efc76770</param></service></servicedata> + <param name="changesrevision">6c36ce0d21d83f307a4e76ee810d9657947e3cf9</param></service></servicedata> (No newline at EOF) ++++++ dyff-1.10.5.obscpio -> dyff-1.11.2.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/.docs/commands/dyff_between.md new/dyff-1.11.2/.docs/commands/dyff_between.md --- old/dyff-1.10.5/.docs/commands/dyff_between.md 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/.docs/commands/dyff_between.md 2026-03-01 17:08:27.000000000 +0100 @@ -16,30 +16,31 @@ ### Options ``` + --swap Swap 'from' and 'to' for comparison + --chroot string change the root level of the input file to another point in the document + --chroot-of-from string only change the root level of the from input file + --chroot-of-to string only change the root level of the to input file + --chroot-list-to-documents in case the change root points to a list, treat this list as a set of documents and not as the list itself + -o, --output string specify the output style, supported styles: human, brief, github, gitlab, gitea (default "human") + --use-indent-lines use indent lines in the output -i, --ignore-order-changes ignore order changes in lists --ignore-whitespace-changes ignore leading or trailing whitespace changes + -v, --ignore-value-changes exclude changes in values + --detect-renames enable detection for renames (document level for Kubernetes resources) (default true) + --format-strings format strings (i.e. inline JSON) before comparison to avoid formatting differences (default true) + -l, --no-table-style do not place blocks next to each other, always use one row per text block + -x, --no-cert-inspection disable x509 certificate inspection, compare as raw text + -g, --use-go-patch-style use Go-Patch style paths in outputs + --minor-change-threshold float minor change threshold (default 0.1) + --multi-line-context-lines int multi-line context lines (default 10) --detect-kubernetes detect kubernetes entities (default true) --additional-identifier stringArray use additional identifier candidates in named entry lists --filter strings filter reports to a subset of differences based on supplied arguments --exclude strings exclude reports from a set of differences based on supplied arguments --filter-regexp strings filter reports to a subset of differences based on supplied regular expressions --exclude-regexp strings exclude reports from a set of differences based on supplied regular expressions - -v, --ignore-value-changes exclude changes in values - --detect-renames enable detection for renames (document level for Kubernetes resources) (default true) - -o, --output string specify the output style, supported styles: human, brief, github, gitlab, gitea (default "human") - --use-indent-lines use indent lines in the output (default true) -b, --omit-header omit the dyff summary header -s, --set-exit-code set program exit code, with 0 meaning no difference, 1 for differences detected, and 255 for program error - -l, --no-table-style do not place blocks next to each other, always use one row per text block - -x, --no-cert-inspection disable x509 certificate inspection, compare as raw text - -g, --use-go-patch-style use Go-Patch style paths in outputs - --minor-change-threshold float minor change threshold (default 0.1) - --multi-line-context-lines int multi-line context lines (default 4) - --swap Swap 'from' and 'to' for comparison - --chroot string change the root level of the input file to another point in the document - --chroot-of-from string only change the root level of the from input file - --chroot-of-to string only change the root level of the to input file - --chroot-list-to-documents in case the change root points to a list, treat this list as a set of documents and not as the list itself -h, --help help for between ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/.docs/commands/dyff_last-applied.md new/dyff-1.11.2/.docs/commands/dyff_last-applied.md --- old/dyff-1.10.5/.docs/commands/dyff_last-applied.md 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/.docs/commands/dyff_last-applied.md 2026-03-01 17:08:27.000000000 +0100 @@ -17,25 +17,26 @@ ### Options ``` + -o, --output string specify the output style, supported styles: human, brief, github, gitlab, gitea (default "human") + --use-indent-lines use indent lines in the output -i, --ignore-order-changes ignore order changes in lists --ignore-whitespace-changes ignore leading or trailing whitespace changes + -v, --ignore-value-changes exclude changes in values + --detect-renames enable detection for renames (document level for Kubernetes resources) (default true) + --format-strings format strings (i.e. inline JSON) before comparison to avoid formatting differences (default true) + -l, --no-table-style do not place blocks next to each other, always use one row per text block + -x, --no-cert-inspection disable x509 certificate inspection, compare as raw text + -g, --use-go-patch-style use Go-Patch style paths in outputs + --minor-change-threshold float minor change threshold (default 0.1) + --multi-line-context-lines int multi-line context lines (default 10) --detect-kubernetes detect kubernetes entities (default true) --additional-identifier stringArray use additional identifier candidates in named entry lists --filter strings filter reports to a subset of differences based on supplied arguments --exclude strings exclude reports from a set of differences based on supplied arguments --filter-regexp strings filter reports to a subset of differences based on supplied regular expressions --exclude-regexp strings exclude reports from a set of differences based on supplied regular expressions - -v, --ignore-value-changes exclude changes in values - --detect-renames enable detection for renames (document level for Kubernetes resources) (default true) - -o, --output string specify the output style, supported styles: human, brief, github, gitlab, gitea (default "human") - --use-indent-lines use indent lines in the output (default true) -b, --omit-header omit the dyff summary header -s, --set-exit-code set program exit code, with 0 meaning no difference, 1 for differences detected, and 255 for program error - -l, --no-table-style do not place blocks next to each other, always use one row per text block - -x, --no-cert-inspection disable x509 certificate inspection, compare as raw text - -g, --use-go-patch-style use Go-Patch style paths in outputs - --minor-change-threshold float minor change threshold (default 0.1) - --multi-line-context-lines int multi-line context lines (default 4) -h, --help help for last-applied ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/assets/format-json-strings/from.yml new/dyff-1.11.2/assets/format-json-strings/from.yml --- old/dyff-1.10.5/assets/format-json-strings/from.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dyff-1.11.2/assets/format-json-strings/from.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,11 @@ +--- +foo: + bar: | + { + "foo": "bar", + "list": [1, 2, 3], + "mapping": { + "foo": "bar", + "bar": "foo" + } + } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/assets/format-json-strings/to.yml new/dyff-1.11.2/assets/format-json-strings/to.yml --- old/dyff-1.10.5/assets/format-json-strings/to.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dyff-1.11.2/assets/format-json-strings/to.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,3 @@ +--- +foo: + bar: '{"foo":"bar","list":[1,2,3],"mapping":{"foo":"bar","bar":"foo"}}' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/assets/issues/issue-580/from.yml new/dyff-1.11.2/assets/issues/issue-580/from.yml --- old/dyff-1.10.5/assets/issues/issue-580/from.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dyff-1.11.2/assets/issues/issue-580/from.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,3 @@ +list: + - 123 + - 123 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/assets/issues/issue-580/to.yml new/dyff-1.11.2/assets/issues/issue-580/to.yml --- old/dyff-1.10.5/assets/issues/issue-580/to.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dyff-1.11.2/assets/issues/issue-580/to.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,3 @@ +list: + - "123" + - 123 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/assets/issues/issue-605/from.yml new/dyff-1.11.2/assets/issues/issue-605/from.yml --- old/dyff-1.10.5/assets/issues/issue-605/from.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dyff-1.11.2/assets/issues/issue-605/from.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,13 @@ +# secrets.yaml +apiVersion: secrets/v1alpha1 +kind: SecretsAccess +metadata: + name: secrets-access + namespace: default +spec: + podSelector: + - app.kubernetes.io/name: argocd-repo-server + - app.kubernetes.io/name: argocd-server + - app.kubernetes.io/name: argocd-application-controller + - app.kubernetes.io/name: argocd-redis + - app.kubernetes.io/name: argocd-dex-server \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/assets/issues/issue-605/to.yml new/dyff-1.11.2/assets/issues/issue-605/to.yml --- old/dyff-1.10.5/assets/issues/issue-605/to.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dyff-1.11.2/assets/issues/issue-605/to.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,13 @@ +# secrets.yaml +apiVersion: secrets/v1alpha1 +kind: SecretsAccess +metadata: + name: secrets-access + namespace: default +spec: + podSelector: + - app.kubernetes.io/name: argocd-repo-server + - app.kubernetes.io/name: argocd-server + - app.kubernetes.io/name: argocd-application-controller + - app.kubernetes.io/name: argocd-redis + - app.kubernetes.io/name: argocd-dex-server \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/go.mod new/dyff-1.11.2/go.mod --- old/dyff-1.10.5/go.mod 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/go.mod 2026-03-01 17:08:27.000000000 +0100 @@ -1,11 +1,12 @@ module github.com/homeport/dyff -go 1.24.9 +go 1.25.0 require ( + github.com/caarlos0/env/v11 v11.4.0 github.com/gonvenience/bunt v1.4.3 github.com/gonvenience/idem v0.0.3 - github.com/gonvenience/neat v1.3.17 + github.com/gonvenience/neat v1.3.18 github.com/gonvenience/term v1.0.5 github.com/gonvenience/text v1.0.10 github.com/gonvenience/ytbx v1.4.8 @@ -13,17 +14,13 @@ github.com/mitchellh/hashstructure v1.1.0 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 + github.com/sergi/go-diff v1.4.0 github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 github.com/texttheater/golang-levenshtein v1.0.1 go.yaml.in/yaml/v3 v3.0.4 ) -// usage untagged version of go-diff -// cause https://github.com/sergi/go-diff/issues/123 -// fixed in https://github.com/sergi/go-diff/pull/136 -// but currently not tagged -require github.com/sergi/go-diff v1.4.0 - require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect @@ -39,11 +36,10 @@ github.com/mitchellh/go-ps v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/go.sum new/dyff-1.11.2/go.sum --- old/dyff-1.10.5/go.sum 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/go.sum 2026-03-01 17:08:27.000000000 +0100 @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc= +github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -27,6 +29,8 @@ github.com/gonvenience/idem v0.0.3/go.mod h1:ChZ+RP8e30+uCBcCIzN/0di6lTO2PucjemgKfzQUQEw= github.com/gonvenience/neat v1.3.17 h1:S/F0XNE4sc/b3APfLDqC9xM476Or55WhXZ7F/Sy07QY= github.com/gonvenience/neat v1.3.17/go.mod h1:h+b8M0LFDZUKS5D4xaoPd2qTLrVpX+bDRlDJphOAS/s= +github.com/gonvenience/neat v1.3.18 h1:WxWoXhsTHA6CStNrGgSEjGTt5MwIm+7Xs+VZmQIuXZA= +github.com/gonvenience/neat v1.3.18/go.mod h1:DTaEyHIOjSkMa066EoZZl3k5KCG/rFGE67n0cjm/9qk= github.com/gonvenience/term v1.0.5 h1:PYfBH7FB1V+tuuJl4KYrqG/tzAOUnvTy8IFa9YqYrJY= github.com/gonvenience/term v1.0.5/go.mod h1:CYvcU7H3nE6eOP0gvGfYz4BjGJzM1GeNp+fx4IBWKLs= github.com/gonvenience/text v1.0.10 h1:QRqtC/KMk57K7y4jHi4HjLxf8u+tg+/tIRCS5afywNE= @@ -106,8 +110,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/internal/cmd/between-usage-template.txt new/dyff-1.11.2/internal/cmd/between-usage-template.txt --- old/dyff-1.10.5/internal/cmd/between-usage-template.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/dyff-1.11.2/internal/cmd/between-usage-template.txt 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,23 @@ +*Usage:*{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}} + +{{if gt (len .Aliases) 0 -}} +*Aliases:* + {{.NameAndAliases}} +{{- end}} + +{{if .HasExample -}} +*Examples:* +{{.Example}} +{{end -}} + +*Flags:* {{ range $i, $name := flagGroups }} +_ {{ .Name }}_ +{{ .FlagUsages | trimTrailingWhitespaces }} +{{ end -}} + +{{if .HasAvailableInheritedFlags }} +*Global Flags:* +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} +{{ end}} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/internal/cmd/between.go new/dyff-1.11.2/internal/cmd/between.go --- old/dyff-1.10.5/internal/cmd/between.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/internal/cmd/between.go 2026-03-01 17:08:27.000000000 +0100 @@ -21,14 +21,20 @@ package cmd import ( + _ "embed" "fmt" + "github.com/gonvenience/bunt" "github.com/gonvenience/ytbx" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/homeport/dyff/pkg/dyff" ) +//go:embed between-usage-template.txt +var betweenCmdUsageTemplate string + type betweenCmdOptions struct { swap bool translateListToDocuments bool @@ -72,47 +78,48 @@ // Change root of 'from' input file if change root flag for 'from' is set if betweenCmdSettings.chrootFrom != "" { - if err = dyff.ChangeRoot(&from, betweenCmdSettings.chrootFrom, reportOptions.useGoPatchPaths, betweenCmdSettings.translateListToDocuments); err != nil { + if err = dyff.ChangeRoot(&from, betweenCmdSettings.chrootFrom, reportOptions.UseGoPatchPaths, betweenCmdSettings.translateListToDocuments); err != nil { return fmt.Errorf("failed to change root of %s to path %s: %w", from.Location, betweenCmdSettings.chrootFrom, err) } } // Change root of 'to' input file if change root flag for 'to' is set if betweenCmdSettings.chrootTo != "" { - if err = dyff.ChangeRoot(&to, betweenCmdSettings.chrootTo, reportOptions.useGoPatchPaths, betweenCmdSettings.translateListToDocuments); err != nil { + if err = dyff.ChangeRoot(&to, betweenCmdSettings.chrootTo, reportOptions.UseGoPatchPaths, betweenCmdSettings.translateListToDocuments); err != nil { return fmt.Errorf("failed to change root of %s to path %s: %w", to.Location, betweenCmdSettings.chrootTo, err) } } report, err := dyff.CompareInputFiles(from, to, - dyff.IgnoreOrderChanges(reportOptions.ignoreOrderChanges), - dyff.IgnoreWhitespaceChanges(reportOptions.ignoreWhitespaceChanges), - dyff.KubernetesEntityDetection(reportOptions.kubernetesEntityDetection), - dyff.AdditionalIdentifiers(reportOptions.additionalIdentifiers...), - dyff.DetectRenames(reportOptions.detectRenames), + dyff.IgnoreOrderChanges(reportOptions.IgnoreOrderChanges), + dyff.IgnoreWhitespaceChanges(reportOptions.IgnoreWhitespaceChanges), + dyff.KubernetesEntityDetection(reportOptions.KubernetesEntityDetection), + dyff.AdditionalIdentifiers(reportOptions.AdditionalIdentifiers...), + dyff.DetectRenames(reportOptions.DetectRenames), + dyff.FormatStrings(reportOptions.FormatStrings), ) if err != nil { return fmt.Errorf("failed to compare input files: %w", err) } - if reportOptions.filters != nil { - report = report.Filter(reportOptions.filters...) + if reportOptions.Filters != nil { + report = report.Filter(reportOptions.Filters...) } - if reportOptions.filterRegexps != nil { - report = report.FilterRegexp(reportOptions.filterRegexps...) + if reportOptions.FilterRegexps != nil { + report = report.FilterRegexp(reportOptions.FilterRegexps...) } - if reportOptions.excludes != nil { - report = report.Exclude(reportOptions.excludes...) + if reportOptions.Excludes != nil { + report = report.Exclude(reportOptions.Excludes...) } - if reportOptions.excludeRegexps != nil { - report = report.ExcludeRegexp(reportOptions.excludeRegexps...) + if reportOptions.ExcludeRegexps != nil { + report = report.ExcludeRegexp(reportOptions.ExcludeRegexps...) } - if reportOptions.ignoreValueChanges { + if reportOptions.IgnoreValueChanges { report = report.IgnoreValueChanges() } @@ -123,14 +130,23 @@ func init() { rootCmd.AddCommand(betweenCmd) - betweenCmd.Flags().SortFlags = false + 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.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") + fs.BoolVar(&betweenCmdSettings.translateListToDocuments, "chroot-list-to-documents", false, "in case the change root points to a list, treat this list as a set of documents and not as the list itself") + }), + } - applyReportOptionsFlags(betweenCmd) + groups = append(groups, reportOptionsFlags()...) + + betweenCmd.Flags().SortFlags = false + for _, group := range groups { + betweenCmd.Flags().AddFlagSet(group) + } - // Input documents modification flags - betweenCmd.Flags().BoolVar(&betweenCmdSettings.swap, "swap", false, "Swap 'from' and 'to' for comparison") - betweenCmd.Flags().StringVar(&betweenCmdSettings.chroot, "chroot", "", "change the root level of the input file to another point in the document") - betweenCmd.Flags().StringVar(&betweenCmdSettings.chrootFrom, "chroot-of-from", "", "only change the root level of the from input file") - betweenCmd.Flags().StringVar(&betweenCmdSettings.chrootTo, "chroot-of-to", "", "only change the root level of the to input file") - betweenCmd.Flags().BoolVar(&betweenCmdSettings.translateListToDocuments, "chroot-list-to-documents", false, "in case the change root points to a list, treat this list as a set of documents and not as the list itself") + betweenCmd.SetUsageTemplate(bunt.Sprint(betweenCmdUsageTemplate)) + cobra.AddTemplateFunc("flagGroups", func() []*pflag.FlagSet { return groups }) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/internal/cmd/cmds_test.go new/dyff-1.11.2/internal/cmd/cmds_test.go --- old/dyff-1.10.5/internal/cmd/cmds_test.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/internal/cmd/cmds_test.go 2026-03-01 17:08:27.000000000 +0100 @@ -326,7 +326,7 @@ type-change-2 ± type change from string to int - - 12 + - "12" + 12 whitespaces diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/internal/cmd/common.go new/dyff-1.11.2/internal/cmd/common.go --- old/dyff-1.10.5/internal/cmd/common.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/internal/cmd/common.go 2026-03-01 17:08:27.000000000 +0100 @@ -29,90 +29,105 @@ "os" "strings" + "github.com/caarlos0/env/v11" "github.com/gonvenience/bunt" "github.com/gonvenience/neat" "github.com/gonvenience/ytbx" "github.com/spf13/cobra" + "github.com/spf13/pflag" yamlv3 "go.yaml.in/yaml/v3" "github.com/homeport/dyff/pkg/dyff" ) type reportConfig struct { - style string - useIndentLines bool - ignoreOrderChanges bool - ignoreWhitespaceChanges bool - kubernetesEntityDetection bool - noTableStyle bool - doNotInspectCerts bool - exitWithCode bool - omitHeader bool - useGoPatchPaths bool - ignoreValueChanges bool - detectRenames bool - minorChangeThreshold float64 - multilineContextLines int - additionalIdentifiers []string - filters []string - excludes []string - filterRegexps []string - excludeRegexps []string + Style string `envDefault:"human"` + UseIndentLines bool `envDefault:"true"` + + IgnoreOrderChanges bool `envDefault:"false"` + IgnoreWhitespaceChanges bool `envDefault:"false"` + IgnoreValueChanges bool `envDefault:"false"` + FormatStrings bool `envDefault:"true"` + DetectRenames bool `envDefault:"true"` + + NoTableStyle bool `envDefault:"false"` + DoNotInspectCerts bool `envDefault:"false"` + UseGoPatchPaths bool `envDefault:"false"` + MinorChangeThreshold float64 `envDefault:"0.1"` + MultilineContextLines int `envDefault:"4"` + + KubernetesEntityDetection bool `envDefault:"true"` + AdditionalIdentifiers []string + Filters []string + Excludes []string + FilterRegexps []string + ExcludeRegexps []string + + ExitWithCode bool `envDefault:"false"` + OmitHeader bool `envDefault:"false"` } -var defaults = reportConfig{ - style: "human", - useIndentLines: true, - ignoreOrderChanges: false, - ignoreWhitespaceChanges: false, - kubernetesEntityDetection: true, - noTableStyle: false, - doNotInspectCerts: false, - exitWithCode: false, - omitHeader: false, - useGoPatchPaths: false, - ignoreValueChanges: false, - detectRenames: true, - minorChangeThreshold: 0.1, - multilineContextLines: 4, - additionalIdentifiers: nil, - filters: nil, - excludes: nil, - filterRegexps: nil, - excludeRegexps: nil, +func initReportConfig() reportConfig { + return env.Must(env.ParseAsWithOptions[reportConfig](env.Options{ + Prefix: "DYFF_", + UseFieldNameByDefault: true, + })) } -var reportOptions reportConfig +var reportOptions = initReportConfig() + +func flagSet(name string, f ...func(*pflag.FlagSet)) *pflag.FlagSet { + var flatSet = pflag.NewFlagSet(name, pflag.ExitOnError) + flatSet.SortFlags = false + + for _, fn := range f { + fn(flatSet) + } + + return flatSet +} -func applyReportOptionsFlags(cmd *cobra.Command) { - // Compare options - cmd.Flags().BoolVarP(&reportOptions.ignoreOrderChanges, "ignore-order-changes", "i", defaults.ignoreOrderChanges, "ignore order changes in lists") - cmd.Flags().BoolVar(&reportOptions.ignoreWhitespaceChanges, "ignore-whitespace-changes", defaults.ignoreWhitespaceChanges, "ignore leading or trailing whitespace changes") - cmd.Flags().BoolVarP(&reportOptions.kubernetesEntityDetection, "detect-kubernetes", "", defaults.kubernetesEntityDetection, "detect kubernetes entities") - cmd.Flags().StringArrayVar(&reportOptions.additionalIdentifiers, "additional-identifier", defaults.additionalIdentifiers, "use additional identifier candidates in named entry lists") - cmd.Flags().StringSliceVar(&reportOptions.filters, "filter", defaults.filters, "filter reports to a subset of differences based on supplied arguments") - cmd.Flags().StringSliceVar(&reportOptions.excludes, "exclude", defaults.excludes, "exclude reports from a set of differences based on supplied arguments") - cmd.Flags().StringSliceVar(&reportOptions.filterRegexps, "filter-regexp", defaults.filterRegexps, "filter reports to a subset of differences based on supplied regular expressions") - cmd.Flags().StringSliceVar(&reportOptions.excludeRegexps, "exclude-regexp", defaults.excludeRegexps, "exclude reports from a set of differences based on supplied regular expressions") - cmd.Flags().BoolVarP(&reportOptions.ignoreValueChanges, "ignore-value-changes", "v", defaults.ignoreValueChanges, "exclude changes in values") - cmd.Flags().BoolVar(&reportOptions.detectRenames, "detect-renames", defaults.detectRenames, "enable detection for renames (document level for Kubernetes resources)") - - // Main output preferences - cmd.Flags().StringVarP(&reportOptions.style, "output", "o", defaults.style, "specify the output style, supported styles: human, brief, github, gitlab, gitea") - cmd.Flags().BoolVar(&reportOptions.useIndentLines, "use-indent-lines", defaults.useIndentLines, "use indent lines in the output") - cmd.Flags().BoolVarP(&reportOptions.omitHeader, "omit-header", "b", defaults.omitHeader, "omit the dyff summary header") - cmd.Flags().BoolVarP(&reportOptions.exitWithCode, "set-exit-code", "s", defaults.exitWithCode, "set program exit code, with 0 meaning no difference, 1 for differences detected, and 255 for program error") - - // Human/BOSH output related flags - cmd.Flags().BoolVarP(&reportOptions.noTableStyle, "no-table-style", "l", defaults.noTableStyle, "do not place blocks next to each other, always use one row per text block") - cmd.Flags().BoolVarP(&reportOptions.doNotInspectCerts, "no-cert-inspection", "x", defaults.doNotInspectCerts, "disable x509 certificate inspection, compare as raw text") - cmd.Flags().BoolVarP(&reportOptions.useGoPatchPaths, "use-go-patch-style", "g", defaults.useGoPatchPaths, "use Go-Patch style paths in outputs") - cmd.Flags().Float64VarP(&reportOptions.minorChangeThreshold, "minor-change-threshold", "", defaults.minorChangeThreshold, "minor change threshold") - cmd.Flags().IntVarP(&reportOptions.multilineContextLines, "multi-line-context-lines", "", defaults.multilineContextLines, "multi-line context lines") - - // Deprecated - cmd.Flags().BoolVar(&reportOptions.exitWithCode, "set-exit-status", defaults.exitWithCode, "set program exit code, with 0 meaning no difference, 1 for differences detected, and 255 for program error") - _ = cmd.Flags().MarkDeprecated("set-exit-status", "use --set-exit-code instead") +func reportOptionsFlags() []*pflag.FlagSet { + return []*pflag.FlagSet{ + flagSet("Output Preferences", func(fs *pflag.FlagSet) { + fs.StringVarP(&reportOptions.Style, "output", "o", reportOptions.Style, "specify the output style, supported styles: human, brief, github, gitlab, gitea") + fs.BoolVar(&reportOptions.UseIndentLines, "use-indent-lines", reportOptions.UseIndentLines, "use indent lines in the output") + }), + + flagSet("Compare Options", func(fs *pflag.FlagSet) { + fs.BoolVarP(&reportOptions.IgnoreOrderChanges, "ignore-order-changes", "i", reportOptions.IgnoreOrderChanges, "ignore order changes in lists") + fs.BoolVar(&reportOptions.IgnoreWhitespaceChanges, "ignore-whitespace-changes", reportOptions.IgnoreWhitespaceChanges, "ignore leading or trailing whitespace changes") + fs.BoolVarP(&reportOptions.IgnoreValueChanges, "ignore-value-changes", "v", reportOptions.IgnoreValueChanges, "exclude changes in values") + fs.BoolVar(&reportOptions.DetectRenames, "detect-renames", reportOptions.DetectRenames, "enable detection for renames (document level for Kubernetes resources)") + fs.BoolVar(&reportOptions.FormatStrings, "format-strings", reportOptions.FormatStrings, "format strings (i.e. inline JSON) before comparison to avoid formatting differences") + }), + + flagSet("Human Output Preferences", func(fs *pflag.FlagSet) { + fs.BoolVarP(&reportOptions.NoTableStyle, "no-table-style", "l", reportOptions.NoTableStyle, "do not place blocks next to each other, always use one row per text block") + fs.BoolVarP(&reportOptions.DoNotInspectCerts, "no-cert-inspection", "x", reportOptions.DoNotInspectCerts, "disable x509 certificate inspection, compare as raw text") + fs.BoolVarP(&reportOptions.UseGoPatchPaths, "use-go-patch-style", "g", reportOptions.UseGoPatchPaths, "use Go-Patch style paths in outputs") + fs.Float64VarP(&reportOptions.MinorChangeThreshold, "minor-change-threshold", "", reportOptions.MinorChangeThreshold, "minor change threshold") + fs.IntVarP(&reportOptions.MultilineContextLines, "multi-line-context-lines", "", reportOptions.MultilineContextLines, "multi-line context lines") + }), + + flagSet("Filter Options", func(fs *pflag.FlagSet) { + fs.BoolVarP(&reportOptions.KubernetesEntityDetection, "detect-kubernetes", "", reportOptions.KubernetesEntityDetection, "detect kubernetes entities") + fs.StringArrayVar(&reportOptions.AdditionalIdentifiers, "additional-identifier", reportOptions.AdditionalIdentifiers, "use additional identifier candidates in named entry lists") + fs.StringSliceVar(&reportOptions.Filters, "filter", reportOptions.Filters, "filter reports to a subset of differences based on supplied arguments") + fs.StringSliceVar(&reportOptions.Excludes, "exclude", reportOptions.Excludes, "exclude reports from a set of differences based on supplied arguments") + fs.StringSliceVar(&reportOptions.FilterRegexps, "filter-regexp", reportOptions.FilterRegexps, "filter reports to a subset of differences based on supplied regular expressions") + fs.StringSliceVar(&reportOptions.ExcludeRegexps, "exclude-regexp", reportOptions.ExcludeRegexps, "exclude reports from a set of differences based on supplied regular expressions") + }), + + flagSet("General Options", func(fs *pflag.FlagSet) { + fs.BoolVarP(&reportOptions.OmitHeader, "omit-header", "b", reportOptions.OmitHeader, "omit the dyff summary header") + fs.BoolVarP(&reportOptions.ExitWithCode, "set-exit-code", "s", reportOptions.ExitWithCode, "set program exit code, with 0 meaning no difference, 1 for differences detected, and 255 for program error") + + // Deprecated + fs.BoolVar(&reportOptions.ExitWithCode, "set-exit-status", reportOptions.ExitWithCode, "set program exit code, with 0 meaning no difference, 1 for differences detected, and 255 for program error") + _ = fs.MarkDeprecated("set-exit-status", "use --set-exit-code instead") + }), + } } // OutputWriter encapsulates the required fields to define the look and feel of @@ -216,18 +231,18 @@ func writeReport(cmd *cobra.Command, report dyff.Report) error { var reportWriter dyff.ReportWriter - switch strings.ToLower(reportOptions.style) { + switch strings.ToLower(reportOptions.Style) { case "human", "bosh": reportWriter = &dyff.HumanReport{ Report: report, Indent: 2, - UseIndentLines: reportOptions.useIndentLines, - DoNotInspectCerts: reportOptions.doNotInspectCerts, - NoTableStyle: reportOptions.noTableStyle, - OmitHeader: reportOptions.omitHeader, - UseGoPatchPaths: reportOptions.useGoPatchPaths, - MinorChangeThreshold: reportOptions.minorChangeThreshold, - MultilineContextLines: reportOptions.multilineContextLines, + UseIndentLines: reportOptions.UseIndentLines, + DoNotInspectCerts: reportOptions.DoNotInspectCerts, + NoTableStyle: reportOptions.NoTableStyle, + OmitHeader: reportOptions.OmitHeader, + UseGoPatchPaths: reportOptions.UseGoPatchPaths, + MinorChangeThreshold: reportOptions.MinorChangeThreshold, + MultilineContextLines: reportOptions.MultilineContextLines, PrefixMultiline: false, } @@ -239,13 +254,13 @@ HumanReport: dyff.HumanReport{ Report: report, Indent: 0, - UseIndentLines: reportOptions.useIndentLines, - DoNotInspectCerts: reportOptions.doNotInspectCerts, + UseIndentLines: reportOptions.UseIndentLines, + DoNotInspectCerts: reportOptions.DoNotInspectCerts, NoTableStyle: true, OmitHeader: true, - UseGoPatchPaths: reportOptions.useGoPatchPaths, - MinorChangeThreshold: reportOptions.minorChangeThreshold, - MultilineContextLines: reportOptions.multilineContextLines, + UseGoPatchPaths: reportOptions.UseGoPatchPaths, + MinorChangeThreshold: reportOptions.MinorChangeThreshold, + MultilineContextLines: reportOptions.MultilineContextLines, PrefixMultiline: true, }, } @@ -258,13 +273,13 @@ HumanReport: dyff.HumanReport{ Report: report, Indent: 0, - UseIndentLines: reportOptions.useIndentLines, - DoNotInspectCerts: reportOptions.doNotInspectCerts, + UseIndentLines: reportOptions.UseIndentLines, + DoNotInspectCerts: reportOptions.DoNotInspectCerts, NoTableStyle: true, OmitHeader: true, - UseGoPatchPaths: reportOptions.useGoPatchPaths, - MinorChangeThreshold: reportOptions.minorChangeThreshold, - MultilineContextLines: reportOptions.multilineContextLines, + UseGoPatchPaths: reportOptions.UseGoPatchPaths, + MinorChangeThreshold: reportOptions.MinorChangeThreshold, + MultilineContextLines: reportOptions.MultilineContextLines, PrefixMultiline: true, }, } @@ -277,13 +292,13 @@ HumanReport: dyff.HumanReport{ Report: report, Indent: 0, - UseIndentLines: reportOptions.useIndentLines, - DoNotInspectCerts: reportOptions.doNotInspectCerts, + UseIndentLines: reportOptions.UseIndentLines, + DoNotInspectCerts: reportOptions.DoNotInspectCerts, NoTableStyle: true, OmitHeader: true, - UseGoPatchPaths: reportOptions.useGoPatchPaths, - MinorChangeThreshold: reportOptions.minorChangeThreshold, - MultilineContextLines: reportOptions.multilineContextLines, + UseGoPatchPaths: reportOptions.UseGoPatchPaths, + MinorChangeThreshold: reportOptions.MinorChangeThreshold, + MultilineContextLines: reportOptions.MultilineContextLines, PrefixMultiline: true, }, } @@ -294,7 +309,7 @@ } default: - return fmt.Errorf("unknown output style %s: %w", reportOptions.style, errors.New(cmd.UsageString())) + return fmt.Errorf("unknown output style %s: %w", reportOptions.Style, errors.New(cmd.UsageString())) } if err := reportWriter.WriteReport(os.Stdout); err != nil { @@ -302,7 +317,7 @@ } // If configured, make sure `dyff` exists with an exit status - if reportOptions.exitWithCode { + if reportOptions.ExitWithCode { switch len(report.Diffs) { case 0: return errorWithExitCode{value: 0} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/internal/cmd/lastApplied.go new/dyff-1.11.2/internal/cmd/lastApplied.go --- old/dyff-1.10.5/internal/cmd/lastApplied.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/internal/cmd/lastApplied.go 2026-03-01 17:08:27.000000000 +0100 @@ -58,7 +58,7 @@ purgeWellKnownMetadataEntries(inputFile.Documents[0]) - report, err := dyff.CompareInputFiles(lastConfiguration, inputFile, dyff.IgnoreOrderChanges(reportOptions.ignoreOrderChanges)) + report, err := dyff.CompareInputFiles(lastConfiguration, inputFile, dyff.IgnoreOrderChanges(reportOptions.IgnoreOrderChanges)) if err != nil { return fmt.Errorf("failed to compare input files: %w", err) } @@ -72,7 +72,9 @@ lastAppliedCmd.Flags().SortFlags = false - applyReportOptionsFlags(lastAppliedCmd) + for _, group := range reportOptionsFlags() { + lastAppliedCmd.Flags().AddFlagSet(group) + } } func lookUpLastAppliedConfiguration(inputFile ytbx.InputFile) (ytbx.InputFile, error) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/internal/cmd/root.go new/dyff-1.11.2/internal/cmd/root.go --- old/dyff-1.10.5/internal/cmd/root.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/internal/cmd/root.go 2026-03-01 17:08:27.000000000 +0100 @@ -60,7 +60,7 @@ // ResetSettings resets command settings to default. This is only required by // the test suite to make sure that the flag parsing works correctly. func ResetSettings() { - reportOptions = defaults + reportOptions = initReportConfig() betweenCmdSettings = betweenCmdOptions{} yamlCmdSettings = yamlCmdOptions{} jsonCmdSettings = jsonCmdOptions{} @@ -94,13 +94,13 @@ os.Args = rearrange() // Enable Kubernetes specific entity detection implicitly - reportOptions.kubernetesEntityDetection = true + reportOptions.KubernetesEntityDetection = true // Add implicit exclude for metadata.managedFields as this cannot // be configured via a command-line flag using KUBECTL_EXTERNAL_DIFF // due to an bug/feature in kubectl that ignore command-line flags // in the diff environment variable with non alphanumeric characters - reportOptions.excludeRegexps = append(reportOptions.excludeRegexps, "^/metadata/managedFields") + reportOptions.ExcludeRegexps = append(reportOptions.ExcludeRegexps, "^/metadata/managedFields") } if err := rootCmd.Execute(); err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/pkg/dyff/compare_test.go new/dyff-1.11.2/pkg/dyff/compare_test.go --- old/dyff-1.10.5/pkg/dyff/compare_test.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/pkg/dyff/compare_test.go 2026-03-01 17:08:27.000000000 +0100 @@ -844,6 +844,18 @@ singleDiff("/yaml/map/removed", dyff.REMOVAL, nil, "removed"), }})) }) + + It("should format strings to rule out formatting differences", func() { + report, err := dyff.CompareInputFiles( + file(assets("format-json-strings/from.yml")), + file(assets("format-json-strings/to.yml")), + dyff.FormatStrings(true), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(report).NotTo(BeNil()) + Expect(report.Diffs).To(HaveLen(0)) + }) }) Context("change root for comparison", func() { @@ -925,7 +937,7 @@ }) }) - Context("checking known issues of compare", func() { + Context("checking known/reported compare issues", func() { It("should not return order change differences in case the named-entry list does not have unique identifiers", func() { from, to, err := ytbx.LoadFiles("../../assets/issues/issue-38/from.yml", "../../assets/issues/issue-38/to.yml") Expect(err).To(BeNil()) @@ -1011,6 +1023,30 @@ Expect(results).ToNot(BeNil()) Expect(results.Diffs).To(HaveLen(0)) }) + + It("should work with non-standard identifier containing dots and slashes in named entry lists", func() { + from, to, err := ytbx.LoadFiles(assets("issues/issue-605/from.yml"), assets("issues/issue-605/to.yml")) + Expect(err).ToNot(HaveOccurred()) + Expect(from).ToNot(BeNil()) + Expect(to).ToNot(BeNil()) + + results, err := dyff.CompareInputFiles(from, to) + Expect(err).ToNot(HaveOccurred()) + Expect(results).ToNot(BeNil()) + Expect(results.Diffs).To(HaveLen(0)) + }) + + It("should differentiate correctly between number as string and number", func() { + from, to, err := ytbx.LoadFiles(assets("issues/issue-580/from.yml"), assets("issues/issue-580/to.yml")) + Expect(err).ToNot(HaveOccurred()) + Expect(from).ToNot(BeNil()) + Expect(to).ToNot(BeNil()) + + results, err := dyff.CompareInputFiles(from, to) + Expect(err).ToNot(HaveOccurred()) + Expect(results).ToNot(BeNil()) + Expect(results.Diffs).To(HaveLen(1)) + }) }) Context("input files containing Kubernetes resources", func() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/pkg/dyff/core.go new/dyff-1.11.2/pkg/dyff/core.go --- old/dyff-1.10.5/pkg/dyff/core.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/pkg/dyff/core.go 2026-03-01 17:08:27.000000000 +0100 @@ -21,6 +21,8 @@ package dyff import ( + "bytes" + "encoding/json" "fmt" "sort" "strings" @@ -43,6 +45,7 @@ IgnoreWhitespaceChanges bool KubernetesEntityDetection bool DetectRenames bool + FormatStrings bool AdditionalIdentifiers []string } @@ -96,6 +99,12 @@ } } +func FormatStrings(value bool) CompareOption { + return func(settings *compareSettings) { + settings.FormatStrings = value + } +} + // CompareInputFiles is one of the convenience main entry points for comparing // objects. In this case the representation of an input file, which might // contain multiple documents. It returns a report with the list of differences. @@ -514,11 +523,8 @@ } func (compare *compare) simpleLists(path ytbx.Path, from *yamlv3.Node, to *yamlv3.Node) ([]Diff, error) { - removals := make([]*yamlv3.Node, 0) - additions := make([]*yamlv3.Node, 0) - - fromLength := len(from.Content) - toLength := len(to.Content) + var removals, additions []*yamlv3.Node + var fromLength, toLength = len(from.Content), len(to.Content) // Special case if both lists only contain one entry, then directly compare // the two entries with each other @@ -530,12 +536,25 @@ ) } - fromLookup := compare.createLookUpMap(from) - toLookup := compare.createLookUpMap(to) + var createLookUpMap = func(sequenceNode *yamlv3.Node) map[uint64][]int { + var result = make(map[uint64][]int, len(sequenceNode.Content)) + for idx, entry := range sequenceNode.Content { + var hash = compare.calcNodeHash(entry) + if _, ok := result[hash]; !ok { + result[hash] = []int{} + } + + result[hash] = append(result[hash], idx) + } + + return result + } + + fromLookup := createLookUpMap(from) + toLookup := createLookUpMap(to) // Fill two lists with the hashes of the entries of each list - fromCommon := make([]*yamlv3.Node, 0, fromLength) - toCommon := make([]*yamlv3.Node, 0, toLength) + var fromCommon, toCommon []*yamlv3.Node for idxPos, fromValue := range from.Content { hash := compare.calcNodeHash(fromValue) @@ -592,10 +611,8 @@ } func (compare *compare) namedEntryLists(path ytbx.Path, identifier listItemIdentifier, from *yamlv3.Node, to *yamlv3.Node) ([]Diff, error) { - removals := make([]*yamlv3.Node, 0) - additions := make([]*yamlv3.Node, 0) - - result := make([]Diff, 0) + var removals, additions []*yamlv3.Node + var result []Diff // Fill two lists with the names of the entries that are common in both lists fromLength := len(from.Content) @@ -655,6 +672,32 @@ } func (compare *compare) nodeValues(path ytbx.Path, from *yamlv3.Node, to *yamlv3.Node) ([]Diff, error) { + if compare.settings.FormatStrings { + var jsonFormat = func(input string) (string, bool) { + var tmp any + + if err := json.Unmarshal([]byte(input), &tmp); err != nil { + return "", false + } + + var buf bytes.Buffer + var encoder = json.NewEncoder(&buf) + encoder.SetIndent("", " ") + if err := encoder.Encode(tmp); err != nil { + return "", false + } + + return buf.String(), true + } + + if fromValue, ok := jsonFormat(from.Value); ok { + if toValue, ok := jsonFormat(to.Value); ok { + from.Value = fromValue + to.Value = toValue + } + } + } + if strings.Compare(from.Value, to.Value) != 0 { // leave and don't report any differences if ignore whitespaces changes is // configured and it is really only a whitespace only change between the strings @@ -662,14 +705,11 @@ return nil, nil } - return []Diff{{ - &path, - []Detail{{ - Kind: MODIFICATION, - From: from, - To: to, - }}, - }}, nil + return []Diff{{&path, []Detail{{ + Kind: MODIFICATION, + From: from, + To: to, + }}}}, nil } return nil, nil @@ -1008,20 +1048,6 @@ return false } -func (compare *compare) createLookUpMap(sequenceNode *yamlv3.Node) map[uint64][]int { - result := make(map[uint64][]int, len(sequenceNode.Content)) - for idx, entry := range sequenceNode.Content { - hash := compare.calcNodeHash(entry) - if _, ok := result[hash]; !ok { - result[hash] = []int{} - } - - result[hash] = append(result[hash], idx) - } - - return result -} - func (compare *compare) basicType(node *yamlv3.Node) interface{} { switch node.Kind { case yamlv3.DocumentNode: @@ -1068,7 +1094,7 @@ hash, err = hashstructure.Hash(compare.basicType(node), nil) case yamlv3.ScalarNode: - hash, err = hashstructure.Hash(node.Value, nil) + hash, err = hashstructure.Hash(node.Tag+"/"+node.Value, nil) case yamlv3.AliasNode: hash = compare.calcNodeHash(followAlias(node)) @@ -1105,22 +1131,6 @@ }) } -func min(a, b int) int { - if a < b { - return a - } - - return b -} - -func max(a, b int) int { - if a > b { - return a - } - - return b -} - func isList(node *yamlv3.Node) bool { switch node.Kind { case yamlv3.SequenceNode: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dyff-1.10.5/pkg/dyff/core_identifier.go new/dyff-1.11.2/pkg/dyff/core_identifier.go --- old/dyff-1.10.5/pkg/dyff/core_identifier.go 2026-02-22 13:42:39.000000000 +0100 +++ new/dyff-1.11.2/pkg/dyff/core_identifier.go 2026-03-01 17:08:27.000000000 +0100 @@ -49,7 +49,7 @@ IdentifierFieldName string } -var _ listItemIdentifier = &singleField{} +var _ listItemIdentifier = (*singleField)(nil) func (sf *singleField) FindNodeByName(sequenceNode *yamlv3.Node, name string) (*yamlv3.Node, error) { for _, mappingNode := range sequenceNode.Content { @@ -67,12 +67,14 @@ } func (sf *singleField) Name(mappingNode *yamlv3.Node) (string, error) { - result, err := grab(mappingNode, sf.IdentifierFieldName) - if err != nil { - return "", err + for i := 0; i < len(mappingNode.Content); i += 2 { + k, v := mappingNode.Content[i], mappingNode.Content[i+1] + if k.Value == sf.IdentifierFieldName { + return followAlias(v).Value, nil + } } - return followAlias(result).Value, nil + return "", fmt.Errorf("no key %q found in map", sf.IdentifierFieldName) } func (sf *singleField) String() string { @@ -85,7 +87,7 @@ // api version, kind, and name field to be used type k8sItemIdentifier struct{} -var k8sItem listItemIdentifier = &k8sItemIdentifier{} +var k8sItem listItemIdentifier = (*k8sItemIdentifier)(nil) func (i *k8sItemIdentifier) FindNodeByName(sequenceNode *yamlv3.Node, name string) (*yamlv3.Node, error) { for _, mappingNode := range sequenceNode.Content { ++++++ dyff.obsinfo ++++++ --- /var/tmp/diff_new_pack.1uavjC/_old 2026-03-02 17:39:53.402262271 +0100 +++ /var/tmp/diff_new_pack.1uavjC/_new 2026-03-02 17:39:53.418262938 +0100 @@ -1,5 +1,5 @@ name: dyff -version: 1.10.5 -mtime: 1771764159 -commit: 0c7ba9461bf06f54f9527d9945c3f879efc76770 +version: 1.11.2 +mtime: 1772381307 +commit: 6c36ce0d21d83f307a4e76ee810d9657947e3cf9 ++++++ vendor.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/.editorconfig new/vendor/github.com/caarlos0/env/v11/.editorconfig --- old/vendor/github.com/caarlos0/env/v11/.editorconfig 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/.editorconfig 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,28 @@ +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +max_line_length = 120 + +[{go.mod,go.sum,*.go}] +insert_final_newline = true +indent_size = tab +indent_style = tab +tab_width = 4 + +[Makefile] +max_line_length = off +insert_final_newline = true +indent_size = tab +indent_style = tab +tab_width = 4 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false +indent_size = tab +indent_style = space +tab_width = 2 + +[.mailmap] +max_line_length = off diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/.gitignore new/vendor/github.com/caarlos0/env/v11/.gitignore --- old/vendor/github.com/caarlos0/env/v11/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/.gitignore 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,5 @@ +coverage.txt +bin +card.png +dist +codecov* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/.golangci.yml new/vendor/github.com/caarlos0/env/v11/.golangci.yml --- old/vendor/github.com/caarlos0/env/v11/.golangci.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/.golangci.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,115 @@ +version: "2" +run: + go: "1.24" +linters: + enable: + - bodyclose + - copyloopvar + - depguard + - forbidigo + - gocritic + - misspell + - noctx + - nolintlint + - perfsprint + - revive + - testifylint + - thelper + - tparallel + - unconvert + - unparam + - usetesting + - wastedassign + settings: + depguard: + rules: + main: + deny: + - pkg: github.com/pkg/errors + desc: use stdlib instead + - pkg: math/rand$ + desc: use math/rand/v2 instead + - pkg: github.com/apex/log + desc: use caarlos0/log instead + forbidigo: + forbid: + - pattern: ioutil\.* + gocritic: + disabled-checks: + - appendAssign + perfsprint: + int-conversion: false + err-error: false + errorf: true + sprintf1: false + strconcat: false + revive: + enable-all-rules: false + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: comment-spacings + - name: dot-imports + - name: empty-block + - name: empty-lines + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: increment-decrement + - name: indent-error-flow + - name: modifies-value-receiver + - name: range + - name: receiver-naming + - name: redefines-builtin-id + - name: superfluous-else + - name: time-naming + - name: unexported-return + - name: unreachable-code + - name: unused-parameter + - name: var-declaration + - name: var-naming + # we are not providing an API anyway... maybe someday... + # - name: exported + # - name: package-comments + staticcheck: + checks: + - all + - -SA1019 + testifylint: + enable-all: true + disable: + - error-is-as + usetesting: + context-background: true + context-todo: true + os-chdir: true + os-mkdir-temp: true + os-setenv: true + os-create-temp: true + os-temp-dir: true + exclusions: + generated: lax + presets: + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - noctx + - perfsprint + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofumpt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/.goreleaser.yml new/vendor/github.com/caarlos0/env/v11/.goreleaser.yml --- old/vendor/github.com/caarlos0/env/v11/.goreleaser.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/.goreleaser.yml 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json +version: 2 +includes: + - from_url: + url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/.mailmap new/vendor/github.com/caarlos0/env/v11/.mailmap --- old/vendor/github.com/caarlos0/env/v11/.mailmap 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/.mailmap 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,7 @@ +Carlos Alexandro Becker <[email protected]> Carlos A Becker <[email protected]> +Carlos Alexandro Becker <[email protected]> Carlos A Becker <[email protected]> +Carlos Alexandro Becker <[email protected]> Carlos Alexandro Becker <[email protected]> +Carlos Alexandro Becker <[email protected]> Carlos Alexandro Becker <[email protected]> +Carlos Alexandro Becker <[email protected]> Carlos Becker <[email protected]> +dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> +actions-user <[email protected]> github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/LICENSE.md new/vendor/github.com/caarlos0/env/v11/LICENSE.md --- old/vendor/github.com/caarlos0/env/v11/LICENSE.md 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/LICENSE.md 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2024 Carlos Alexandro Becker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/Makefile new/vendor/github.com/caarlos0/env/v11/Makefile --- old/vendor/github.com/caarlos0/env/v11/Makefile 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/Makefile 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,37 @@ +SOURCE_FILES?=./... +TEST_PATTERN?=. + +export GO111MODULE := on + +setup: + go mod tidy +.PHONY: setup + +build: + go build +.PHONY: build + +test: + go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m +.PHONY: test + +cover: test + go tool cover -html=coverage.txt +.PHONY: cover + +fmt: + gofumpt -w -l . +.PHONY: fmt + +lint: + golangci-lint run ./... +.PHONY: lint + +ci: build test +.PHONY: ci + +card: + wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" +.PHONY: card + +.DEFAULT_GOAL := ci diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/README.md new/vendor/github.com/caarlos0/env/v11/README.md --- old/vendor/github.com/caarlos0/env/v11/README.md 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/README.md 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,156 @@ +<p align="center"> + <img alt="GoReleaser Logo" src="https://becker.software/env.png" height="140" /> + <p align="center">A simple, zero-dependencies library to parse environment variables into structs.</p> +</p> + +###### Installation + +```bash +go get github.com/caarlos0/env/v11 +``` + +###### Getting started + +```go +type config struct { + Home string `env:"HOME"` +} + +// parse +var cfg config +err := env.Parse(&cfg) + +// parse with generics +cfg, err := env.ParseAs[config]() +``` + +You can see the full documentation and list of examples at [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11). + +--- + +## Used and supported by + +<p> + <a href="https://encore.dev"> + <img src="https://user-images.githubusercontent.com/78424526/214602214-52e0483a-b5fc-4d4c-b03e-0b7b23e012df.svg" width="120px" alt="encore icon" /> + </a> + <br/> + <br/> + <b>Encore – the platform for building Go-based cloud backends.</b> + <br/> +</p> + +## Usage + +### Caveats + +> [!CAUTION] +> +> _Unexported fields_ will be **ignored** by `env`. +> This is by design and will not change. + +### Functions + +- `Parse`: parse the current environment into a type +- `ParseAs`: parse the current environment into a type using generics +- `ParseWithOptions`: parse the current environment into a type with custom options +- `ParseAsWithOptions`: parse the current environment into a type with custom options and using generics +- `Must`: can be used to wrap `Parse.*` calls to panic on error +- `GetFieldParams`: get the `env` parsed options for a type +- `GetFieldParamsWithOptions`: get the `env` parsed options for a type with custom options + +### Supported types + +Out of the box all built-in types are supported, plus a few others that are commonly used. + +Complete list: + +- `bool` +- `float32` +- `float64` +- `int16` +- `int32` +- `int64` +- `int8` +- `int` +- `string` +- `uint16` +- `uint32` +- `uint64` +- `uint8` +- `uint` +- `time.Duration` +- `time.Location` +- `encoding.TextUnmarshaler` +- `url.URL` + +Pointers, slices and slices of pointers, and maps of those types are also supported. + +You may also add custom parsers for your types. + +### Tags + +The following tags are provided: + +- `env`: sets the environment variable name and optionally takes the tag options described below +- `envDefault`: sets the default value for the field +- `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it +- `envSeparator`: sets the character to be used to separate items in slices and maps (default: `,`) +- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (default: `:`) + +### `env` tag options + +Here are all the options available for the `env` tag: + +- `,expand`: expands environment variables, e.g. `FOO_${BAR}` +- `,file`: instructs that the content of the variable is a path to a file that should be read +- `,init`: initialize nil pointers +- `,notEmpty`: make the field errors if the environment variable is empty +- `,required`: make the field errors if the environment variable is not set +- `,unset`: unset the environment variable after use + +### Parse Options + +There are a few options available in the functions that end with `WithOptions`: + +- `Environment`: keys and values to be used instead of `os.Environ()` +- `TagName`: specifies another tag name to use rather than the default `env` +- `PrefixTagName`: specifies another prefix tag name to use rather than the default `envPrefix` +- `DefaultValueTagName`: specifies another default tag name to use rather than the default `envDefault` +- `RequiredIfNoDef`: set all `env` fields as required if they do not declare `envDefault` +- `OnSet`: allows to hook into the `env` parsing and do something when a value is set +- `Prefix`: prefix to be used in all environment variables +- `UseFieldNameByDefault`: defines whether or not `env` should use the field name by default if the `env` key is missing +- `FuncMap`: custom parse functions for custom types + +### Documentation and examples + +Examples are live in [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11), +and also in the [example test file](./example_test.go). + +## Current state + +`env` is considered feature-complete. + +I do not intent to add any new features unless they really make sense, and are +requested by many people. + +Eventual bug fixes will keep being merged. + +## Badges + +[](https://github.com/goreleaser/goreleaser/releases/latest) +[](/LICENSE.md) +[](https://github.com/caarlos0/env/actions?workflow=build) +[](https://codecov.io/gh/caarlos0/env) +[](http://godoc.org/github.com/caarlos0/env/v11) +[](https://github.com/goreleaser) +[](https://conventionalcommits.org) + +## Related projects + +- [envdoc](https://github.com/g4s8/envdoc) - generate documentation for environment variables from `env` tags + +## Stargazers over time + +[](https://starchart.cc/caarlos0/env) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/env.go new/vendor/github.com/caarlos0/env/v11/env.go --- old/vendor/github.com/caarlos0/env/v11/env.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/env.go 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,853 @@ +// Package env is a simple, zero-dependencies library to parse environment +// variables into structs. +// +// Example: +// +// type config struct { +// Home string `env:"HOME"` +// } +// // parse +// var cfg config +// err := env.Parse(&cfg) +// // or parse with generics +// cfg, err := env.ParseAs[config]() +// +// Check the examples and README for more detailed usage. +package env + +import ( + "encoding" + "fmt" + "net/url" + "os" + "reflect" + "strconv" + "strings" + "time" + "unicode" +) + +var defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ //nolint:gochecknoglobals + reflect.Bool: func(v string) (interface{}, error) { + return strconv.ParseBool(v) + }, + reflect.String: func(v string) (interface{}, error) { + return v, nil + }, + reflect.Int: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int(i), err + }, + reflect.Int16: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 16) + return int16(i), err + }, + reflect.Int32: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int32(i), err + }, + reflect.Int64: func(v string) (interface{}, error) { + return strconv.ParseInt(v, 10, 64) + }, + reflect.Int8: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 8) + return int8(i), err + }, + reflect.Uint: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint(i), err + }, + reflect.Uint16: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 16) + return uint16(i), err + }, + reflect.Uint32: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint32(i), err + }, + reflect.Uint64: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 64) + return i, err + }, + reflect.Uint8: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 8) + return uint8(i), err + }, + reflect.Float64: func(v string) (interface{}, error) { + return strconv.ParseFloat(v, 64) + }, + reflect.Float32: func(v string) (interface{}, error) { + f, err := strconv.ParseFloat(v, 32) + return float32(f), err + }, +} + +func defaultTypeParsers() map[reflect.Type]ParserFunc { + return map[reflect.Type]ParserFunc{ + reflect.TypeOf(url.URL{}): parseURL, + reflect.TypeOf(time.Nanosecond): parseDuration, + reflect.TypeOf(time.Location{}): parseLocation, + } +} + +func parseURL(v string) (interface{}, error) { + u, err := url.Parse(v) + if err != nil { + return nil, newParseValueError("unable to parse URL", err) + } + return *u, nil +} + +func parseDuration(v string) (interface{}, error) { + d, err := time.ParseDuration(v) + if err != nil { + return nil, newParseValueError("unable to parse duration", err) + } + return d, err +} + +func parseLocation(v string) (interface{}, error) { + loc, err := time.LoadLocation(v) + if err != nil { + return nil, newParseValueError("unable to parse location", err) + } + return *loc, nil +} + +// ParserFunc defines the signature of a function that can be used within +// `Options`' `FuncMap`. +type ParserFunc func(v string) (interface{}, error) + +// OnSetFn is a hook that can be run when a value is set. +type OnSetFn func(tag string, value interface{}, isDefault bool) + +// processFieldFn is a function which takes all information about a field and processes it. +type processFieldFn func( + refField reflect.Value, + refTypeField reflect.StructField, + opts Options, + fieldParams FieldParams, +) error + +// Options for the parser. +type Options struct { + // Environment keys and values that will be accessible for the service. + Environment map[string]string + + // TagName specifies another tag name to use rather than the default 'env'. + TagName string + + // PrefixTagName specifies another prefix tag name to use rather than the default 'envPrefix'. + PrefixTagName string + + // DefaultValueTagName specifies another default tag name to use rather than the default 'envDefault'. + DefaultValueTagName string + + // RequiredIfNoDef automatically sets all fields as required if they do not + // declare 'envDefault'. + RequiredIfNoDef bool + + // OnSet allows to run a function when a value is set. + OnSet OnSetFn + + // Prefix define a prefix for every key. + Prefix string + + // UseFieldNameByDefault defines whether or not `env` should use the field + // name by default if the `env` key is missing. + // Note that the field name will be "converted" to conform with environment + // variable names conventions. + UseFieldNameByDefault bool + + // SetDefaultsForZeroValuesOnly defines whether to set defaults for zero values + // If the `env` variable for the value is not set + // and `envDefault` is set + // and the value is not a zero value for the the type + // and SetDefaultsForZeroValuesOnly=true + // the value from `envDefault` will be ignored + // Useful for mixing default values from `envDefault` and struct initialization + SetDefaultsForZeroValuesOnly bool + + // Custom parse functions for different types. + FuncMap map[reflect.Type]ParserFunc + + // Used internally. maps the env variable key to its resolved string value. + // (for env var expansion) + rawEnvVars map[string]string +} + +func (opts *Options) getRawEnv(s string) string { + val := opts.rawEnvVars[s] + if val == "" { + val = opts.Environment[s] + } + return os.Expand(val, opts.getRawEnv) +} + +func defaultOptions() Options { + return Options{ + TagName: "env", + PrefixTagName: "envPrefix", + DefaultValueTagName: "envDefault", + Environment: toMap(os.Environ()), + FuncMap: defaultTypeParsers(), + rawEnvVars: make(map[string]string), + } +} + +func mergeOptions[T any](target, source *T) { + targetPtr := reflect.ValueOf(target).Elem() + sourcePtr := reflect.ValueOf(source).Elem() + + targetType := targetPtr.Type() + for i := 0; i < targetPtr.NumField(); i++ { + fieldName := targetType.Field(i).Name + targetField := targetPtr.Field(i) + sourceField := sourcePtr.FieldByName(fieldName) + + if targetField.CanSet() && !isZero(sourceField) { + // FuncMaps are being merged, while Environments must be overwritten + if fieldName == "FuncMap" { + if !sourceField.IsZero() { + iter := sourceField.MapRange() + for iter.Next() { + targetField.SetMapIndex(iter.Key(), iter.Value()) + } + } + } else { + targetField.Set(sourceField) + } + } + } +} + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + default: + zero := reflect.Zero(v.Type()) + return v.Interface() == zero.Interface() + } +} + +func customOptions(opts Options) Options { + defOpts := defaultOptions() + mergeOptions(&defOpts, &opts) + return defOpts +} + +func optionsWithSliceEnvPrefix(opts Options, index int) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + PrefixTagName: opts.PrefixTagName, + DefaultValueTagName: opts.DefaultValueTagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + SetDefaultsForZeroValuesOnly: opts.SetDefaultsForZeroValuesOnly, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + +func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + PrefixTagName: opts.PrefixTagName, + DefaultValueTagName: opts.DefaultValueTagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: opts.Prefix + field.Tag.Get(opts.PrefixTagName), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + SetDefaultsForZeroValuesOnly: opts.SetDefaultsForZeroValuesOnly, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + +// Parse parses a struct containing `env` tags and loads its values from +// environment variables. +func Parse(v interface{}) error { + return parseInternal(v, setField, defaultOptions()) +} + +// ParseWithOptions parses a struct containing `env` tags and loads its values from +// environment variables. +func ParseWithOptions(v interface{}, opts Options) error { + return parseInternal(v, setField, customOptions(opts)) +} + +// ParseAs parses the given struct type containing `env` tags and loads its +// values from environment variables. +func ParseAs[T any]() (T, error) { + var t T + err := Parse(&t) + return t, err +} + +// ParseAsWithOptions parses the given struct type containing `env` tags and +// loads its values from environment variables. +func ParseAsWithOptions[T any](opts Options) (T, error) { + var t T + err := ParseWithOptions(&t, opts) + return t, err +} + +// Must panic is if err is not nil, and returns t otherwise. +func Must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} + +// GetFieldParams parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParams(v interface{}) ([]FieldParams, error) { + return GetFieldParamsWithOptions(v, defaultOptions()) +} + +// GetFieldParamsWithOptions parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParamsWithOptions(v interface{}, opts Options) ([]FieldParams, error) { + var result []FieldParams + err := parseInternal( + v, + func(_ reflect.Value, _ reflect.StructField, _ Options, fieldParams FieldParams) error { + if fieldParams.OwnKey != "" { + result = append(result, fieldParams) + } + return nil + }, + customOptions(opts), + ) + if err != nil { + return nil, err + } + + return result, nil +} + +func parseInternal(v interface{}, processField processFieldFn, opts Options) error { + ptrRef := reflect.ValueOf(v) + if ptrRef.Kind() != reflect.Ptr { + return newAggregateError(NotStructPtrError{}) + } + ref := ptrRef.Elem() + if ref.Kind() != reflect.Struct { + return newAggregateError(NotStructPtrError{}) + } + + return doParse(ref, processField, opts) +} + +func doParse(ref reflect.Value, processField processFieldFn, opts Options) error { + refType := ref.Type() + + var agrErr AggregateError + + for i := 0; i < refType.NumField(); i++ { + refField := ref.Field(i) + refTypeField := refType.Field(i) + + if err := doParseField(refField, refTypeField, processField, opts); err != nil { + if val, ok := err.(AggregateError); ok { + agrErr.Errors = append(agrErr.Errors, val.Errors...) + } else { + agrErr.Errors = append(agrErr.Errors, err) + } + } + } + + if len(agrErr.Errors) == 0 { + return nil + } + + return agrErr +} + +func doParseField( + refField reflect.Value, + refTypeField reflect.StructField, + processField processFieldFn, + opts Options, +) error { + if !refField.CanSet() { + return nil + } + if refField.Kind() == reflect.Ptr && refField.Elem().Kind() == reflect.Struct && !refField.IsNil() { + return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + if refField.Kind() == reflect.Struct && refField.CanAddr() && refField.Type().Name() == "" { + return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + params, err := parseFieldParams(refTypeField, opts) + if err != nil { + return err + } + + if params.Ignored { + return nil + } + + if err := processField(refField, refTypeField, opts, params); err != nil { + return err + } + + if params.Init && isInvalidPtr(refField) { + refField.Set(reflect.New(refField.Type().Elem())) + refField = refField.Elem() + } + + if refField.Kind() == reflect.Struct { + return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + if isSliceOfStructs(refTypeField) { + return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + return nil +} + +func isSliceOfStructs(refTypeField reflect.StructField) bool { + field := refTypeField.Type + + // *[]struct + if field.Kind() == reflect.Ptr { + field = field.Elem() + if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { + return true + } + } + + // []struct{} + if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { + return true + } + + return false +} + +func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) error { + if opts.Prefix != "" && !strings.HasSuffix(opts.Prefix, string(underscore)) { + opts.Prefix += string(underscore) + } + + var environments []string + for environment := range opts.Environment { + if strings.HasPrefix(environment, opts.Prefix) { + environments = append(environments, environment) + } + } + + if len(environments) > 0 { + counter := 0 + for finished := false; !finished; { + finished = true + prefix := fmt.Sprintf("%s%d%c", opts.Prefix, counter, underscore) + for _, variable := range environments { + if strings.HasPrefix(variable, prefix) { + counter++ + finished = false + break + } + } + } + + sliceType := ref.Type() + var initialized int + if reflect.Ptr == ref.Kind() { + sliceType = sliceType.Elem() + // Due to the rest of code the pre-initialized slice has no chance for this situation + initialized = 0 + } else { + initialized = ref.Len() + } + + var capacity int + if capacity = initialized; counter > initialized { + capacity = counter + } + result := reflect.MakeSlice(sliceType, capacity, capacity) + for i := 0; i < capacity; i++ { + item := result.Index(i) + if i < initialized { + item.Set(ref.Index(i)) + } + if err := doParse(item, processField, optionsWithSliceEnvPrefix(opts, i)); err != nil { + return err + } + } + + if result.Len() > 0 { + if reflect.Ptr == ref.Kind() { + resultPtr := reflect.New(sliceType) + resultPtr.Elem().Set(result) + result = resultPtr + } + ref.Set(result) + } + } + + return nil +} + +func setField(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error { + value, err := get(fieldParams, opts) + if err != nil { + return err + } + + if value != "" && (!opts.SetDefaultsForZeroValuesOnly || refField.IsZero()) { + return set(refField, refTypeField, value, opts.FuncMap) + } + + return nil +} + +const underscore rune = '_' + +func toEnvName(input string) string { + var output []rune + for i, c := range input { + if c == underscore { + continue + } + if len(output) > 0 && unicode.IsUpper(c) { + if len(input) > i+1 { + peek := rune(input[i+1]) + if unicode.IsLower(peek) || unicode.IsLower(rune(input[i-1])) { + output = append(output, underscore) + } + } + } + output = append(output, unicode.ToUpper(c)) + } + return string(output) +} + +// FieldParams contains information about parsed field tags. +type FieldParams struct { + OwnKey string + Key string + DefaultValue string + HasDefaultValue bool + Required bool + LoadFile bool + Unset bool + NotEmpty bool + Expand bool + Init bool + Ignored bool +} + +func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) { + ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName)) + if ownKey == "" && opts.UseFieldNameByDefault { + ownKey = toEnvName(field.Name) + } + + defaultValue, hasDefaultValue := field.Tag.Lookup(opts.DefaultValueTagName) + + result := FieldParams{ + OwnKey: ownKey, + Key: opts.Prefix + ownKey, + Required: opts.RequiredIfNoDef, + DefaultValue: defaultValue, + HasDefaultValue: hasDefaultValue, + Ignored: ownKey == "-", + } + + for _, tag := range tags { + switch tag { + case "": + continue + case "file": + result.LoadFile = true + case "required": + result.Required = true + case "unset": + result.Unset = true + case "notEmpty": + result.NotEmpty = true + case "expand": + result.Expand = true + case "init": + result.Init = true + case "-": + result.Ignored = true + default: + return FieldParams{}, newNoSupportedTagOptionError(tag) + } + } + + return result, nil +} + +func get(fieldParams FieldParams, opts Options) (val string, err error) { + var exists, isDefault bool + + val, exists, isDefault = getOr( + fieldParams.Key, + fieldParams.DefaultValue, + fieldParams.HasDefaultValue, + opts.Environment, + ) + + if fieldParams.Expand { + val = os.Expand(val, opts.getRawEnv) + } + + opts.rawEnvVars[fieldParams.OwnKey] = val + + if fieldParams.Unset { + defer os.Unsetenv(fieldParams.Key) + } + + if fieldParams.Required && !exists && fieldParams.OwnKey != "" { + return "", newVarIsNotSetError(fieldParams.Key) + } + + if fieldParams.NotEmpty && val == "" { + return "", newEmptyVarError(fieldParams.Key) + } + + if fieldParams.LoadFile && val != "" { + filename := val + val, err = getFromFile(filename) + if err != nil { + return "", newLoadFileContentError(filename, fieldParams.Key, err) + } + } + + if opts.OnSet != nil { + if fieldParams.OwnKey != "" { + opts.OnSet(fieldParams.Key, val, isDefault) + } + } + return val, err +} + +// split the env tag's key into the expected key and desired option, if any. +func parseKeyForOption(key string) (string, []string) { + opts := strings.Split(key, ",") + return opts[0], opts[1:] +} + +func getFromFile(filename string) (value string, err error) { + b, err := os.ReadFile(filename) + return string(b), err +} + +func getOr(key, defaultValue string, defExists bool, envs map[string]string) (val string, exists, isDefault bool) { + value, exists := envs[key] + switch { + case (!exists || key == "") && defExists: + return defaultValue, true, true + case exists && value == "" && defExists: + return defaultValue, true, true + case !exists: + return "", false, false + } + + return value, true, false +} + +func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { + if tm := asTextUnmarshaler(field); tm != nil { + if err := tm.UnmarshalText([]byte(value)); err != nil { + return newParseError(sf, err) + } + return nil + } + + typee := sf.Type + fieldee := field + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + fieldee = field.Elem() + } + + parserFunc, ok := funcMap[typee] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val)) + return nil + } + + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val).Convert(typee)) + return nil + } + + switch field.Kind() { + case reflect.Slice: + return handleSlice(field, value, sf, funcMap) + case reflect.Map: + return handleMap(field, value, sf, funcMap) + } + + return newNoParserError(sf) +} + +func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + parts := strings.Split(value, separator) + + typee := sf.Type.Elem() + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + } + + if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { + return parseTextUnmarshalers(field, parts, sf) + } + + parserFunc, ok := funcMap[typee] + if !ok { + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + result := reflect.MakeSlice(sf.Type, 0, len(parts)) + for _, part := range parts { + r, err := parserFunc(part) + if err != nil { + return newParseError(sf, err) + } + v := reflect.ValueOf(r).Convert(typee) + if sf.Type.Elem().Kind() == reflect.Ptr { + v = reflect.New(typee) + v.Elem().Set(reflect.ValueOf(r).Convert(typee)) + } + result = reflect.Append(result, v) + } + field.Set(result) + return nil +} + +func handleMap(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + keyType := sf.Type.Key() + keyParserFunc, ok := funcMap[keyType] + if !ok { + keyParserFunc, ok = defaultBuiltInParsers[keyType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + elemType := sf.Type.Elem() + elemParserFunc, ok := funcMap[elemType] + if !ok { + elemParserFunc, ok = defaultBuiltInParsers[elemType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + + keyValSeparator := sf.Tag.Get("envKeyValSeparator") + if keyValSeparator == "" { + keyValSeparator = ":" + } + + result := reflect.MakeMap(sf.Type) + for _, part := range strings.Split(value, separator) { + pairs := strings.SplitN(part, keyValSeparator, 2) + if len(pairs) != 2 { + return newParseError(sf, fmt.Errorf(`%q should be in "key%svalue" format`, part, keyValSeparator)) + } + + key, err := keyParserFunc(pairs[0]) + if err != nil { + return newParseError(sf, err) + } + + elem, err := elemParserFunc(pairs[1]) + if err != nil { + return newParseError(sf, err) + } + + result.SetMapIndex(reflect.ValueOf(key).Convert(keyType), reflect.ValueOf(elem).Convert(elemType)) + } + + field.Set(result) + return nil +} + +func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { + if field.Kind() == reflect.Ptr { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + } else if field.CanAddr() { + field = field.Addr() + } + + tm, ok := field.Interface().(encoding.TextUnmarshaler) + if !ok { + return nil + } + return tm +} + +func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { + s := len(data) + elemType := field.Type().Elem() + slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) + for i, v := range data { + sv := slice.Index(i) + kind := sv.Kind() + if kind == reflect.Ptr { + sv = reflect.New(elemType.Elem()) + } else { + sv = sv.Addr() + } + tm := sv.Interface().(encoding.TextUnmarshaler) + if err := tm.UnmarshalText([]byte(v)); err != nil { + return newParseError(sf, err) + } + if kind == reflect.Ptr { + slice.Index(i).Set(sv) + } + } + + field.Set(slice) + + return nil +} + +// ToMap Converts list of env vars as provided by os.Environ() to map you +// can use as Options.Environment field +func ToMap(env []string) map[string]string { + return toMap(env) +} + +func isInvalidPtr(v reflect.Value) bool { + return reflect.Ptr == v.Kind() && v.Elem().Kind() == reflect.Invalid +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/env_tomap.go new/vendor/github.com/caarlos0/env/v11/env_tomap.go --- old/vendor/github.com/caarlos0/env/v11/env_tomap.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/env_tomap.go 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,16 @@ +//go:build !windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go new/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go --- old/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,29 @@ +//go:build windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + + // On Windows, environment variables can start with '='. If so, Split at next character. + // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 + prefixEqualSign := false + if len(e) > 0 && e[0] == '=' { + e = e[1:] + prefixEqualSign = true + } + p = strings.SplitN(e, "=", 2) + if prefixEqualSign { + p[0] = "=" + p[0] + } + + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/caarlos0/env/v11/error.go new/vendor/github.com/caarlos0/env/v11/error.go --- old/vendor/github.com/caarlos0/env/v11/error.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/caarlos0/env/v11/error.go 2026-03-01 17:08:27.000000000 +0100 @@ -0,0 +1,173 @@ +package env + +import ( + "fmt" + "reflect" + "strings" +) + +// AggregateError is an aggregated error wrapper to combine gathered errors. +// This allows either to display all errors or convert them individually +// List of the available errors +// ParseError +// NotStructPtrError +// NoParserError +// NoSupportedTagOptionError +// VarIsNotSetError +// EmptyVarError +// LoadFileContentError +// ParseValueError +type AggregateError struct { + Errors []error +} + +func newAggregateError(initErr error) error { + return AggregateError{ + []error{ + initErr, + }, + } +} + +func (e AggregateError) Error() string { + var sb strings.Builder + + sb.WriteString("env:") + + for _, err := range e.Errors { + fmt.Fprintf(&sb, " %v;", err.Error()) + } + + return strings.TrimRight(sb.String(), ";") +} + +// Unwrap implements std errors.Join go1.20 compatibility +func (e AggregateError) Unwrap() []error { + return e.Errors +} + +// Is conforms with errors.Is. +func (e AggregateError) Is(err error) bool { + for _, ie := range e.Errors { + if reflect.TypeOf(ie) == reflect.TypeOf(err) { + return true + } + } + return false +} + +// ParseError occurs when it's impossible to convert the value for given type. +type ParseError struct { + Name string + Type reflect.Type + Err error +} + +func newParseError(sf reflect.StructField, err error) error { + return ParseError{sf.Name, sf.Type, err} +} + +func (e ParseError) Error() string { + return fmt.Sprintf("parse error on field %q of type %q: %v", e.Name, e.Type, e.Err) +} + +// NotStructPtrError occurs when pass something that is not a pointer to a struct to Parse. +type NotStructPtrError struct{} + +func (e NotStructPtrError) Error() string { + return "expected a pointer to a Struct" +} + +// NoParserError occurs when there is no parser provided for given type. +type NoParserError struct { + Name string + Type reflect.Type +} + +func newNoParserError(sf reflect.StructField) error { + return NoParserError{sf.Name, sf.Type} +} + +func (e NoParserError) Error() string { + return fmt.Sprintf("no parser found for field %q of type %q", e.Name, e.Type) +} + +// NoSupportedTagOptionError occurs when the given tag is not supported. +// Built-in supported tags: "", "file", "required", "unset", "notEmpty", +// "expand", "envDefault", and "envSeparator". +type NoSupportedTagOptionError struct { + Tag string +} + +func newNoSupportedTagOptionError(tag string) error { + return NoSupportedTagOptionError{tag} +} + +func (e NoSupportedTagOptionError) Error() string { + return fmt.Sprintf("tag option %q not supported", e.Tag) +} + +// EnvVarIsNotSetError occurs when the required variable is not set. +// +// Deprecated: use VarIsNotSetError. +type EnvVarIsNotSetError = VarIsNotSetError + +// VarIsNotSetError occurs when the required variable is not set. +type VarIsNotSetError struct { + Key string +} + +func newVarIsNotSetError(key string) error { + return VarIsNotSetError{key} +} + +func (e VarIsNotSetError) Error() string { + return fmt.Sprintf(`required environment variable %q is not set`, e.Key) +} + +// EmptyEnvVarError occurs when the variable which must be not empty is existing but has an empty value +// +// Deprecated: use EmptyVarError. +type EmptyEnvVarError = EmptyVarError + +// EmptyVarError occurs when the variable which must be not empty is existing but has an empty value +type EmptyVarError struct { + Key string +} + +func newEmptyVarError(key string) error { + return EmptyVarError{key} +} + +func (e EmptyVarError) Error() string { + return fmt.Sprintf("environment variable %q should not be empty", e.Key) +} + +// LoadFileContentError occurs when it's impossible to load the value from the file. +type LoadFileContentError struct { + Filename string + Key string + Err error +} + +func newLoadFileContentError(filename, key string, err error) error { + return LoadFileContentError{filename, key, err} +} + +func (e LoadFileContentError) Error() string { + return fmt.Sprintf("could not load content of file %q from variable %s: %v", e.Filename, e.Key, e.Err) +} + +// ParseValueError occurs when it's impossible to convert value using given parser. +type ParseValueError struct { + Msg string + Err error +} + +func newParseValueError(message string, err error) error { + return ParseValueError{message, err} +} + +func (e ParseValueError) Error() string { + return fmt.Sprintf("%s: %v", e.Msg, e.Err) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/gonvenience/neat/output_yaml.go new/vendor/github.com/gonvenience/neat/output_yaml.go --- old/vendor/github.com/gonvenience/neat/output_yaml.go 2026-02-22 13:42:39.000000000 +0100 +++ new/vendor/github.com/gonvenience/neat/output_yaml.go 2026-03-01 17:08:27.000000000 +0100 @@ -23,6 +23,7 @@ import ( "fmt" "reflect" + "regexp" "strings" "time" @@ -33,7 +34,7 @@ ) var ( - yamlReservedKeywords = []string{"true", "false", "null"} + numberRegEx = regexp.MustCompile(`^(-|\+)?[0-9.e+]+$`) // YAML Spec regarding timestamp: https://yaml.org/type/timestamp.html yamlTimeLayouts = [...]string{ @@ -371,7 +372,7 @@ } // check if string matches one of the known reserved keywords - for _, chk := range yamlReservedKeywords { + for _, chk := range []string{"true", "false", "null", ".nan", ".inf", "-.inf", "+.inf"} { if node.Value == chk { return true } @@ -382,6 +383,11 @@ return true } + // check if string looks like a number + if numberRegEx.MatchString(node.Value) { + return true + } + // check if string contains special characters return strings.ContainsAny(node.Value, " *&:,") } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/golang.org/x/net/html/iter.go new/vendor/golang.org/x/net/html/iter.go --- old/vendor/golang.org/x/net/html/iter.go 2026-02-22 13:42:39.000000000 +0100 +++ new/vendor/golang.org/x/net/html/iter.go 2026-03-01 17:08:27.000000000 +0100 @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.23 - package html import "iter" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt --- old/vendor/modules.txt 2026-02-22 13:42:39.000000000 +0100 +++ new/vendor/modules.txt 2026-03-01 17:08:27.000000000 +0100 @@ -5,6 +5,9 @@ # github.com/Masterminds/semver/v3 v3.4.0 ## explicit; go 1.21 github.com/Masterminds/semver/v3 +# github.com/caarlos0/env/v11 v11.4.0 +## explicit; go 1.18 +github.com/caarlos0/env/v11 # github.com/cpuguy83/go-md2man/v2 v2.0.7 ## explicit; go 1.12 github.com/cpuguy83/go-md2man/v2/md2man @@ -23,8 +26,8 @@ # github.com/gonvenience/idem v0.0.3 ## explicit; go 1.24.0 github.com/gonvenience/idem -# github.com/gonvenience/neat v1.3.17 -## explicit; go 1.24.0 +# github.com/gonvenience/neat v1.3.18 +## explicit; go 1.25.0 github.com/gonvenience/neat # github.com/gonvenience/term v1.0.5 ## explicit; go 1.24.0 @@ -130,8 +133,8 @@ # golang.org/x/mod v0.33.0 ## explicit; go 1.24.0 golang.org/x/mod/semver -# golang.org/x/net v0.50.0 -## explicit; go 1.24.0 +# golang.org/x/net v0.51.0 +## explicit; go 1.25.0 golang.org/x/net/html golang.org/x/net/html/atom golang.org/x/net/html/charset
