Merge pull request #339 from Masterminds/feat/strip-vendor

Add ability to remove nested vendor and Godeps/_workspace directories
diff --git a/Makefile b/Makefile
index 7adc280..777d92c 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@
 	install -m 755 ./glide ${DESTDIR}/usr/local/bin/glide
 
 test:
-	go test . ./gb ./path ./action ./tree ./util ./godep ./gpm ./cfg ./dependency ./importer ./msg ./repo
+	go test . ./gb ./path ./action ./tree ./util ./godep ./godep/strip ./gpm ./cfg ./dependency ./importer ./msg ./repo
 
 clean:
 	rm -f ./glide.test
diff --git a/action/get.go b/action/get.go
index e27030b..e5135e8 100644
--- a/action/get.go
+++ b/action/get.go
@@ -6,6 +6,7 @@
 	"strings"
 
 	"github.com/Masterminds/glide/cfg"
+	"github.com/Masterminds/glide/godep"
 	"github.com/Masterminds/glide/msg"
 	gpath "github.com/Masterminds/glide/path"
 	"github.com/Masterminds/glide/repo"
@@ -15,7 +16,7 @@
 // Get fetches one or more dependencies and installs.
 //
 // This includes resolving dependency resolution and re-generating the lock file.
-func Get(names []string, installer *repo.Installer, insecure, skipRecursive, strip bool) {
+func Get(names []string, installer *repo.Installer, insecure, skipRecursive, strip, stripVendor bool) {
 	base := gpath.Basepath()
 	EnsureGopath()
 	EnsureVendorDir()
@@ -75,6 +76,9 @@
 	}
 	if !skipRecursive {
 		// Write lock
+		if stripVendor {
+			confcopy = godep.RemoveGodepSubpackages(confcopy)
+		}
 		writeLock(conf, confcopy, base)
 	} else {
 		msg.Warn("Skipping lockfile generation because full dependency tree is not being calculated")
@@ -84,6 +88,14 @@
 		msg.Info("Removing version control data from vendor directory...")
 		gpath.StripVcs()
 	}
+
+	if stripVendor {
+		msg.Info("Removing nested vendor and Godeps/_workspace directories...")
+		err := gpath.StripVendor()
+		if err != nil {
+			msg.Err("Unable to strip vendor directories: %s", err)
+		}
+	}
 }
 
 func writeLock(conf, confcopy *cfg.Config, base string) {
diff --git a/action/install.go b/action/install.go
index bed3744..3ae39c0 100644
--- a/action/install.go
+++ b/action/install.go
@@ -12,7 +12,7 @@
 )
 
 // Install installs a vendor directory based on an existing Glide configuration.
-func Install(installer *repo.Installer, strip bool) {
+func Install(installer *repo.Installer, strip, stripVendor bool) {
 	base := "."
 	// Ensure GOPATH
 	EnsureGopath()
@@ -22,7 +22,7 @@
 	// Lockfile exists
 	if !gpath.HasLock(base) {
 		msg.Info("Lock file (glide.lock) does not exist. Performing update.")
-		Update(installer, false, strip)
+		Update(installer, false, strip, stripVendor)
 		return
 	}
 	// Load lockfile
@@ -63,6 +63,14 @@
 		msg.Info("Removing version control data from vendor directory...")
 		gpath.StripVcs()
 	}
+
+	if stripVendor {
+		msg.Info("Removing nested vendor and Godeps/_workspace directories...")
+		err := gpath.StripVendor()
+		if err != nil {
+			msg.Err("Unable to strip vendor directories: %s", err)
+		}
+	}
 }
 
 // LoadLockfile loads the contents of a glide.lock file.
diff --git a/action/update.go b/action/update.go
index cf87b28..4b0da7b 100644
--- a/action/update.go
+++ b/action/update.go
@@ -5,13 +5,14 @@
 
 	"github.com/Masterminds/glide/cfg"
 	"github.com/Masterminds/glide/dependency"
+	"github.com/Masterminds/glide/godep"
 	"github.com/Masterminds/glide/msg"
 	gpath "github.com/Masterminds/glide/path"
 	"github.com/Masterminds/glide/repo"
 )
 
 // Update updates repos and the lock file from the main glide yaml.
