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  }