Skip to content
This repository has been archived by the owner on Sep 9, 2020. It is now read-only.

Add dep prune subcommand #322

Merged
merged 2 commits into from Apr 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/dep/main.go
Expand Up @@ -38,6 +38,7 @@ func main() {
&ensureCommand{},
&removeCommand{},
&hashinCommand{},
&pruneCommand{},
}

examples := [][2]string{
Expand Down
83 changes: 83 additions & 0 deletions cmd/dep/prune.go
@@ -0,0 +1,83 @@
// 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"
"fmt"
"log"
"os"

"github.com/golang/dep"
"github.com/sdboyer/gps"
"github.com/sdboyer/gps/pkgtree"

"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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I now just have one docs nit...let's please have this say the following:

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.

I just want to warn people everywhere we can 😄


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.AbsRoot, 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 := gps.SolveParameters{
RootDir: p.AbsRoot,
RootPackageTree: ptree,
Manifest: p.Manifest,
// Locks aren't a part of the input hash check, so we can omit it.
}
if *verbose {
params.Trace = true
params.TraceLogger = log.New(os.Stderr, "", 0)
}

s, err := gps.Prepare(params, sm)
if err != nil {
return errors.Wrap(err, "could not set up solver for input hashing")
}

if !bytes.Equal(s.HashInputs(), p.Lock.Memo) {
return fmt.Errorf("lock hash doesn't match")
}

return dep.PruneProject(p, sm)
}
104 changes: 104 additions & 0 deletions txn_writer.go
Expand Up @@ -594,3 +594,107 @@ 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); 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)
if err != nil {
return err
}

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 = 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
}

func calculatePrune(vendorDir string, keep []string) ([]string, error) {
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+"/")
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]) }