-func Update(installer *repo.Installer, skipRecursive, strip bool) {
+func Update(installer *repo.Installer, skipRecursive, strip, stripVendor bool) {
 	base := "."
 	EnsureGopath()
 	EnsureVendorDir()
@@ -73,6 +74,10 @@
 	// from the project. A removed dependency should warn and an added dependency
 	// should be added to the glide.yaml file. See issue #193.
 
+	if stripVendor {
+		confcopy = godep.RemoveGodepSubpackages(confcopy)
+	}
+
 	if !skipRecursive {
 		// Write lock
 		hash, err := conf.Hash()
@@ -94,4 +99,12 @@
 		msg.Info("Removing version control data from vendor directory...")
 		gpath.StripVcs()
 	}
+
+	if stripVendor {
+		msg.Info("Removing nested vendor and Godeps/_workspace directories...")
+		err := gpath.StripVendor()
+		if err != nil {
+			msg.Err("Unable to strip vendor directories: %s", err)
+		}
+	}
 }
diff --git a/glide.go b/glide.go
index 23c4745..f1a4162 100644
--- a/glide.go
+++ b/glide.go
@@ -169,7 +169,18 @@
 	When adding a new dependency Glide will perform an update to work out the
 	the versions to use from the dependency tree. This will generate an updated
 	glide.lock file with specific locked versions to use.
-	`,
+
+	If you are storing the outside dependencies in your version control system
+	(VCS), also known as vendoring, there are a few flags that may be useful.
+	The '--update-vendored' flag will cause Glide to update packages when VCS
+	information is unavailable. This can be used with the '--strip-vcs' flag which
+	will strip VCS data found in the vendor directory. This is useful for
+	removing VCS data from transitive dependencies and initial setups. The
+	'--strip-vendor' flag will remove any nested 'vendor' folders and
+	'Godeps/_workspace' folders after an update (along with undoing any Godep
+	import rewriting). Note, The Godeps specific functionality is deprecated and
+	will be removed when most Godeps users have migrated to using the vendor
+	folder.`,
 			Flags: []cli.Flag{
 				cli.BoolFlag{
 					Name:  "insecure",
@@ -211,8 +222,16 @@
 					Name:  "strip-vcs, s",
 					Usage: "Removes version control metada (e.g, .git directory) from the vendor folder.",
 				},
+				cli.BoolFlag{
+					Name:  "strip-vendor, v",
+					Usage: "Removes nested vendor and Godeps/_workspace directories. Requires --strip-vcs.",
+				},
 			},
 			Action: func(c *cli.Context) {
+				if c.Bool("strip-vendor") && !c.Bool("strip-vcs") {
+					msg.Die("--strip-vendor cannot be used without --strip-vcs")
+				}
+
 				if len(c.Args()) < 1 {
 					fmt.Println("Oops! Package name is required.")
 					os.Exit(1)
@@ -232,7 +251,7 @@
 				inst.ResolveAllFiles = c.Bool("all-dependencies")
 				packages := []string(c.Args())
 				insecure := c.Bool("insecure")
-				action.Get(packages, inst, insecure, c.Bool("no-recursive"), c.Bool("strip-vcs"))
+				action.Get(packages, inst, insecure, c.Bool("no-recursive"), c.Bool("strip-vcs"), c.Bool("strip-vendor"))
 			},
 		},
 		{
@@ -412,8 +431,16 @@
 					Name:  "strip-vcs, s",
 					Usage: "Removes version control metada (e.g, .git directory) from the vendor folder.",
 				},
+				cli.BoolFlag{
+					Name:  "strip-vendor, v",
+					Usage: "Removes nested vendor and Godeps/_workspace directories. Requires --strip-vcs.",
+				},
 			},
 			Action: func(c *cli.Context) {
+				if c.Bool("strip-vendor") && !c.Bool("strip-vcs") {
+					msg.Die("--strip-vendor cannot be used without --strip-vcs")
+				}
+
 				installer := repo.NewInstaller()
 				installer.Force = c.Bool("force")
 				installer.UseCache = c.Bool("cache")
@@ -423,7 +450,7 @@
 				installer.Home = gpath.Home()
 				installer.DeleteUnused = c.Bool("deleteOptIn")
 
-				action.Install(installer, c.Bool("strip-vcs"))
+				action.Install(installer, c.Bool("strip-vcs"), c.Bool("strip-vendor"))
 			},
 		},
 		{
@@ -446,10 +473,17 @@
 	It will create a glide.yaml file from the Godeps data, and then update. This
 	has no effect if '--no-recursive' is set.
 
-	If the '--update-vendored' flag (aliased to '-u') is present vendored
-	dependencies, stored in your projects VCS repository, will be updated. This
-	works by removing the old package, checking out an the repo and setting the
-	version, and removing the VCS directory.
+	If you are storing the outside dependencies in your version control system
+	(VCS), also known as vendoring, there are a few flags that may be useful.
+	The '--update-vendored' flag will cause Glide to update packages when VCS
+	information is unavailable. This can be used with the '--strip-vcs' flag which
+	will strip VCS data found in the vendor directory. This is useful for
+	removing VCS data from transitive dependencies and initial setups. The
+	'--strip-vendor' flag will remove any nested 'vendor' folders and
+	'Godeps/_workspace' folders after an update (along with undoing any Godep
+	import rewriting). Note, The Godeps specific functionality is deprecated and
+	will be removed when most Godeps users have migrated to using the vendor
+	folder.
 
 	By default, packages that are discovered are considered transient, and are
 	not stored in the glide.yaml file. The --file=NAME.yaml flag allows you
@@ -500,8 +534,15 @@
 					Name:  "strip-vcs, s",
 					Usage: "Removes version control metada (e.g, .git directory) from the vendor folder.",
 				},
