@@ -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