Skip to content

Commit 853e6c1

Browse files
committed
fix lint errors
1 parent 3dec2e2 commit 853e6c1

File tree

4 files changed

+164
-78
lines changed

4 files changed

+164
-78
lines changed

diff/diff_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,8 @@ spec:
636636
require.Len(t, entry.Changes, 2)
637637
replicasChange, ok := findChange(entry.Changes, "spec", "replicas")
638638
require.True(t, ok)
639-
require.Equal(t, float64(2), replicasChange.OldValue)
640-
require.Equal(t, float64(3), replicasChange.NewValue)
639+
require.InDelta(t, float64(2), replicasChange.OldValue, 0.001)
640+
require.InDelta(t, float64(3), replicasChange.NewValue, 0.001)
641641

642642
imageChange, ok := findChange(entry.Changes, "spec.template.spec.containers[0]", "image")
643643
require.True(t, ok)
@@ -713,7 +713,7 @@ data:
713713
require.NoError(t, json.Unmarshal(buf.Bytes(), &entries))
714714
require.Len(t, entries, 1)
715715
require.True(t, entries[0].ChangesSuppressed)
716-
require.Len(t, entries[0].Changes, 0)
716+
require.Empty(t, entries[0].Changes)
717717
}
718718

719719
func findChange(changes []FieldChange, path, field string) (FieldChange, bool) {

diff/structured.go

Lines changed: 161 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package diff
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
7+
"reflect"
8+
"sort"
69
"strconv"
710
"strings"
811

9-
jsonpatch "gomodules.xyz/jsonpatch/v2"
12+
jsonpatch "github.com/evanphx/json-patch/v5"
1013
"sigs.k8s.io/yaml"
1114

1215
"github.com/databus23/helm-diff/v3/manifest"
@@ -151,115 +154,201 @@ func (e *StructuredEntry) populateMetadata(key string, objects ...map[string]int
151154
}
152155

153156
func calculateFieldChanges(oldJSON, newJSON []byte) ([]FieldChange, error) {
154-
patch, err := jsonpatch.CreatePatch(oldJSON, newJSON)
157+
patchBytes, err := jsonpatch.CreateMergePatch(oldJSON, newJSON)
155158
if err != nil {
156159
return nil, err
157160
}
158-
if len(patch) == 0 {
161+
trimmed := bytes.TrimSpace(patchBytes)
162+
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("{}")) {
159163
return nil, nil
160164
}
161165

162-
var oldDoc interface{}
163-
if err := json.Unmarshal(oldJSON, &oldDoc); err != nil {
166+
var patch interface{}
167+
if err := json.Unmarshal(patchBytes, &patch); err != nil {
164168
return nil, err
165169
}
166170

167-
changes := make([]FieldChange, 0, len(patch))
168-
for _, operation := range patch {
169-
tokens := pointerTokens(operation.Path)
170-
path, field := splitPointer(tokens)
171+
var oldDoc interface{}
172+
if len(oldJSON) > 0 {
173+
if err := json.Unmarshal(oldJSON, &oldDoc); err != nil {
174+
return nil, err
175+
}
176+
}
171177

172-
change := FieldChange{
173-
Path: path,
174-
Field: field,
175-
Change: operation.Operation,
178+
var newDoc interface{}
179+
if len(newJSON) > 0 {
180+
if err := json.Unmarshal(newJSON, &newDoc); err != nil {
181+
return nil, err
176182
}
183+
}
177184

178-
if (operation.Operation == "remove" || operation.Operation == "replace") && operation.Path != "" {
179-
if value, err := resolveJSONPointer(oldDoc, operation.Path); err == nil {
180-
change.OldValue = value
181-
}
185+
var changes []FieldChange
186+
if err := walkPatch(&changes, nil, patch, oldDoc, newDoc); err != nil {
187+
return nil, err
188+
}
189+
return changes, nil
190+
}
191+
192+
func walkPatch(changes *[]FieldChange, tokens []string, patchNode, oldNode, newNode interface{}) error {
193+
switch typed := patchNode.(type) {
194+
case map[string]interface{}:
195+
if len(typed) == 0 {
196+
return nil
197+
}
198+
oldMap, _ := oldNode.(map[string]interface{})
199+
newMap, _ := newNode.(map[string]interface{})
200+
keys := make([]string, 0, len(typed))
201+
for key := range typed {
202+
keys = append(keys, key)
182203
}
204+
sort.Strings(keys)
183205

184-
if (operation.Operation == "add" || operation.Operation == "replace") && operation.Value != nil {
185-
change.NewValue = operation.Value
206+
for _, key := range keys {
207+
var oldChild interface{}
208+
var newChild interface{}
209+
if oldMap != nil {
210+
oldChild = oldMap[key]
211+
}
212+
if newMap != nil {
213+
newChild = newMap[key]
214+
}
215+
if err := walkPatch(changes, append(tokens, key), typed[key], oldChild, newChild); err != nil {
216+
return err
217+
}
218+
}
219+
case []interface{}:
220+
return diffArrayNodes(changes, tokens, oldNode, newNode)
221+
default:
222+
path, field := splitTokens(tokens)
223+
change := FieldChange{
224+
Path: path,
225+
Field: field,
186226
}
187227

188-
changes = append(changes, change)
228+
if patchNode == nil {
229+
change.Change = "remove"
230+
change.OldValue = oldNode
231+
} else {
232+
if oldNode == nil {
233+
change.Change = "add"
234+
change.NewValue = newNode
235+
} else {
236+
if reflect.DeepEqual(oldNode, newNode) {
237+
return nil
238+
}
239+
change.Change = "replace"
240+
change.OldValue = oldNode
241+
change.NewValue = newNode
242+
}
243+
}
244+
*changes = append(*changes, change)
189245
}
190-
191-
return changes, nil
246+
return nil
192247
}
193248

194-
func resolveJSONPointer(doc interface{}, pointer string) (interface{}, error) {
195-
if pointer == "" {
196-
return doc, nil
197-
}
249+
func diffArrayNodes(changes *[]FieldChange, tokens []string, oldNode, newNode interface{}) error {
250+
oldArr, _ := oldNode.([]interface{})
251+
newArr, _ := newNode.([]interface{})
198252

199-
rawTokens := strings.Split(pointer, "/")[1:]
200-
tokens := make([]string, 0, len(rawTokens))
201-
for _, rawToken := range rawTokens {
202-
tokens = append(tokens, decodePointerToken(rawToken))
253+
maxLen := len(oldArr)
254+
if len(newArr) > maxLen {
255+
maxLen = len(newArr)
203256
}
204257

205-
current := doc
258+
for i := 0; i < maxLen; i++ {
259+
next := append(tokens, strconv.Itoa(i))
260+
var oldVal interface{}
261+
var newVal interface{}
262+
if i < len(oldArr) {
263+
oldVal = oldArr[i]
264+
}
265+
if i < len(newArr) {
266+
newVal = newArr[i]
267+
}
206268

207-
for _, rawToken := range tokens {
208-
token := rawToken
209-
switch typed := current.(type) {
210-
case map[string]interface{}:
211-
current = typed[token]
212-
case []interface{}:
213-
if token == "-" {
214-
return nil, fmt.Errorf("pointer '-' not addressable")
269+
switch {
270+
case oldVal != nil && newVal != nil:
271+
if reflect.DeepEqual(oldVal, newVal) {
272+
continue
273+
}
274+
subPatch, err := createNodePatch(oldVal, newVal)
275+
if err != nil {
276+
return err
215277
}
216-
index, err := strconv.Atoi(token)
217-
if err != nil || index < 0 || index >= len(typed) {
218-
return nil, fmt.Errorf("invalid array index %s", token)
278+
if subPatch == nil {
279+
path, field := splitTokens(next)
280+
*changes = append(*changes, FieldChange{
281+
Path: path,
282+
Field: field,
283+
Change: "replace",
284+
OldValue: oldVal,
285+
NewValue: newVal,
286+
})
287+
continue
219288
}
220-
current = typed[index]
221-
default:
222-
return nil, fmt.Errorf("unable to navigate pointer through %T", current)
289+
if err := walkPatch(changes, next, subPatch, oldVal, newVal); err != nil {
290+
return err
291+
}
292+
case oldVal != nil:
293+
path, field := splitTokens(next)
294+
*changes = append(*changes, FieldChange{
295+
Path: path,
296+
Field: field,
297+
Change: "remove",
298+
OldValue: oldVal,
299+
})
300+
case newVal != nil:
301+
path, field := splitTokens(next)
302+
*changes = append(*changes, FieldChange{
303+
Path: path,
304+
Field: field,
305+
Change: "add",
306+
NewValue: newVal,
307+
})
223308
}
224309
}
225310

226-
return current, nil
311+
return nil
227312
}
228313

229-
func containsKind(list []string, target string) bool {
230-
for _, item := range list {
231-
if item == target {
232-
return true
233-
}
314+
func createNodePatch(oldNode, newNode interface{}) (interface{}, error) {
315+
oldJSON, err := json.Marshal(oldNode)
316+
if err != nil {
317+
return nil, err
234318
}
235-
return false
236-
}
237-
238-
func pointerTokens(pointer string) []string {
239-
if pointer == "" {
240-
return nil
319+
newJSON, err := json.Marshal(newNode)
320+
if err != nil {
321+
return nil, err
322+
}
323+
patchBytes, err := jsonpatch.CreateMergePatch(oldJSON, newJSON)
324+
if err != nil {
325+
return nil, err
241326
}
242-
rawTokens := strings.Split(pointer, "/")[1:]
243-
tokens := make([]string, 0, len(rawTokens))
244-
for _, token := range rawTokens {
245-
tokens = append(tokens, decodePointerToken(token))
327+
trimmed := bytes.TrimSpace(patchBytes)
328+
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("{}")) {
329+
return nil, nil
246330
}
247-
return tokens
248-
}
249-
250-
func decodePointerToken(token string) string {
251-
token = strings.ReplaceAll(token, "~1", "/")
252-
token = strings.ReplaceAll(token, "~0", "~")
253-
return token
331+
var patch interface{}
332+
if err := json.Unmarshal(patchBytes, &patch); err != nil {
333+
return nil, err
334+
}
335+
return patch, nil
254336
}
255337

256-
func splitPointer(tokens []string) (string, string) {
338+
func splitTokens(tokens []string) (string, string) {
257339
if len(tokens) == 0 {
258340
return "", ""
259341
}
260-
parent := formatPath(tokens[:len(tokens)-1])
261-
field := tokens[len(tokens)-1]
262-
return parent, field
342+
return formatPath(tokens[:len(tokens)-1]), tokens[len(tokens)-1]
343+
}
344+
345+
func containsKind(list []string, target string) bool {
346+
for _, item := range list {
347+
if item == target {
348+
return true
349+
}
350+
}
351+
return false
263352
}
264353

265354
func formatPath(tokens []string) string {

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ require (
120120
golang.org/x/sys v0.38.0 // indirect
121121
golang.org/x/text v0.31.0 // indirect
122122
golang.org/x/time v0.12.0 // indirect
123-
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
124123
google.golang.org/protobuf v1.36.6 // indirect
125124
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
126125
gopkg.in/inf.v0 v0.9.1 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
412412
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
413413
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
414414
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
415-
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
416-
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
417415
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
418416
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
419417
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=

0 commit comments

Comments
 (0)