+				cli.BoolFlag{
+					Name:  "strip-vendor, v",
+					Usage: "Removes nested vendor and Godeps/_workspace directories. Requires --strip-vcs.",
+				},
 			},
 			Action: func(c *cli.Context) {
+				if c.Bool("strip-vendor") && !c.Bool("strip-vcs") {
+					msg.Die("--strip-vendor cannot be used without --strip-vcs")
+				}
 
 				if c.Bool("resolve-current") {
 					util.ResolveCurrent = true
@@ -518,7 +559,7 @@
 				installer.Home = gpath.Home()
 				installer.DeleteUnused = c.Bool("deleteOptIn")
 
-				action.Update(installer, c.Bool("no-recursive"), c.Bool("strip-vcs"))
+				action.Update(installer, c.Bool("no-recursive"), c.Bool("strip-vcs"), c.Bool("strip-vendor"))
 			},
 		},
 		{
diff --git a/godep/godep.go b/godep/godep.go
index aad445b..a7d0525 100644
--- a/godep/godep.go
+++ b/godep/godep.go
@@ -7,6 +7,7 @@
 	"encoding/json"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/Masterminds/glide/cfg"
 	"github.com/Masterminds/glide/msg"
@@ -96,3 +97,29 @@
 
 	return buf, nil
 }
