44 "encoding/json"
55 "fmt"
66 "os"
7+ "reflect"
8+ "sort"
79 "strings"
810
911 "github.com/open-feature/cli/internal/config"
@@ -26,6 +28,7 @@ func GetCompareCmd() *cobra.Command {
2628 sourcePath := config .GetManifestPath (cmd )
2729 targetPath , _ := cmd .Flags ().GetString ("against" )
2830 outputFormat , _ := cmd .Flags ().GetString ("output" )
31+ ignorePatterns , _ := cmd .Flags ().GetStringArray ("ignore" )
2932
3033 // Validate flags
3134 if sourcePath == "" || targetPath == "" {
@@ -49,8 +52,10 @@ func GetCompareCmd() *cobra.Command {
4952 return fmt .Errorf ("error loading target manifest: %w" , err )
5053 }
5154
52- // Compare manifests
53- changes , err := manifest .Compare (sourceManifest , targetManifest )
55+ // Compare manifests with ignore patterns
56+ changes , err := manifest .Compare (sourceManifest , targetManifest , manifest.CompareOptions {
57+ IgnorePatterns : ignorePatterns ,
58+ })
5459 if err != nil {
5560 return fmt .Errorf ("error comparing manifests: %w" , err )
5661 }
@@ -79,6 +84,9 @@ func GetCompareCmd() *cobra.Command {
7984 compareCmd .Flags ().StringP ("against" , "a" , "" , "Path to the target manifest file to compare against" )
8085 compareCmd .Flags ().StringP ("output" , "o" , string (manifest .OutputFormatTree ),
8186 fmt .Sprintf ("Output format. Valid formats: %s" , strings .Join (manifest .GetValidOutputFormats (), ", " )))
87+ compareCmd .Flags ().StringArrayP ("ignore" , "i" , []string {},
88+ "Field pattern to ignore during comparison (can be specified multiple times). " +
89+ "Supports shorthand (e.g., 'description') and full paths with wildcards (e.g., 'flags.*.description', 'metadata.*')" )
8290
8391 // Mark required flags
8492 _ = compareCmd .MarkFlagRequired ("against" )
@@ -156,26 +164,128 @@ func renderTreeDiff(changes []manifest.Change, cmd *cobra.Command) error {
156164 flagName := strings .TrimPrefix (change .Path , "flags." )
157165 pterm .FgYellow .Printf (" ~ %s\n " , flagName )
158166
159- // Marshall the values
160- oldJSON , _ := json .MarshalIndent (change .OldValue , "" , " " )
161- newJSON , _ := json .MarshalIndent (change .NewValue , "" , " " )
162-
163- // Print the diff
164- fmt .Println (" Before:" )
165- for _ , line := range strings .Split (string (oldJSON ), "\n " ) {
166- fmt .Printf (" %s\n " , line )
167- }
168-
169- fmt .Println (" After:" )
170- for _ , line := range strings .Split (string (newJSON ), "\n " ) {
171- fmt .Printf (" %s\n " , line )
167+ // Show field-level diff
168+ fieldChanges := getFieldChanges (flagName , change .OldValue , change .NewValue )
169+ if len (fieldChanges ) > 0 {
170+ for _ , fc := range fieldChanges {
171+ fmt .Printf (" • %s: %s → %s\n " , fc .Field , fc .OldValue , fc .NewValue )
172+ }
173+ } else {
174+ // Fallback to full object display if we can't parse
175+ oldJSON , _ := json .MarshalIndent (change .OldValue , " " , " " )
176+ newJSON , _ := json .MarshalIndent (change .NewValue , " " , " " )
177+ fmt .Println (" Before:" )
178+ fmt .Printf (" %s\n " , oldJSON )
179+ fmt .Println (" After:" )
180+ fmt .Printf (" %s\n " , newJSON )
172181 }
173182 }
174183 }
175184
176185 return nil
177186}
178187
188+ // fieldChange represents a change to a specific field
189+ type fieldChange struct {
190+ Field string
191+ OldValue string
192+ NewValue string
193+ }
194+
195+ // getFieldChanges extracts field-level changes between two flag objects
196+ func getFieldChanges (flagName string , oldVal , newVal any ) []fieldChange {
197+ var changes []fieldChange
198+
199+ // Convert to maps
200+ oldMap , oldOk := oldVal .(map [string ]any )
201+ newMap , newOk := newVal .(map [string ]any )
202+
203+ if ! oldOk || ! newOk {
204+ return changes // Return empty if not maps
205+ }
206+
207+ // Get all unique field names
208+ allFields := make (map [string ]bool )
209+ for field := range oldMap {
210+ allFields [field ] = true
211+ }
212+ for field := range newMap {
213+ allFields [field ] = true
214+ }
215+
216+ // Compare each field
217+ // Note: Fields are already filtered at the Compare() level, so we don't need to filter here
218+ for field := range allFields {
219+ oldFieldVal , oldExists := oldMap [field ]
220+ newFieldVal , newExists := newMap [field ]
221+
222+ // Field was added
223+ if ! oldExists && newExists {
224+ changes = append (changes , fieldChange {
225+ Field : field ,
226+ OldValue : "(not set)" ,
227+ NewValue : formatFieldValue (newFieldVal ),
228+ })
229+ continue
230+ }
231+
232+ // Field was removed
233+ if oldExists && ! newExists {
234+ changes = append (changes , fieldChange {
235+ Field : field ,
236+ OldValue : formatFieldValue (oldFieldVal ),
237+ NewValue : "(removed)" ,
238+ })
239+ continue
240+ }
241+
242+ // Field changed
243+ if ! reflect .DeepEqual (oldFieldVal , newFieldVal ) {
244+ changes = append (changes , fieldChange {
245+ Field : field ,
246+ OldValue : formatFieldValue (oldFieldVal ),
247+ NewValue : formatFieldValue (newFieldVal ),
248+ })
249+ }
250+ }
251+
252+ // Sort changes by field name for consistent output
253+ sort .Slice (changes , func (i , j int ) bool {
254+ return changes [i ].Field < changes [j ].Field
255+ })
256+
257+ return changes
258+ }
259+
260+ // formatFieldValue converts a value to a human-readable string for field-level diff display
261+ func formatFieldValue (val any ) string {
262+ if val == nil {
263+ return "null"
264+ }
265+
266+ switch v := val .(type ) {
267+ case string :
268+ return fmt .Sprintf ("%q" , v )
269+ case bool :
270+ return fmt .Sprintf ("%t" , v )
271+ case float64 :
272+ // Check if it's actually an integer
273+ if v == float64 (int64 (v )) {
274+ return fmt .Sprintf ("%d" , int64 (v ))
275+ }
276+ return fmt .Sprintf ("%g" , v )
277+ case map [string ]any , []any :
278+ // For complex types, marshal to compact JSON
279+ jsonBytes , err := json .Marshal (v )
280+ if err != nil {
281+ return fmt .Sprintf ("%v" , v )
282+ }
283+ return string (jsonBytes )
284+ default :
285+ return fmt .Sprintf ("%v" , v )
286+ }
287+ }
288+
179289// renderFlatDiff renders changes in a flat format
180290func renderFlatDiff (changes []manifest.Change , cmd * cobra.Command ) error {
181291 pterm .Info .Printf ("Found %d difference(s) between manifests:\n \n " , len (changes ))
0 commit comments