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 }