+
+// RemoveGodepSubpackages strips subpackages from a cfg.Config dependencies that
+// contain "Godeps/_workspace/src" as part of the path.
+func RemoveGodepSubpackages(c *cfg.Config) *cfg.Config {
+	for _, d := range c.Imports {
+		n := []string{}
+		for _, v := range d.Subpackages {
+			if !strings.HasPrefix(v, "Godeps/_workspace/src") {
+				n = append(n, v)
+			}
+		}
+		d.Subpackages = n
+	}
+
+	for _, d := range c.DevImports {
+		n := []string{}
+		for _, v := range d.Subpackages {
+			if !strings.HasPrefix(v, "Godeps/_workspace/src") {
+				n = append(n, v)
+			}
+		}
+		d.Subpackages = n
+	}
+
+	return c
+}
diff --git a/godep/strip/strip.go b/godep/strip/strip.go
new file mode 100644
index 0000000..e70d607
--- /dev/null
+++ b/godep/strip/strip.go
@@ -0,0 +1,156 @@
+// Package strip removes Godeps/_workspace and undoes the Godep rewrites. This
+// essentially removes the old style (pre-vendor) Godep vendoring.
+//
+// Note, this functionality is deprecated. Once more projects use the Godep
+// support for the core vendoring this will no longer be needed.
+package strip
+
+import (
+	"bytes"
+	"go/ast"
+	"go/parser"
+	"go/printer"
+	"go/token"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/Masterminds/glide/msg"
+)
+
+var godepMark = map[string]bool{}
+
+var vPath = "vendor"
+
+// GodepWorkspace removes any Godeps/_workspace directories and makes sure
+// any rewrites are undone.
+// Note, this is not concuccency safe.
+func GodepWorkspace(v string) error {
+	vPath = v
+	if _, err := os.Stat(vPath); err != nil {
+		if os.IsNotExist(err) {
+			msg.Debug("Vendor directory does not exist.")
+		}
+
+		return err
+	}
+
+	err := filepath.Walk(vPath, stripGodepWorkspaceHandler)
+	if err != nil {
+		return err
+	}
+
+	// Walk the marked projects to make sure rewrites are undone.
+	for k := range godepMark {
+		msg.Info("Removing Godep rewrites for %s", k)
+		err := filepath.Walk(k, rewriteGodepfilesHandler)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func stripGodepWorkspaceHandler(path string, info os.FileInfo, err error) error {
+	// Skip the base vendor directory
+	if path == vPath {
+		return nil
+	}
+
+	name := info.Name()
+	p := filepath.Dir(path)
+	pn := filepath.Base(p)
+	if name == "_workspace" && pn == "Godeps" {
+		if _, err := os.Stat(path); err == nil {
+			if info.IsDir() {
+				// Marking this location to make sure rewrites are undone.
+				pp := filepath.Dir(p)
+				godepMark[pp] = true
+
+				msg.Info("Removing: %s", path)
+				return os.RemoveAll(path)
+			}
+
+			msg.Debug("%s is not a directory. Skipping removal", path)
+			return nil
+		}
+	}
+	return nil
+}
+
+func rewriteGodepfilesHandler(path string, info os.FileInfo, err error) error {
+	name := info.Name()
+	if name == "testdata" || name == "vendor" {
+		return filepath.SkipDir
+	}
+
+	if info.IsDir() {
+		return nil
+	}
+
+	if e := filepath.Ext(path); e != ".go" {
+		return nil
+	}
+
+	fset := token.NewFileSet()
+	f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
+	if err != nil {
+		return err
+	}
+
+	var changed bool
+	for _, s := range f.Imports {
+		n, err := strconv.Unquote(s.Path.Value)
+		if err != nil {
+			return err
+		}
+		q := rewriteGodepImport(n)
+		if q != name {
+			s.Path.Value = strconv.Quote(q)
+			changed = true
+		}
+	}
+	if !changed {
+		return nil
+	}
+
+	printerConfig := &printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}
+	var buffer bytes.Buffer
+	if err = printerConfig.Fprint(&buffer, fset, f); err != nil {
+		return err
+	}
+	fset = token.NewFileSet()
+	f, err = parser.ParseFile(fset, name, &buffer, parser.ParseComments)
+	ast.SortImports(fset, f)
+	tpath := path + ".temp"
+	t, err := os.Create(tpath)
+	if err != nil {
+		return err
+	}
+	if err = printerConfig.Fprint(t, fset, f); err != nil {
+		return err
+	}
+	if err = t.Close(); err != nil {
+		return err
+	}
+
+	msg.Debug("Rewriting Godep imports for %s", path)
+
+	// This is required before the rename on windows.
+	if err = os.Remove(path); err != nil {
+		return err
+	}
+	return os.Rename(tpath, path)
+}
+
+func rewriteGodepImport(n string) string {
+	if !strings.Contains(n, "Godeps/_workspace/src") {
+		return n
+	}
+
+	i := strings.LastIndex(n, "Godeps/_workspace/src")
+
+	return strings.TrimPrefix(n[i:], "Godeps/_workspace/src/")
+}
diff --git a/godep/strip/strip_test.go b/godep/strip/strip_test.go
new file mode 100644
index 0000000..e260a30
--- /dev/null
+++ b/godep/strip/strip_test.go
@@ -0,0 +1,17 @@
+package strip
+
+import "testing"
+
+func TestRewriteGodepImport(t *testing.T) {
+	tests := map[string]string{
+		"github.com/Masterminds/glide/action":                           "github.com/Masterminds/glide/action",
+		"github.com/tools/godep/Godeps/_workspace/src/github.com/kr/fs": "github.com/kr/fs",
+	}
+
+	for k, v := range tests {
+		o := rewriteGodepImport(k)
+		if o != v {
+			t.Errorf("Incorrect Godep import path rewritten %s: %s", v, o)
+		}
+	}
+}
diff --git a/path/strip.go b/path/strip.go
index e7abe29..1162f38 100644
--- a/path/strip.go
+++ b/path/strip.go
@@ -4,6 +4,7 @@
 	"os"
 	"path/filepath"
 
+	"github.com/Masterminds/glide/godep/strip"
 	"github.com/Masterminds/glide/msg"
 )
 
@@ -36,3 +37,44 @@
 	}
 	return nil
 }
+
+// StripVendor removes nested vendor and Godeps/_workspace/ directories.
+func StripVendor() error {
+	if _, err := os.Stat(VendorDir); err != nil {
+		if os.IsNotExist(err) {
+			msg.Debug("Vendor directory does not exist.")
+		}
+
+		return err
+	}
+
+	err := filepath.Walk(VendorDir, stripVendorHandler)
+	if err != nil {
+		return err
+	}
+
+	err = strip.GodepWorkspace(VendorDir)
+
+	return err
+}
+
+func stripVendorHandler(path string, info os.FileInfo, err error) error {
+	// Skip the base vendor directory
+	if path == VendorDir {
+		return nil
+	}
+
+	name := info.Name()
+	if name == "vendor" {
+		if _, err := os.Stat(path); err == nil {
+			if info.IsDir() {
+				msg.Info("Removing: %s", path)
+				return os.RemoveAll(path)
+			}
+
+			msg.Debug("%s is not a directory. Skipping removal", path)
+			return nil
+		}
+	}
+	return nil
+}