This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
802 lines (705 sloc)
23.6 KB
// 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) | |
} |