diff --git a/cmd/dep/prune.go b/cmd/dep/prune.go index cda0f8b69e..a138370e01 100644 --- a/cmd/dep/prune.go +++ b/cmd/dep/prune.go @@ -10,24 +10,19 @@ import ( "fmt" "log" "os" - "path/filepath" - "sort" - "strings" "github.com/golang/dep" "github.com/sdboyer/gps" + + "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. Its works by -following all imports from your project into the vendor tree and then -transitively following all import between packages in the vendor tree, to -calculate a set of packages to keep. It then delete everything else. +Prune is used to remove unused packages from your vendor tree. ` type pruneCommand struct { - dryRun bool } func (cmd *pruneCommand) Name() string { return "prune" } @@ -37,7 +32,6 @@ func (cmd *pruneCommand) LongHelp() string { return pruneLongHelp } func (cmd *pruneCommand) Hidden() bool { return false } func (cmd *pruneCommand) Register(fs *flag.FlagSet) { - fs.BoolVar(&cmd.dryRun, "n", false, "dry run, don't actually delete anything") } func (cmd *pruneCommand) Run(ctx *dep.Ctx, args []string) error { @@ -53,25 +47,11 @@ func (cmd *pruneCommand) Run(ctx *dep.Ctx, args []string) error { sm.UseDefaultSignalHandling() defer sm.Release() - keep, err := cmd.checkLockAndGetDependencies(p, sm) - if err != nil { - return err - } - - toDelete := cmd.calculatePrune(p, keep) - if err := cmd.deleteDirs(p, toDelete); err != nil { - return err - } - - return nil -} - -func (cmd *pruneCommand) checkLockAndGetDependencies(p *dep.Project, sm *gps.SourceMgr) ([]string, error) { // While the network churns on ListVersions() requests, statically analyze // code from the current project. ptree, err := gps.ListPackages(p.AbsRoot, string(p.ImportRoot)) if err != nil { - return nil, fmt.Errorf("analysis of local packages failed: %v", err) + return errors.Wrap(err, "analysis of local packages failed: %v") } // Set up a solver in order to check the InputHash. @@ -88,73 +68,12 @@ func (cmd *pruneCommand) checkLockAndGetDependencies(p *dep.Project, sm *gps.Sou s, err := gps.Prepare(params, sm) if err != nil { - return nil, fmt.Errorf("could not set up solver for input hashing: %s", err) + return errors.Wrap(err, "could not set up solver for input hashing") } if !bytes.Equal(s.HashInputs(), p.Lock.Memo) { - return nil, fmt.Errorf("lock hash doesn't match") + return fmt.Errorf("lock hash doesn't match") } - var result []string - for _, project := range p.Lock.P { - for _, pkg := range project.Packages() { - fullPkg := filepath.Join(string(project.Ident().ProjectRoot), pkg) - verboseLn("-", fullPkg) - result = append(result, fullPkg) - } - } - sort.Strings(result) - return result, nil + return dep.PruneProject(p, sm) } - -func (cmd *pruneCommand) calculatePrune(p *dep.Project, keep []string) []string { - vendor := filepath.FromSlash(filepath.Join(p.AbsRoot, "vendor")) - toDelete := []string{} - filepath.Walk(vendor, 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 == vendor { - return nil - } - - name := strings.TrimPrefix(path, vendor+"/") - 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 -} - -func (cmd *pruneCommand) deleteDirs(p *dep.Project, toDelete []string) error { - // sort by length so we delete sub dirs first - sort.Sort(byLen(toDelete)) - for _, path := range toDelete { - verboseLn("rm -rf", strings.TrimPrefix(path, p.AbsRoot+"/")) - if !cmd.dryRun { - if err := os.RemoveAll(path); err != nil { - return err - } - } - } - return nil -} - -func verboseLn(a ...interface{}) { - if *verbose { - fmt.Println(a...) - } -} - -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/txn_writer.go b/txn_writer.go index bf92a46656..a04a12f595 100644 --- a/txn_writer.go +++ b/txn_writer.go @@ -251,7 +251,7 @@ func (sw *SafeWriter) Write(root string, sm gps.SourceManager) error { } if sw.Payload.HasVendor() { - err = gps.WriteDepTree(filepath.Join(td, "vendor"), sw.Payload.Lock, sm, true) + err = gps.WriteDepTree(filepath.Join(td, "vendor"), sw.Payload.Lock, sm, true, false) if err != nil { return errors.Wrap(err, "error while writing out vendor tree") } @@ -570,3 +570,47 @@ func diffProjects(lp1 gps.LockedProject, lp2 gps.LockedProject) *LockedProjectDi } return &diff } + +func PruneProject(p *Project, sm gps.SourceManager) 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, true); 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 = renameWithFallback(vpath, vendorbak) + if failerr != nil { + goto fail + } + } + + // Move in the new one. + failerr = renameWithFallback(td, vpath) + if failerr != nil { + goto fail + } + + os.RemoveAll(vendorbak) + + return nil + +fail: + renameWithFallback(vendorbak, vpath) + return failerr +} diff --git a/vendor/github.com/sdboyer/gps/result.go b/vendor/github.com/sdboyer/gps/result.go index 14200ab0cb..f34da87510 100644 --- a/vendor/github.com/sdboyer/gps/result.go +++ b/vendor/github.com/sdboyer/gps/result.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + "sort" + "strings" ) // A Solution is returned by a solver run. It is mostly just a Lock, with some @@ -32,8 +34,9 @@ type solution struct { // // 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) error { +// dependencies. Also takes a flag (prune) indicating whether to remove unused +// modules. +func WriteDepTree(basedir string, l Lock, sm SourceManager, sv bool, prune bool) error { if l == nil { return fmt.Errorf("must provide non-nil Lock to WriteDepTree") } @@ -55,6 +58,10 @@ func WriteDepTree(basedir string, l Lock, sm SourceManager, sv bool) error { if sv { filepath.Walk(to, stripVendor) } + if prune { + toDelete := calculatePrune(to, p.Packages()) + deleteDirs(toDelete) + } // TODO(sdboyer) dump version metadata file } @@ -72,3 +79,45 @@ func (r solution) Attempts() int { func (r solution) InputHash() []byte { return r.hd } + +func calculatePrune(to string, keep []string) []string { + toDelete := []string{} + filepath.Walk(to, 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 == to { + return nil + } + + name := strings.TrimPrefix(path, to+"/") + 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 +} + +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]) }