diff --git a/Makefile b/Makefile index 08c7fa6..a7e9136 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ clean: .PHONY: test test: - @ginkgo run \ + @go run -mod=mod github.com/onsi/ginkgo/v2/ginkgo run \ --coverprofile=unit.coverprofile \ --randomize-all \ --randomize-suites \ diff --git a/assets/issues/issue-225/expected-dyff-spruce.human b/assets/issues/issue-225/expected-dyff-spruce.human index cb85639..5f2d6cf 100644 --- a/assets/issues/issue-225/expected-dyff-spruce.human +++ b/assets/issues/issue-225/expected-dyff-spruce.human @@ -1,10 +1,9 @@ data - ± value change in multiline text (one insert) - string foo = "bar"; - string x_forwarded_host = 53; - string worker_status = 54; - uint64 worker_cpu_time_micro = 55; - + ± value change in multiline text (one insert, no deletions) +  string foo = "bar"; +  + string x_forwarded_host = 53; +  + string worker_status = 54; +  + uint64 worker_cpu_time_micro = 55; diff --git a/assets/kubernetes/configmaps/expected-dyff-spruce.human b/assets/kubernetes/configmaps/expected-dyff-spruce.human index bb569a2..e43965c 100644 --- a/assets/kubernetes/configmaps/expected-dyff-spruce.human +++ b/assets/kubernetes/configmaps/expected-dyff-spruce.human @@ -1,29 +1,28 @@ data.pinniped.yaml - ± value change in multiline text (one insert, one deletion) - discovery: -  url: null - api: -  servingCertificate: -  durationSeconds: 2592000 -  renewBeforeSeconds: 12160000 - apiGroupSuffix: pinniped.dev - # aggregatedAPIServerPort may be set here, although other YAML references to the default port (10250) may also need to be updated - # impersonationProxyServerPort may be set here, although other YAML references to the default port (8444) may also need to be updated - names: -  servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate -  credentialIssuer: pinniped-concierge-config -  apiService: pinniped-concierge-api -  impersonationLoadBalancerService: pinniped-concierge-impersonation-proxy-load-balancer -  impersonationClusterIPService: pinniped-concierge-impersonation-proxy-cluster-ip -  impersonationTLSCertificateSecret: pinniped-concierge-impersonation-proxy-tls-serving-certificate -  impersonationCACertificateSecret: pinniped-concierge-impersonation-proxy-ca-certificate -  impersonationSignerSecret: pinniped-concierge-impersonation-proxy-signer-ca-certificate -  agentServiceAccount: pinniped-concierge-kube-cert-agent - labels: {"app": "pinniped-concierge"} - kubeCertAgent: -  namePrefix: pinniped-concierge-kube-cert-agent- -  image: projects.registry.vmware.com/pinniped/pinniped-server:latest - + ± value change in multiline text (one insert, two deletions) +  discovery: +  url: null +  api: +  servingCertificate: +  durationSeconds: 2592000 +  - renewBeforeSeconds: 2160000 +  + renewBeforeSeconds: 12160000 +  apiGroupSuffix: pinniped.dev +  # aggregatedAPIServerPort may be set here, although other YAML references to the default port (10250) may also need to be updated +  # impersonationProxyServerPort may be set here, although other YAML references to the default port (8444) may also need to be updated +  names: +   +  [five lines unchanged)] +   +  impersonationTLSCertificateSecret: pinniped-concierge-impersonation-proxy-tls-serving-certificate +  impersonationCACertificateSecret: pinniped-concierge-impersonation-proxy-ca-certificate +  impersonationSignerSecret: pinniped-concierge-impersonation-proxy-signer-ca-certificate +  agentServiceAccount: pinniped-concierge-kube-cert-agent +  - labels: {"app": "pinniped-concierge"} +  kubeCertAgent: +  namePrefix: pinniped-concierge-kube-cert-agent- +  image: projects.registry.vmware.com/pinniped/pinniped-server:latest + diff --git a/assets/multiline/expected-dyff-spruce.human b/assets/multiline/expected-dyff-spruce.human new file mode 100644 index 0000000..413a13c --- /dev/null +++ b/assets/multiline/expected-dyff-spruce.human @@ -0,0 +1,97 @@ + +files.simple.content + ± value change in multiline text (three inserts, three deletions) +  UnChanged line +  - This line will change 1 +  + This line changed 1 +  UnChanged line +  - This line will change 2 +  + This line changed 2 +  UnChanged line +  - This line will change 3 +  + This line changed 3 + + +files.newline.content + ± value change in multiline text (four inserts, four deletions) +   +  -  +  - This line will change 1 +  + This line changed 1 +  UnChanged line +   +  - This line will change 2 +  + This line changed 2 +  UnChanged line +   +   +  -  +  Moved line +  +  +   +   +  UnChanged line +  - This line will change 3 +  + This line changed 3 +  +  +   +   + + +files.complex.content + ± value change in multiline text (two inserts, two deletions) +  Begin line 1 +  Begin line 2 +  Begin line 3 +  Begin line 4 +   +  [four lines unchanged)] +   +  PreChange line 1 +  PreChange line 2 +  PreChange line 3 +  PreChange line 4 +  - This line will change 1 +  - This line will change 2 +  + This line changed 1 +  + This line changed 2 +  PostChange line 1 +  PostChange line 2 +  PostChange line 3 +  PostChange line 4 +   +  [three lines unchanged)] +   +  PreAdd line 1 +  PreAdd line 2 +  PreAdd line 3 +  PreAdd line 4 +  + This line was added +  + This line was added +  PostAdd line 1 +  PostAdd line 2 +  PostAdd line 3 +  PostAdd line 4 +   +  [two lines unchanged)] +   +  PreDelete line 1 +  PreDelete line 2 +  PreDelete line 3 +  PreDelete line 4 +  - This line will be deleted +  - This line will be deleted +  - This line will be deleted +  PostDelete line 1 +  PostDelete line 2 +  PostDelete line 3 +  PostDelete line 4 +   +  [22 lines unchanged)] +   +  End line 1 +  End line 2 +  End line 3 +  End line 4 + + diff --git a/assets/multiline/from.yml b/assets/multiline/from.yml new file mode 100644 index 0000000..4881d47 --- /dev/null +++ b/assets/multiline/from.yml @@ -0,0 +1,100 @@ +files: +- name: simple + content: | + UnChanged line + This line will change 1 + UnChanged line + This line will change 2 + UnChanged line + This line will change 3 + +- name: newline + # + to keep trailing newlines + content: |+ + + + This line will change 1 + UnChanged line + + This line will change 2 + UnChanged line + + + + Moved line + + + UnChanged line + This line will change 3 + + +- name: complex + content: | + Begin line 1 + Begin line 2 + Begin line 3 + Begin line 4 + Truncated line + Truncated line + Truncated line + Truncated line + PreChange line 1 + PreChange line 2 + PreChange line 3 + PreChange line 4 + This line will change 1 + This line will change 2 + PostChange line 1 + PostChange line 2 + PostChange line 3 + PostChange line 4 + Truncated line + Truncated line + Truncated line + PreAdd line 1 + PreAdd line 2 + PreAdd line 3 + PreAdd line 4 + PostAdd line 1 + PostAdd line 2 + PostAdd line 3 + PostAdd line 4 + Truncated line + Truncated line + PreDelete line 1 + PreDelete line 2 + PreDelete line 3 + PreDelete line 4 + This line will be deleted + This line will be deleted + This line will be deleted + PostDelete line 1 + PostDelete line 2 + PostDelete line 3 + PostDelete line 4 + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + End line 1 + End line 2 + End line 3 + End line 4 diff --git a/assets/multiline/to.yml b/assets/multiline/to.yml new file mode 100644 index 0000000..f3ee4c6 --- /dev/null +++ b/assets/multiline/to.yml @@ -0,0 +1,99 @@ +files: +- name: simple + content: | + UnChanged line + This line changed 1 + UnChanged line + This line changed 2 + UnChanged line + This line changed 3 + +- name: newline + # + to keep trailing newlines + content: |+ + + This line changed 1 + UnChanged line + + This line changed 2 + UnChanged line + + + Moved line + + + + UnChanged line + This line changed 3 + + + +- name: complex + content: | + Begin line 1 + Begin line 2 + Begin line 3 + Begin line 4 + Truncated line + Truncated line + Truncated line + Truncated line + PreChange line 1 + PreChange line 2 + PreChange line 3 + PreChange line 4 + This line changed 1 + This line changed 2 + PostChange line 1 + PostChange line 2 + PostChange line 3 + PostChange line 4 + Truncated line + Truncated line + Truncated line + PreAdd line 1 + PreAdd line 2 + PreAdd line 3 + PreAdd line 4 + This line was added + This line was added + PostAdd line 1 + PostAdd line 2 + PostAdd line 3 + PostAdd line 4 + Truncated line + Truncated line + PreDelete line 1 + PreDelete line 2 + PreDelete line 3 + PreDelete line 4 + PostDelete line 1 + PostDelete line 2 + PostDelete line 3 + PostDelete line 4 + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + Truncated line + End line 1 + End line 2 + End line 3 + End line 4 diff --git a/assets/testbed/expected-dyff-gopatch.human b/assets/testbed/expected-dyff-gopatch.human index 177edf9..a55c0b1 100644 --- a/assets/testbed/expected-dyff-gopatch.human +++ b/assets/testbed/expected-dyff-gopatch.human @@ -30,12 +30,14 @@ + one, two, three, four, five, six /multiline (document #1) - ± value change - - Yes, + Yes, strings - strings can have multiple - can lines - have - multiple + ± value change in multiline text (one insert, one deletion) + - Yes, + - strings + - can + - have + - multiple + + Yes, strings + + can have multiple lines diff --git a/assets/testbed/expected-dyff-spruce.human b/assets/testbed/expected-dyff-spruce.human index 06e3df1..5c9274a 100644 --- a/assets/testbed/expected-dyff-spruce.human +++ b/assets/testbed/expected-dyff-spruce.human @@ -30,12 +30,14 @@ orderchanges (document #1) + one, two, three, four, five, six multiline (document #1) - ± value change - - Yes, + Yes, strings - strings can have multiple - can lines - have - multiple + ± value change in multiline text (one insert, one deletion) + - Yes, + - strings + - can + - have + - multiple + + Yes, strings + + can have multiple lines diff --git a/go.mod b/go.mod index 965e356..a14b973 100644 --- a/go.mod +++ b/go.mod @@ -14,12 +14,17 @@ require ( github.com/mitchellh/hashstructure v1.1.0 github.com/onsi/ginkgo/v2 v2.15.0 github.com/onsi/gomega v1.31.1 - github.com/sergi/go-diff v1.3.1 github.com/spf13/cobra v1.8.0 github.com/texttheater/golang-levenshtein v1.0.1 gopkg.in/yaml.v3 v3.0.1 ) +// 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.3.2-0.20230802210424-5b0b94c5c0d3 + require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/go-logr/logr v1.3.0 // indirect @@ -33,11 +38,11 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.4 // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/tools v0.17.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index a895b97..7bbc345 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -71,17 +73,27 @@ github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtb github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/internal/cmd/common.go b/internal/cmd/common.go index d28b069..8d85a5e 100644 --- a/internal/cmd/common.go +++ b/internal/cmd/common.go @@ -46,6 +46,8 @@ type reportConfig struct { exitWithCode bool omitHeader bool useGoPatchPaths bool + minorChangeThreshold float64 + multilineContextLines int additionalIdentifiers []string filters []string excludes []string @@ -62,6 +64,8 @@ var defaults = reportConfig{ exitWithCode: false, omitHeader: false, useGoPatchPaths: false, + minorChangeThreshold: 0.1, + multilineContextLines: 4, additionalIdentifiers: nil, filters: nil, excludes: nil, @@ -200,12 +204,13 @@ func writeReport(cmd *cobra.Command, report dyff.Report) error { switch strings.ToLower(reportOptions.style) { case "human", "bosh": reportWriter = &dyff.HumanReport{ - Report: report, - DoNotInspectCerts: reportOptions.doNotInspectCerts, - NoTableStyle: reportOptions.noTableStyle, - OmitHeader: reportOptions.omitHeader, - UseGoPatchPaths: reportOptions.useGoPatchPaths, - MinorChangeThreshold: 0.1, + Report: report, + DoNotInspectCerts: reportOptions.doNotInspectCerts, + NoTableStyle: reportOptions.noTableStyle, + OmitHeader: reportOptions.omitHeader, + UseGoPatchPaths: reportOptions.useGoPatchPaths, + MinorChangeThreshold: reportOptions.minorChangeThreshold, + MultilineContextLines: reportOptions.multilineContextLines, } case "brief", "short", "summary": diff --git a/pkg/dyff/core_suite_test.go b/pkg/dyff/core_suite_test.go index d524519..5d3e3b8 100644 --- a/pkg/dyff/core_suite_test.go +++ b/pkg/dyff/core_suite_test.go @@ -124,11 +124,13 @@ func compareAgainstExpected(fromPath string, toPath string, expectedPath string, Expect(err).To(BeNil()) reportWriter := &dyff.HumanReport{ - Report: report, - DoNotInspectCerts: false, - NoTableStyle: false, - OmitHeader: true, - UseGoPatchPaths: useGoPatchPaths, + Report: report, + DoNotInspectCerts: false, + NoTableStyle: false, + OmitHeader: true, + UseGoPatchPaths: useGoPatchPaths, + MinorChangeThreshold: 0.1, + MultilineContextLines: 4, } buffer := &bytes.Buffer{} diff --git a/pkg/dyff/output_human.go b/pkg/dyff/output_human.go index 47f1038..0fbb76c 100644 --- a/pkg/dyff/output_human.go +++ b/pkg/dyff/output_human.go @@ -29,6 +29,7 @@ import ( "encoding/pem" "fmt" "io" + "math" "strings" "unicode/utf8" @@ -51,11 +52,12 @@ type stringWriter interface { // HumanReport is a reporter with human readable output in mind type HumanReport struct { Report - MinorChangeThreshold float64 - NoTableStyle bool - DoNotInspectCerts bool - OmitHeader bool - UseGoPatchPaths bool + MinorChangeThreshold float64 + MultilineContextLines int + NoTableStyle bool + DoNotInspectCerts bool + OmitHeader bool + UseGoPatchPaths bool } // WriteReport writes a human readable report to the provided writer @@ -342,48 +344,57 @@ func (report *HumanReport) writeStringDiff(output stringWriter, from string, to ) case isMultiLine(from, to): - if !bunt.UseColors() { - _, _ = output.WriteString(yellow("%c value change\n", MODIFICATION)) - report.writeTextBlocks(output, 0, - red("%s", createStringWithPrefix(" - ", from)), - green("%s", createStringWithPrefix(" + ", to)), - ) - } else { - dmp := diffmatchpatch.New() - diff := dmp.DiffMain(from, to, true) - diff = dmp.DiffCleanupSemantic(diff) - diff = dmp.DiffCleanupEfficiency(diff) - - var ins, del int - var buf bytes.Buffer - for _, d := range diff { - switch d.Type { - case diffmatchpatch.DiffInsert: - fmt.Fprint(&buf, green("%s", d.Text)) - ins++ - - case diffmatchpatch.DiffDelete: - fmt.Fprint(&buf, red("%s", d.Text)) - del++ - - case diffmatchpatch.DiffEqual: - fmt.Fprint(&buf, dimgray("%s", d.Text)) + // create line by line diff + dmp := diffmatchpatch.New() + oldIdx, newIdx, lines := dmp.DiffLinesToChars(from, to) + diff := dmp.DiffMain(oldIdx, newIdx, false) + diff = dmp.DiffCharsToLines(diff, lines) + + var ins, del int + var buf bytes.Buffer + multilineContextLines := report.MultilineContextLines + for _, d := range diff { + // color and format each diff by type + switch d.Type { + case diffmatchpatch.DiffInsert: + fmt.Fprint(&buf, green(createStringWithContinuousPrefix(" + ", d.Text))) + ins++ + + case diffmatchpatch.DiffDelete: + fmt.Fprint(&buf, red(createStringWithContinuousPrefix(" - ", d.Text))) + del++ + + case diffmatchpatch.DiffEqual: + // skip eqaul output if requested context is 0 or the equal text is empty + if multilineContextLines <= 0 || len(d.Text) == 0 { + continue } + // add amount of unchanged lines as configured + lines := strings.Split(d.Text, "\n") + lower := int(math.Min(float64(len(lines)), float64(multilineContextLines))) + upper := len(lines) - multilineContextLines + // if string ends with \n we need to display one more line on the upper limit + if strings.HasSuffix(d.Text, "\n") { + upper-- + } + var val string + if upper <= lower { + val = strings.Join(lines, "\n") + } else { + val = fmt.Sprintf("%s\n\n[%s unchanged)]\n\n%s", + strings.Join(lines[:lower], "\n"), + text.Plural((upper-lower), "line"), + strings.Join(lines[upper:], "\n")) + } + fmt.Fprint(&buf, dimgray(createStringWithContinuousPrefix(" ", val))) } - fmt.Fprintln(&buf) - - var insDelDetails []string - if ins > 0 { - insDelDetails = append(insDelDetails, text.Plural(ins, "insert")) - } - if del > 0 { - insDelDetails = append(insDelDetails, text.Plural(del, "deletion")) - } - - _, _ = output.WriteString(yellow("%c value change in multiline text (%s)\n", MODIFICATION, strings.Join(insDelDetails, ", "))) - _, _ = output.WriteString(createStringWithPrefix(" ", buf.String())) } + _, _ = output.WriteString( + yellow("%c value change in multiline text (%s, %s)\n", + MODIFICATION, text.Plural(ins, "insert"), text.Plural(del, "deletion"))) + _, _ = output.WriteString(buf.String()) + _, _ = output.WriteString("\n") case isMinorChange(from, to, report.MinorChangeThreshold): _, _ = output.WriteString(yellow("%c value change\n", MODIFICATION)) @@ -614,6 +625,20 @@ func showWhitespaceCharacters(text string) string { return strings.Replace(strings.Replace(text, "\n", bold("↵\n"), -1), " ", bold("·"), -1) } +// createStringWithContinuousPrefix adds the defined prefix to each line of the +// objects string representation. +// The resulting string will always end with a newline. +func createStringWithContinuousPrefix(prefix string, obj interface{}) string { + trimmed := strings.TrimSuffix(fmt.Sprint(obj), "\n") // avoid add. additional empty newline if orig string ends with \n + var buf bytes.Buffer + for _, line := range strings.Split(trimmed, "\n") { + buf.WriteString(prefix) + buf.WriteString(line) + buf.WriteString("\n") // always adds a newline, even if orig string does not contain any + } + return buf.String() +} + func createStringWithPrefix(prefix string, obj interface{}) string { var buf bytes.Buffer for i, line := range strings.Split(fmt.Sprintf("%v", obj), "\n") { diff --git a/pkg/dyff/output_human_test.go b/pkg/dyff/output_human_test.go index 5ec04a2..120c866 100644 --- a/pkg/dyff/output_human_test.go +++ b/pkg/dyff/output_human_test.go @@ -199,6 +199,15 @@ variables.ROUTER_TLS_PEM.options false, ) }) + + It("should use human friendly compact diff of multiline text differences for complex files", func() { + compareAgainstExpected( + assets("multiline/from.yml"), + assets("multiline/to.yml"), + assets("multiline/expected-dyff-spruce.human"), + false, + ) + }) }) Context("reported output issues (without colors)", func() {