@@ -22,17 +22,22 @@ import (
22
22
"errors"
23
23
"fmt"
24
24
"io"
25
+ "io/fs"
25
26
"os"
26
27
"os/exec"
27
28
"path/filepath"
28
29
"sort"
29
30
"strings"
30
31
32
+ "bitbucket.org/creachadair/stringset"
31
33
oci "github.com/fluxcd/pkg/oci/client"
32
34
"github.com/fluxcd/pkg/tar"
33
35
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
34
36
"github.com/gonvenience/ytbx"
35
37
"github.com/google/shlex"
38
+ "github.com/hexops/gotextdiff"
39
+ "github.com/hexops/gotextdiff/myers"
40
+ "github.com/hexops/gotextdiff/span"
36
41
"github.com/homeport/dyff/pkg/dyff"
37
42
"github.com/spf13/cobra"
38
43
"golang.org/x/exp/maps"
@@ -61,29 +66,33 @@ type diffArtifactFlags struct {
61
66
provider flags.SourceOCIProvider
62
67
ignorePaths []string
63
68
brief bool
64
- differ * semanticDiffFlag
69
+ differ * differFlag
65
70
}
66
71
67
72
var diffArtifactArgs = newDiffArtifactArgs ()
68
73
69
74
func newDiffArtifactArgs () diffArtifactFlags {
70
- defaultDiffer := mustExternalDiff ()
71
-
72
75
return diffArtifactFlags {
73
76
provider : flags .SourceOCIProvider (sourcev1 .GenericOCIProvider ),
74
77
75
- differ : & semanticDiffFlag {
78
+ differ : & differFlag {
76
79
options : map [string ]differ {
77
- "yaml " : dyffBuiltin {
80
+ "dyff " : dyffBuiltin {
78
81
opts : []dyff.CompareOption {
79
82
dyff .IgnoreOrderChanges (false ),
80
83
dyff .KubernetesEntityDetection (true ),
81
84
},
82
85
},
83
- "false" : defaultDiffer ,
86
+ "external" : externalDiff {},
87
+ "unified" : unifiedDiff {},
88
+ },
89
+ description : map [string ]string {
90
+ "dyff" : `semantic diff for YAML inputs` ,
91
+ "external" : `execute the command in the "` + externalDiffVar + `" environment variable` ,
92
+ "unified" : "generic unified diff for arbitrary text inputs" ,
84
93
},
85
- value : "false " ,
86
- differ : defaultDiffer ,
94
+ value : "unified " ,
95
+ differ : unifiedDiff {} ,
87
96
},
88
97
}
89
98
}
@@ -94,7 +103,7 @@ func init() {
94
103
diffArtifactCmd .Flags ().Var (& diffArtifactArgs .provider , "provider" , sourceOCIRepositoryArgs .provider .Description ())
95
104
diffArtifactCmd .Flags ().StringSliceVar (& diffArtifactArgs .ignorePaths , "ignore-paths" , excludeOCI , "set paths to ignore in .gitignore format" )
96
105
diffArtifactCmd .Flags ().BoolVarP (& diffArtifactArgs .brief , "brief" , "q" , false , "just print a line when the resources differ; does not output a list of changes" )
97
- diffArtifactCmd .Flags ().Var (diffArtifactArgs .differ , "semantic-diff " , "use a semantic diffing algorithm" )
106
+ diffArtifactCmd .Flags ().Var (diffArtifactArgs .differ , "differ " , diffArtifactArgs . differ . usage () )
98
107
99
108
diffCmd .AddCommand (diffArtifactCmd )
100
109
}
@@ -297,53 +306,125 @@ type differ interface {
297
306
Diff (ctx context.Context , from , to string ) (string , error )
298
307
}
299
308
300
- // externalDiffCommand implements the differ interface using an external diff command.
301
- type externalDiffCommand struct {
302
- name string
303
- flags []string
309
+ type unifiedDiff struct {}
310
+
311
+ func (d unifiedDiff ) Diff (_ context.Context , fromDir , toDir string ) (string , error ) {
312
+ fromFiles , err := filesInDir (fromDir )
313
+ if err != nil {
314
+ return "" , err
315
+ }
316
+
317
+ toFiles , err := filesInDir (toDir )
318
+ if err != nil {
319
+ return "" , err
320
+ }
321
+
322
+ allFiles := fromFiles .Union (toFiles )
323
+
324
+ var sb strings.Builder
325
+
326
+ for _ , relPath := range allFiles .Elements () {
327
+ diff , err := d .diffFiles (fromDir , toDir , relPath )
328
+ if err != nil {
329
+ return "" , err
330
+ }
331
+
332
+ fmt .Fprint (& sb , diff )
333
+ }
334
+
335
+ return sb .String (), nil
336
+ }
337
+
338
+ func (d unifiedDiff ) diffFiles (fromDir , toDir , relPath string ) (string , error ) {
339
+ fromPath := filepath .Join (fromDir , relPath )
340
+ fromData , err := d .readFile (fromPath )
341
+ if err != nil {
342
+ return "" , fmt .Errorf ("readFile(%q): %w" , fromPath , err )
343
+ }
344
+
345
+ toPath := filepath .Join (toDir , relPath )
346
+ toData , err := d .readFile (toPath )
347
+ if err != nil {
348
+ return "" , fmt .Errorf ("readFile(%q): %w" , toPath , err )
349
+ }
350
+
351
+ edits := myers .ComputeEdits (span .URIFromPath (fromPath ), string (fromData ), string (toData ))
352
+ return fmt .Sprint (gotextdiff .ToUnified (fromPath , toPath , string (fromData ), edits )), nil
304
353
}
305
354
355
+ func (d unifiedDiff ) readFile (path string ) ([]byte , error ) {
356
+ file , err := os .Open (path )
357
+ if err != nil {
358
+ return nil , fmt .Errorf ("os.Open(%q): %w" , path , err )
359
+ }
360
+ defer file .Close ()
361
+
362
+ return io .ReadAll (file )
363
+ }
364
+
365
+ func filesInDir (root string ) (stringset.Set , error ) {
366
+ var files stringset.Set
367
+
368
+ err := filepath .WalkDir (root , func (path string , d fs.DirEntry , err error ) error {
369
+ if err != nil {
370
+ return err
371
+ }
372
+
373
+ if ! d .Type ().IsRegular () {
374
+ return nil
375
+ }
376
+
377
+ relPath , err := filepath .Rel (root , path )
378
+ if err != nil {
379
+ return fmt .Errorf ("filepath.Rel(%q, %q): %w" , root , path , err )
380
+ }
381
+
382
+ files .Add (relPath )
383
+ return nil
384
+ })
385
+ if err != nil {
386
+ return nil , err
387
+ }
388
+
389
+ return files , err
390
+ }
391
+
392
+ // externalDiff implements the differ interface using an external diff command.
393
+ type externalDiff struct {}
394
+
306
395
// externalDiffVar is the environment variable users can use to overwrite the external diff command.
307
396
const externalDiffVar = "FLUX_EXTERNAL_DIFF"
308
397
309
- // mustExternalDiff initializes an externalDiffCommand using the externalDiffVar environment variable.
310
- func mustExternalDiff () externalDiffCommand {
398
+ func (externalDiff ) Diff (ctx context.Context , fromDir , toDir string ) (string , error ) {
311
399
cmdline := os .Getenv (externalDiffVar )
312
400
if cmdline == "" {
313
- cmdline = "diff -ur"
401
+ return "" , fmt . Errorf ( "the required %q environment variable is unset" , externalDiffVar )
314
402
}
315
403
316
404
args , err := shlex .Split (cmdline )
317
405
if err != nil {
318
- panic ( fmt .Sprintf ("shlex.Split(%q): %v " , cmdline , err ) )
406
+ return "" , fmt .Errorf ("shlex.Split(%q): %w " , cmdline , err )
319
407
}
320
408
321
- return externalDiffCommand {
322
- name : args [0 ],
323
- flags : args [1 :],
324
- }
325
- }
409
+ var executable string
410
+ executable , args = args [0 ], args [1 :]
326
411
327
- func (c externalDiffCommand ) Diff (ctx context.Context , fromDir , toDir string ) (string , error ) {
328
- var args []string
329
-
330
- args = append (args , c .flags ... )
331
412
args = append (args , fromDir , toDir )
332
413
333
- cmd := exec .CommandContext (ctx , c . name , args ... )
414
+ cmd := exec .CommandContext (ctx , executable , args ... )
334
415
335
416
var stdout bytes.Buffer
336
417
337
418
cmd .Stdout = & stdout
338
419
cmd .Stderr = os .Stderr
339
420
340
- err : = cmd .Run ()
421
+ err = cmd .Run ()
341
422
342
423
var exitErr * exec.ExitError
343
424
if errors .As (err , & exitErr ) && exitErr .ExitCode () == 1 {
344
425
// exit code 1 only means there was a difference => ignore
345
426
} else if err != nil {
346
- return "" , fmt .Errorf ("executing %q: %w" , c . name , err )
427
+ return "" , fmt .Errorf ("executing %q: %w" , executable , err )
347
428
}
348
429
349
430
return stdout .String (), nil
@@ -383,14 +464,15 @@ func (d dyffBuiltin) Diff(ctx context.Context, fromDir, toDir string) (string, e
383
464
return buf .String (), nil
384
465
}
385
466
386
- // semanticDiffFlag implements pflag.Value for choosing a semantic diffing algorithm.
387
- type semanticDiffFlag struct {
388
- options map [string ]differ
389
- value string
467
+ // differFlag implements pflag.Value for choosing a diffing implementation.
468
+ type differFlag struct {
469
+ options map [string ]differ
470
+ description map [string ]string
471
+ value string
390
472
differ
391
473
}
392
474
393
- func (f * semanticDiffFlag ) Set (s string ) error {
475
+ func (f * differFlag ) Set (s string ) error {
394
476
d , ok := f .options [s ]
395
477
if ! ok {
396
478
return fmt .Errorf ("invalid value: %q" , s )
@@ -402,14 +484,29 @@ func (f *semanticDiffFlag) Set(s string) error {
402
484
return nil
403
485
}
404
486
405
- func (f * semanticDiffFlag ) String () string {
487
+ func (f * differFlag ) String () string {
406
488
return f .value
407
489
}
408
490
409
- func (f * semanticDiffFlag ) Type () string {
491
+ func (f * differFlag ) Type () string {
410
492
keys := maps .Keys (f .options )
411
493
412
494
sort .Strings (keys )
413
495
414
496
return strings .Join (keys , "|" )
415
497
}
498
+
499
+ func (f * differFlag ) usage () string {
500
+ var b strings.Builder
501
+ fmt .Fprint (& b , "how the diff is generated:" )
502
+
503
+ keys := maps .Keys (f .options )
504
+
505
+ sort .Strings (keys )
506
+
507
+ for _ , key := range keys {
508
+ fmt .Fprintf (& b , "\n %q: %s" , key , f .description [key ])
509
+ }
510
+
511
+ return b .String ()
512
+ }
0 commit comments