github.com/mckael/restic@v0.8.3/cmd/restic/cmd_diff.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"path"
     6  	"reflect"
     7  	"sort"
     8  
     9  	"github.com/restic/restic/internal/debug"
    10  	"github.com/restic/restic/internal/errors"
    11  	"github.com/restic/restic/internal/repository"
    12  	"github.com/restic/restic/internal/restic"
    13  	"github.com/spf13/cobra"
    14  )
    15  
    16  var cmdDiff = &cobra.Command{
    17  	Use:   "diff snapshot-ID snapshot-ID",
    18  	Short: "Show differences between two snapshots",
    19  	Long: `
    20  The "diff" command shows differences from the first to the second snapshot. The
    21  first characters in each line display what has happened to a particular file or
    22  directory:
    23  
    24   +  The item was added
    25   -  The item was removed
    26   U  The metadata (access mode, timestamps, ...) for the item was updated
    27   M  The file's content was modified
    28   T  The type was changed, e.g. a file was made a symlink
    29  `,
    30  	DisableAutoGenTag: true,
    31  	RunE: func(cmd *cobra.Command, args []string) error {
    32  		return runDiff(diffOptions, globalOptions, args)
    33  	},
    34  }
    35  
    36  // DiffOptions collects all options for the diff command.
    37  type DiffOptions struct {
    38  	ShowMetadata bool
    39  }
    40  
    41  var diffOptions DiffOptions
    42  
    43  func init() {
    44  	cmdRoot.AddCommand(cmdDiff)
    45  
    46  	f := cmdDiff.Flags()
    47  	f.BoolVar(&diffOptions.ShowMetadata, "metadata", false, "print changes in metadata")
    48  }
    49  
    50  func loadSnapshot(ctx context.Context, repo *repository.Repository, desc string) (*restic.Snapshot, error) {
    51  	id, err := restic.FindSnapshot(repo, desc)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	return restic.LoadSnapshot(ctx, repo, id)
    57  }
    58  
    59  // Comparer collects all things needed to compare two snapshots.
    60  type Comparer struct {
    61  	repo restic.Repository
    62  	opts DiffOptions
    63  }
    64  
    65  // DiffStat collects stats for all types of items.
    66  type DiffStat struct {
    67  	Files, Dirs, Others  int
    68  	DataBlobs, TreeBlobs int
    69  	Bytes                int
    70  }
    71  
    72  // Add adds stats information for node to s.
    73  func (s *DiffStat) Add(node *restic.Node) {
    74  	if node == nil {
    75  		return
    76  	}
    77  
    78  	switch node.Type {
    79  	case "file":
    80  		s.Files++
    81  	case "dir":
    82  		s.Dirs++
    83  	default:
    84  		s.Others++
    85  	}
    86  }
    87  
    88  // addBlobs adds the blobs of node to s.
    89  func addBlobs(bs restic.BlobSet, node *restic.Node) {
    90  	if node == nil {
    91  		return
    92  	}
    93  
    94  	switch node.Type {
    95  	case "file":
    96  		for _, blob := range node.Content {
    97  			h := restic.BlobHandle{
    98  				ID:   blob,
    99  				Type: restic.DataBlob,
   100  			}
   101  			bs.Insert(h)
   102  		}
   103  	case "dir":
   104  		h := restic.BlobHandle{
   105  			ID:   *node.Subtree,
   106  			Type: restic.TreeBlob,
   107  		}
   108  		bs.Insert(h)
   109  	}
   110  }
   111  
   112  // DiffStats collects the differences between two snapshots.
   113  type DiffStats struct {
   114  	ChangedFiles            int
   115  	Added                   DiffStat
   116  	Removed                 DiffStat
   117  	BlobsBefore, BlobsAfter restic.BlobSet
   118  }
   119  
   120  // NewDiffStats creates new stats for a diff run.
   121  func NewDiffStats() *DiffStats {
   122  	return &DiffStats{
   123  		BlobsBefore: restic.NewBlobSet(),
   124  		BlobsAfter:  restic.NewBlobSet(),
   125  	}
   126  }
   127  
   128  // updateBlobs updates the blob counters in the stats struct.
   129  func updateBlobs(repo restic.Repository, blobs restic.BlobSet, stats *DiffStat) {
   130  	for h := range blobs {
   131  		switch h.Type {
   132  		case restic.DataBlob:
   133  			stats.DataBlobs++
   134  		case restic.TreeBlob:
   135  			stats.TreeBlobs++
   136  		}
   137  
   138  		size, found := repo.LookupBlobSize(h.ID, h.Type)
   139  		if !found {
   140  			Warnf("unable to find blob size for %v\n", h)
   141  			continue
   142  		}
   143  
   144  		stats.Bytes += int(size)
   145  	}
   146  }
   147  
   148  func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, blobs restic.BlobSet, prefix string, id restic.ID) error {
   149  	debug.Log("print %v tree %v", mode, id)
   150  	tree, err := c.repo.LoadTree(ctx, id)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	for _, node := range tree.Nodes {
   156  		name := path.Join(prefix, node.Name)
   157  		if node.Type == "dir" {
   158  			name += "/"
   159  		}
   160  		Printf("%-5s%v\n", mode, name)
   161  		stats.Add(node)
   162  		addBlobs(blobs, node)
   163  
   164  		if node.Type == "dir" {
   165  			err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree)
   166  			if err != nil {
   167  				Warnf("error: %v\n", err)
   168  			}
   169  		}
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
   176  	names := make(map[string]struct{})
   177  	tree1Nodes = make(map[string]*restic.Node)
   178  	for _, node := range tree1.Nodes {
   179  		tree1Nodes[node.Name] = node
   180  		names[node.Name] = struct{}{}
   181  	}
   182  
   183  	tree2Nodes = make(map[string]*restic.Node)
   184  	for _, node := range tree2.Nodes {
   185  		tree2Nodes[node.Name] = node
   186  		names[node.Name] = struct{}{}
   187  	}
   188  
   189  	uniqueNames = make([]string, 0, len(names))
   190  	for name := range names {
   191  		uniqueNames = append(uniqueNames, name)
   192  	}
   193  
   194  	sort.Sort(sort.StringSlice(uniqueNames))
   195  	return tree1Nodes, tree2Nodes, uniqueNames
   196  }
   197  
   198  func (c *Comparer) diffTree(ctx context.Context, stats *DiffStats, prefix string, id1, id2 restic.ID) error {
   199  	debug.Log("diffing %v to %v", id1, id2)
   200  	tree1, err := c.repo.LoadTree(ctx, id1)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	tree2, err := c.repo.LoadTree(ctx, id2)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	tree1Nodes, tree2Nodes, names := uniqueNodeNames(tree1, tree2)
   211  
   212  	for _, name := range names {
   213  		node1, t1 := tree1Nodes[name]
   214  		node2, t2 := tree2Nodes[name]
   215  
   216  		addBlobs(stats.BlobsBefore, node1)
   217  		addBlobs(stats.BlobsAfter, node2)
   218  
   219  		switch {
   220  		case t1 && t2:
   221  			name := path.Join(prefix, name)
   222  			mod := ""
   223  
   224  			if node1.Type != node2.Type {
   225  				mod += "T"
   226  			}
   227  
   228  			if node2.Type == "dir" {
   229  				name += "/"
   230  			}
   231  
   232  			if node1.Type == "file" &&
   233  				node2.Type == "file" &&
   234  				!reflect.DeepEqual(node1.Content, node2.Content) {
   235  				mod += "M"
   236  				stats.ChangedFiles++
   237  			} else if c.opts.ShowMetadata && !node1.Equals(*node2) {
   238  				mod += "U"
   239  			}
   240  
   241  			if mod != "" {
   242  				Printf("%-5s%v\n", mod, name)
   243  			}
   244  
   245  			if node1.Type == "dir" && node2.Type == "dir" {
   246  				err := c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
   247  				if err != nil {
   248  					Warnf("error: %v\n", err)
   249  				}
   250  			}
   251  		case t1 && !t2:
   252  			prefix := path.Join(prefix, name)
   253  			if node1.Type == "dir" {
   254  				prefix += "/"
   255  			}
   256  			Printf("%-5s%v\n", "-", prefix)
   257  			stats.Removed.Add(node1)
   258  
   259  			if node1.Type == "dir" {
   260  				err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree)
   261  				if err != nil {
   262  					Warnf("error: %v\n", err)
   263  				}
   264  			}
   265  		case !t1 && t2:
   266  			prefix := path.Join(prefix, name)
   267  			if node2.Type == "dir" {
   268  				prefix += "/"
   269  			}
   270  			Printf("%-5s%v\n", "+", prefix)
   271  			stats.Added.Add(node2)
   272  
   273  			if node2.Type == "dir" {
   274  				err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree)
   275  				if err != nil {
   276  					Warnf("error: %v\n", err)
   277  				}
   278  			}
   279  		}
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  func runDiff(opts DiffOptions, gopts GlobalOptions, args []string) error {
   286  	if len(args) != 2 {
   287  		return errors.Fatalf("specify two snapshot IDs")
   288  	}
   289  
   290  	ctx, cancel := context.WithCancel(gopts.ctx)
   291  	defer cancel()
   292  
   293  	repo, err := OpenRepository(gopts)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	if err = repo.LoadIndex(ctx); err != nil {
   299  		return err
   300  	}
   301  
   302  	if !gopts.NoLock {
   303  		lock, err := lockRepo(repo)
   304  		defer unlockRepo(lock)
   305  		if err != nil {
   306  			return err
   307  		}
   308  	}
   309  
   310  	sn1, err := loadSnapshot(ctx, repo, args[0])
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	sn2, err := loadSnapshot(ctx, repo, args[1])
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	Verbosef("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str())
   321  
   322  	if sn1.Tree == nil {
   323  		return errors.Errorf("snapshot %v has nil tree", sn1.ID().Str())
   324  	}
   325  
   326  	if sn2.Tree == nil {
   327  		return errors.Errorf("snapshot %v has nil tree", sn2.ID().Str())
   328  	}
   329  
   330  	c := &Comparer{
   331  		repo: repo,
   332  		opts: diffOptions,
   333  	}
   334  
   335  	stats := NewDiffStats()
   336  
   337  	err = c.diffTree(ctx, stats, "/", *sn1.Tree, *sn2.Tree)
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
   343  	updateBlobs(repo, stats.BlobsBefore.Sub(both), &stats.Removed)
   344  	updateBlobs(repo, stats.BlobsAfter.Sub(both), &stats.Added)
   345  
   346  	Printf("\n")
   347  	Printf("Files:       %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
   348  	Printf("Dirs:        %5d new, %5d removed\n", stats.Added.Dirs, stats.Removed.Dirs)
   349  	Printf("Others:      %5d new, %5d removed\n", stats.Added.Others, stats.Removed.Others)
   350  	Printf("Data Blobs:  %5d new, %5d removed\n", stats.Added.DataBlobs, stats.Removed.DataBlobs)
   351  	Printf("Tree Blobs:  %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
   352  	Printf("  Added:   %-5s\n", formatBytes(uint64(stats.Added.Bytes)))
   353  	Printf("  Removed: %-5s\n", formatBytes(uint64(stats.Removed.Bytes)))
   354  
   355  	return nil
   356  }