diff --git a/cmd/dep/ensure.go b/cmd/dep/ensure.go index 7d14778368..c338d6d9a5 100644 --- a/cmd/dep/ensure.go +++ b/cmd/dep/ensure.go @@ -265,7 +265,7 @@ func (cmd *ensureCommand) runDefault(ctx *dep.Ctx, args []string, p *dep.Project // that "verification" is supposed to look like (#121); in the meantime, // we unconditionally write out vendor/ so that `dep ensure`'s behavior // is maximally compatible with what it will eventually become. - sw, err := dep.NewSafeWriter(nil, p.Lock, p.Lock, dep.VendorAlways) + sw, err := dep.NewSafeWriter(nil, p.Lock, p.Lock, dep.VendorAlways, p.Manifest.PruneOptions) if err != nil { return err } @@ -290,7 +290,7 @@ func (cmd *ensureCommand) runDefault(ctx *dep.Ctx, args []string, p *dep.Project return handleAllTheFailuresOfTheWorld(err) } - sw, err := dep.NewSafeWriter(nil, p.Lock, dep.LockFromSolution(solution), cmd.vendorBehavior()) + sw, err := dep.NewSafeWriter(nil, p.Lock, dep.LockFromSolution(solution), cmd.vendorBehavior(), p.Manifest.PruneOptions) if err != nil { return err } @@ -315,7 +315,7 @@ func (cmd *ensureCommand) runVendorOnly(ctx *dep.Ctx, args []string, p *dep.Proj } // Pass the same lock as old and new so that the writer will observe no // difference and choose not to write it out. - sw, err := dep.NewSafeWriter(nil, p.Lock, p.Lock, dep.VendorAlways) + sw, err := dep.NewSafeWriter(nil, p.Lock, p.Lock, dep.VendorAlways, p.Manifest.PruneOptions) if err != nil { return err } @@ -380,7 +380,7 @@ func (cmd *ensureCommand) runUpdate(ctx *dep.Ctx, args []string, p *dep.Project, return handleAllTheFailuresOfTheWorld(err) } - sw, err := dep.NewSafeWriter(nil, p.Lock, dep.LockFromSolution(solution), cmd.vendorBehavior()) + sw, err := dep.NewSafeWriter(nil, p.Lock, dep.LockFromSolution(solution), cmd.vendorBehavior(), p.Manifest.PruneOptions) if err != nil { return err } @@ -679,7 +679,7 @@ func (cmd *ensureCommand) runAdd(ctx *dep.Ctx, args []string, p *dep.Project, sm } sort.Strings(reqlist) - sw, err := dep.NewSafeWriter(nil, p.Lock, dep.LockFromSolution(solution), dep.VendorOnChanged) + sw, err := dep.NewSafeWriter(nil, p.Lock, dep.LockFromSolution(solution), dep.VendorOnChanged, p.Manifest.PruneOptions) if err != nil { return err } diff --git a/cmd/dep/init.go b/cmd/dep/init.go index 205ce83f4e..7f88763849 100644 --- a/cmd/dep/init.go +++ b/cmd/dep/init.go @@ -211,7 +211,7 @@ func (cmd *initCommand) Run(ctx *dep.Ctx, args []string) error { ctx.Err.Printf("Old vendor backed up to %v", vendorbak) } - sw, err := dep.NewSafeWriter(p.Manifest, nil, p.Lock, dep.VendorAlways) + sw, err := dep.NewSafeWriter(p.Manifest, nil, p.Lock, dep.VendorAlways, gps.DefaultRootPruneOptions()) if err != nil { return errors.Wrap(err, "init failed: unable to create a SafeWriter") } diff --git a/cmd/dep/main.go b/cmd/dep/main.go index aa29e0a589..7aba6af43a 100644 --- a/cmd/dep/main.go +++ b/cmd/dep/main.go @@ -67,7 +67,6 @@ func (c *Config) Run() int { &statusCommand{}, &ensureCommand{}, &hashinCommand{}, - &pruneCommand{}, &versionCommand{}, } diff --git a/cmd/dep/prune.go b/cmd/dep/prune.go deleted file mode 100644 index 202a0980a6..0000000000 --- a/cmd/dep/prune.go +++ /dev/null @@ -1,206 +0,0 @@ -// 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 main - -import ( - "bytes" - "flag" - "io/ioutil" - "log" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/golang/dep" - "github.com/golang/dep/gps" - "github.com/golang/dep/gps/pkgtree" - "github.com/golang/dep/internal/fs" - "github.com/pkg/errors" -) - -const pruneShortHelp = `Prune the vendor tree of unused packages` -const pruneLongHelp = ` -Prune is used to remove unused packages from your vendor tree. - -STABILITY NOTICE: this command creates problems for vendor/ verification. As -such, it may be removed and/or moved out into a separate project later on. -` - -type pruneCommand struct { -} - -func (cmd *pruneCommand) Name() string { return "prune" } -func (cmd *pruneCommand) Args() string { return "" } -func (cmd *pruneCommand) ShortHelp() string { return pruneShortHelp } -func (cmd *pruneCommand) LongHelp() string { return pruneLongHelp } -func (cmd *pruneCommand) Hidden() bool { return false } - -func (cmd *pruneCommand) Register(fs *flag.FlagSet) { -} - -func (cmd *pruneCommand) Run(ctx *dep.Ctx, args []string) error { - p, err := ctx.LoadProject() - if err != nil { - return err - } - - sm, err := ctx.SourceManager() - if err != nil { - return err - } - sm.UseDefaultSignalHandling() - defer sm.Release() - - // While the network churns on ListVersions() requests, statically analyze - // code from the current project. - ptree, err := pkgtree.ListPackages(p.ResolvedAbsRoot, string(p.ImportRoot)) - if err != nil { - return errors.Wrap(err, "analysis of local packages failed: %v") - } - - // Set up a solver in order to check the InputHash. - params := p.MakeParams() - params.RootPackageTree = ptree - - if ctx.Verbose { - params.TraceLogger = ctx.Err - } - - s, err := gps.Prepare(params, sm) - if err != nil { - return errors.Wrap(err, "could not set up solver for input hashing") - } - - if p.Lock == nil { - return errors.Errorf("Gopkg.lock must exist for prune to know what files are safe to remove.") - } - - if !bytes.Equal(s.HashInputs(), p.Lock.SolveMeta.InputsDigest) { - return errors.Errorf("Gopkg.lock is out of sync; run dep ensure before pruning.") - } - - pruneLogger := ctx.Err - if !ctx.Verbose { - pruneLogger = log.New(ioutil.Discard, "", 0) - } - return pruneProject(p, sm, pruneLogger) -} - -// pruneProject removes unused packages from a project. -func pruneProject(p *dep.Project, sm gps.SourceManager, logger *log.Logger) error { - 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 err := gps.WriteDepTree(td, p.Lock, sm, true, logger); err != nil { - return err - } - - var toKeep []string - for _, project := range p.Lock.Projects() { - projectRoot := string(project.Ident().ProjectRoot) - for _, pkg := range project.Packages() { - toKeep = append(toKeep, filepath.Join(projectRoot, pkg)) - } - } - - toDelete, err := calculatePrune(td, toKeep, logger) - if err != nil { - return err - } - - if len(toDelete) > 0 { - logger.Println("Calculated the following directories to prune:") - for _, d := range toDelete { - logger.Printf(" %s\n", d) - } - } else { - logger.Println("No directories found to prune") - } - - if err := deleteDirs(toDelete); err != nil { - return err - } - - vpath := filepath.Join(p.AbsRoot, "vendor") - vendorbak := vpath + ".orig" - var failerr error - 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. - 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 - } - } - - // Move in the new one. - failerr = fs.RenameWithFallback(td, vpath) - if failerr != nil { - goto fail - } - - os.RemoveAll(vendorbak) - - return nil - -fail: - fs.RenameWithFallback(vendorbak, vpath) - return failerr -} - -func calculatePrune(vendorDir string, keep []string, logger *log.Logger) ([]string, error) { - logger.Println("Calculating prune. Checking the following packages:") - sort.Strings(keep) - toDelete := []string{} - err := filepath.Walk(vendorDir, func(path string, info os.FileInfo, err error) error { - if _, err := os.Lstat(path); err != nil { - return nil - } - if !info.IsDir() { - return nil - } - if path == vendorDir { - return nil - } - - name := strings.TrimPrefix(path, vendorDir+string(filepath.Separator)) - logger.Printf(" %s", name) - i := sort.Search(len(keep), func(i int) bool { - return name <= keep[i] - }) - if i >= len(keep) || !strings.HasPrefix(keep[i], name) { - toDelete = append(toDelete, path) - } - return nil - }) - return toDelete, err -} - -func deleteDirs(toDelete []string) error { - // sort by length so we delete sub dirs first - sort.Sort(byLen(toDelete)) - for _, path := range toDelete { - if err := os.RemoveAll(path); err != nil { - return err - } - } - return nil -} - -type byLen []string - -func (a byLen) Len() int { return len(a) } -func (a byLen) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a byLen) Less(i, j int) bool { return len(a[i]) > len(a[j]) } diff --git a/cmd/dep/prune_test.go b/cmd/dep/prune_test.go deleted file mode 100644 index 8a9c1d1d96..0000000000 --- a/cmd/dep/prune_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2017 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 main - -import ( - "io/ioutil" - "log" - "path/filepath" - "reflect" - "sort" - "testing" - - "github.com/golang/dep/internal/test" -) - -func TestCalculatePrune(t *testing.T) { - h := test.NewHelper(t) - defer h.Cleanup() - - vendorDir := "vendor" - h.TempDir(vendorDir) - h.TempDir(filepath.Join(vendorDir, "github.com/keep/pkg/sub")) - h.TempDir(filepath.Join(vendorDir, "github.com/prune/pkg/sub")) - - toKeep := []string{ - filepath.FromSlash("github.com/keep/pkg"), - filepath.FromSlash("github.com/keep/pkg/sub"), - } - - discardLogger := log.New(ioutil.Discard, "", 0) - - got, err := calculatePrune(h.Path(vendorDir), toKeep, discardLogger) - if err != nil { - t.Fatal(err) - } - - sort.Sort(byLen(got)) - - want := []string{ - h.Path(filepath.Join(vendorDir, "github.com/prune/pkg/sub")), - h.Path(filepath.Join(vendorDir, "github.com/prune/pkg")), - h.Path(filepath.Join(vendorDir, "github.com/prune")), - } - - if !reflect.DeepEqual(want, got) { - t.Fatalf("calculated prune paths are not as expected.\n(WNT) %s\n(GOT) %s", want, got) - } -} diff --git a/cmd/dep/testdata/harness_tests/prune/without_lock/final/Gopkg.toml b/cmd/dep/testdata/harness_tests/prune/without_lock/final/Gopkg.toml deleted file mode 100644 index 94deb714a4..0000000000 --- a/cmd/dep/testdata/harness_tests/prune/without_lock/final/Gopkg.toml +++ /dev/null @@ -1,3 +0,0 @@ -[[constraint]] - name = "github.com/sdboyer/deptest" - version = "^0.8.0" diff --git a/cmd/dep/testdata/harness_tests/prune/without_lock/initial/Gopkg.toml b/cmd/dep/testdata/harness_tests/prune/without_lock/initial/Gopkg.toml deleted file mode 100644 index 94deb714a4..0000000000 --- a/cmd/dep/testdata/harness_tests/prune/without_lock/initial/Gopkg.toml +++ /dev/null @@ -1,3 +0,0 @@ -[[constraint]] - name = "github.com/sdboyer/deptest" - version = "^0.8.0" diff --git a/cmd/dep/testdata/harness_tests/prune/without_lock/testcase.json b/cmd/dep/testdata/harness_tests/prune/without_lock/testcase.json deleted file mode 100644 index da6463ed6a..0000000000 --- a/cmd/dep/testdata/harness_tests/prune/without_lock/testcase.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "commands": [ - ["prune"] - ], - "error-expected": "Gopkg.lock must exist" -} diff --git a/gps/filesystem.go b/gps/filesystem.go new file mode 100644 index 0000000000..fd683f325c --- /dev/null +++ b/gps/filesystem.go @@ -0,0 +1,136 @@ +// Copyright 2017 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 gps + +import ( + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +// fsLink represents a symbolic link. +type fsLink struct { + path string + to string + + // circular denotes if evaluating the symlink fails with "too many links" error. + // This errors means that it's very likely that the symlink has circual refernce. + circular bool + + // broken denotes that attempting to resolve the link fails, most likely because + // the destaination doesn't exist. + broken bool +} + +// filesystemState represents the state of a file system. +type filesystemState struct { + root string + dirs []string + files []string + links []fsLink +} + +func (s filesystemState) setup() error { + for _, dir := range s.dirs { + p := filepath.Join(s.root, dir) + + if err := os.MkdirAll(p, 0777); err != nil { + return errors.Errorf("os.MkdirAll(%q, 0777) err=%q", p, err) + } + } + + for _, file := range s.files { + p := filepath.Join(s.root, file) + + f, err := os.Create(p) + if err != nil { + return errors.Errorf("os.Create(%q) err=%q", p, err) + } + + if err := f.Close(); err != nil { + return errors.Errorf("file %q Close() err=%q", p, err) + } + } + + for _, link := range s.links { + p := filepath.Join(s.root, link.path) + + // On Windows, relative symlinks confuse filepath.Walk. So, we'll just sigh + // and do absolute links, assuming they are relative to the directory of + // link.path. + // + // Reference: https://github.com/golang/go/issues/17540 + // + // TODO(ibrasho): This was fixed in Go 1.9. Remove this when support for + // 1.8 is dropped. + dir := filepath.Dir(p) + to := "" + if link.to != "" { + to = filepath.Join(dir, link.to) + } + + if err := os.Symlink(to, p); err != nil { + return errors.Errorf("os.Symlink(%q, %q) err=%q", to, p, err) + } + } + + return nil +} + +// deriveFilesystemState returns a filesystemState based on the state of +// the filesystem on root. +func deriveFilesystemState(root string) (filesystemState, error) { + fs := filesystemState{root: root} + + err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if path == fs.root { + return nil + } + + relPath, err := filepath.Rel(fs.root, path) + if err != nil { + return err + } + + if (info.Mode() & os.ModeSymlink) != 0 { + l := fsLink{path: relPath} + + l.to, err = filepath.EvalSymlinks(path) + if err != nil && strings.HasSuffix(err.Error(), "too many links") { + l.circular = true + } else if err != nil && os.IsNotExist(err) { + l.broken = true + } else if err != nil { + return err + } + + fs.links = append(fs.links, l) + + return nil + } + + if info.IsDir() { + fs.dirs = append(fs.dirs, relPath) + + return nil + } + + fs.files = append(fs.files, relPath) + + return nil + }) + + if err != nil { + return filesystemState{}, err + } + + return fs, nil +} diff --git a/gps/filesystem_test.go b/gps/filesystem_test.go index faa4dd5f5c..af29846fd0 100644 --- a/gps/filesystem_test.go +++ b/gps/filesystem_test.go @@ -5,61 +5,45 @@ package gps import ( + "fmt" "os" "path/filepath" + "reflect" "testing" + + "github.com/golang/dep/internal/test" ) // This file contains utilities for running tests around file system state. -// fspath represents a file system path in an OS-agnostic way. -type fsPath []string - -func (f fsPath) String() string { return filepath.Join(f...) } - -func (f fsPath) prepend(prefix string) fsPath { - p := fsPath{filepath.FromSlash(prefix)} - return append(p, f...) -} - type fsTestCase struct { before, after filesystemState } -// filesystemState represents the state of a file system. It has a setup method -// which inflates its state to the actual host file system, and an assert -// method which checks that the actual file system matches the described state. -type filesystemState struct { - root string - dirs []fsPath - files []fsPath - links []fsLink -} - -// assert makes sure that the fs state matches the state of the actual host -// file system -func (fs filesystemState) assert(t *testing.T) { +// assert makes sure that the tc.after state matches the state of the actual host +// file system at tc.after.root. +func (tc fsTestCase) assert(t *testing.T) { dirMap := make(map[string]bool) fileMap := make(map[string]bool) linkMap := make(map[string]bool) - for _, d := range fs.dirs { - dirMap[d.prepend(fs.root).String()] = true + for _, d := range tc.after.dirs { + dirMap[filepath.Join(tc.after.root, d)] = true } - for _, f := range fs.files { - fileMap[f.prepend(fs.root).String()] = true + for _, f := range tc.after.files { + fileMap[filepath.Join(tc.after.root, f)] = true } - for _, l := range fs.links { - linkMap[l.path.prepend(fs.root).String()] = true + for _, l := range tc.after.links { + linkMap[filepath.Join(tc.after.root, l.path)] = true } - err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(tc.after.root, func(path string, info os.FileInfo, err error) error { if err != nil { t.Errorf("filepath.Walk path=%q err=%q", path, err) return err } - if path == fs.root { + if path == tc.after.root { return nil } @@ -106,53 +90,131 @@ func (fs filesystemState) assert(t *testing.T) { } } -// fsLink represents a symbolic link. -type fsLink struct { - path fsPath - to string -} - -// setup inflates fs onto the actual host file system -func (fs filesystemState) setup(t *testing.T) { - fs.setupDirs(t) - fs.setupFiles(t) - fs.setupLinks(t) -} - -func (fs filesystemState) setupDirs(t *testing.T) { - for _, dir := range fs.dirs { - p := dir.prepend(fs.root) - if err := os.MkdirAll(p.String(), 0777); err != nil { - t.Fatalf("os.MkdirAll(%q, 0777) err=%q", p, err) - } +// setup inflates fs onto the actual host file system at tc.before.root. +// It doesn't delete existing files and should be used on empty roots only. +func (tc fsTestCase) setup(t *testing.T) { + if err := tc.before.setup(); err != nil { + t.Fatal(err) } } -func (fs filesystemState) setupFiles(t *testing.T) { - for _, file := range fs.files { - p := file.prepend(fs.root) - f, err := os.Create(p.String()) - if err != nil { - t.Fatalf("os.Create(%q) err=%q", p, err) - } - if err := f.Close(); err != nil { - t.Fatalf("file %q Close() err=%q", p, err) - } +func TestDeriveFilesystemState(t *testing.T) { + testcases := []struct { + name string + fs fsTestCase + }{ + { + name: "simple-case", + fs: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "simple-dir", + }, + files: []string{ + "simple-file", + }, + }, + after: filesystemState{ + dirs: []string{ + "simple-dir", + }, + files: []string{ + "simple-file", + }, + }, + }, + }, + { + name: "simple-symlink-case", + fs: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "simple-dir", + }, + files: []string{ + "simple-file", + }, + links: []fsLink{ + fsLink{ + path: "link", + to: "nonexisting", + broken: true, + }, + }, + }, + after: filesystemState{ + dirs: []string{ + "simple-dir", + }, + files: []string{ + "simple-file", + }, + links: []fsLink{ + fsLink{ + path: "link", + to: "", + broken: true, + }, + }, + }, + }, + }, + { + name: "complex-symlink-case", + fs: fsTestCase{ + before: filesystemState{ + links: []fsLink{ + fsLink{ + path: "link1", + to: "link2", + circular: true, + }, + fsLink{ + path: "link2", + to: "link1", + circular: true, + }, + }, + }, + after: filesystemState{ + links: []fsLink{ + fsLink{ + path: "link1", + to: "", + circular: true, + }, + fsLink{ + path: "link2", + to: "", + circular: true, + }, + }, + }, + }, + }, } -} -func (fs filesystemState) setupLinks(t *testing.T) { - for _, link := range fs.links { - p := link.path.prepend(fs.root) + for _, tc := range testcases { + h := test.NewHelper(t) + + h.TempDir(tc.name) - // On Windows, relative symlinks confuse filepath.Walk. This is golang/go - // issue 17540. So, we'll just sigh and do absolute links, assuming they are - // relative to the directory of link.path. - dir := filepath.Dir(p.String()) - to := filepath.Join(dir, link.to) + tc.fs.before.root = h.Path(tc.name) + tc.fs.after.root = h.Path(tc.name) - if err := os.Symlink(to, p.String()); err != nil { - t.Fatalf("os.Symlink(%q, %q) err=%q", to, p, err) + tc.fs.setup(t) + + state, err := deriveFilesystemState(h.Path(tc.name)) + if err != nil { + t.Fatal(err) } + + if !reflect.DeepEqual(tc.fs.after, state) { + fmt.Println(tc.fs.after) + fmt.Println(state) + t.Fatal("filesystem state mismatch") + } + + h.Cleanup() } } diff --git a/gps/prune.go b/gps/prune.go index 84ac74eb6c..40d645460e 100644 --- a/gps/prune.go +++ b/gps/prune.go @@ -8,8 +8,10 @@ import ( "log" "os" "path/filepath" + "sort" "strings" + "github.com/golang/dep/internal/fs" "github.com/pkg/errors" ) @@ -19,6 +21,13 @@ type PruneOptions uint8 // PruneProjectOptions is map of prune options per project name. type PruneProjectOptions map[ProjectRoot]PruneOptions +// RootPruneOptions represents the root prune options for the project. +// It contains the global options and a map of options per project. +type RootPruneOptions struct { + PruneOptions PruneOptions + ProjectOptions PruneProjectOptions +} + const ( // PruneNestedVendorDirs indicates if nested vendor directories should be pruned. PruneNestedVendorDirs PruneOptions = 1 << iota @@ -32,6 +41,26 @@ const ( PruneGoTestFiles ) +// DefaultRootPruneOptions instantiates a copy of the default root prune options. +func DefaultRootPruneOptions() RootPruneOptions { + return RootPruneOptions{ + PruneOptions: PruneNestedVendorDirs, + ProjectOptions: PruneProjectOptions{}, + } +} + +// PruneOptionsFor returns the prune options for the passed project root. +// +// It will return the root prune options if the project does not have specific +// options or if it does not exist in the manifest. +func (o *RootPruneOptions) PruneOptionsFor(pr ProjectRoot) PruneOptions { + if po, ok := o.ProjectOptions[pr]; ok { + return po + } + + return o.PruneOptions +} + var ( // licenseFilePrefixes is a list of name prefixes for license files. licenseFilePrefixes = []string{ @@ -56,204 +85,181 @@ var ( } ) -// Prune removes excess files from the dep tree whose root is baseDir based -// on the PruneOptions passed. -// -// A Lock must be passed if PruneUnusedPackages is toggled on. -func Prune(baseDir string, options PruneOptions, l Lock, logger *log.Logger) error { - // TODO(ibrasho) allow passing specific options per project - for _, lp := range l.Projects() { - projectDir := filepath.Join(baseDir, string(lp.Ident().ProjectRoot)) - err := PruneProject(projectDir, lp, options, logger) - if err != nil { - return err - } - } - - return nil -} - // PruneProject remove excess files according to the options passed, from // the lp directory in baseDir. func PruneProject(baseDir string, lp LockedProject, options PruneOptions, logger *log.Logger) error { - projectDir := filepath.Join(baseDir, string(lp.Ident().ProjectRoot)) + fsState, err := deriveFilesystemState(baseDir) + + if err != nil { + return errors.Wrap(err, "could not derive filesystem state") + } if (options & PruneNestedVendorDirs) != 0 { - if err := pruneNestedVendorDirs(projectDir); err != nil { + if err := pruneVendorDirs(fsState); err != nil { return errors.Wrapf(err, "failed to prune nested vendor directories") } } if (options & PruneUnusedPackages) != 0 { - if err := pruneUnusedPackages(lp, projectDir, logger); err != nil { + if _, err := pruneUnusedPackages(lp, fsState); err != nil { return errors.Wrap(err, "failed to prune unused packages") } } if (options & PruneNonGoFiles) != 0 { - if err := pruneNonGoFiles(projectDir, logger); err != nil { + if err := pruneNonGoFiles(fsState); err != nil { return errors.Wrap(err, "failed to prune non-Go files") } } if (options & PruneGoTestFiles) != 0 { - if err := pruneGoTestFiles(projectDir, logger); err != nil { + if err := pruneGoTestFiles(fsState); err != nil { return errors.Wrap(err, "failed to prune Go test files") } } - return nil -} + if err := deleteEmptyDirs(fsState); err != nil { + return errors.Wrap(err, "could not delete empty dirs") + } -// pruneNestedVendorDirs deletes all nested vendor directories within baseDir. -func pruneNestedVendorDirs(baseDir string) error { - return filepath.Walk(baseDir, stripVendor) + return nil } -// pruneUnusedPackages deletes unimported packages found within baseDir. -// Determining whether packages are imported or not is based on the passed LockedProject. -func pruneUnusedPackages(lp LockedProject, projectDir string, logger *log.Logger) error { - pr := string(lp.Ident().ProjectRoot) - logger.Printf("Calculating unused packages in %s to prune.\n", pr) - - unusedPackages, err := calculateUnusedPackages(lp, projectDir) - if err != nil { - return errors.Wrapf(err, "could not calculate unused packages in %s", pr) +// pruneVendorDirs deletes all nested vendor directories within baseDir. +func pruneVendorDirs(fsState filesystemState) error { + for _, dir := range fsState.dirs { + if filepath.Base(dir) == "vendor" { + err := os.RemoveAll(filepath.Join(fsState.root, dir)) + if err != nil && !os.IsNotExist(err) { + return err + } + } } - logger.Printf("Found the following unused packages in %s:\n", pr) - for pkg := range unusedPackages { - logger.Printf(" * %s\n", filepath.Join(pr, pkg)) + for _, link := range fsState.links { + if filepath.Base(link.path) == "vendor" { + err := os.Remove(filepath.Join(fsState.root, link.path)) + if err != nil && !os.IsNotExist(err) { + return err + } + } } - unusedPackagesFiles, err := collectUnusedPackagesFiles(projectDir, unusedPackages) - if err != nil { - return errors.Wrapf(err, "could not collect unused packages' files in %s", pr) - } + return nil +} - if err := deleteFiles(unusedPackagesFiles); err != nil { - return errors.Wrapf(err, "") +// pruneUnusedPackages deletes unimported packages found in fsState. +// Determining whether packages are imported or not is based on the passed LockedProject. +func pruneUnusedPackages(lp LockedProject, fsState filesystemState) (map[string]interface{}, error) { + unusedPackages := calculateUnusedPackages(lp, fsState) + toDelete := collectUnusedPackagesFiles(fsState, unusedPackages) + + for _, path := range toDelete { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return nil, err + } } - return nil + return unusedPackages, nil } // calculateUnusedPackages generates a list of unused packages in lp. -func calculateUnusedPackages(lp LockedProject, projectDir string) (map[string]struct{}, error) { - unused := make(map[string]struct{}) - imported := make(map[string]struct{}) +func calculateUnusedPackages(lp LockedProject, fsState filesystemState) map[string]interface{} { + unused := make(map[string]interface{}) + imported := make(map[string]interface{}) + for _, pkg := range lp.Packages() { - imported[pkg] = struct{}{} + imported[pkg] = nil } - err := filepath.Walk(projectDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } + // Add the root package if it's not imported. + if _, ok := imported["."]; !ok { + unused["."] = nil + } - // Ignore anything that's not a directory. - if !info.IsDir() { - return nil - } + for _, dirPath := range fsState.dirs { + pkg := filepath.ToSlash(dirPath) - pkg, err := filepath.Rel(projectDir, path) - if err != nil { - return errors.Wrap(err, "unexpected error while calculating unused packages") - } - - pkg = filepath.ToSlash(pkg) if _, ok := imported[pkg]; !ok { - unused[pkg] = struct{}{} + unused[pkg] = nil } + } - return nil - }) - - return unused, err + return unused } -// collectUnusedPackagesFiles returns a slice of all files in the unused packages in projectDir. -func collectUnusedPackagesFiles(projectDir string, unusedPackages map[string]struct{}) ([]string, error) { +// collectUnusedPackagesFiles returns a slice of all files in the unused +// packages based on fsState. +func collectUnusedPackagesFiles(fsState filesystemState, unusedPackages map[string]interface{}) []string { + // TODO(ibrasho): is this useful? files := make([]string, 0, len(unusedPackages)) - err := filepath.Walk(projectDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Ignore directories. - if info.IsDir() { - return nil - } - - // Ignore preserved files. - if isPreservedFile(info.Name()) { - return nil + for _, path := range fsState.files { + // Keep perserved files. + if isPreservedFile(filepath.Base(path)) { + continue } - pkg, err := filepath.Rel(projectDir, filepath.Dir(path)) - if err != nil { - return errors.Wrap(err, "unexpected error while calculating unused packages") - } + pkg := filepath.ToSlash(filepath.Dir(path)) - pkg = filepath.ToSlash(pkg) if _, ok := unusedPackages[pkg]; ok { - files = append(files, path) + files = append(files, filepath.Join(fsState.root, path)) } - - return nil - }) - - return files, err -} - -// pruneNonGoFiles delete all non-Go files existing within baseDir. -// Files with names that are prefixed by any entry in preservedNonGoFiles -// are not deleted. -func pruneNonGoFiles(baseDir string, logger *log.Logger) error { - files, err := collectNonGoFiles(baseDir, logger) - if err != nil { - return errors.Wrap(err, "could not collect non-Go files") } - if err := deleteFiles(files); err != nil { - return errors.Wrap(err, "could not prune Go test files") - } - - return nil + return files } -// collectNonGoFiles returns a slice containing all non-Go files in baseDir. -// Files meeting the checks in isPreservedFile are not returned. -func collectNonGoFiles(baseDir string, logger *log.Logger) ([]string, error) { - files := make([]string, 0) - - err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err +// pruneNonGoFiles delete all non-Go files existing in fsState. +// +// Files matching licenseFilePrefixes and legalFileSubstrings are not pruned. +func pruneNonGoFiles(fsState filesystemState) error { + toDelete := make([]string, 0, len(fsState.files)/4) + + for _, path := range fsState.files { + ext := fileExt(path) + + // Refer to: https://github.com/golang/go/blob/release-branch.go1.9/src/go/build/build.go#L750 + switch ext { + case ".go": + continue + case ".c": + continue + case ".cc", ".cpp", ".cxx": + continue + case ".m": + continue + case ".h", ".hh", ".hpp", ".hxx": + continue + case ".f", ".F", ".for", ".f90": + continue + case ".s": + continue + case ".S": + continue + case ".swig": + continue + case ".swigcxx": + continue + case ".syso": + continue } - // Ignore directories. - if info.IsDir() { - return nil + // Ignore perserved files. + if isPreservedFile(filepath.Base(path)) { + continue } - // Ignore all Go files. - if strings.HasSuffix(info.Name(), ".go") { - return nil - } + toDelete = append(toDelete, filepath.Join(fsState.root, path)) + } - // Ignore preserved files. - if isPreservedFile(info.Name()) { - return nil + for _, path := range toDelete { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err } + } - files = append(files, path) - - return nil - }) - - return files, err + return nil } // isPreservedFile checks if the file name indicates that the file should be @@ -276,51 +282,50 @@ func isPreservedFile(name string) bool { return false } -// pruneGoTestFiles deletes all Go test files (*_test.go) within baseDir. -func pruneGoTestFiles(baseDir string, logger *log.Logger) error { - files, err := collectGoTestFiles(baseDir) - if err != nil { - return errors.Wrap(err, "could not collect Go test files") +// pruneGoTestFiles deletes all Go test files (*_test.go) in fsState. +func pruneGoTestFiles(fsState filesystemState) error { + toDelete := make([]string, 0, len(fsState.files)/2) + + for _, path := range fsState.files { + if strings.HasSuffix(path, "_test.go") { + toDelete = append(toDelete, filepath.Join(fsState.root, path)) + } } - if err := deleteFiles(files); err != nil { - return errors.Wrap(err, "could not prune Go test files") + for _, path := range toDelete { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } } return nil } -// collectGoTestFiles returns a slice contains all Go test files (any files -// prefixed with _test.go) in baseDir. -func collectGoTestFiles(baseDir string) ([]string, error) { - files := make([]string, 0) +func deleteEmptyDirs(fsState filesystemState) error { + sort.Sort(sort.Reverse(sort.StringSlice(fsState.dirs))) + + for _, dir := range fsState.dirs { + path := filepath.Join(fsState.root, dir) - err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { + notEmpty, err := fs.IsNonEmptyDir(path) if err != nil { return err } - // Ignore directories. - if info.IsDir() { - return nil + if !notEmpty { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } } + } - // Ignore any files that is not a Go test file. - if strings.HasSuffix(info.Name(), "_test.go") { - files = append(files, path) - } - - return nil - }) - - return files, err + return nil } -func deleteFiles(paths []string) error { - for _, path := range paths { - if err := os.Remove(path); err != nil { - return err - } +func fileExt(name string) string { + i := strings.LastIndex(name, ".") + if i < 0 { + return "" } - return nil + return name[i:] } diff --git a/gps/prune_test.go b/gps/prune_test.go index de7791cd77..d4c7371237 100644 --- a/gps/prune_test.go +++ b/gps/prune_test.go @@ -7,18 +7,58 @@ package gps import ( "io/ioutil" "log" + "os" "testing" "github.com/golang/dep/internal/test" ) +func TestRootPruneOptions_PruneOptionsFor(t *testing.T) { + pr := ProjectRoot("github.com/golang/dep") + + o := RootPruneOptions{ + PruneOptions: PruneNestedVendorDirs, + ProjectOptions: PruneProjectOptions{ + pr: PruneGoTestFiles, + }, + } + + if (o.PruneOptionsFor(pr) & PruneGoTestFiles) != PruneGoTestFiles { + t.Fatalf("invalid prune options.\n\t(GOT): %d\n\t(WNT): %d", o.PruneOptionsFor(pr), PruneGoTestFiles) + } +} + +func TestPruneProject(t *testing.T) { + h := test.NewHelper(t) + defer h.Cleanup() + + pr := "github.com/project/repository" + h.TempDir(pr) + + baseDir := h.Path(".") + lp := LockedProject{ + pi: ProjectIdentifier{ + ProjectRoot: ProjectRoot(pr), + }, + pkgs: []string{}, + } + + options := PruneNestedVendorDirs | PruneNonGoFiles | PruneGoTestFiles | PruneUnusedPackages + logger := log.New(ioutil.Discard, "", 0) + + err := PruneProject(baseDir, lp, options, logger) + if err != nil { + t.Fatal(err) + } +} + func TestPruneUnusedPackages(t *testing.T) { h := test.NewHelper(t) defer h.Cleanup() h.TempDir(".") - pr := "github.com/test/project" + pr := "github.com/sample/repository" pi := ProjectIdentifier{ProjectRoot: ProjectRoot(pr)} testcases := []struct { @@ -30,18 +70,20 @@ func TestPruneUnusedPackages(t *testing.T) { { "one-package", LockedProject{ - pi: pi, - pkgs: []string{"."}, + pi: pi, + pkgs: []string{ + ".", + }, }, fsTestCase{ before: filesystemState{ - files: []fsPath{ - {"main.go"}, + files: []string{ + "main.go", }, }, after: filesystemState{ - files: []fsPath{ - {"main.go"}, + files: []string{ + "main.go", }, }, }, @@ -50,25 +92,27 @@ func TestPruneUnusedPackages(t *testing.T) { { "nested-package", LockedProject{ - pi: pi, - pkgs: []string{"pkg"}, + pi: pi, + pkgs: []string{ + "pkg", + }, }, fsTestCase{ before: filesystemState{ - dirs: []fsPath{ - {"pkg"}, + dirs: []string{ + "pkg", }, - files: []fsPath{ - {"main.go"}, - {"pkg", "main.go"}, + files: []string{ + "main.go", + "pkg/main.go", }, }, after: filesystemState{ - dirs: []fsPath{ - {"pkg"}, + dirs: []string{ + "pkg", }, - files: []fsPath{ - {"pkg", "main.go"}, + files: []string{ + "pkg/main.go", }, }, }, @@ -77,36 +121,39 @@ func TestPruneUnusedPackages(t *testing.T) { { "complex-project", LockedProject{ - pi: pi, - pkgs: []string{"pkg", "pkg/nestedpkg/otherpkg"}, + pi: pi, + pkgs: []string{ + "pkg", + "pkg/nestedpkg/otherpkg", + }, }, fsTestCase{ before: filesystemState{ - dirs: []fsPath{ - {"pkg"}, - {"pkg", "nestedpkg"}, - {"pkg", "nestedpkg", "otherpkg"}, + dirs: []string{ + "pkg", + "pkg/nestedpkg", + "pkg/nestedpkg/otherpkg", }, - files: []fsPath{ - {"main.go"}, - {"COPYING"}, - {"pkg", "main.go"}, - {"pkg", "nestedpkg", "main.go"}, - {"pkg", "nestedpkg", "PATENT.md"}, - {"pkg", "nestedpkg", "otherpkg", "main.go"}, + files: []string{ + "main.go", + "COPYING", + "pkg/main.go", + "pkg/nestedpkg/main.go", + "pkg/nestedpkg/PATENT.md", + "pkg/nestedpkg/otherpkg/main.go", }, }, after: filesystemState{ - dirs: []fsPath{ - {"pkg"}, - {"pkg", "nestedpkg"}, - {"pkg", "nestedpkg", "otherpkg"}, + dirs: []string{ + "pkg", + "pkg/nestedpkg", + "pkg/nestedpkg/otherpkg", }, - files: []fsPath{ - {"COPYING"}, - {"pkg", "main.go"}, - {"pkg", "nestedpkg", "PATENT.md"}, - {"pkg", "nestedpkg", "otherpkg", "main.go"}, + files: []string{ + "COPYING", + "pkg/main.go", + "pkg/nestedpkg/PATENT.md", + "pkg/nestedpkg/otherpkg/main.go", }, }, }, @@ -114,25 +161,27 @@ func TestPruneUnusedPackages(t *testing.T) { }, } - logger := log.New(ioutil.Discard, "", 0) - for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { h.TempDir(pr) - projectDir := h.Path(pr) - tc.fs.before.root = projectDir - tc.fs.after.root = projectDir + baseDir := h.Path(pr) + tc.fs.before.root = baseDir + tc.fs.after.root = baseDir + tc.fs.setup(t) - tc.fs.before.setup(t) + fs, err := deriveFilesystemState(baseDir) + if err != nil { + t.Fatal(err) + } - err := pruneUnusedPackages(tc.lp, projectDir, logger) + _, err = pruneUnusedPackages(tc.lp, fs) if tc.err && err == nil { - t.Errorf("expected an error, got nil") + t.Fatalf("expected an error, got nil") } else if !tc.err && err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } - tc.fs.after.assert(t) + tc.fs.assert(t) }) } } @@ -152,8 +201,8 @@ func TestPruneNonGoFiles(t *testing.T) { "one-file", fsTestCase{ before: filesystemState{ - files: []fsPath{ - {"README.md"}, + files: []string{ + "README.md", }, }, after: filesystemState{}, @@ -164,16 +213,16 @@ func TestPruneNonGoFiles(t *testing.T) { "multiple-files", fsTestCase{ before: filesystemState{ - files: []fsPath{ - {"main.go"}, - {"main_test.go"}, - {"README"}, + files: []string{ + "main.go", + "main_test.go", + "README", }, }, after: filesystemState{ - files: []fsPath{ - {"main.go"}, - {"main_test.go"}, + files: []string{ + "main.go", + "main_test.go", }, }, }, @@ -183,22 +232,22 @@ func TestPruneNonGoFiles(t *testing.T) { "mixed-files", fsTestCase{ before: filesystemState{ - dirs: []fsPath{ - {"dir"}, + dirs: []string{ + "dir", }, - files: []fsPath{ - {"dir", "main.go"}, - {"dir", "main_test.go"}, - {"dir", "db.sqlite"}, + files: []string{ + "dir/main.go", + "dir/main_test.go", + "dir/db.sqlite", }, }, after: filesystemState{ - dirs: []fsPath{ - {"dir"}, + dirs: []string{ + "dir", }, - files: []fsPath{ - {"dir", "main.go"}, - {"dir", "main_test.go"}, + files: []string{ + "dir/main.go", + "dir/main_test.go", }, }, }, @@ -206,8 +255,6 @@ func TestPruneNonGoFiles(t *testing.T) { }, } - logger := log.New(ioutil.Discard, "", 0) - for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { h.TempDir(tc.name) @@ -215,16 +262,21 @@ func TestPruneNonGoFiles(t *testing.T) { tc.fs.before.root = baseDir tc.fs.after.root = baseDir - tc.fs.before.setup(t) + tc.fs.setup(t) + + fs, err := deriveFilesystemState(baseDir) + if err != nil { + t.Fatal(err) + } - err := pruneNonGoFiles(baseDir, logger) + err = pruneNonGoFiles(fs) if tc.err && err == nil { t.Errorf("expected an error, got nil") } else if !tc.err && err != nil { t.Errorf("unexpected error: %s", err) } - tc.fs.after.assert(t) + tc.fs.assert(t) }) } } @@ -244,8 +296,8 @@ func TestPruneGoTestFiles(t *testing.T) { "one-test-file", fsTestCase{ before: filesystemState{ - files: []fsPath{ - {"main_test.go"}, + files: []string{ + "main_test.go", }, }, after: filesystemState{}, @@ -256,17 +308,17 @@ func TestPruneGoTestFiles(t *testing.T) { "multiple-files", fsTestCase{ before: filesystemState{ - dirs: []fsPath{ - {"dir"}, + dirs: []string{ + "dir", }, - files: []fsPath{ - {"dir", "main_test.go"}, - {"dir", "main2_test.go"}, + files: []string{ + "dir/main_test.go", + "dir/main2_test.go", }, }, after: filesystemState{ - dirs: []fsPath{ - {"dir"}, + dirs: []string{ + "dir", }, }, }, @@ -276,23 +328,23 @@ func TestPruneGoTestFiles(t *testing.T) { "mixed-files", fsTestCase{ before: filesystemState{ - dirs: []fsPath{ - {"dir"}, + dirs: []string{ + "dir", }, - files: []fsPath{ - {"dir", "main.go"}, - {"dir", "main2.go"}, - {"dir", "main_test.go"}, - {"dir", "main2_test.go"}, + files: []string{ + "dir/main.go", + "dir/main2.go", + "dir/main_test.go", + "dir/main2_test.go", }, }, after: filesystemState{ - dirs: []fsPath{ - {"dir"}, + dirs: []string{ + "dir", }, - files: []fsPath{ - {"dir", "main.go"}, - {"dir", "main2.go"}, + files: []string{ + "dir/main.go", + "dir/main2.go", }, }, }, @@ -300,8 +352,6 @@ func TestPruneGoTestFiles(t *testing.T) { }, } - logger := log.New(ioutil.Discard, "", 0) - for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { h.TempDir(tc.name) @@ -309,16 +359,356 @@ func TestPruneGoTestFiles(t *testing.T) { tc.fs.before.root = baseDir tc.fs.after.root = baseDir - tc.fs.before.setup(t) + tc.fs.setup(t) + + fs, err := deriveFilesystemState(baseDir) + if err != nil { + t.Fatal(err) + } - err := pruneGoTestFiles(baseDir, logger) + err = pruneGoTestFiles(fs) if tc.err && err == nil { - t.Errorf("expected an error, got nil") + t.Fatalf("expected an error, got nil") } else if !tc.err && err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) + } + + tc.fs.assert(t) + }) + } +} + +func TestPruneVendorDirs(t *testing.T) { + tests := []struct { + name string + test fsTestCase + }{ + { + name: "vendor directory", + test: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "package", + "package/vendor", + }, + }, + after: filesystemState{ + dirs: []string{ + "package", + }, + }, + }, + }, + { + name: "vendor file", + test: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "package", + }, + files: []string{ + "package/vendor", + }, + }, + after: filesystemState{ + dirs: []string{ + "package", + }, + files: []string{ + "package/vendor", + }, + }, + }, + }, + { + name: "vendor symlink", + test: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "package", + "package/_vendor", + }, + links: []fsLink{ + { + path: "package/vendor", + to: "_vendor", + }, + }, + }, + after: filesystemState{ + dirs: []string{ + "package", + "package/_vendor", + }, + }, + }, + }, + { + name: "nonvendor symlink", + test: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "package", + "package/_vendor", + }, + links: []fsLink{ + { + path: "package/link", + to: "_vendor", + }, + }, + }, + after: filesystemState{ + dirs: []string{ + "package", + "package/_vendor", + }, + links: []fsLink{ + { + path: "package/link", + to: "_vendor", + }, + }, + }, + }, + }, + { + name: "vendor symlink to file", + test: fsTestCase{ + before: filesystemState{ + files: []string{ + "file", + }, + links: []fsLink{ + { + path: "vendor", + to: "file", + }, + }, + }, + after: filesystemState{ + files: []string{ + "file", + }, + }, + }, + }, + { + name: "broken vendor symlink", + test: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "package", + }, + links: []fsLink{ + { + path: "package/vendor", + to: "nonexistence", + }, + }, + }, + after: filesystemState{ + dirs: []string{ + "package", + }, + links: []fsLink{}, + }, + }, + }, + { + name: "chained symlinks", + test: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "_vendor", + }, + links: []fsLink{ + { + path: "vendor", + to: "vendor2", + }, + { + path: "vendor2", + to: "_vendor", + }, + }, + }, + after: filesystemState{ + dirs: []string{ + "_vendor", + }, + links: []fsLink{ + { + path: "vendor2", + to: "_vendor", + }, + }, + }, + }, + }, + { + name: "circular symlinks", + test: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "package", + }, + links: []fsLink{ + { + path: "package/link1", + to: "link2", + }, + { + path: "package/link2", + to: "link1", + }, + }, + }, + after: filesystemState{ + dirs: []string{ + "package", + }, + links: []fsLink{ + { + path: "package/link1", + to: "link2", + }, + { + path: "package/link2", + to: "link1", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, pruneVendorDirsTestCase(test.test)) + } +} + +func pruneVendorDirsTestCase(tc fsTestCase) func(*testing.T) { + return func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "pruneVendorDirsTestCase") + if err != nil { + t.Fatalf("ioutil.TempDir err=%q", err) + } + defer func() { + if err := os.RemoveAll(tempDir); err != nil { + t.Errorf("os.RemoveAll(%q) err=%q", tempDir, err) + } + }() + + tc.before.root = tempDir + tc.after.root = tempDir + + tc.setup(t) + + fs, err := deriveFilesystemState(tempDir) + if err != nil { + t.Fatalf("deriveFilesystemState failed: %s", err) + } + + if err := pruneVendorDirs(fs); err != nil { + t.Errorf("pruneVendorDirs err=%q", err) + } + + tc.assert(t) + } +} + +func TestDeleteEmptyDirs(t *testing.T) { + testcases := []struct { + name string + fs fsTestCase + }{ + { + name: "empty-dir", + fs: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "pkg1", + }, + }, + after: filesystemState{}, + }, + }, + { + name: "nested-empty-dirs", + fs: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "pkg1", + "pkg1/pkg2", + }, + }, + after: filesystemState{}, + }, + }, + { + name: "non-empty-dir", + fs: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "pkg1", + }, + files: []string{ + "pkg1/file1", + }, + }, + after: filesystemState{ + dirs: []string{ + "pkg1", + }, + files: []string{ + "pkg1/file1", + }, + }, + }, + }, + { + name: "mixed-dirs", + fs: fsTestCase{ + before: filesystemState{ + dirs: []string{ + "pkg1", + "pkg1/pkg2", + }, + files: []string{ + "pkg1/file1", + }, + }, + after: filesystemState{ + dirs: []string{ + "pkg1", + }, + files: []string{ + "pkg1/file1", + }, + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + h := test.NewHelper(t) + h.Cleanup() + h.TempDir(".") + + tc.fs.before.root = h.Path(".") + tc.fs.after.root = h.Path(".") + + if err := tc.fs.before.setup(); err != nil { + t.Fatal("unexpected error in fs setup: ", err) + } + + if err := deleteEmptyDirs(tc.fs.before); err != nil { + t.Fatal("unexpected error in deleteEmptyDirs: ", err) } - tc.fs.after.assert(t) + tc.fs.assert(t) }) } } diff --git a/gps/solution.go b/gps/solution.go index 2422caabe6..f737cab976 100644 --- a/gps/solution.go +++ b/gps/solution.go @@ -50,16 +50,15 @@ type solution struct { const concurrentWriters = 16 -// WriteDepTree takes a basedir and a Lock, and exports all the projects -// listed in the lock to the appropriate target location within the basedir. +// WriteDepTree takes a basedir, a Lock and a RootPruneOptions and exports all +// the projects listed in the lock to the appropriate target location within basedir. // // If the goal is to populate a vendor directory, basedir should be the absolute // path to that vendor directory, not its parent (a project root, typically). // -// It requires a SourceManager to do the work, and takes a flag indicating -// whether or not to strip vendor directories contained in the exported -// dependencies. -func WriteDepTree(basedir string, l Lock, sm SourceManager, sv bool, logger *log.Logger) error { +// It requires a SourceManager to do the work. Prune options are read from the +// passed manifest. +func WriteDepTree(basedir string, l Lock, sm SourceManager, rpo RootPruneOptions, logger *log.Logger) error { if l == nil { return fmt.Errorf("must provide non-nil Lock to WriteDepTree") } @@ -96,14 +95,13 @@ func WriteDepTree(basedir string, l Lock, sm SourceManager, sv bool, logger *log return errors.Wrapf(err, "failed to export %s", projectRoot) } - if sv { - if err := ctx.Err(); err != nil { - return err - } + err := PruneProject(to, p, rpo.PruneOptionsFor(ident.ProjectRoot), logger) + if err != nil { + return errors.Wrapf(err, "failed to prune %s", projectRoot) + } - if err := filepath.Walk(to, stripVendor); err != nil { - return errors.Wrapf(err, "failed to strip vendor from %s", projectRoot) - } + if err := ctx.Err(); err != nil { + return err } return nil diff --git a/gps/solution_test.go b/gps/solution_test.go index d044f12ad3..171f1d8a6c 100644 --- a/gps/solution_test.go +++ b/gps/solution_test.go @@ -97,12 +97,12 @@ func testWriteDepTree(t *testing.T) { } // nil lock/result should err immediately - err = WriteDepTree(tmp, nil, sm, true, discardLogger()) + err = WriteDepTree(tmp, nil, sm, DefaultRootPruneOptions(), discardLogger()) if err == nil { t.Errorf("Should error if nil lock is passed to WriteDepTree") } - err = WriteDepTree(tmp, r, sm, true, discardLogger()) + err = WriteDepTree(tmp, r, sm, DefaultRootPruneOptions(), discardLogger()) if err != nil { t.Errorf("Unexpected error while creating vendor tree: %s", err) } @@ -154,7 +154,7 @@ func BenchmarkCreateVendorTree(b *testing.B) { // ease manual inspection os.RemoveAll(exp) b.StartTimer() - err = WriteDepTree(exp, r, sm, true, logger) + err = WriteDepTree(exp, r, sm, DefaultRootPruneOptions(), logger) b.StopTimer() if err != nil { b.Errorf("unexpected error after %v iterations: %s", i, err) diff --git a/gps/strip_vendor.go b/gps/strip_vendor.go deleted file mode 100644 index aaaf9bcd22..0000000000 --- a/gps/strip_vendor.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017 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. - -//+build !windows - -package gps - -import ( - "os" - "path/filepath" -) - -func stripVendor(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Skip anything not named vendor - if info.Name() != "vendor" { - return nil - } - - // If the file is a symlink to a directory, delete the symlink. - if (info.Mode() & os.ModeSymlink) != 0 { - if realInfo, err := os.Stat(path); err == nil && realInfo.IsDir() { - return os.Remove(path) - } - } - - if info.IsDir() { - if err := os.RemoveAll(path); err != nil { - return err - } - return filepath.SkipDir - } - - return nil -} diff --git a/gps/strip_vendor_nonwindows_test.go b/gps/strip_vendor_nonwindows_test.go deleted file mode 100644 index 8ff55155b0..0000000000 --- a/gps/strip_vendor_nonwindows_test.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2017 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. - -// +build !windows - -package gps - -import "testing" - -func TestStripVendorSymlinks(t *testing.T) { - t.Run("vendor symlink", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "vendor"}, - to: "_vendor", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - }, - })) - - t.Run("nonvendor symlink", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link"}, - to: "_vendor", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link"}, - to: "_vendor", - }, - }, - }, - })) - - t.Run("vendor symlink to file", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - files: []fsPath{ - {"file"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor"}, - to: "file", - }, - }, - }, - after: filesystemState{ - files: []fsPath{ - {"file"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor"}, - to: "file", - }, - }, - }, - })) - - t.Run("broken vendor symlink", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "vendor"}, - to: "nonexistence", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "vendor"}, - to: "nonexistence", - }, - }, - }, - })) - - t.Run("chained symlinks", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor"}, - to: "vendor2", - }, - { - path: fsPath{"vendor2"}, - to: "_vendor", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor2"}, - to: "_vendor", - }, - }, - }, - })) - - t.Run("circular symlinks", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link1"}, - to: "link2", - }, - { - path: fsPath{"package", "link2"}, - to: "link1", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link1"}, - to: "link2", - }, - { - path: fsPath{"package", "link2"}, - to: "link1", - }, - }, - }, - })) -} diff --git a/gps/strip_vendor_test.go b/gps/strip_vendor_test.go deleted file mode 100644 index 18a722cd9c..0000000000 --- a/gps/strip_vendor_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2017 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 gps - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func stripVendorTestCase(tc fsTestCase) func(*testing.T) { - return func(t *testing.T) { - tempDir, err := ioutil.TempDir("", "TestStripVendor") - if err != nil { - t.Fatalf("ioutil.TempDir err=%q", err) - } - defer func() { - if err := os.RemoveAll(tempDir); err != nil { - t.Errorf("os.RemoveAll(%q) err=%q", tempDir, err) - } - }() - tc.before.root = tempDir - tc.after.root = tempDir - - tc.before.setup(t) - - if err := filepath.Walk(tempDir, stripVendor); err != nil { - t.Errorf("filepath.Walk err=%q", err) - } - - tc.after.assert(t) - } -} - -func TestStripVendorDirectory(t *testing.T) { - t.Run("vendor directory", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "vendor"}, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - }, - })) - - t.Run("vendor file", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - files: []fsPath{ - {"package", "vendor"}, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - files: []fsPath{ - {"package", "vendor"}, - }, - }, - })) -} diff --git a/gps/strip_vendor_windows.go b/gps/strip_vendor_windows.go deleted file mode 100644 index c6b0a13346..0000000000 --- a/gps/strip_vendor_windows.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2017 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 gps - -import ( - "os" - "path/filepath" -) - -func stripVendor(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.Name() != "vendor" { - return nil - } - - if _, err := os.Lstat(path); err != nil { - return nil - } - - symlink := (info.Mode() & os.ModeSymlink) != 0 - dir := info.IsDir() - - switch { - case symlink && dir: - // This could be a windows junction directory. Support for these in the - // standard library is spotty, and we could easily delete an important - // folder if we called os.Remove or os.RemoveAll. Just skip these. - // - // TODO: If we could distinguish between junctions and Windows symlinks, - // we might be able to safely delete symlinks, even though junctions are - // dangerous. - return filepath.SkipDir - - case symlink: - if realInfo, err := os.Stat(path); err == nil && realInfo.IsDir() { - return os.Remove(path) - } - - case dir: - if err := os.RemoveAll(path); err != nil { - return err - } - return filepath.SkipDir - } - - return nil -} diff --git a/gps/strip_vendor_windows_test.go b/gps/strip_vendor_windows_test.go deleted file mode 100644 index 67ff900299..0000000000 --- a/gps/strip_vendor_windows_test.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2017 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. - -// +build windows - -package gps - -import "testing" - -func TestStripVendorSymlinks(t *testing.T) { - // On windows, we skip symlinks, even if they're named 'vendor', because - // they're too hard to distinguish from junctions. - t.Run("vendor symlink", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "vendor"}, - to: "_vendor", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "vendor"}, - to: "_vendor", - }, - }, - }, - })) - - t.Run("nonvendor symlink", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link"}, - to: "_vendor", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - {"package", "_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link"}, - to: "_vendor", - }, - }, - }, - })) - - t.Run("vendor symlink to file", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - files: []fsPath{ - {"file"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor"}, - to: "file", - }, - }, - }, - after: filesystemState{ - files: []fsPath{ - {"file"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor"}, - to: "file", - }, - }, - }, - })) - - t.Run("broken vendor symlink", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "vendor"}, - to: "nonexistence", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "vendor"}, - to: "nonexistence", - }, - }, - }, - })) - - t.Run("chained symlinks", stripVendorTestCase(fsTestCase{ - // Curiously, if a symlink on windows points to *another* symlink which - // eventually points at a directory, we'll correctly remove that first - // symlink, because the first symlink doesn't appear to Go to be a - // directory. - before: filesystemState{ - dirs: []fsPath{ - {"_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor"}, - to: "vendor2", - }, - { - path: fsPath{"vendor2"}, - to: "_vendor", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"_vendor"}, - }, - links: []fsLink{ - { - path: fsPath{"vendor2"}, - to: "_vendor", - }, - }, - }, - })) - - t.Run("circular symlinks", stripVendorTestCase(fsTestCase{ - before: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link1"}, - to: "link2", - }, - { - path: fsPath{"package", "link2"}, - to: "link1", - }, - }, - }, - after: filesystemState{ - dirs: []fsPath{ - {"package"}, - }, - links: []fsLink{ - { - path: fsPath{"package", "link1"}, - to: "link2", - }, - { - path: fsPath{"package", "link2"}, - to: "link1", - }, - }, - }, - })) -} diff --git a/manifest.go b/manifest.go index 6115a5f482..3b75f607ec 100644 --- a/manifest.go +++ b/manifest.go @@ -50,8 +50,7 @@ type Manifest struct { Ignored []string Required []string - PruneOptions gps.PruneOptions - PruneProjectOptions gps.PruneProjectOptions + PruneOptions gps.RootPruneOptions } type rawManifest struct { @@ -96,7 +95,7 @@ func NewManifest() *Manifest { return &Manifest{ Constraints: make(gps.ProjectConstraints), Ovr: make(gps.ProjectConstraints), - PruneOptions: gps.PruneNestedVendorDirs, + PruneOptions: gps.DefaultRootPruneOptions(), } } @@ -293,7 +292,7 @@ func ValidateProjectRoots(c *Ctx, m *Manifest, sm gps.SourceManager) error { wg.Add(1) go validate(pr) } - for pr := range m.PruneProjectOptions { + for pr := range m.PruneOptions.ProjectOptions { wg.Add(1) go validate(pr) } @@ -369,41 +368,43 @@ func fromRawManifest(raw rawManifest) (*Manifest, error) { m.Ovr[name] = prj } - m.PruneOptions, m.PruneProjectOptions = fromRawPruneOptions(raw.PruneOptions) + m.PruneOptions = fromRawPruneOptions(raw.PruneOptions) return m, nil } -func fromRawPruneOptions(raw rawPruneOptions) (gps.PruneOptions, gps.PruneProjectOptions) { - rootOptions := gps.PruneNestedVendorDirs - pruneProjects := make(gps.PruneProjectOptions) +func fromRawPruneOptions(raw rawPruneOptions) gps.RootPruneOptions { + opts := gps.RootPruneOptions{ + PruneOptions: gps.PruneNestedVendorDirs, + ProjectOptions: make(gps.PruneProjectOptions), + } if raw.UnusedPackages { - rootOptions |= gps.PruneUnusedPackages + opts.PruneOptions |= gps.PruneUnusedPackages } if raw.GoTests { - rootOptions |= gps.PruneGoTestFiles + opts.PruneOptions |= gps.PruneGoTestFiles } if raw.NonGoFiles { - rootOptions |= gps.PruneNonGoFiles + opts.PruneOptions |= gps.PruneNonGoFiles } for _, p := range raw.Projects { pr := gps.ProjectRoot(p.Name) - pruneProjects[pr] = gps.PruneNestedVendorDirs + opts.ProjectOptions[pr] = gps.PruneNestedVendorDirs if raw.UnusedPackages { - pruneProjects[pr] |= gps.PruneUnusedPackages + opts.ProjectOptions[pr] |= gps.PruneUnusedPackages } if raw.GoTests { - pruneProjects[pr] |= gps.PruneGoTestFiles + opts.ProjectOptions[pr] |= gps.PruneGoTestFiles } if raw.NonGoFiles { - pruneProjects[pr] |= gps.PruneNonGoFiles + opts.ProjectOptions[pr] |= gps.PruneNonGoFiles } } - return rootOptions, pruneProjects + return opts } // toProject interprets the string representations of project information held in @@ -562,15 +563,3 @@ func (m *Manifest) RequiredPackages() map[string]bool { return mp } - -// PruneOptionsFor returns the prune options for the passed project root. -// -// It will return the root prune options if the project does not have specific -// options or if it does not exists in the manifest. -func (m *Manifest) PruneOptionsFor(pr gps.ProjectRoot) gps.PruneOptions { - if po, ok := m.PruneProjectOptions[pr]; ok { - return po - } - - return m.PruneOptions -} diff --git a/manifest_test.go b/manifest_test.go index 93eb95ab83..216e8abfe3 100644 --- a/manifest_test.go +++ b/manifest_test.go @@ -45,11 +45,13 @@ func TestReadManifest(t *testing.T) { Constraint: gps.NewBranch("master"), }, }, - Ignored: []string{"github.com/foo/bar"}, - PruneOptions: gps.PruneNestedVendorDirs | gps.PruneNonGoFiles, - PruneProjectOptions: gps.PruneProjectOptions{ - gps.ProjectRoot("github.com/golang/dep"): gps.PruneNestedVendorDirs, - gps.ProjectRoot("github.com/babble/brook"): gps.PruneNestedVendorDirs | gps.PruneGoTestFiles, + Ignored: []string{"github.com/foo/bar"}, + PruneOptions: gps.RootPruneOptions{ + PruneOptions: gps.PruneNestedVendorDirs | gps.PruneNonGoFiles, + ProjectOptions: gps.PruneProjectOptions{ + gps.ProjectRoot("github.com/golang/dep"): gps.PruneNestedVendorDirs, + gps.ProjectRoot("github.com/babble/brook"): gps.PruneNestedVendorDirs | gps.PruneGoTestFiles, + }, }, } @@ -595,10 +597,6 @@ func TestValidateProjectRoots(t *testing.T) { } } -func TestPruneOptionsFor(t *testing.T) { - -} - func containsErr(s []error, e error) bool { for _, a := range s { if a.Error() == e.Error() { diff --git a/txn_writer.go b/txn_writer.go index f7974d98e0..4256d0175a 100644 --- a/txn_writer.go +++ b/txn_writer.go @@ -56,11 +56,12 @@ var lockFileComment = []byte(`# This file is autogenerated, do not edit; changes // 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 *gps.LockDiff - writeVendor bool - writeLock bool + Manifest *Manifest + lock *Lock + lockDiff *gps.LockDiff + writeVendor bool + writeLock bool + pruneOptions gps.RootPruneOptions } // NewSafeWriter sets up a SafeWriter to write a set of manifest, lock, and @@ -78,10 +79,11 @@ type SafeWriter struct { // - If oldLock is provided without newLock, error. // // - If vendor is VendorAlways without a newLock, error. -func NewSafeWriter(manifest *Manifest, oldLock, newLock *Lock, vendor VendorBehavior) (*SafeWriter, error) { +func NewSafeWriter(manifest *Manifest, oldLock, newLock *Lock, vendor VendorBehavior, prune gps.RootPruneOptions) (*SafeWriter, error) { sw := &SafeWriter{ - Manifest: manifest, - lock: newLock, + Manifest: manifest, + lock: newLock, + pruneOptions: prune, } if oldLock != nil { @@ -312,7 +314,7 @@ func (sw *SafeWriter) Write(root string, sm gps.SourceManager, examples bool, lo } if sw.writeVendor { - err = gps.WriteDepTree(filepath.Join(td, "vendor"), sw.lock, sm, true, logger) + err = gps.WriteDepTree(filepath.Join(td, "vendor"), sw.lock, sm, sw.pruneOptions, logger) if err != nil { return errors.Wrap(err, "error while writing out vendor tree") } diff --git a/txn_writer_test.go b/txn_writer_test.go index 141e1f8cc1..45181f5c11 100644 --- a/txn_writer_test.go +++ b/txn_writer_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/golang/dep/gps" "github.com/golang/dep/internal/test" "github.com/pkg/errors" ) @@ -25,7 +26,7 @@ func TestSafeWriter_BadInput_MissingRoot(t *testing.T) { pc := NewTestProjectContext(h, safeWriterProject) defer pc.Release() - sw, _ := NewSafeWriter(nil, nil, nil, VendorOnChanged) + sw, _ := NewSafeWriter(nil, nil, nil, VendorOnChanged, gps.DefaultRootPruneOptions()) err := sw.Write("", pc.SourceManager, true, discardLogger()) if err == nil { @@ -43,7 +44,7 @@ func TestSafeWriter_BadInput_MissingSourceManager(t *testing.T) { pc.CopyFile(LockName, safeWriterGoldenLock) pc.Load() - sw, _ := NewSafeWriter(nil, nil, pc.Project.Lock, VendorAlways) + sw, _ := NewSafeWriter(nil, nil, pc.Project.Lock, VendorAlways, gps.DefaultRootPruneOptions()) err := sw.Write(pc.Project.AbsRoot, nil, true, discardLogger()) if err == nil { @@ -59,7 +60,7 @@ func TestSafeWriter_BadInput_ForceVendorMissingLock(t *testing.T) { pc := NewTestProjectContext(h, safeWriterProject) defer pc.Release() - _, err := NewSafeWriter(nil, nil, nil, VendorAlways) + _, err := NewSafeWriter(nil, nil, nil, VendorAlways, gps.DefaultRootPruneOptions()) if err == nil { t.Fatal("should have errored without a lock when forceVendor is true, but did not") } else if !strings.Contains(err.Error(), "newLock") { @@ -75,7 +76,7 @@ func TestSafeWriter_BadInput_OldLockOnly(t *testing.T) { pc.CopyFile(LockName, safeWriterGoldenLock) pc.Load() - _, err := NewSafeWriter(nil, pc.Project.Lock, nil, VendorAlways) + _, err := NewSafeWriter(nil, pc.Project.Lock, nil, VendorAlways, gps.DefaultRootPruneOptions()) if err == nil { t.Fatal("should have errored with only an old lock, but did not") } else if !strings.Contains(err.Error(), "oldLock") { @@ -89,7 +90,7 @@ func TestSafeWriter_BadInput_NonexistentRoot(t *testing.T) { pc := NewTestProjectContext(h, safeWriterProject) defer pc.Release() - sw, _ := NewSafeWriter(nil, nil, nil, VendorOnChanged) + sw, _ := NewSafeWriter(nil, nil, nil, VendorOnChanged, gps.DefaultRootPruneOptions()) missingroot := filepath.Join(pc.Project.AbsRoot, "nonexistent") err := sw.Write(missingroot, pc.SourceManager, true, discardLogger()) @@ -107,7 +108,7 @@ func TestSafeWriter_BadInput_RootIsFile(t *testing.T) { pc := NewTestProjectContext(h, safeWriterProject) defer pc.Release() - sw, _ := NewSafeWriter(nil, nil, nil, VendorOnChanged) + sw, _ := NewSafeWriter(nil, nil, nil, VendorOnChanged, gps.DefaultRootPruneOptions()) fileroot := pc.CopyFile("fileroot", "txn_writer/badinput_fileroot") err := sw.Write(fileroot, pc.SourceManager, true, discardLogger()) @@ -131,7 +132,7 @@ func TestSafeWriter_Manifest(t *testing.T) { pc.CopyFile(ManifestName, safeWriterGoldenManifest) pc.Load() - sw, _ := NewSafeWriter(pc.Project.Manifest, nil, nil, VendorOnChanged) + sw, _ := NewSafeWriter(pc.Project.Manifest, nil, nil, VendorOnChanged, gps.DefaultRootPruneOptions()) // Verify prepared actions if !sw.HasManifest() { @@ -173,7 +174,7 @@ func TestSafeWriter_ManifestAndUnmodifiedLock(t *testing.T) { pc.CopyFile(LockName, safeWriterGoldenLock) pc.Load() - sw, _ := NewSafeWriter(pc.Project.Manifest, pc.Project.Lock, pc.Project.Lock, VendorOnChanged) + sw, _ := NewSafeWriter(pc.Project.Manifest, pc.Project.Lock, pc.Project.Lock, VendorOnChanged, gps.DefaultRootPruneOptions()) // Verify prepared actions if !sw.HasManifest() { @@ -218,7 +219,7 @@ func TestSafeWriter_ManifestAndUnmodifiedLockWithForceVendor(t *testing.T) { pc.CopyFile(LockName, safeWriterGoldenLock) pc.Load() - sw, _ := NewSafeWriter(pc.Project.Manifest, pc.Project.Lock, pc.Project.Lock, VendorAlways) + sw, _ := NewSafeWriter(pc.Project.Manifest, pc.Project.Lock, pc.Project.Lock, VendorAlways, gps.DefaultRootPruneOptions()) // Verify prepared actions if !sw.HasManifest() { @@ -268,7 +269,7 @@ func TestSafeWriter_ModifiedLock(t *testing.T) { originalLock := new(Lock) *originalLock = *pc.Project.Lock originalLock.SolveMeta.InputsDigest = []byte{} // zero out the input hash to ensure non-equivalency - sw, _ := NewSafeWriter(nil, originalLock, pc.Project.Lock, VendorOnChanged) + sw, _ := NewSafeWriter(nil, originalLock, pc.Project.Lock, VendorOnChanged, gps.DefaultRootPruneOptions()) // Verify prepared actions if sw.HasManifest() { @@ -318,7 +319,7 @@ func TestSafeWriter_ModifiedLockSkipVendor(t *testing.T) { originalLock := new(Lock) *originalLock = *pc.Project.Lock originalLock.SolveMeta.InputsDigest = []byte{} // zero out the input hash to ensure non-equivalency - sw, _ := NewSafeWriter(nil, originalLock, pc.Project.Lock, VendorNever) + sw, _ := NewSafeWriter(nil, originalLock, pc.Project.Lock, VendorNever, gps.DefaultRootPruneOptions()) // Verify prepared actions if sw.HasManifest() { @@ -362,12 +363,12 @@ func TestSafeWriter_ForceVendorWhenVendorAlreadyExists(t *testing.T) { pc.CopyFile(LockName, safeWriterGoldenLock) pc.Load() - sw, _ := NewSafeWriter(nil, pc.Project.Lock, pc.Project.Lock, VendorAlways) + sw, _ := NewSafeWriter(nil, pc.Project.Lock, pc.Project.Lock, VendorAlways, gps.DefaultRootPruneOptions()) err := sw.Write(pc.Project.AbsRoot, pc.SourceManager, true, discardLogger()) h.Must(errors.Wrap(err, "SafeWriter.Write failed")) // Verify prepared actions - sw, _ = NewSafeWriter(nil, nil, pc.Project.Lock, VendorAlways) + sw, _ = NewSafeWriter(nil, nil, pc.Project.Lock, VendorAlways, gps.DefaultRootPruneOptions()) if sw.HasManifest() { t.Fatal("Did not expect the payload to contain the manifest") } @@ -414,7 +415,7 @@ func TestSafeWriter_NewLock(t *testing.T) { defer lf.Close() newLock, err := readLock(lf) h.Must(err) - sw, _ := NewSafeWriter(nil, nil, newLock, VendorOnChanged) + sw, _ := NewSafeWriter(nil, nil, newLock, VendorOnChanged, gps.DefaultRootPruneOptions()) // Verify prepared actions if sw.HasManifest() { @@ -461,7 +462,7 @@ func TestSafeWriter_NewLockSkipVendor(t *testing.T) { defer lf.Close() newLock, err := readLock(lf) h.Must(err) - sw, _ := NewSafeWriter(nil, nil, newLock, VendorNever) + sw, _ := NewSafeWriter(nil, nil, newLock, VendorNever, gps.DefaultRootPruneOptions()) // Verify prepared actions if sw.HasManifest() { @@ -510,7 +511,7 @@ func TestSafeWriter_DiffLocks(t *testing.T) { updatedLock, err := readLock(ulf) h.Must(err) - sw, _ := NewSafeWriter(nil, pc.Project.Lock, updatedLock, VendorOnChanged) + sw, _ := NewSafeWriter(nil, pc.Project.Lock, updatedLock, VendorOnChanged, gps.DefaultRootPruneOptions()) // Verify lock diff diff := sw.lockDiff @@ -555,7 +556,7 @@ func TestSafeWriter_VendorDotGitPreservedWithForceVendor(t *testing.T) { pc.CopyFile(LockName, safeWriterGoldenLock) pc.Load() - sw, _ := NewSafeWriter(pc.Project.Manifest, pc.Project.Lock, pc.Project.Lock, VendorAlways) + sw, _ := NewSafeWriter(pc.Project.Manifest, pc.Project.Lock, pc.Project.Lock, VendorAlways, gps.DefaultRootPruneOptions()) // Verify prepared actions if !sw.HasManifest() {