Skip to content

Commit 3537cb4

Browse files
committed
Discover renames based on content
If we rename objects, they will appear as add and remove actions, which makes it difficult to assess the semantic delta, which may only be a simple renaming. The current implementation creates potentially a diff between every add/remove item, which means O(n^2) runtime. So it should only be enabled if such changes are rare
1 parent 97f7749 commit 3537cb4

File tree

6 files changed

+229
-51
lines changed

6 files changed

+229
-51
lines changed

cmd/release.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type release struct {
1818
suppressedKinds []string
1919
releases []string
2020
outputContext int
21+
maxChangeFraction float32
2122
includeTests bool
2223
showSecrets bool
2324
output string
@@ -79,6 +80,7 @@ func releaseCmd() *cobra.Command {
7980
releaseCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes")
8081
releaseCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output")
8182
releaseCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes")
83+
releaseCmd.Flags().Float32VarP(&diff.maxChangeFraction, "max-content-change-fraction", "D", 0, "maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a moved resource")
8284
releaseCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks")
8385
releaseCmd.Flags().StringVar(&diff.output, "output", "diff", "Possible values: diff, simple, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.")
8486
releaseCmd.Flags().BoolVar(&diff.stripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input")
@@ -124,6 +126,7 @@ func (d *release) differentiateHelm3() error {
124126
d.suppressedKinds,
125127
d.showSecrets,
126128
d.outputContext,
129+
d.maxChangeFraction,
127130
d.output,
128131
d.stripTrailingCR,
129132
os.Stdout)
@@ -159,6 +162,7 @@ func (d *release) differentiate() error {
159162
d.suppressedKinds,
160163
d.showSecrets,
161164
d.outputContext,
165+
d.maxChangeFraction,
162166
d.output,
163167
d.stripTrailingCR,
164168
os.Stdout)

cmd/revision.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type revision struct {
2020
suppressedKinds []string
2121
revisions []string
2222
outputContext int
23+
maxChangeFraction float32
2324
includeTests bool
2425
showSecrets bool
2526
output string
@@ -89,6 +90,7 @@ func revisionCmd() *cobra.Command {
8990
revisionCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes")
9091
revisionCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output")
9192
revisionCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes")
93+
revisionCmd.Flags().Float32VarP(&diff.maxChangeFraction, "max-content-change-fraction", "D", 0, "maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a moved resource")
9294
revisionCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks")
9395
revisionCmd.Flags().StringVar(&diff.output, "output", "diff", "Possible values: diff, simple, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.")
9496
revisionCmd.Flags().BoolVar(&diff.stripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input")
@@ -129,6 +131,7 @@ func (d *revision) differentiateHelm3() error {
129131
d.suppressedKinds,
130132
d.showSecrets,
131133
d.outputContext,
134+
d.maxChangeFraction,
132135
d.output,
133136
d.stripTrailingCR,
134137
os.Stdout)
@@ -156,6 +159,7 @@ func (d *revision) differentiateHelm3() error {
156159
d.suppressedKinds,
157160
d.showSecrets,
158161
d.outputContext,
162+
d.maxChangeFraction,
159163
d.output,
160164
d.stripTrailingCR,
161165
os.Stdout)
@@ -196,6 +200,7 @@ func (d *revision) differentiate() error {
196200
d.suppressedKinds,
197201
d.showSecrets,
198202
d.outputContext,
203+
d.maxChangeFraction,
199204
d.output,
200205
d.stripTrailingCR,
201206
os.Stdout)
@@ -223,6 +228,7 @@ func (d *revision) differentiate() error {
223228
d.suppressedKinds,
224229
d.showSecrets,
225230
d.outputContext,
231+
d.maxChangeFraction,
226232
d.output,
227233
d.stripTrailingCR,
228234
os.Stdout)

cmd/rollback.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type rollback struct {
2020
suppressedKinds []string
2121
revisions []string
2222
outputContext int
23+
maxChangeFraction float32
2324
includeTests bool
2425
showSecrets bool
2526
output string
@@ -81,6 +82,7 @@ func rollbackCmd() *cobra.Command {
8182
rollbackCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes")
8283
rollbackCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output")
8384
rollbackCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes")
85+
rollbackCmd.Flags().Float32VarP(&diff.maxChangeFraction, "max-content-change-fraction", "D", 0, "maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a moved resource")
8486
rollbackCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks")
8587
rollbackCmd.Flags().StringVar(&diff.output, "output", "diff", "Possible values: diff, simple, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.")
8688
rollbackCmd.Flags().BoolVar(&diff.stripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input")
@@ -122,6 +124,7 @@ func (d *rollback) backcastHelm3() error {
122124
d.suppressedKinds,
123125
d.showSecrets,
124126
d.outputContext,
127+
d.maxChangeFraction,
125128
d.output,
126129
d.stripTrailingCR,
127130
os.Stdout)
@@ -159,6 +162,7 @@ func (d *rollback) backcast() error {
159162
d.suppressedKinds,
160163
d.showSecrets,
161164
d.outputContext,
165+
d.maxChangeFraction,
162166
d.output,
163167
d.stripTrailingCR,
164168
os.Stdout)

cmd/upgrade.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type diffCmd struct {
3636
includeTests bool
3737
suppressedKinds []string
3838
outputContext int
39+
maxChangeFraction float32
3940
showSecrets bool
4041
postRenderer string
4142
output string
@@ -116,6 +117,7 @@ func newChartCommand() *cobra.Command {
116117
f.BoolVar(&diff.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
117118
f.StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output")
118119
f.IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes")
120+
f.Float32VarP(&diff.maxChangeFraction, "max-content-change-fraction", "D", 0, "maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a moved resource")
119121
f.BoolVar(&diff.disableValidation, "disable-validation", false, "disables rendered templates validation against the Kubernetes cluster you are currently pointing to. This is the same validation performed on an install")
120122
f.BoolVar(&diff.disableOpenAPIValidation, "disable-openapi-validation", false, "disables rendered templates validation against the Kubernetes OpenAPI Schema")
121123
f.BoolVar(&diff.dryRun, "dry-run", false, "disables cluster access and show diff as if it was install. Implies --install, --reset-values, and --disable-validation")
@@ -190,7 +192,7 @@ func (d *diffCmd) runHelm3() error {
190192
} else {
191193
newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests, helm3TestHook, helm2TestSuccessHook)
192194
}
193-
seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, d.suppressedKinds, d.showSecrets, d.outputContext, d.output, d.stripTrailingCR, os.Stdout)
195+
seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, d.suppressedKinds, d.showSecrets, d.outputContext, d.maxChangeFraction, d.output, d.stripTrailingCR, os.Stdout)
194196

195197
if d.detailedExitCode && seenAnyChanges {
196198
return Error{
@@ -276,7 +278,7 @@ func (d *diffCmd) run() error {
276278
}
277279
}
278280

279-
seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, d.suppressedKinds, d.showSecrets, d.outputContext, d.output, d.stripTrailingCR, os.Stdout)
281+
seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, d.suppressedKinds, d.showSecrets, d.outputContext, d.maxChangeFraction, d.output, d.stripTrailingCR, os.Stdout)
280282

281283
if d.detailedExitCode && seenAnyChanges {
282284
return Error{

diff/diff.go

Lines changed: 128 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,58 +19,135 @@ import (
1919
)
2020

2121
// Manifests diff on manifests
22-
func Manifests(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, showSecrets bool, context int, output string, stripTrailingCR bool, to io.Writer) bool {
22+
func Manifests(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, showSecrets bool, context int, maxChangeFraction float32, output string, stripTrailingCR bool, to io.Writer) bool {
2323
report.setupReportFormat(output)
2424
seenAnyChanges := false
25-
emptyMapping := &manifest.MappingResult{}
25+
var possiblyRemoved []string
26+
2627
for _, key := range sortedKeys(oldIndex) {
2728
oldContent := oldIndex[key]
2829

2930
if newContent, ok := newIndex[key]; ok {
30-
if oldContent.Content != newContent.Content {
31-
// modified
32-
if !showSecrets {
33-
redactSecrets(oldContent, newContent)
34-
}
35-
36-
diffs := diffMappingResults(oldContent, newContent, stripTrailingCR)
37-
if len(diffs) > 0 {
38-
seenAnyChanges = true
39-
}
40-
report.addEntry(key, suppressedKinds, oldContent.Kind, context, diffs, "MODIFY")
41-
}
42-
} else {
43-
// removed
44-
if !showSecrets {
45-
redactSecrets(oldContent, nil)
46-
47-
}
48-
diffs := diffMappingResults(oldContent, emptyMapping, stripTrailingCR)
49-
if len(diffs) > 0 {
31+
// modified
32+
if doDiff(key, oldContent, newContent, suppressedKinds, showSecrets, stripTrailingCR, context) {
5033
seenAnyChanges = true
5134
}
52-
report.addEntry(key, suppressedKinds, oldContent.Kind, context, diffs, "REMOVE")
35+
} else {
36+
possiblyRemoved = append(possiblyRemoved, key)
5337
}
5438
}
5539

40+
var possiblyAdded []string
5641
for _, key := range sortedKeys(newIndex) {
42+
if _, ok := oldIndex[key]; !ok {
43+
possiblyAdded = append(possiblyAdded, key)
44+
}
45+
}
46+
47+
removed, added, anyChanges := contentSearch(possiblyRemoved, oldIndex, possiblyAdded, newIndex, showSecrets, stripTrailingCR, suppressedKinds, context, maxChangeFraction)
48+
if anyChanges {
49+
seenAnyChanges = true
50+
}
51+
52+
for _, key := range removed {
53+
oldContent := oldIndex[key]
54+
55+
if doDiff(key, oldContent, nil, suppressedKinds, showSecrets, stripTrailingCR, context) {
56+
seenAnyChanges = true
57+
}
58+
}
59+
60+
for _, key := range added {
5761
newContent := newIndex[key]
5862

59-
if _, ok := oldIndex[key]; !ok {
60-
// added
63+
if doDiff(key, nil, newContent, suppressedKinds, showSecrets, stripTrailingCR, context) {
64+
seenAnyChanges = true
65+
}
66+
}
67+
68+
report.print(to)
69+
report.clean()
70+
return seenAnyChanges
71+
}
72+
73+
func actualChanges(diff []difflib.DiffRecord) int {
74+
changes := 0
75+
for _, record := range diff {
76+
if record.Delta != difflib.Common {
77+
changes++
78+
}
79+
}
80+
return changes
81+
}
82+
83+
func contentSearch(possiblyRemoved []string, oldIndex map[string]*manifest.MappingResult, possiblyAdded []string, newIndex map[string]*manifest.MappingResult, showSecrets bool, stripTrailingCR bool, suppressedKinds []string, context int, maxChangeFraction float32) ([]string, []string, bool) {
84+
if maxChangeFraction <= 0 {
85+
return possiblyRemoved, possiblyAdded, false
86+
}
87+
88+
var removed []string
89+
seenAnyChanges := false
90+
91+
for _, removedKey := range possiblyRemoved {
92+
oldContent := oldIndex[removedKey]
93+
var smallestKey string
94+
var smallestFraction float32 = maxChangeFraction
95+
for _, addedKey := range possiblyAdded {
96+
newContent := newIndex[addedKey]
6197
if !showSecrets {
62-
redactSecrets(nil, newContent)
98+
redactSecrets(oldContent, newContent)
99+
}
100+
101+
diff := diffMappingResults(oldContent, newContent, stripTrailingCR)
102+
delta := actualChanges(diff)
103+
if delta == 0 || len(diff) == 0 {
104+
continue // Should never happen, but better safe than sorry
105+
}
106+
fraction := float32(delta) / float32(len(diff))
107+
if fraction > 0 && fraction < smallestFraction {
108+
smallestKey = addedKey
109+
smallestFraction = fraction
63110
}
64-
diffs := diffMappingResults(emptyMapping, newContent, stripTrailingCR)
65-
if len(diffs) > 0 {
111+
}
112+
113+
if smallestFraction < maxChangeFraction {
114+
index := sort.SearchStrings(possiblyAdded, smallestKey)
115+
possiblyAdded = append(possiblyAdded[:index], possiblyAdded[index+1:]...)
116+
newContent := newIndex[smallestKey]
117+
if doDiff(removedKey, oldContent, newContent, suppressedKinds, showSecrets, stripTrailingCR, context) {
66118
seenAnyChanges = true
67119
}
68-
report.addEntry(key, suppressedKinds, newContent.Kind, context, diffs, "ADD")
120+
121+
} else {
122+
removed = append(removed, removedKey)
69123
}
70124
}
71-
report.print(to)
72-
report.clean()
73-
return seenAnyChanges
125+
126+
return removed, possiblyAdded, seenAnyChanges
127+
}
128+
129+
func doDiff(key string, oldContent *manifest.MappingResult, newContent *manifest.MappingResult, suppressedKinds []string, showSecrets bool, stripTrailingCR bool, context int) bool {
130+
if !showSecrets {
131+
redactSecrets(oldContent, newContent)
132+
}
133+
134+
diffs := diffMappingResults(oldContent, newContent, stripTrailingCR)
135+
var kind string
136+
var changeType string
137+
if oldContent == nil {
138+
kind = newContent.Kind
139+
changeType = "ADD"
140+
} else if newContent == nil {
141+
kind = oldContent.Kind
142+
changeType = "REMOVE"
143+
} else {
144+
if oldContent.Content == newContent.Content {
145+
return false
146+
}
147+
changeType = "MODIFY"
148+
}
149+
report.addEntry(key, suppressedKinds, kind, context, diffs, changeType)
150+
return len(diffs) > 0
74151
}
75152

76153
func redactSecrets(old, new *manifest.MappingResult) {
@@ -116,15 +193,15 @@ func redactSecrets(old, new *manifest.MappingResult) {
116193
if old != nil {
117194
oldSecret.Data = nil
118195
if err := serializer.Encode(&oldSecret, &buf); err != nil {
119-
196+
new.Content = fmt.Sprintf("Error encoding new secret: %s", err)
120197
}
121198
old.Content = getComment(old.Content) + strings.Replace(strings.Replace(buf.String(), "stringData", "data", 1), " creationTimestamp: null\n", "", 1)
122199
buf.Reset() //reuse buffer for new secret
123200
}
124201
if new != nil {
125202
newSecret.Data = nil
126203
if err := serializer.Encode(&newSecret, &buf); err != nil {
127-
204+
new.Content = fmt.Sprintf("Error encoding new secret: %s", err)
128205
}
129206
new.Content = getComment(new.Content) + strings.Replace(strings.Replace(buf.String(), "stringData", "data", 1), " creationTimestamp: null\n", "", 1)
130207
}
@@ -142,14 +219,22 @@ func getComment(s string) string {
142219
}
143220

144221
// Releases reindex the content based on the template names and pass it to Manifests
145-
func Releases(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, showSecrets bool, context int, output string, stripTrailingCR bool, to io.Writer) bool {
222+
func Releases(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, showSecrets bool, context int, maxChangeFraction float32, output string, stripTrailingCR bool, to io.Writer) bool {
146223
oldIndex = reIndexForRelease(oldIndex)
147224
newIndex = reIndexForRelease(newIndex)
148-
return Manifests(oldIndex, newIndex, suppressedKinds, showSecrets, context, output, stripTrailingCR, to)
225+
return Manifests(oldIndex, newIndex, suppressedKinds, showSecrets, context, maxChangeFraction, output, stripTrailingCR, to)
226+
}
227+
228+
func getContentOrEmptyString(mappingResult *manifest.MappingResult) string {
229+
if mappingResult == nil {
230+
return ""
231+
}
232+
233+
return mappingResult.Content
149234
}
150235

151-
func diffMappingResults(oldContent *manifest.MappingResult, newContent *manifest.MappingResult, stripTrailingCR bool ) []difflib.DiffRecord {
152-
return diffStrings(oldContent.Content, newContent.Content, stripTrailingCR)
236+
func diffMappingResults(oldContent *manifest.MappingResult, newContent *manifest.MappingResult, stripTrailingCR bool) []difflib.DiffRecord {
237+
return diffStrings(getContentOrEmptyString(oldContent), getContentOrEmptyString(newContent), stripTrailingCR)
153238
}
154239

155240
func diffStrings(before, after string, stripTrailingCR bool) []difflib.DiffRecord {
@@ -209,7 +294,11 @@ func printDiffRecord(diff difflib.DiffRecord, to io.Writer) {
209294
case difflib.LeftOnly:
210295
fmt.Fprintf(to, "%s\n", ansi.Color("- "+text, "red"))
211296
case difflib.Common:
212-
fmt.Fprintf(to, "%s\n", " "+text)
297+
if text == "\n" || text == "" {
298+
fmt.Fprintln(to)
299+
} else {
300+
fmt.Fprintf(to, "%s\n", " "+text)
301+
}
213302
}
214303
}
215304

0 commit comments

Comments
 (0)