| // Copyright 2016 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package dep |
| |
| import ( |
| "context" |
| "encoding/hex" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "os" |
| "path/filepath" |
| |
| "github.com/golang/dep/gps" |
| "github.com/golang/dep/gps/verify" |
| "github.com/golang/dep/internal/fs" |
| "github.com/pkg/errors" |
| ) |
| |
| const ( |
| // Helper consts for common diff-checking patterns. |
| anyExceptHash verify.DeltaDimension = verify.AnyChanged & ^verify.HashVersionChanged & ^verify.HashChanged |
| ) |
| |
| // Example string to be written to the manifest file |
| // if no dependencies are found in the project |
| // during `dep init` |
| var exampleTOML = []byte(`# Gopkg.toml example |
| # |
| # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html |
| # for detailed Gopkg.toml documentation. |
| # |
| # required = ["github.com/user/thing/cmd/thing"] |
| # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] |
| # |
| # [[constraint]] |
| # name = "github.com/user/project" |
| # version = "1.0.0" |
| # |
| # [[constraint]] |
| # name = "github.com/user/project2" |
| # branch = "dev" |
| # source = "github.com/myfork/project2" |
| # |
| # [[override]] |
| # name = "github.com/x/y" |
| # version = "2.4.0" |
| # |
| # [prune] |
| # non-go = false |
| # go-tests = true |
| # unused-packages = true |
| |
| `) |
| |
| // String added on top of lock file |
| var lockFileComment = []byte(`# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. |
| |
| `) |
| |
| // SafeWriter transactionalizes writes of manifest, lock, and vendor dir, both |
| // individually and in any combination, into a pseudo-atomic action with |
| // transactional rollback. |
| // |
| // It is not impervious to errors (writing to disk is hard), but it should |
| // guard against non-arcane failure conditions. |
| type SafeWriter struct { |
| Manifest *Manifest |
| lock *Lock |
| lockDiff verify.LockDelta |
| writeVendor bool |
| writeLock bool |
| pruneOptions gps.CascadingPruneOptions |
| } |
| |
| // NewSafeWriter sets up a SafeWriter to write a set of manifest, lock, and |
| // vendor tree. |
| // |
| // - If manifest is provided, it will be written to the standard manifest file |
| // name beneath root. |
| // |
| // - If newLock is provided, it will be written to the standard lock file |
| // name beneath root. |
| // |
| // - If vendor is VendorAlways, or is VendorOnChanged and the locks are different, |
| // the vendor directory will be written beneath root based on newLock. |
| // |
| // - If oldLock is provided without newLock, error. |
| // |
| // - If vendor is VendorAlways without a newLock, error. |
| func NewSafeWriter(manifest *Manifest, oldLock, newLock *Lock, vendor VendorBehavior, prune gps.CascadingPruneOptions, status map[string]verify.VendorStatus) (*SafeWriter, error) { |
| sw := &SafeWriter{ |
| Manifest: manifest, |
| lock: newLock, |
| pruneOptions: prune, |
| } |
| |
| if oldLock != nil { |
| if newLock == nil { |
| return nil, errors.New("must provide newLock when oldLock is specified") |
| } |
| |
| sw.lockDiff = verify.DiffLocks(oldLock, newLock) |
| if sw.lockDiff.Changed(anyExceptHash) { |
| sw.writeLock = true |
| } |
| } else if newLock != nil { |
| sw.writeLock = true |
| } |
| |
| switch vendor { |
| case VendorAlways: |
| sw.writeVendor = true |
| case VendorOnChanged: |
| if newLock != nil && oldLock == nil { |
| sw.writeVendor = true |
| } else if sw.lockDiff.Changed(anyExceptHash & ^verify.InputImportsChanged) { |
| sw.writeVendor = true |
| } else { |
| for _, stat := range status { |
| if stat != verify.NoMismatch { |
| sw.writeVendor = true |
| break |
| } |
| } |
| } |
| } |
| |
| if sw.writeVendor && newLock == nil { |
| return nil, errors.New("must provide newLock in order to write out vendor") |
| } |
| |
| return sw, nil |
| } |
| |
| // HasLock checks if a Lock is present in the SafeWriter |
| func (sw *SafeWriter) HasLock() bool { |
| return sw.lock != nil |
| } |
| |
| // HasManifest checks if a Manifest is present in the SafeWriter |
| func (sw *SafeWriter) HasManifest() bool { |
| return sw.Manifest != nil |
| } |
| |
| // VendorBehavior defines when the vendor directory should be written. |
| type VendorBehavior int |
| |
| const ( |
| // VendorOnChanged indicates that the vendor directory should be written |
| // when the lock is new or changed, or a project in vendor differs from its |
| // intended state. |
| VendorOnChanged VendorBehavior = iota |
| // VendorAlways forces the vendor directory to always be written. |
| VendorAlways |
| // VendorNever indicates the vendor directory should never be written. |
| VendorNever |
| ) |
| |
| func (sw SafeWriter) validate(root string, sm gps.SourceManager) error { |
| if root == "" { |
| return errors.New("root path must be non-empty") |
| } |
| if is, err := fs.IsDir(root); !is { |
| if err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| return errors.Errorf("root path %q does not exist", root) |
| } |
| |
| if sw.writeVendor && sm == nil { |
| return errors.New("must provide a SourceManager if writing out a vendor dir") |
| } |
| |
| return nil |
| } |
| |
| // Write saves some combination of manifest, lock, and a vendor tree. root is |
| // the absolute path of root dir in which to write. sm is only required if |
| // vendor is being written. |
| // |
| // It first writes to a temp dir, then moves them in place if and only if all |
| // the write operations succeeded. It also does its best to roll back if any |
| // moves fail. This mostly guarantees that dep cannot exit with a partial write |
| // that would leave an undefined state on disk. |
| // |
| // If logger is not nil, progress will be logged after each project write. |
| func (sw *SafeWriter) Write(root string, sm gps.SourceManager, examples bool, logger *log.Logger) error { |
| err := sw.validate(root, sm) |
| if err != nil { |
| return err |
| } |
| |
| if !sw.HasManifest() && !sw.writeLock && !sw.writeVendor { |
| // nothing to do |
| return nil |
| } |
| |
| mpath := filepath.Join(root, ManifestName) |
| lpath := filepath.Join(root, LockName) |
| vpath := filepath.Join(root, "vendor") |
| |
| td, err := ioutil.TempDir(os.TempDir(), "dep") |
| if err != nil { |
| return errors.Wrap(err, "error while creating temp dir for writing manifest/lock/vendor") |
| } |
| defer os.RemoveAll(td) |
| |
| if sw.HasManifest() { |
| // Always write the example text to the bottom of the TOML file. |
| tb, err := sw.Manifest.MarshalTOML() |
| if err != nil { |
| return errors.Wrap(err, "failed to marshal manifest to TOML") |
| } |
| |
| var initOutput []byte |
| |
| // If examples are enabled, use the example text |
| if examples { |
| initOutput = exampleTOML |
| } |
| |
| if err = ioutil.WriteFile(filepath.Join(td, ManifestName), append(initOutput, tb...), 0666); err != nil { |
| return errors.Wrap(err, "failed to write manifest file to temp dir") |
| } |
| } |
| |
| if sw.writeVendor { |
| var onWrite func(gps.WriteProgress) |
| if logger != nil { |
| onWrite = func(progress gps.WriteProgress) { |
| logger.Println(progress) |
| } |
| } |
| err = gps.WriteDepTree(filepath.Join(td, "vendor"), sw.lock, sm, sw.pruneOptions, onWrite) |
| if err != nil { |
| return errors.Wrap(err, "error while writing out vendor tree") |
| } |
| |
| for k, lp := range sw.lock.Projects() { |
| vp := lp.(verify.VerifiableProject) |
| vp.Digest, err = verify.DigestFromDirectory(filepath.Join(td, "vendor", string(lp.Ident().ProjectRoot))) |
| if err != nil { |
| return errors.Wrapf(err, "error while hashing tree of %s in vendor", lp.Ident().ProjectRoot) |
| } |
| sw.lock.P[k] = vp |
| } |
| } |
| |
| if sw.writeLock { |
| l, err := sw.lock.MarshalTOML() |
| if err != nil { |
| return errors.Wrap(err, "failed to marshal lock to TOML") |
| } |
| |
| if err = ioutil.WriteFile(filepath.Join(td, LockName), append(lockFileComment, l...), 0666); err != nil { |
| return errors.Wrap(err, "failed to write lock file to temp dir") |
| } |
| } |
| |
| // Ensure vendor/.git is preserved if present |
| if hasDotGit(vpath) { |
| err = fs.RenameWithFallback(filepath.Join(vpath, ".git"), filepath.Join(td, "vendor/.git")) |
| if _, ok := err.(*os.LinkError); ok { |
| return errors.Wrap(err, "failed to preserve vendor/.git") |
| } |
| } |
| |
| // Move the existing files and dirs to the temp dir while we put the new |
| // ones in, to provide insurance against errors for as long as possible. |
| type pathpair struct { |
| from, to string |
| } |
| var restore []pathpair |
| var failerr error |
| var vendorbak string |
| |
| if sw.HasManifest() { |
| if _, err := os.Stat(mpath); err == nil { |
| // Move out the old one. |
| tmploc := filepath.Join(td, ManifestName+".orig") |
| failerr = fs.RenameWithFallback(mpath, tmploc) |
| if failerr != nil { |
| goto fail |
| } |
| restore = append(restore, pathpair{from: tmploc, to: mpath}) |
| } |
| |
| // Move in the new one. |
| failerr = fs.RenameWithFallback(filepath.Join(td, ManifestName), mpath) |
| if failerr != nil { |
| goto fail |
| } |
| } |
| |
| if sw.writeLock { |
| if _, err := os.Stat(lpath); err == nil { |
| // Move out the old one. |
| tmploc := filepath.Join(td, LockName+".orig") |
| |
| failerr = fs.RenameWithFallback(lpath, tmploc) |
| if failerr != nil { |
| goto fail |
| } |
| restore = append(restore, pathpair{from: tmploc, to: lpath}) |
| } |
| |
| // Move in the new one. |
| failerr = fs.RenameWithFallback(filepath.Join(td, LockName), lpath) |
| if failerr != nil { |
| goto fail |
| } |
| } |
| |
| if sw.writeVendor { |
| if _, err := os.Stat(vpath); err == nil { |
| // Move out the old vendor dir. just do it into an adjacent dir, to |
| // try to mitigate the possibility of a pointless cross-filesystem |
| // move with a temp directory. |
| vendorbak = vpath + ".orig" |
| if _, err := os.Stat(vendorbak); err == nil { |
| // If the adjacent dir already exists, bite the bullet and move |
| // to a proper tempdir. |
| vendorbak = filepath.Join(td, ".vendor.orig") |
| } |
| |
| failerr = fs.RenameWithFallback(vpath, vendorbak) |
| if failerr != nil { |
| goto fail |
| } |
| restore = append(restore, pathpair{from: vendorbak, to: vpath}) |
| } |
| |
| // Move in the new one. |
| failerr = fs.RenameWithFallback(filepath.Join(td, "vendor"), vpath) |
| if failerr != nil { |
| goto fail |
| } |
| } |
| |
| // Renames all went smoothly. The deferred os.RemoveAll will get the temp |
| // dir, but if we wrote vendor, we have to clean that up directly |
| if sw.writeVendor { |
| // Nothing we can really do about an error at this point, so ignore it |
| os.RemoveAll(vendorbak) |
| } |
| |
| return nil |
| |
| fail: |
| // If we failed at any point, move all the things back into place, then bail. |
| for _, pair := range restore { |
| // Nothing we can do on err here, as we're already in recovery mode. |
| fs.RenameWithFallback(pair.from, pair.to) |
| } |
| return failerr |
| } |
| |
| // PrintPreparedActions logs the actions a call to Write would perform. |
| func (sw *SafeWriter) PrintPreparedActions(output *log.Logger, verbose bool) error { |
| if output == nil { |
| output = log.New(ioutil.Discard, "", 0) |
| } |
| if sw.HasManifest() { |
| if verbose { |
| m, err := sw.Manifest.MarshalTOML() |
| if err != nil { |
| return errors.Wrap(err, "ensure DryRun cannot serialize manifest") |
| } |
| output.Printf("Would have written the following %s:\n%s\n", ManifestName, string(m)) |
| } else { |
| output.Printf("Would have written %s.\n", ManifestName) |
| } |
| } |
| |
| if sw.writeLock { |
| if verbose { |
| l, err := sw.lock.MarshalTOML() |
| if err != nil { |
| return errors.Wrap(err, "ensure DryRun cannot serialize lock") |
| } |
| output.Printf("Would have written the following %s:\n%s\n", LockName, string(l)) |
| } else { |
| output.Printf("Would have written %s.\n", LockName) |
| } |
| } |
| |
| if sw.writeVendor { |
| if verbose { |
| output.Printf("Would have written the following %d projects to the vendor directory:\n", len(sw.lock.Projects())) |
| lps := sw.lock.Projects() |
| for i, p := range lps { |
| output.Printf("(%d/%d) %s@%s\n", i+1, len(lps), p.Ident(), p.Version()) |
| } |
| } else { |
| output.Printf("Would have written %d projects to the vendor directory.\n", len(sw.lock.Projects())) |
| } |
| } |
| |
| return nil |
| } |
| |
| // hasDotGit checks if a given path has .git file or directory in it. |
| func hasDotGit(path string) bool { |
| gitfilepath := filepath.Join(path, ".git") |
| _, err := os.Stat(gitfilepath) |
| return err == nil |
| } |
| |
| // DeltaWriter manages batched writes to populate vendor/ and update Gopkg.lock. |
| // Its primary design goal is to minimize writes by only writing things that |
| // have changed. |
| type DeltaWriter struct { |
| lock *Lock |
| lockDiff verify.LockDelta |
| vendorDir string |
| changed map[gps.ProjectRoot]changeType |
| behavior VendorBehavior |
| } |
| |
| type changeType uint8 |
| |
| const ( |
| hashMismatch changeType = iota + 1 |
| hashVersionMismatch |
| hashAbsent |
| noVerify |
| solveChanged |
| pruneOptsChanged |
| missingFromTree |
| projectAdded |
| projectRemoved |
| pathPreserved |
| ) |
| |
| // NewDeltaWriter prepares a vendor writer that will construct a vendor |
| // directory by writing out only those projects that actually need to be written |
| // out - they have changed in some way, or they lack the necessary hash |
| // information to be verified. |
| func NewDeltaWriter(p *Project, newLock *Lock, behavior VendorBehavior) (TreeWriter, error) { |
| dw := &DeltaWriter{ |
| lock: newLock, |
| vendorDir: filepath.Join(p.AbsRoot, "vendor"), |
| changed: make(map[gps.ProjectRoot]changeType), |
| behavior: behavior, |
| } |
| |
| if newLock == nil { |
| return nil, errors.New("must provide a non-nil newlock") |
| } |
| |
| status, err := p.VerifyVendor() |
| if err != nil { |
| return nil, err |
| } |
| |
| _, err = os.Stat(dw.vendorDir) |
| if err != nil { |
| if os.IsNotExist(err) { |
| // Provided dir does not exist, so there's no disk contents to compare |
| // against. Fall back to the old SafeWriter. |
| return NewSafeWriter(nil, p.Lock, newLock, behavior, p.Manifest.PruneOptions, status) |
| } |
| return nil, err |
| } |
| |
| dw.lockDiff = verify.DiffLocks(p.Lock, newLock) |
| |
| for pr, lpd := range dw.lockDiff.ProjectDeltas { |
| // Hash changes aren't relevant at this point, as they could be empty |
| // in the new lock, and therefore a symptom of a solver change. |
| if lpd.Changed(anyExceptHash) { |
| if lpd.WasAdded() { |
| dw.changed[pr] = projectAdded |
| } else if lpd.WasRemoved() { |
| dw.changed[pr] = projectRemoved |
| } else if lpd.PruneOptsChanged() { |
| dw.changed[pr] = pruneOptsChanged |
| } else { |
| dw.changed[pr] = solveChanged |
| } |
| } |
| } |
| |
| for spr, stat := range status { |
| pr := gps.ProjectRoot(spr) |
| // These cases only matter if there was no change already recorded via |
| // the differ. |
| if _, has := dw.changed[pr]; !has { |
| switch stat { |
| case verify.NotInTree: |
| dw.changed[pr] = missingFromTree |
| case verify.NotInLock: |
| dw.changed[pr] = projectRemoved |
| case verify.DigestMismatchInLock: |
| dw.changed[pr] = hashMismatch |
| case verify.HashVersionMismatch: |
| dw.changed[pr] = hashVersionMismatch |
| case verify.EmptyDigestInLock: |
| dw.changed[pr] = hashAbsent |
| } |
| } |
| } |
| |
| // Apply noverify last, as it should only supersede changeTypes with lower |
| // values. It is NOT applied if no existing change is registered. |
| for _, spr := range p.Manifest.NoVerify { |
| pr := gps.ProjectRoot(spr) |
| // We don't validate this field elsewhere as it can be difficult to know |
| // at the beginning of a dep ensure command whether or not the noverify |
| // project actually will exist as part of the Lock by the end of the |
| // run. So, only apply if it's in the lockdiff. |
| if _, has := dw.lockDiff.ProjectDeltas[pr]; has { |
| if typ, has := dw.changed[pr]; has { |
| if typ < noVerify { |
| // Avoid writing noverify projects at all for the lower change |
| // types. |
| delete(dw.changed, pr) |
| |
| // Uncomment this if we want to switch to the safer behavior, |
| // where we ALWAYS write noverify projects. |
| //dw.changed[pr] = noVerify |
| } else if typ == projectRemoved { |
| // noverify can also be used to preserve files that would |
| // otherwise be removed. |
| dw.changed[pr] = pathPreserved |
| } |
| } |
| // It's also allowed to preserve entirely unknown paths using noverify. |
| } else if _, has := status[spr]; has { |
| dw.changed[pr] = pathPreserved |
| } |
| } |
| |
| return dw, nil |
| } |
| |
| // Write executes the planned changes. |
| // |
| // This writes recreated projects to a new directory, then moves in existing, |
| // unchanged projects from the original vendor directory. If any failures occur, |
| // reasonable attempts are made to roll back the changes. |
| func (dw *DeltaWriter) Write(path string, sm gps.SourceManager, examples bool, logger *log.Logger) error { |
| // TODO(sdboyer) remove path from the signature for this |
| if path != filepath.Dir(dw.vendorDir) { |
| return errors.Errorf("target path (%q) must be the parent of the original vendor path (%q)", path, dw.vendorDir) |
| } |
| |
| if logger == nil { |
| logger = log.New(ioutil.Discard, "", 0) |
| } |
| |
| lpath := filepath.Join(path, LockName) |
| vpath := dw.vendorDir |
| |
| // Write the modified projects to a new adjacent directory. We use an |
| // adjacent directory to minimize the possibility of cross-filesystem renames |
| // becoming expensive copies, and to make removal of unneeded projects implicit |
| // and automatic. |
| vnewpath := filepath.Join(filepath.Dir(vpath), ".vendor-new") |
| if _, err := os.Stat(vnewpath); err == nil { |
| return errors.Errorf("scratch directory %s already exists, please remove it", vnewpath) |
| } |
| err := os.MkdirAll(vnewpath, os.FileMode(0777)) |
| if err != nil { |
| return errors.Wrapf(err, "error while creating scratch directory at %s", vnewpath) |
| } |
| |
| // Write out all the deltas to the newpath |
| projs := make(map[gps.ProjectRoot]gps.LockedProject) |
| for _, lp := range dw.lock.Projects() { |
| projs[lp.Ident().ProjectRoot] = lp |
| } |
| |
| var dropped, preserved []gps.ProjectRoot |
| i := 0 |
| tot := len(dw.changed) |
| for _, reason := range dw.changed { |
| if reason != pathPreserved { |
| logger.Println("# Bringing vendor into sync") |
| break |
| } |
| } |
| |
| for pr, reason := range dw.changed { |
| switch reason { |
| case projectRemoved: |
| dropped = append(dropped, pr) |
| continue |
| case pathPreserved: |
| preserved = append(preserved, pr) |
| continue |
| } |
| |
| to := filepath.FromSlash(filepath.Join(vnewpath, string(pr))) |
| po := projs[pr].(verify.VerifiableProject).PruneOpts |
| if err := sm.ExportPrunedProject(context.TODO(), projs[pr], po, to); err != nil { |
| return errors.Wrapf(err, "failed to export %s", pr) |
| } |
| |
| i++ |
| lpd := dw.lockDiff.ProjectDeltas[pr] |
| v, id := projs[pr].Version(), projs[pr].Ident() |
| |
| // Only print things if we're actually going to leave behind a new |
| // vendor dir. |
| if dw.behavior != VendorNever { |
| logger.Printf("(%d/%d) Wrote %s@%s: %s", i, tot, id, v, changeExplanation(reason, lpd)) |
| } |
| |
| digest, err := verify.DigestFromDirectory(to) |
| if err != nil { |
| return errors.Wrapf(err, "failed to hash %s", pr) |
| } |
| |
| // Update the new Lock with verification information. |
| for k, lp := range dw.lock.P { |
| if lp.Ident().ProjectRoot == pr { |
| vp := lp.(verify.VerifiableProject) |
| vp.Digest = digest |
| dw.lock.P[k] = verify.VerifiableProject{ |
| LockedProject: lp, |
| PruneOpts: po, |
| Digest: digest, |
| } |
| } |
| } |
| } |
| |
| // Write out the lock, now that it's fully updated with digests. |
| l, err := dw.lock.MarshalTOML() |
| if err != nil { |
| return errors.Wrap(err, "failed to marshal lock to TOML") |
| } |
| |
| if err = ioutil.WriteFile(lpath, append(lockFileComment, l...), 0666); err != nil { |
| return errors.Wrap(err, "failed to write new lock file") |
| } |
| |
| if dw.behavior == VendorNever { |
| return os.RemoveAll(vnewpath) |
| } |
| |
| // Changed projects are fully populated. Now, iterate over the lock's |
| // projects and move any remaining ones not in the changed list to vnewpath. |
| for _, lp := range dw.lock.Projects() { |
| pr := lp.Ident().ProjectRoot |
| tgt := filepath.Join(vnewpath, string(pr)) |
| err := os.MkdirAll(filepath.Dir(tgt), os.FileMode(0777)) |
| if err != nil { |
| return errors.Wrapf(err, "error creating parent directory in vendor for %s", tgt) |
| } |
| |
| if _, has := dw.changed[pr]; !has { |
| err = fs.RenameWithFallback(filepath.Join(vpath, string(pr)), tgt) |
| if err != nil { |
| return errors.Wrapf(err, "error moving unchanged project %s into scratch vendor dir", pr) |
| } |
| } |
| } |
| |
| for i, pr := range dropped { |
| // Kind of a lie to print this. ¯\_(ツ)_/¯ |
| fi, err := os.Stat(filepath.Join(vpath, string(pr))) |
| if err != nil { |
| return errors.Wrap(err, "could not stat file that VerifyVendor claimed existed") |
| } |
| |
| if fi.IsDir() { |
| logger.Printf("(%d/%d) Removed unused project %s", tot-(len(dropped)-i-1), tot, pr) |
| } else { |
| logger.Printf("(%d/%d) Removed orphaned file %s", tot-(len(dropped)-i-1), tot, pr) |
| } |
| } |
| |
| // Special case: ensure vendor/.git is preserved if present |
| if hasDotGit(vpath) { |
| preserved = append(preserved, ".git") |
| } |
| |
| for _, path := range preserved { |
| err = fs.RenameWithFallback(filepath.Join(vpath, string(path)), filepath.Join(vnewpath, string(path))) |
| if err != nil { |
| return errors.Wrapf(err, "failed to preserve vendor/%s", path) |
| } |
| } |
| |
| err = os.RemoveAll(vpath) |
| if err != nil { |
| return errors.Wrap(err, "failed to remove original vendor directory") |
| } |
| err = fs.RenameWithFallback(vnewpath, vpath) |
| if err != nil { |
| return errors.Wrap(err, "failed to put new vendor directory into place") |
| } |
| |
| return nil |
| } |
| |
| // changeExplanation outputs a string explaining what changed for each different |
| // possible changeType. |
| func changeExplanation(c changeType, lpd verify.LockedProjectDelta) string { |
| switch c { |
| case noVerify: |
| return "verification is disabled" |
| case solveChanged: |
| if lpd.SourceChanged() { |
| return fmt.Sprintf("source changed (%s -> %s)", lpd.SourceBefore, lpd.SourceAfter) |
| } else if lpd.VersionChanged() { |
| if lpd.VersionBefore == nil { |
| return fmt.Sprintf("version changed (was a bare revision)") |
| } |
| return fmt.Sprintf("version changed (was %s)", lpd.VersionBefore.String()) |
| } else if lpd.RevisionChanged() { |
| return fmt.Sprintf("revision changed (%s -> %s)", trimSHA(lpd.RevisionBefore), trimSHA(lpd.RevisionAfter)) |
| } else if lpd.PackagesChanged() { |
| la, lr := len(lpd.PackagesAdded), len(lpd.PackagesRemoved) |
| if la > 0 && lr > 0 { |
| return fmt.Sprintf("packages changed (%v added, %v removed)", la, lr) |
| } else if la > 0 { |
| return fmt.Sprintf("packages changed (%v added)", la) |
| } |
| return fmt.Sprintf("packages changed (%v removed)", lr) |
| } |
| case pruneOptsChanged: |
| // Override what's on the lockdiff with the extra info we have; |
| // this lets us excise PruneNestedVendorDirs and get the real |
| // value from the input param in place. |
| old := lpd.PruneOptsBefore & ^gps.PruneNestedVendorDirs |
| new := lpd.PruneOptsAfter & ^gps.PruneNestedVendorDirs |
| return fmt.Sprintf("prune options changed (%s -> %s)", old, new) |
| case hashMismatch: |
| return "hash of vendored tree didn't match digest in Gopkg.lock" |
| case hashVersionMismatch: |
| return "hashing algorithm mismatch" |
| case hashAbsent: |
| return "hash digest absent from lock" |
| case projectAdded: |
| return "new project" |
| case missingFromTree: |
| return "missing from vendor" |
| default: |
| panic(fmt.Sprintf("unrecognized changeType value %v", c)) |
| } |
| |
| return "" |
| } |
| |
| // PrintPreparedActions indicates what changes the DeltaWriter plans to make. |
| func (dw *DeltaWriter) PrintPreparedActions(output *log.Logger, verbose bool) error { |
| if verbose { |
| l, err := dw.lock.MarshalTOML() |
| if err != nil { |
| return errors.Wrap(err, "ensure DryRun cannot serialize lock") |
| } |
| output.Printf("Would have written the following %s (hash digests may be incorrect):\n%s\n", LockName, string(l)) |
| } else { |
| output.Printf("Would have written %s.\n", LockName) |
| } |
| |
| projs := make(map[gps.ProjectRoot]gps.LockedProject) |
| for _, lp := range dw.lock.Projects() { |
| projs[lp.Ident().ProjectRoot] = lp |
| } |
| |
| tot := len(dw.changed) |
| if tot > 0 { |
| output.Print("Would have updated the following projects in the vendor directory:\n\n") |
| i := 0 |
| for pr, reason := range dw.changed { |
| lpd := dw.lockDiff.ProjectDeltas[pr] |
| if reason == projectRemoved { |
| output.Printf("(%d/%d) Would have removed %s", i, tot, pr) |
| } else { |
| output.Printf("(%d/%d) Would have written %s@%s: %s", i, tot, projs[pr].Ident(), projs[pr].Version(), changeExplanation(reason, lpd)) |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| // A TreeWriter is responsible for writing important dep states to disk - |
| // Gopkg.lock, vendor, and possibly Gopkg.toml. |
| type TreeWriter interface { |
| PrintPreparedActions(output *log.Logger, verbose bool) error |
| Write(path string, sm gps.SourceManager, examples bool, logger *log.Logger) error |
| } |
| |
| // trimSHA checks if revision is a valid SHA1 digest and trims to 10 characters. |
| func trimSHA(revision gps.Revision) string { |
| if len(revision) == 40 { |
| if _, err := hex.DecodeString(string(revision)); err == nil { |
| // Valid SHA1 digest |
| revision = revision[0:10] |
| } |
| } |
| |
| return string(revision) |
| } |