github.com/golang/dep@v0.5.4/txn_writer.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package dep 6 7 import ( 8 "context" 9 "encoding/hex" 10 "fmt" 11 "io/ioutil" 12 "log" 13 "os" 14 "path/filepath" 15 16 "github.com/golang/dep/gps" 17 "github.com/golang/dep/gps/verify" 18 "github.com/golang/dep/internal/fs" 19 "github.com/pkg/errors" 20 ) 21 22 const ( 23 // Helper consts for common diff-checking patterns. 24 anyExceptHash verify.DeltaDimension = verify.AnyChanged & ^verify.HashVersionChanged & ^verify.HashChanged 25 ) 26 27 // Example string to be written to the manifest file 28 // if no dependencies are found in the project 29 // during `dep init` 30 var exampleTOML = []byte(`# Gopkg.toml example 31 # 32 # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 33 # for detailed Gopkg.toml documentation. 34 # 35 # required = ["github.com/user/thing/cmd/thing"] 36 # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 37 # 38 # [[constraint]] 39 # name = "github.com/user/project" 40 # version = "1.0.0" 41 # 42 # [[constraint]] 43 # name = "github.com/user/project2" 44 # branch = "dev" 45 # source = "github.com/myfork/project2" 46 # 47 # [[override]] 48 # name = "github.com/x/y" 49 # version = "2.4.0" 50 # 51 # [prune] 52 # non-go = false 53 # go-tests = true 54 # unused-packages = true 55 56 `) 57 58 // String added on top of lock file 59 var lockFileComment = []byte(`# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 60 61 `) 62 63 // SafeWriter transactionalizes writes of manifest, lock, and vendor dir, both 64 // individually and in any combination, into a pseudo-atomic action with 65 // transactional rollback. 66 // 67 // It is not impervious to errors (writing to disk is hard), but it should 68 // guard against non-arcane failure conditions. 69 type SafeWriter struct { 70 Manifest *Manifest 71 lock *Lock 72 lockDiff verify.LockDelta 73 writeVendor bool 74 writeLock bool 75 pruneOptions gps.CascadingPruneOptions 76 } 77 78 // NewSafeWriter sets up a SafeWriter to write a set of manifest, lock, and 79 // vendor tree. 80 // 81 // - If manifest is provided, it will be written to the standard manifest file 82 // name beneath root. 83 // 84 // - If newLock is provided, it will be written to the standard lock file 85 // name beneath root. 86 // 87 // - If vendor is VendorAlways, or is VendorOnChanged and the locks are different, 88 // the vendor directory will be written beneath root based on newLock. 89 // 90 // - If oldLock is provided without newLock, error. 91 // 92 // - If vendor is VendorAlways without a newLock, error. 93 func NewSafeWriter(manifest *Manifest, oldLock, newLock *Lock, vendor VendorBehavior, prune gps.CascadingPruneOptions, status map[string]verify.VendorStatus) (*SafeWriter, error) { 94 sw := &SafeWriter{ 95 Manifest: manifest, 96 lock: newLock, 97 pruneOptions: prune, 98 } 99 100 if oldLock != nil { 101 if newLock == nil { 102 return nil, errors.New("must provide newLock when oldLock is specified") 103 } 104 105 sw.lockDiff = verify.DiffLocks(oldLock, newLock) 106 if sw.lockDiff.Changed(anyExceptHash) { 107 sw.writeLock = true 108 } 109 } else if newLock != nil { 110 sw.writeLock = true 111 } 112 113 switch vendor { 114 case VendorAlways: 115 sw.writeVendor = true 116 case VendorOnChanged: 117 if newLock != nil && oldLock == nil { 118 sw.writeVendor = true 119 } else if sw.lockDiff.Changed(anyExceptHash & ^verify.InputImportsChanged) { 120 sw.writeVendor = true 121 } else { 122 for _, stat := range status { 123 if stat != verify.NoMismatch { 124 sw.writeVendor = true 125 break 126 } 127 } 128 } 129 } 130 131 if sw.writeVendor && newLock == nil { 132 return nil, errors.New("must provide newLock in order to write out vendor") 133 } 134 135 return sw, nil 136 } 137 138 // HasLock checks if a Lock is present in the SafeWriter 139 func (sw *SafeWriter) HasLock() bool { 140 return sw.lock != nil 141 } 142 143 // HasManifest checks if a Manifest is present in the SafeWriter 144 func (sw *SafeWriter) HasManifest() bool { 145 return sw.Manifest != nil 146 } 147 148 // VendorBehavior defines when the vendor directory should be written. 149 type VendorBehavior int 150 151 const ( 152 // VendorOnChanged indicates that the vendor directory should be written 153 // when the lock is new or changed, or a project in vendor differs from its 154 // intended state. 155 VendorOnChanged VendorBehavior = iota 156 // VendorAlways forces the vendor directory to always be written. 157 VendorAlways 158 // VendorNever indicates the vendor directory should never be written. 159 VendorNever 160 ) 161 162 func (sw SafeWriter) validate(root string, sm gps.SourceManager) error { 163 if root == "" { 164 return errors.New("root path must be non-empty") 165 } 166 if is, err := fs.IsDir(root); !is { 167 if err != nil && !os.IsNotExist(err) { 168 return err 169 } 170 return errors.Errorf("root path %q does not exist", root) 171 } 172 173 if sw.writeVendor && sm == nil { 174 return errors.New("must provide a SourceManager if writing out a vendor dir") 175 } 176 177 return nil 178 } 179 180 // Write saves some combination of manifest, lock, and a vendor tree. root is 181 // the absolute path of root dir in which to write. sm is only required if 182 // vendor is being written. 183 // 184 // It first writes to a temp dir, then moves them in place if and only if all 185 // the write operations succeeded. It also does its best to roll back if any 186 // moves fail. This mostly guarantees that dep cannot exit with a partial write 187 // that would leave an undefined state on disk. 188 // 189 // If logger is not nil, progress will be logged after each project write. 190 func (sw *SafeWriter) Write(root string, sm gps.SourceManager, examples bool, logger *log.Logger) error { 191 err := sw.validate(root, sm) 192 if err != nil { 193 return err 194 } 195 196 if !sw.HasManifest() && !sw.writeLock && !sw.writeVendor { 197 // nothing to do 198 return nil 199 } 200 201 mpath := filepath.Join(root, ManifestName) 202 lpath := filepath.Join(root, LockName) 203 vpath := filepath.Join(root, "vendor") 204 205 td, err := ioutil.TempDir(os.TempDir(), "dep") 206 if err != nil { 207 return errors.Wrap(err, "error while creating temp dir for writing manifest/lock/vendor") 208 } 209 defer os.RemoveAll(td) 210 211 if sw.HasManifest() { 212 // Always write the example text to the bottom of the TOML file. 213 tb, err := sw.Manifest.MarshalTOML() 214 if err != nil { 215 return errors.Wrap(err, "failed to marshal manifest to TOML") 216 } 217 218 var initOutput []byte 219 220 // If examples are enabled, use the example text 221 if examples { 222 initOutput = exampleTOML 223 } 224 225 if err = ioutil.WriteFile(filepath.Join(td, ManifestName), append(initOutput, tb...), 0666); err != nil { 226 return errors.Wrap(err, "failed to write manifest file to temp dir") 227 } 228 } 229 230 if sw.writeVendor { 231 var onWrite func(gps.WriteProgress) 232 if logger != nil { 233 onWrite = func(progress gps.WriteProgress) { 234 logger.Println(progress) 235 } 236 } 237 err = gps.WriteDepTree(filepath.Join(td, "vendor"), sw.lock, sm, sw.pruneOptions, onWrite) 238 if err != nil { 239 return errors.Wrap(err, "error while writing out vendor tree") 240 } 241 242 for k, lp := range sw.lock.Projects() { 243 vp := lp.(verify.VerifiableProject) 244 vp.Digest, err = verify.DigestFromDirectory(filepath.Join(td, "vendor", string(lp.Ident().ProjectRoot))) 245 if err != nil { 246 return errors.Wrapf(err, "error while hashing tree of %s in vendor", lp.Ident().ProjectRoot) 247 } 248 sw.lock.P[k] = vp 249 } 250 } 251 252 if sw.writeLock { 253 l, err := sw.lock.MarshalTOML() 254 if err != nil { 255 return errors.Wrap(err, "failed to marshal lock to TOML") 256 } 257 258 if err = ioutil.WriteFile(filepath.Join(td, LockName), append(lockFileComment, l...), 0666); err != nil { 259 return errors.Wrap(err, "failed to write lock file to temp dir") 260 } 261 } 262 263 // Ensure vendor/.git is preserved if present 264 if hasDotGit(vpath) { 265 err = fs.RenameWithFallback(filepath.Join(vpath, ".git"), filepath.Join(td, "vendor/.git")) 266 if _, ok := err.(*os.LinkError); ok { 267 return errors.Wrap(err, "failed to preserve vendor/.git") 268 } 269 } 270 271 // Move the existing files and dirs to the temp dir while we put the new 272 // ones in, to provide insurance against errors for as long as possible. 273 type pathpair struct { 274 from, to string 275 } 276 var restore []pathpair 277 var failerr error 278 var vendorbak string 279 280 if sw.HasManifest() { 281 if _, err := os.Stat(mpath); err == nil { 282 // Move out the old one. 283 tmploc := filepath.Join(td, ManifestName+".orig") 284 failerr = fs.RenameWithFallback(mpath, tmploc) 285 if failerr != nil { 286 goto fail 287 } 288 restore = append(restore, pathpair{from: tmploc, to: mpath}) 289 } 290 291 // Move in the new one. 292 failerr = fs.RenameWithFallback(filepath.Join(td, ManifestName), mpath) 293 if failerr != nil { 294 goto fail 295 } 296 } 297 298 if sw.writeLock { 299 if _, err := os.Stat(lpath); err == nil { 300 // Move out the old one. 301 tmploc := filepath.Join(td, LockName+".orig") 302 303 failerr = fs.RenameWithFallback(lpath, tmploc) 304 if failerr != nil { 305 goto fail 306 } 307 restore = append(restore, pathpair{from: tmploc, to: lpath}) 308 } 309 310 // Move in the new one. 311 failerr = fs.RenameWithFallback(filepath.Join(td, LockName), lpath) 312 if failerr != nil { 313 goto fail 314 } 315 } 316 317 if sw.writeVendor { 318 if _, err := os.Stat(vpath); err == nil { 319 // Move out the old vendor dir. just do it into an adjacent dir, to 320 // try to mitigate the possibility of a pointless cross-filesystem 321 // move with a temp directory. 322 vendorbak = vpath + ".orig" 323 if _, err := os.Stat(vendorbak); err == nil { 324 // If the adjacent dir already exists, bite the bullet and move 325 // to a proper tempdir. 326 vendorbak = filepath.Join(td, ".vendor.orig") 327 } 328 329 failerr = fs.RenameWithFallback(vpath, vendorbak) 330 if failerr != nil { 331 goto fail 332 } 333 restore = append(restore, pathpair{from: vendorbak, to: vpath}) 334 } 335 336 // Move in the new one. 337 failerr = fs.RenameWithFallback(filepath.Join(td, "vendor"), vpath) 338 if failerr != nil { 339 goto fail 340 } 341 } 342 343 // Renames all went smoothly. The deferred os.RemoveAll will get the temp 344 // dir, but if we wrote vendor, we have to clean that up directly 345 if sw.writeVendor { 346 // Nothing we can really do about an error at this point, so ignore it 347 os.RemoveAll(vendorbak) 348 } 349 350 return nil 351 352 fail: 353 // If we failed at any point, move all the things back into place, then bail. 354 for _, pair := range restore { 355 // Nothing we can do on err here, as we're already in recovery mode. 356 fs.RenameWithFallback(pair.from, pair.to) 357 } 358 return failerr 359 } 360 361 // PrintPreparedActions logs the actions a call to Write would perform. 362 func (sw *SafeWriter) PrintPreparedActions(output *log.Logger, verbose bool) error { 363 if output == nil { 364 output = log.New(ioutil.Discard, "", 0) 365 } 366 if sw.HasManifest() { 367 if verbose { 368 m, err := sw.Manifest.MarshalTOML() 369 if err != nil { 370 return errors.Wrap(err, "ensure DryRun cannot serialize manifest") 371 } 372 output.Printf("Would have written the following %s:\n%s\n", ManifestName, string(m)) 373 } else { 374 output.Printf("Would have written %s.\n", ManifestName) 375 } 376 } 377 378 if sw.writeLock { 379 if verbose { 380 l, err := sw.lock.MarshalTOML() 381 if err != nil { 382 return errors.Wrap(err, "ensure DryRun cannot serialize lock") 383 } 384 output.Printf("Would have written the following %s:\n%s\n", LockName, string(l)) 385 } else { 386 output.Printf("Would have written %s.\n", LockName) 387 } 388 } 389 390 if sw.writeVendor { 391 if verbose { 392 output.Printf("Would have written the following %d projects to the vendor directory:\n", len(sw.lock.Projects())) 393 lps := sw.lock.Projects() 394 for i, p := range lps { 395 output.Printf("(%d/%d) %s@%s\n", i+1, len(lps), p.Ident(), p.Version()) 396 } 397 } else { 398 output.Printf("Would have written %d projects to the vendor directory.\n", len(sw.lock.Projects())) 399 } 400 } 401 402 return nil 403 } 404 405 // hasDotGit checks if a given path has .git file or directory in it. 406 func hasDotGit(path string) bool { 407 gitfilepath := filepath.Join(path, ".git") 408 _, err := os.Stat(gitfilepath) 409 return err == nil 410 } 411 412 // DeltaWriter manages batched writes to populate vendor/ and update Gopkg.lock. 413 // Its primary design goal is to minimize writes by only writing things that 414 // have changed. 415 type DeltaWriter struct { 416 lock *Lock 417 lockDiff verify.LockDelta 418 vendorDir string 419 changed map[gps.ProjectRoot]changeType 420 behavior VendorBehavior 421 } 422 423 type changeType uint8 424 425 const ( 426 hashMismatch changeType = iota + 1 427 hashVersionMismatch 428 hashAbsent 429 noVerify 430 solveChanged 431 pruneOptsChanged 432 missingFromTree 433 projectAdded 434 projectRemoved 435 pathPreserved 436 ) 437 438 // NewDeltaWriter prepares a vendor writer that will construct a vendor 439 // directory by writing out only those projects that actually need to be written 440 // out - they have changed in some way, or they lack the necessary hash 441 // information to be verified. 442 func NewDeltaWriter(p *Project, newLock *Lock, behavior VendorBehavior) (TreeWriter, error) { 443 dw := &DeltaWriter{ 444 lock: newLock, 445 vendorDir: filepath.Join(p.AbsRoot, "vendor"), 446 changed: make(map[gps.ProjectRoot]changeType), 447 behavior: behavior, 448 } 449 450 if newLock == nil { 451 return nil, errors.New("must provide a non-nil newlock") 452 } 453 454 status, err := p.VerifyVendor() 455 if err != nil { 456 return nil, err 457 } 458 459 _, err = os.Stat(dw.vendorDir) 460 if err != nil { 461 if os.IsNotExist(err) { 462 // Provided dir does not exist, so there's no disk contents to compare 463 // against. Fall back to the old SafeWriter. 464 return NewSafeWriter(nil, p.Lock, newLock, behavior, p.Manifest.PruneOptions, status) 465 } 466 return nil, err 467 } 468 469 dw.lockDiff = verify.DiffLocks(p.Lock, newLock) 470 471 for pr, lpd := range dw.lockDiff.ProjectDeltas { 472 // Hash changes aren't relevant at this point, as they could be empty 473 // in the new lock, and therefore a symptom of a solver change. 474 if lpd.Changed(anyExceptHash) { 475 if lpd.WasAdded() { 476 dw.changed[pr] = projectAdded 477 } else if lpd.WasRemoved() { 478 dw.changed[pr] = projectRemoved 479 } else if lpd.PruneOptsChanged() { 480 dw.changed[pr] = pruneOptsChanged 481 } else { 482 dw.changed[pr] = solveChanged 483 } 484 } 485 } 486 487 for spr, stat := range status { 488 pr := gps.ProjectRoot(spr) 489 // These cases only matter if there was no change already recorded via 490 // the differ. 491 if _, has := dw.changed[pr]; !has { 492 switch stat { 493 case verify.NotInTree: 494 dw.changed[pr] = missingFromTree 495 case verify.NotInLock: 496 dw.changed[pr] = projectRemoved 497 case verify.DigestMismatchInLock: 498 dw.changed[pr] = hashMismatch 499 case verify.HashVersionMismatch: 500 dw.changed[pr] = hashVersionMismatch 501 case verify.EmptyDigestInLock: 502 dw.changed[pr] = hashAbsent 503 } 504 } 505 } 506 507 // Apply noverify last, as it should only supersede changeTypes with lower 508 // values. It is NOT applied if no existing change is registered. 509 for _, spr := range p.Manifest.NoVerify { 510 pr := gps.ProjectRoot(spr) 511 // We don't validate this field elsewhere as it can be difficult to know 512 // at the beginning of a dep ensure command whether or not the noverify 513 // project actually will exist as part of the Lock by the end of the 514 // run. So, only apply if it's in the lockdiff. 515 if _, has := dw.lockDiff.ProjectDeltas[pr]; has { 516 if typ, has := dw.changed[pr]; has { 517 if typ < noVerify { 518 // Avoid writing noverify projects at all for the lower change 519 // types. 520 delete(dw.changed, pr) 521 522 // Uncomment this if we want to switch to the safer behavior, 523 // where we ALWAYS write noverify projects. 524 //dw.changed[pr] = noVerify 525 } else if typ == projectRemoved { 526 // noverify can also be used to preserve files that would 527 // otherwise be removed. 528 dw.changed[pr] = pathPreserved 529 } 530 } 531 // It's also allowed to preserve entirely unknown paths using noverify. 532 } else if _, has := status[spr]; has { 533 dw.changed[pr] = pathPreserved 534 } 535 } 536 537 return dw, nil 538 } 539 540 // Write executes the planned changes. 541 // 542 // This writes recreated projects to a new directory, then moves in existing, 543 // unchanged projects from the original vendor directory. If any failures occur, 544 // reasonable attempts are made to roll back the changes. 545 func (dw *DeltaWriter) Write(path string, sm gps.SourceManager, examples bool, logger *log.Logger) error { 546 // TODO(sdboyer) remove path from the signature for this 547 if path != filepath.Dir(dw.vendorDir) { 548 return errors.Errorf("target path (%q) must be the parent of the original vendor path (%q)", path, dw.vendorDir) 549 } 550 551 if logger == nil { 552 logger = log.New(ioutil.Discard, "", 0) 553 } 554 555 lpath := filepath.Join(path, LockName) 556 vpath := dw.vendorDir 557 558 // Write the modified projects to a new adjacent directory. We use an 559 // adjacent directory to minimize the possibility of cross-filesystem renames 560 // becoming expensive copies, and to make removal of unneeded projects implicit 561 // and automatic. 562 vnewpath := filepath.Join(filepath.Dir(vpath), ".vendor-new") 563 if _, err := os.Stat(vnewpath); err == nil { 564 return errors.Errorf("scratch directory %s already exists, please remove it", vnewpath) 565 } 566 err := os.MkdirAll(vnewpath, os.FileMode(0777)) 567 if err != nil { 568 return errors.Wrapf(err, "error while creating scratch directory at %s", vnewpath) 569 } 570 571 // Write out all the deltas to the newpath 572 projs := make(map[gps.ProjectRoot]gps.LockedProject) 573 for _, lp := range dw.lock.Projects() { 574 projs[lp.Ident().ProjectRoot] = lp 575 } 576 577 var dropped, preserved []gps.ProjectRoot 578 i := 0 579 tot := len(dw.changed) 580 for _, reason := range dw.changed { 581 if reason != pathPreserved { 582 logger.Println("# Bringing vendor into sync") 583 break 584 } 585 } 586 587 for pr, reason := range dw.changed { 588 switch reason { 589 case projectRemoved: 590 dropped = append(dropped, pr) 591 continue 592 case pathPreserved: 593 preserved = append(preserved, pr) 594 continue 595 } 596 597 to := filepath.FromSlash(filepath.Join(vnewpath, string(pr))) 598 po := projs[pr].(verify.VerifiableProject).PruneOpts 599 if err := sm.ExportPrunedProject(context.TODO(), projs[pr], po, to); err != nil { 600 return errors.Wrapf(err, "failed to export %s", pr) 601 } 602 603 i++ 604 lpd := dw.lockDiff.ProjectDeltas[pr] 605 v, id := projs[pr].Version(), projs[pr].Ident() 606 607 // Only print things if we're actually going to leave behind a new 608 // vendor dir. 609 if dw.behavior != VendorNever { 610 logger.Printf("(%d/%d) Wrote %s@%s: %s", i, tot, id, v, changeExplanation(reason, lpd)) 611 } 612 613 digest, err := verify.DigestFromDirectory(to) 614 if err != nil { 615 return errors.Wrapf(err, "failed to hash %s", pr) 616 } 617 618 // Update the new Lock with verification information. 619 for k, lp := range dw.lock.P { 620 if lp.Ident().ProjectRoot == pr { 621 vp := lp.(verify.VerifiableProject) 622 vp.Digest = digest 623 dw.lock.P[k] = verify.VerifiableProject{ 624 LockedProject: lp, 625 PruneOpts: po, 626 Digest: digest, 627 } 628 } 629 } 630 } 631 632 // Write out the lock, now that it's fully updated with digests. 633 l, err := dw.lock.MarshalTOML() 634 if err != nil { 635 return errors.Wrap(err, "failed to marshal lock to TOML") 636 } 637 638 if err = ioutil.WriteFile(lpath, append(lockFileComment, l...), 0666); err != nil { 639 return errors.Wrap(err, "failed to write new lock file") 640 } 641 642 if dw.behavior == VendorNever { 643 return os.RemoveAll(vnewpath) 644 } 645 646 // Changed projects are fully populated. Now, iterate over the lock's 647 // projects and move any remaining ones not in the changed list to vnewpath. 648 for _, lp := range dw.lock.Projects() { 649 pr := lp.Ident().ProjectRoot 650 tgt := filepath.Join(vnewpath, string(pr)) 651 err := os.MkdirAll(filepath.Dir(tgt), os.FileMode(0777)) 652 if err != nil { 653 return errors.Wrapf(err, "error creating parent directory in vendor for %s", tgt) 654 } 655 656 if _, has := dw.changed[pr]; !has { 657 err = fs.RenameWithFallback(filepath.Join(vpath, string(pr)), tgt) 658 if err != nil { 659 return errors.Wrapf(err, "error moving unchanged project %s into scratch vendor dir", pr) 660 } 661 } 662 } 663 664 for i, pr := range dropped { 665 // Kind of a lie to print this. ¯\_(ツ)_/¯ 666 fi, err := os.Stat(filepath.Join(vpath, string(pr))) 667 if err != nil { 668 return errors.Wrap(err, "could not stat file that VerifyVendor claimed existed") 669 } 670 671 if fi.IsDir() { 672 logger.Printf("(%d/%d) Removed unused project %s", tot-(len(dropped)-i-1), tot, pr) 673 } else { 674 logger.Printf("(%d/%d) Removed orphaned file %s", tot-(len(dropped)-i-1), tot, pr) 675 } 676 } 677 678 // Special case: ensure vendor/.git is preserved if present 679 if hasDotGit(vpath) { 680 preserved = append(preserved, ".git") 681 } 682 683 for _, path := range preserved { 684 err = fs.RenameWithFallback(filepath.Join(vpath, string(path)), filepath.Join(vnewpath, string(path))) 685 if err != nil { 686 return errors.Wrapf(err, "failed to preserve vendor/%s", path) 687 } 688 } 689 690 err = os.RemoveAll(vpath) 691 if err != nil { 692 return errors.Wrap(err, "failed to remove original vendor directory") 693 } 694 err = fs.RenameWithFallback(vnewpath, vpath) 695 if err != nil { 696 return errors.Wrap(err, "failed to put new vendor directory into place") 697 } 698 699 return nil 700 } 701 702 // changeExplanation outputs a string explaining what changed for each different 703 // possible changeType. 704 func changeExplanation(c changeType, lpd verify.LockedProjectDelta) string { 705 switch c { 706 case noVerify: 707 return "verification is disabled" 708 case solveChanged: 709 if lpd.SourceChanged() { 710 return fmt.Sprintf("source changed (%s -> %s)", lpd.SourceBefore, lpd.SourceAfter) 711 } else if lpd.VersionChanged() { 712 if lpd.VersionBefore == nil { 713 return fmt.Sprintf("version changed (was a bare revision)") 714 } 715 return fmt.Sprintf("version changed (was %s)", lpd.VersionBefore.String()) 716 } else if lpd.RevisionChanged() { 717 return fmt.Sprintf("revision changed (%s -> %s)", trimSHA(lpd.RevisionBefore), trimSHA(lpd.RevisionAfter)) 718 } else if lpd.PackagesChanged() { 719 la, lr := len(lpd.PackagesAdded), len(lpd.PackagesRemoved) 720 if la > 0 && lr > 0 { 721 return fmt.Sprintf("packages changed (%v added, %v removed)", la, lr) 722 } else if la > 0 { 723 return fmt.Sprintf("packages changed (%v added)", la) 724 } 725 return fmt.Sprintf("packages changed (%v removed)", lr) 726 } 727 case pruneOptsChanged: 728 // Override what's on the lockdiff with the extra info we have; 729 // this lets us excise PruneNestedVendorDirs and get the real 730 // value from the input param in place. 731 old := lpd.PruneOptsBefore & ^gps.PruneNestedVendorDirs 732 new := lpd.PruneOptsAfter & ^gps.PruneNestedVendorDirs 733 return fmt.Sprintf("prune options changed (%s -> %s)", old, new) 734 case hashMismatch: 735 return "hash of vendored tree didn't match digest in Gopkg.lock" 736 case hashVersionMismatch: 737 return "hashing algorithm mismatch" 738 case hashAbsent: 739 return "hash digest absent from lock" 740 case projectAdded: 741 return "new project" 742 case missingFromTree: 743 return "missing from vendor" 744 default: 745 panic(fmt.Sprintf("unrecognized changeType value %v", c)) 746 } 747 748 return "" 749 } 750 751 // PrintPreparedActions indicates what changes the DeltaWriter plans to make. 752 func (dw *DeltaWriter) PrintPreparedActions(output *log.Logger, verbose bool) error { 753 if verbose { 754 l, err := dw.lock.MarshalTOML() 755 if err != nil { 756 return errors.Wrap(err, "ensure DryRun cannot serialize lock") 757 } 758 output.Printf("Would have written the following %s (hash digests may be incorrect):\n%s\n", LockName, string(l)) 759 } else { 760 output.Printf("Would have written %s.\n", LockName) 761 } 762 763 projs := make(map[gps.ProjectRoot]gps.LockedProject) 764 for _, lp := range dw.lock.Projects() { 765 projs[lp.Ident().ProjectRoot] = lp 766 } 767 768 tot := len(dw.changed) 769 if tot > 0 { 770 output.Print("Would have updated the following projects in the vendor directory:\n\n") 771 i := 0 772 for pr, reason := range dw.changed { 773 lpd := dw.lockDiff.ProjectDeltas[pr] 774 if reason == projectRemoved { 775 output.Printf("(%d/%d) Would have removed %s", i, tot, pr) 776 } else { 777 output.Printf("(%d/%d) Would have written %s@%s: %s", i, tot, projs[pr].Ident(), projs[pr].Version(), changeExplanation(reason, lpd)) 778 } 779 } 780 } 781 782 return nil 783 } 784 785 // A TreeWriter is responsible for writing important dep states to disk - 786 // Gopkg.lock, vendor, and possibly Gopkg.toml. 787 type TreeWriter interface { 788 PrintPreparedActions(output *log.Logger, verbose bool) error 789 Write(path string, sm gps.SourceManager, examples bool, logger *log.Logger) error 790 } 791 792 // trimSHA checks if revision is a valid SHA1 digest and trims to 10 characters. 793 func trimSHA(revision gps.Revision) string { 794 if len(revision) == 40 { 795 if _, err := hex.DecodeString(string(revision)); err == nil { 796 // Valid SHA1 digest 797 revision = revision[0:10] 798 } 799 } 800 801 return string(revision) 802 }