@@ -22,17 +22,22 @@ import (
2222 "errors"
2323 "fmt"
2424 "io"
25+ "io/fs"
2526 "os"
2627 "os/exec"
2728 "path/filepath"
2829 "sort"
2930 "strings"
3031
32+ "bitbucket.org/creachadair/stringset"
3133 oci "github.com/fluxcd/pkg/oci/client"
3234 "github.com/fluxcd/pkg/tar"
3335 sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
3436 "github.com/gonvenience/ytbx"
3537 "github.com/google/shlex"
38+ "github.com/hexops/gotextdiff"
39+ "github.com/hexops/gotextdiff/myers"
40+ "github.com/hexops/gotextdiff/span"
3641 "github.com/homeport/dyff/pkg/dyff"
3742 "github.com/spf13/cobra"
3843 "golang.org/x/exp/maps"
@@ -61,29 +66,33 @@ type diffArtifactFlags struct {
6166 provider flags.SourceOCIProvider
6267 ignorePaths []string
6368 brief bool
64- differ * semanticDiffFlag
69+ differ * differFlag
6570}
6671
6772var diffArtifactArgs = newDiffArtifactArgs ()
6873
6974func newDiffArtifactArgs () diffArtifactFlags {
70- defaultDiffer := mustExternalDiff ()
71-
7275 return diffArtifactFlags {
7376 provider : flags .SourceOCIProvider (sourcev1 .GenericOCIProvider ),
7477
75- differ : & semanticDiffFlag {
78+ differ : & differFlag {
7679 options : map [string ]differ {
77- "yaml " : dyffBuiltin {
80+ "dyff " : dyffBuiltin {
7881 opts : []dyff.CompareOption {
7982 dyff .IgnoreOrderChanges (false ),
8083 dyff .KubernetesEntityDetection (true ),
8184 },
8285 },
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" ,
8493 },
85- value : "false " ,
86- differ : defaultDiffer ,
94+ value : "unified " ,
95+ differ : unifiedDiff {} ,
8796 },
8897 }
8998}
@@ -94,7 +103,7 @@ func init() {
94103 diffArtifactCmd .Flags ().Var (& diffArtifactArgs .provider , "provider" , sourceOCIRepositoryArgs .provider .Description ())
95104 diffArtifactCmd .Flags ().StringSliceVar (& diffArtifactArgs .ignorePaths , "ignore-paths" , excludeOCI , "set paths to ignore in .gitignore format" )
96105 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 () )
98107
99108 diffCmd .AddCommand (diffArtifactCmd )
100109}
@@ -297,53 +306,125 @@ type differ interface {
297306 Diff (ctx context.Context , from , to string ) (string , error )
298307}
299308
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
304353}
305354
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+
306395// externalDiffVar is the environment variable users can use to overwrite the external diff command.
307396const externalDiffVar = "FLUX_EXTERNAL_DIFF"
308397
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 ) {
311399 cmdline := os .Getenv (externalDiffVar )
312400 if cmdline == "" {
313- cmdline = "diff -ur"
401+ return "" , fmt . Errorf ( "the required %q environment variable is unset" , externalDiffVar )
314402 }
315403
316404 args , err := shlex .Split (cmdline )
317405 if err != nil {
318- panic ( fmt .Sprintf ("shlex.Split(%q): %v " , cmdline , err ) )
406+ return "" , fmt .Errorf ("shlex.Split(%q): %w " , cmdline , err )
319407 }
320408
321- return externalDiffCommand {
322- name : args [0 ],
323- flags : args [1 :],
324- }
325- }
409+ var executable string
410+ executable , args = args [0 ], args [1 :]
326411
327- func (c externalDiffCommand ) Diff (ctx context.Context , fromDir , toDir string ) (string , error ) {
328- var args []string
329-
330- args = append (args , c .flags ... )
331412 args = append (args , fromDir , toDir )
332413
333- cmd := exec .CommandContext (ctx , c . name , args ... )
414+ cmd := exec .CommandContext (ctx , executable , args ... )
334415
335416 var stdout bytes.Buffer
336417
337418 cmd .Stdout = & stdout
338419 cmd .Stderr = os .Stderr
339420
340- err : = cmd .Run ()
421+ err = cmd .Run ()
341422
342423 var exitErr * exec.ExitError
343424 if errors .As (err , & exitErr ) && exitErr .ExitCode () == 1 {
344425 // exit code 1 only means there was a difference => ignore
345426 } else if err != nil {
346- return "" , fmt .Errorf ("executing %q: %w" , c . name , err )
427+ return "" , fmt .Errorf ("executing %q: %w" , executable , err )
347428 }
348429
349430 return stdout .String (), nil
@@ -383,14 +464,15 @@ func (d dyffBuiltin) Diff(ctx context.Context, fromDir, toDir string) (string, e
383464 return buf .String (), nil
384465}
385466
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
390472 differ
391473}
392474
393- func (f * semanticDiffFlag ) Set (s string ) error {
475+ func (f * differFlag ) Set (s string ) error {
394476 d , ok := f .options [s ]
395477 if ! ok {
396478 return fmt .Errorf ("invalid value: %q" , s )
@@ -402,14 +484,29 @@ func (f *semanticDiffFlag) Set(s string) error {
402484 return nil
403485}
404486
405- func (f * semanticDiffFlag ) String () string {
487+ func (f * differFlag ) String () string {
406488 return f .value
407489}
408490
409- func (f * semanticDiffFlag ) Type () string {
491+ func (f * differFlag ) Type () string {
410492 keys := maps .Keys (f .options )
411493
412494 sort .Strings (keys )
413495
414496 return strings .Join (keys , "|" )
415497}
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