forked from cloudfoundry/cli
/
manifest_diff_displayer.go
185 lines (157 loc) · 5.42 KB
/
manifest_diff_displayer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package shared
import (
"path"
"reflect"
"strconv"
"strings"
"code.cloudfoundry.org/cli/command"
"code.cloudfoundry.org/cli/resources"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
type ManifestDiffDisplayer struct {
UI command.UI
}
func NewManifestDiffDisplayer(ui command.UI) *ManifestDiffDisplayer {
return &ManifestDiffDisplayer{
UI: ui,
}
}
func (display *ManifestDiffDisplayer) DisplayDiff(rawManifest []byte, diff resources.ManifestDiff) error {
// If there are no diffs, just print the manifest
if len(diff.Diffs) == 0 {
display.UI.DisplayDiffUnchanged(string(rawManifest), 0, false)
return nil
}
// We unmarshal into a MapSlice vs a map[string]interface{} here to preserve the order of map keys
var yamlManifest yaml.MapSlice
err := yaml.Unmarshal(rawManifest, &yamlManifest)
if err != nil {
return errors.New("Unable to process manifest diff because its format is invalid.")
}
// Distinguish between add/remove and replace diffs
// Replace diffs have the key in the given manifest so we can navigate to that path and display the changed value
// Add/Remove diffs will not, so we need to know their parent's path to insert the diff as a child
pathAddReplaceMap := make(map[string]resources.Diff)
pathRemoveMap := make(map[string]resources.Diff)
for _, diff := range diff.Diffs {
switch diff.Op {
case resources.AddOperation, resources.ReplaceOperation:
pathAddReplaceMap[diff.Path] = diff
case resources.RemoveOperation:
pathRemoveMap[path.Dir(diff.Path)] = diff
}
}
// Always display the yaml header line
display.UI.DisplayDiffUnchanged("---", 0, false)
// For each entry in the provided manifest, process any diffs at or below that entry
for _, entry := range yamlManifest {
display.processDiffsRecursively("/"+interfaceToString(entry.Key), entry.Value, 0, &pathAddReplaceMap, &pathRemoveMap, false)
}
return nil
}
func (display *ManifestDiffDisplayer) processDiffsRecursively(
currentManifestPath string,
value interface{},
depth int,
pathAddReplaceMap, pathRemoveMap *map[string]resources.Diff,
addHyphen bool,
) {
field := path.Base(currentManifestPath)
// If there is a diff at the current path, print it
if diff, ok := diffExistsAtTheCurrentPath(currentManifestPath, pathAddReplaceMap); ok {
display.formatDiff(field, diff, depth, addHyphen)
return
}
// If the value is a slice type (i.e. a yaml.MapSlice or a slice), recurse into it
if isSliceType(value) {
addHyphen = isInt(field)
if !isInt(field) {
display.UI.DisplayDiffUnchanged(field+":", depth, addHyphen)
depth += 1
}
if mapSlice, ok := value.(yaml.MapSlice); ok {
// If a map, recursively process each entry
for i, entry := range mapSlice {
if i > 0 {
addHyphen = false
}
display.processDiffsRecursively(
currentManifestPath+"/"+interfaceToString(entry.Key),
entry.Value,
depth,
pathAddReplaceMap, pathRemoveMap, addHyphen,
)
}
} else if asSlice, ok := value.([]interface{}); ok {
// If a slice, recursively process each element
for index, sliceValue := range asSlice {
display.processDiffsRecursively(
currentManifestPath+"/"+strconv.Itoa(index),
sliceValue,
depth,
pathAddReplaceMap, pathRemoveMap, false,
)
}
}
// Print add/remove diffs after printing the rest of the map or slice
if diff, ok := diffExistsAtTheCurrentPath(currentManifestPath, pathRemoveMap); ok {
display.formatDiff(path.Base(diff.Path), diff, depth, addHyphen)
}
return
}
// Otherwise, print the unchanged field and value
display.UI.DisplayDiffUnchanged(formatKeyValue(field, value), depth, addHyphen)
}
func (display *ManifestDiffDisplayer) formatDiff(field string, diff resources.Diff, depth int, addHyphen bool) {
addHyphen = isInt(field) || addHyphen
switch diff.Op {
case resources.AddOperation:
display.UI.DisplayDiffAddition(formatKeyValue(field, diff.Value), depth, addHyphen)
case resources.ReplaceOperation:
display.UI.DisplayDiffRemoval(formatKeyValue(field, diff.Was), depth, addHyphen)
display.UI.DisplayDiffAddition(formatKeyValue(field, diff.Value), depth, addHyphen)
case resources.RemoveOperation:
display.UI.DisplayDiffRemoval(formatKeyValue(field, diff.Was), depth, addHyphen)
}
}
func diffExistsAtTheCurrentPath(currentManifestPath string, pathDiffMap *map[string]resources.Diff) (resources.Diff, bool) {
diff, ok := (*pathDiffMap)[currentManifestPath]
return diff, ok
}
func formatKeyValue(key string, value interface{}) string {
if isInt(key) {
return interfaceToString(value)
}
if isSliceType(value) {
return key + ":\n" + interfaceToString(value)
}
if isMapType(value) {
return key + ":\n" + indentOneLevelDeeper(interfaceToString(value))
}
return key + ": " + interfaceToString(value)
}
func interfaceToString(value interface{}) string {
val, _ := yaml.Marshal(value)
return strings.TrimSpace(string(val))
}
func indentOneLevelDeeper(input string) string {
inputLines := strings.Split(input, "\n")
outputLines := make([]string, len(inputLines))
for i, line := range inputLines {
outputLines[i] = " " + line
}
return strings.Join(outputLines, "\n")
}
func isSliceType(value interface{}) bool {
valueType := reflect.TypeOf(value)
return valueType != nil && valueType.Kind() == reflect.Slice
}
func isMapType(value interface{}) bool {
valueType := reflect.TypeOf(value)
return valueType != nil && valueType.Kind() == reflect.Map
}
func isInt(field string) bool {
_, err := strconv.Atoi(field)
return err == nil
}