github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/cmd/diff.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/qri-io/ioes"
    10  	"github.com/qri-io/qri/base/component"
    11  	"github.com/qri-io/qri/lib"
    12  	"github.com/spf13/cobra"
    13  )
    14  
    15  // NewDiffCommand creates a new `qri diff` cobra command for comparing changes between datasets
    16  func NewDiffCommand(f Factory, ioStreams ioes.IOStreams) *cobra.Command {
    17  	o := DiffOptions{IOStreams: ioStreams}
    18  	cmd := &cobra.Command{
    19  		Use:   "diff ([COMPONENT] [DATASET [DATASET]])|(PATH PATH)",
    20  		Short: "compare differences between two data sources",
    21  		Long: `'qri diff' is a new & experimental feature, please report bugs here:
    22  https://github.com/qri-io/deepdiff
    23  
    24  Diff compares two data sources & generates a description of the difference
    25  between them. The output of diff describes the steps required to make the 
    26  element on the left (the first argument) equal the element on the right (the
    27  second argument). The steps themselves are the "diff".
    28  
    29  Unlike the classic unix diff utility (which operates on text),
    30  qri diff works on structured data. qri diffs are measured in elements
    31  (think cells in a spreadsheet), each change is either an insert (added 
    32  elements), delete (removed elements), or update (changed values).
    33  
    34  Each change has a path that locates it within the document`,
    35  		Example: `  # Diff between a latest version & the next one back:
    36    $ qri diff me/annual_pop
    37  
    38    # Diff current "qri use" selection:
    39    $ qri diff
    40  
    41    # Diff dataset body against its last version:
    42    $ qri diff body me/annual_pop
    43  
    44    # Diff two dataset meta components:
    45    $ qri diff meta me/population_2016 me/population_2017
    46  
    47    # Diff two local json files:
    48    $ qri diff a.json b.json
    49  
    50    # Diff a json & csv file:
    51    $ qri diff some_table.csv b.json`,
    52  		Annotations: map[string]string{
    53  			"group": "dataset",
    54  		},
    55  		RunE: func(cmd *cobra.Command, args []string) error {
    56  			if err := o.Complete(f, args); err != nil {
    57  				return err
    58  			}
    59  			return o.Run()
    60  		},
    61  	}
    62  
    63  	cmd.Flags().StringVarP(&o.Format, "format", "f", "pretty", "output format. one of [json,pretty]")
    64  	cmd.Flags().BoolVar(&o.Summary, "summary", false, "just output the summary")
    65  
    66  	return cmd
    67  }
    68  
    69  // DiffOptions encapsulates options for the diff command
    70  type DiffOptions struct {
    71  	ioes.IOStreams
    72  
    73  	Refs     *RefSelect
    74  	Selector string
    75  	Format   string
    76  	Summary  bool
    77  
    78  	inst *lib.Instance
    79  }
    80  
    81  // Complete adds any missing configuration that can only be added just before calling Run
    82  func (o *DiffOptions) Complete(f Factory, args []string) (err error) {
    83  	if len(args) > 0 && component.IsKnownFilename(args[0], nil) {
    84  		// Treat a command like `qri diff structure.json` as `qri diff structure`. This mostly
    85  		// makes sense in the context of FSI.
    86  		// TODO(dustmop): Consider if we should support this outside of FSI. That is, if a user
    87  		// has "structure.json" in their current directory (which is not a working directory) and
    88  		// tries to diff it, that file should be compared to the structure component of the
    89  		// dataset ref. Currently doesn't happen because we don't support diffing a dataset in
    90  		// the repository against a local file on disk, but perhaps we should.
    91  		basename := filepath.Base(args[0])
    92  		o.Selector = strings.TrimSuffix(basename, filepath.Ext(basename))
    93  		args = args[1:]
    94  	}
    95  	if len(args) > 0 && component.IsDatasetField.MatchString(args[0]) {
    96  		o.Selector = args[0]
    97  		args = args[1:]
    98  	}
    99  	if o.inst, err = f.Instance(); err != nil {
   100  		return
   101  	}
   102  
   103  	o.Refs, err = GetCurrentRefSelect(f, args, 2)
   104  	return
   105  }
   106  
   107  // Run executes the diff command
   108  func (o *DiffOptions) Run() (err error) {
   109  	p := &lib.DiffParams{
   110  		Selector: o.Selector,
   111  	}
   112  
   113  	if len(o.Refs.RefList()) == 1 {
   114  		// > qri diff me/example_ds
   115  		//
   116  		// left = me/example_ds@previous   right = me/example_ds@head
   117  		p.LeftSide = o.Refs.Ref()
   118  		p.UseLeftPrevVersion = true
   119  	} else if len(o.Refs.RefList()) == 2 {
   120  		// > qri diff me/example_ds me/another_ds
   121  		//
   122  		// left = me/example_ds@head   right = me/another_ds@head
   123  		//OR
   124  		// left = path/to/first.json   right = path/to/second.json
   125  		p.LeftSide = o.Refs.RefList()[0]
   126  		p.RightSide = o.Refs.RefList()[1]
   127  	}
   128  
   129  	ctx := context.TODO()
   130  	res, err := o.inst.Diff().Diff(ctx, p)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	if o.Format == "json" {
   136  		json.NewEncoder(o.Out).Encode(res)
   137  		return
   138  	}
   139  
   140  	return printDiff(o.Out, res, o.Summary)
   141  }