Chase gps again to add prefetching
diff --git a/glide.lock b/glide.lock
index e9e7e6b..9a968e2 100644
--- a/glide.lock
+++ b/glide.lock
@@ -10,7 +10,7 @@
 - name: github.com/Masterminds/vcs
   version: fbe9fb6ad5b5f35b3e82a7c21123cfc526cbf895
 - name: github.com/sdboyer/gps
-  version: 63a033c13497e9f5ca47f5f6d4e02455f5d4d85d
+  version: 278a227dfc3d595a33a77ff3f841fd8ca1bc8cd0
 - name: github.com/termie/go-shutil
   version: bcacb06fecaeec8dc42af03c87c6949f4a05c74c
 - name: gopkg.in/yaml.v2
diff --git a/vendor/github.com/sdboyer/gps/analysis.go b/vendor/github.com/sdboyer/gps/analysis.go
index 0cb93ba..7fcb5bf 100644
--- a/vendor/github.com/sdboyer/gps/analysis.go
+++ b/vendor/github.com/sdboyer/gps/analysis.go
@@ -426,10 +426,12 @@
 			return true
 
 		case grey:
-			// grey means an import cycle; guaranteed badness right here.
+			// grey means an import cycle; guaranteed badness right here. You'd
+			// hope we never encounter it in a dependency (really? you published
+			// that code?), but we have to defend against it.
 			//
-			// FIXME handle import cycles by dropping everything involved. i
-			// think we need to compute SCC, then drop *all* of them?
+			// FIXME handle import cycles by dropping everything involved. (i
+			// think we need to compute SCC, then drop *all* of them?)
 			colors[pkg] = black
 			poison(append(path, pkg)) // poison self and parents
 
@@ -730,12 +732,14 @@
 }
 
 // ExternalReach looks through a PackageTree and computes the list of external
-// packages (not logical children of PackageTree.ImportRoot) that are
-// transitively imported by the internal packages in the tree.
+// import statements (that is, import statements pointing to packages that are
+// not logical children of PackageTree.ImportRoot) that are transitively
+// imported by the internal packages in the tree.
 //
 // main indicates whether (true) or not (false) to include main packages in the
-// analysis. main packages are generally excluded when analyzing anything other
-// than the root project, as they inherently can't be imported.
+// analysis. When utilized by gps' solver, main packages are generally excluded
+// from analyzing anything other than the root project, as they necessarily can't
+// be imported.
 //
 // tests indicates whether (true) or not (false) to include imports from test
 // files in packages when computing the reach map.
@@ -744,25 +748,35 @@
 // analysis. This exclusion applies to both internal and external packages. If
 // an external import path is ignored, it is simply omitted from the results.
 //
-// If an internal path is ignored, then it is excluded from all transitive
-// dependency chains and does not appear as a key in the final map. That is, if
-// you ignore A/foo, then the external package list for all internal packages
-// that import A/foo will not include external packages that are only reachable
-// through A/foo.
+// If an internal path is ignored, then not only does it not appear in the final
+// map, but it is also excluded from the transitive calculations of other
+// internal packages.  That is, if you ignore A/foo, then the external package
+// list for all internal packages that import A/foo will not include external
+// packages that are only reachable through A/foo.
 //
 // Visually, this means that, given a PackageTree with root A and packages at A,
 // A/foo, and A/bar, and the following import chain:
 //
 //  A -> A/foo -> A/bar -> B/baz
 //
-// If you ignore A/foo, then the returned map would be:
+// In this configuration, all of A's packages transitively import B/baz, so the
+// returned map would be:
+//
+//  map[string][]string{
+// 	"A": []string{"B/baz"},
+// 	"A/foo": []string{"B/baz"}
+// 	"A/bar": []string{"B/baz"},
+//  }
+//
+// However, if you ignore A/foo, then A's path to B/baz is broken, and A/foo is
+// omitted entirely. Thus, the returned map would be:
 //
 //  map[string][]string{
 // 	"A": []string{},
 // 	"A/bar": []string{"B/baz"},
 //  }
 //
-// It is safe to pass a nil map if there are no packages to ignore.
+// If there are no packages to ignore, it is safe to pass a nil map.
 func (t PackageTree) ExternalReach(main, tests bool, ignore map[string]bool) map[string][]string {
 	if ignore == nil {
 		ignore = make(map[string]bool)
diff --git a/vendor/github.com/sdboyer/gps/bridge.go b/vendor/github.com/sdboyer/gps/bridge.go
index 2aae74b..298b023 100644
--- a/vendor/github.com/sdboyer/gps/bridge.go
+++ b/vendor/github.com/sdboyer/gps/bridge.go
@@ -5,6 +5,7 @@
 	"os"
 	"path/filepath"
 	"sort"
+	"sync/atomic"
 
 	"github.com/Masterminds/semver"
 )
@@ -21,6 +22,7 @@
 	matches(id ProjectIdentifier, c Constraint, v Version) bool
 	matchesAny(id ProjectIdentifier, c1, c2 Constraint) bool
 	intersect(id ProjectIdentifier, c1, c2 Constraint) Constraint
+	breakLock()
 }
 
 // bridge is an adapter around a proper SourceManager. It provides localized
@@ -51,6 +53,9 @@
 	// is that this keeps the versions sorted in the direction required by the
 	// current solve run
 	vlists map[ProjectIdentifier][]Version
+
+	// Indicates whether lock breaking has already been run
+	lockbroken int32
 }
 
 // Global factory func to create a bridge. This exists solely to allow tests to
@@ -413,6 +418,43 @@
 	return b.sm.DeduceProjectRoot(ip)
 }
 
+// breakLock is called when the solver has to break a version recorded in the
+// lock file. It prefetches all the projects in the solver's lock , so that the
+// information is already on hand if/when the solver needs it.
+//
+// Projects that have already been selected are skipped, as it's generally unlikely that the
+// solver will have to backtrack through and fully populate their version queues.
+func (b *bridge) breakLock() {
+	// No real conceivable circumstance in which multiple calls are made to
+	// this, but being that this is the entrance point to a bunch of async work,
+	// protect it with an atomic CAS in case things change in the future.
+	if !atomic.CompareAndSwapInt32(&b.lockbroken, 0, 1) {
+		return
+	}
+
+	for _, lp := range b.s.rl.Projects() {
+		if _, is := b.s.sel.selected(lp.pi); !is {
+			// ListPackages guarantees that all the necessary network work will
+			// be done, so go with that
+			//
+			// TODO(sdboyer) use this as an opportunity to detect
+			// inconsistencies between upstream and the lock (e.g., moved tags)?
+			pi, v := lp.pi, lp.Version()
+			go func() {
+				// Sync first
+				b.sm.SyncSourceFor(pi)
+				// Preload the package info for the locked version, too, as
+				// we're more likely to need that
+				b.sm.ListPackages(pi, v)
+			}()
+		}
+	}
+}
+
+func (b *bridge) SyncSourceFor(id ProjectIdentifier) error {
+	return b.sm.SyncSourceFor(id)
+}
+
 // versionTypeUnion represents a set of versions that are, within the scope of
 // this solver run, equivalent.
 //
diff --git a/vendor/github.com/sdboyer/gps/codecov.yml b/vendor/github.com/sdboyer/gps/codecov.yml
index 263381f..cdc5202 100644
--- a/vendor/github.com/sdboyer/gps/codecov.yml
+++ b/vendor/github.com/sdboyer/gps/codecov.yml
@@ -2,4 +2,5 @@
   ignore:
   - remove_go16.go
   - remove_go17.go
-  - errors.go
+  - solve_failures.go
+  - discovery.go # copied from stdlib, don't need to test
diff --git a/vendor/github.com/sdboyer/gps/manager_test.go b/vendor/github.com/sdboyer/gps/manager_test.go
index 439d8b4..df85293 100644
--- a/vendor/github.com/sdboyer/gps/manager_test.go
+++ b/vendor/github.com/sdboyer/gps/manager_test.go
@@ -98,7 +98,7 @@
 	}
 }
 
-func TestProjectManagerInit(t *testing.T) {
+func TestSourceInit(t *testing.T) {
 	// This test is a bit slow, skip it on -short
 	if testing.Short() {
 		t.Skip("Skipping project manager init test in short mode")
@@ -165,10 +165,10 @@
 		t.Errorf("Unexpected error during initial project setup/fetching %s", err)
 	}
 
+	rev := Revision("30605f6ac35fcb075ad0bfa9296f90a7d891523e")
 	if len(v) != 3 {
 		t.Errorf("Expected three version results from the test repo, got %v", len(v))
 	} else {
-		rev := Revision("30605f6ac35fcb075ad0bfa9296f90a7d891523e")
 		expected := []Version{
 			NewVersion("1.0.0").Is(rev),
 			NewBranch("master").Is(rev),
@@ -182,9 +182,18 @@
 		}
 	}
 
-	// use ListPackages to ensure the repo is actually on disk
-	// TODO(sdboyer) ugh, maybe we do need an explicit prefetch method
-	smc.ListPackages(id, NewVersion("1.0.0"))
+	present, err := smc.RevisionPresentIn(id, rev)
+	if err != nil {
+		t.Errorf("Should have found revision in source, but got err: %s", err)
+	} else if !present {
+		t.Errorf("Should have found revision in source, but did not")
+	}
+
+	// SyncSourceFor will ensure we have everything
+	err = smc.SyncSourceFor(id)
+	if err != nil {
+		t.Errorf("SyncSourceFor failed with unexpected error: %s", err)
+	}
 
 	// Ensure that the appropriate cache dirs and files exist
 	_, err = os.Stat(filepath.Join(cpath, "sources", "https---github.com-Masterminds-VCSTestRepo", ".git"))
@@ -209,6 +218,36 @@
 	}
 }
 
+func TestMgrMethodsFailWithBadPath(t *testing.T) {
+	// a symbol will always bork it up
+	bad := mkPI("foo/##&^")
+	sm, clean := mkNaiveSM(t)
+	defer clean()
+
+	var err error
+	if _, err = sm.SourceExists(bad); err == nil {
+		t.Error("SourceExists() did not error on bad input")
+	}
+	if err = sm.SyncSourceFor(bad); err == nil {
+		t.Error("SyncSourceFor() did not error on bad input")
+	}
+	if _, err = sm.ListVersions(bad); err == nil {
+		t.Error("ListVersions() did not error on bad input")
+	}
+	if _, err = sm.RevisionPresentIn(bad, Revision("")); err == nil {
+		t.Error("RevisionPresentIn() did not error on bad input")
+	}
+	if _, err = sm.ListPackages(bad, nil); err == nil {
+		t.Error("ListPackages() did not error on bad input")
+	}
+	if _, _, err = sm.GetManifestAndLock(bad, nil); err == nil {
+		t.Error("GetManifestAndLock() did not error on bad input")
+	}
+	if err = sm.ExportProject(bad, nil, ""); err == nil {
+		t.Error("ExportProject() did not error on bad input")
+	}
+}
+
 func TestGetSources(t *testing.T) {
 	// This test is a tad slow, skip it on -short
 	if testing.Short() {
diff --git a/vendor/github.com/sdboyer/gps/solve_basic_test.go b/vendor/github.com/sdboyer/gps/solve_basic_test.go
index 6b6a092..e4c1352 100644
--- a/vendor/github.com/sdboyer/gps/solve_basic_test.go
+++ b/vendor/github.com/sdboyer/gps/solve_basic_test.go
@@ -1293,6 +1293,14 @@
 	return false, nil
 }
 
+func (sm *depspecSourceManager) SyncSourceFor(id ProjectIdentifier) error {
+	// Ignore err because it can't happen
+	if exist, _ := sm.SourceExists(id); !exist {
+		return fmt.Errorf("Source %s does not exist", id.errString())
+	}
+	return nil
+}
+
 func (sm *depspecSourceManager) VendorCodeExists(id ProjectIdentifier) (bool, error) {
 	return false, nil
 }
diff --git a/vendor/github.com/sdboyer/gps/solver.go b/vendor/github.com/sdboyer/gps/solver.go
index d82a40c..507133b 100644
--- a/vendor/github.com/sdboyer/gps/solver.go
+++ b/vendor/github.com/sdboyer/gps/solver.go
@@ -169,8 +169,8 @@
 // else fail with an informative error.
 //
 // If a Solution is found, an implementing tool may persist it - typically into
-// what a "lock file" - and/or use it to write out a directory tree of
-// dependencies, suitable to be a vendor directory, via CreateVendorTree.
+// a "lock file" - and/or use it to write out a directory tree of dependencies,
+// suitable to be a vendor directory, via CreateVendorTree.
 type Solver interface {
 	// HashInputs produces a hash digest representing the unique inputs to this
 	// solver. It is guaranteed that, if the hash digest is equal to the digest
@@ -179,6 +179,9 @@
 	//
 	// In such a case, it may not be necessary to run Solve() at all.
 	HashInputs() ([]byte, error)
+
+	// Solve initiates a solving run. It will either complete successfully with
+	// a Solution, or fail with an informative error.
 	Solve() (Solution, error)
 }
 
@@ -474,6 +477,13 @@
 	}
 
 	for _, dep := range deps {
+		// If we have no lock, or if this dep isn't in the lock, then prefetch
+		// it. See explanation longer comment in selectRoot() for how we benefit
+		// from parallelism here.
+		if _, has := s.rlm[dep.Ident]; !has {
+			go s.b.SyncSourceFor(dep.Ident)
+		}
+
 		s.sel.pushDep(dependency{depender: pa, dep: dep})
 		// Add all to unselected queue
 		s.names[dep.Ident.ProjectRoot] = dep.Ident.netName()
@@ -817,6 +827,8 @@
 		// though, then we have to try to use what's in the lock, because that's
 		// the only version we'll be able to get.
 		if exist, _ := s.b.SourceExists(id); exist {
+			// Upgrades mean breaking the lock
+			s.b.breakLock()
 			return nil, nil
 		}
 
@@ -864,6 +876,8 @@
 		}
 
 		if !found {
+			// No match found, which means we're going to be breaking the lock
+			s.b.breakLock()
 			return nil, nil
 		}
 	}
@@ -1060,6 +1074,10 @@
 
 	// If this atom has a lock, pull it out so that we can potentially inject
 	// preferred versions into any bmis we enqueue
+	//
+	// TODO(sdboyer) making this call here could be the first thing to trigger
+	// network activity...maybe? if so, can we mitigate by deferring the work to
+	// queue consumption time?
 	_, l, _ := s.b.GetManifestAndLock(a.a.id, a.a.v)
 	var lmap map[ProjectIdentifier]Version
 	if l != nil {
@@ -1070,13 +1088,31 @@
 	}
 
 	for _, dep := range deps {
+		// If this is dep isn't in the lock, do some prefetching. (If it is, we
+		// might be able to get away with zero network activity for it, so don't
+		// prefetch). This provides an opportunity for some parallelism wins, on
+		// two fronts:
+		//
+		// 1. Because this loop may have multiple deps in it, we could end up
+		// simultaneously fetching both in the background while solving proceeds
+		//
+		// 2. Even if only one dep gets prefetched here, the worst case is that
+		// that same dep comes out of the unselected queue next, and we gain a
+		// few microseconds before blocking later. Best case, the dep doesn't
+		// come up next, but some other dep comes up that wasn't prefetched, and
+		// both fetches proceed in parallel.
+		if _, has := s.rlm[dep.Ident]; !has {
+			go s.b.SyncSourceFor(dep.Ident)
+		}
+
 		s.sel.pushDep(dependency{depender: a.a, dep: dep})
 		// Go through all the packages introduced on this dep, selecting only
-		// the ones where the only depper on them is what we pushed in. Then,
-		// put those into the unselected queue.
+		// the ones where the only depper on them is what the previous line just
+		// pushed in. Then, put those into the unselected queue.
 		rpm := s.sel.getRequiredPackagesIn(dep.Ident)
 		var newp []string
 		for _, pkg := range dep.pl {
+			// Just one means that the dep we're visiting is the sole importer.
 			if rpm[pkg] == 1 {
 				newp = append(newp, pkg)
 			}
diff --git a/vendor/github.com/sdboyer/gps/source.go b/vendor/github.com/sdboyer/gps/source.go
index feaba15..6256c51 100644
--- a/vendor/github.com/sdboyer/gps/source.go
+++ b/vendor/github.com/sdboyer/gps/source.go
@@ -1,8 +1,12 @@
 package gps
 
-import "fmt"
+import (
+	"fmt"
+	"sync"
+)
 
 type source interface {
+	syncLocal() error
 	checkExistence(sourceExistence) bool
 	exportVersionTo(Version, string) error
 	getManifestAndLock(ProjectRoot, Version) (Manifest, Lock, error)
@@ -54,9 +58,6 @@
 	// ProjectAnalyzer used to fulfill getManifestAndLock
 	an ProjectAnalyzer
 
-	// Whether the cache has the latest info on versions
-	cvsync bool
-
 	// The project metadata cache. This is (or is intended to be) persisted to
 	// disk, for reuse across solver runs.
 	dc *sourceMetaCache
@@ -64,6 +65,20 @@
 	// lvfunc allows the other vcs source types that embed this type to inject
 	// their listVersions func into the baseSource, for use as needed.
 	lvfunc func() (vlist []Version, err error)
+
+	// lock to serialize access to syncLocal
+	synclock sync.Mutex
+
+	// Globalish flag indicating whether a "full" sync has been performed. Also
+	// used as a one-way gate to ensure that the full syncing routine is never
+	// run more than once on a given source instance.
+	allsync bool
+
+	// The error, if any, that occurred on syncLocal
+	syncerr error
+
+	// Whether the cache has the latest info on versions
+	cvsync bool
 }
 
 func (bs *baseVCSSource) getManifestAndLock(r ProjectRoot, v Version) (Manifest, Lock, error) {
@@ -201,7 +216,7 @@
 			bs.crepo.mut.Unlock()
 
 			if err != nil {
-				return fmt.Errorf("failed to create repository cache for %s", bs.crepo.r.Remote())
+				return fmt.Errorf("failed to create repository cache for %s with err:\n%s", bs.crepo.r.Remote(), err)
 			}
 			bs.crepo.synced = true
 			bs.ex.s |= existsInCache
@@ -247,6 +262,44 @@
 	return ex&bs.ex.f == ex
 }
 
+// syncLocal ensures the local data we have about the source is fully up to date
+// with what's out there over the network.
+func (bs *baseVCSSource) syncLocal() error {
+	// Ensure we only have one goroutine doing this at a time
+	bs.synclock.Lock()
+	defer bs.synclock.Unlock()
+
+	// ...and that we only ever do it once
+	if bs.allsync {
+		// Return the stored err, if any
+		return bs.syncerr
+	}
+
+	bs.allsync = true
+	// First, ensure the local instance exists
+	bs.syncerr = bs.ensureCacheExistence()
+	if bs.syncerr != nil {
+		return bs.syncerr
+	}
+
+	_, bs.syncerr = bs.lvfunc()
+	if bs.syncerr != nil {
+		return bs.syncerr
+	}
+
+	// This case is really just for git repos, where the lvfunc doesn't
+	// guarantee that the local repo is synced
+	if !bs.crepo.synced {
+		bs.syncerr = bs.crepo.r.Update()
+		if bs.syncerr != nil {
+			return bs.syncerr
+		}
+		bs.crepo.synced = true
+	}
+
+	return nil
+}
+
 func (bs *baseVCSSource) listPackages(pr ProjectRoot, v Version) (ptree PackageTree, err error) {
 	if err = bs.ensureCacheExistence(); err != nil {
 		return
diff --git a/vendor/github.com/sdboyer/gps/source_manager.go b/vendor/github.com/sdboyer/gps/source_manager.go
index 11ec567..dc5a7ef 100644
--- a/vendor/github.com/sdboyer/gps/source_manager.go
+++ b/vendor/github.com/sdboyer/gps/source_manager.go
@@ -28,6 +28,10 @@
 	// SourceManager's central repository cache.
 	SourceExists(ProjectIdentifier) (bool, error)
 
+	// SyncSourceFor will attempt to bring all local information about a source
+	// fully up to date.
+	SyncSourceFor(ProjectIdentifier) error
+
 	// ListVersions retrieves a list of the available versions for a given
 	// repository name.
 	ListVersions(ProjectIdentifier) ([]Version, error)
@@ -217,6 +221,19 @@
 	return src.checkExistence(existsInCache) || src.checkExistence(existsUpstream), nil
 }
 
+// SyncSourceFor will ensure that all local caches and information about a
+// source are up to date with any network-acccesible information.
+//
+// The primary use case for this is prefetching.
+func (sm *SourceMgr) SyncSourceFor(id ProjectIdentifier) error {
+	src, err := sm.getSourceFor(id)
+	if err != nil {
+		return err
+	}
+
+	return src.syncLocal()
+}
+
 // ExportProject writes out the tree of the provided ProjectIdentifier's
 // ProjectRoot, at the provided version, to the provided directory.
 func (sm *SourceMgr) ExportProject(id ProjectIdentifier, v Version, to string) error {
diff --git a/vendor/github.com/sdboyer/gps/types.go b/vendor/github.com/sdboyer/gps/types.go
index b40807d..657d786 100644
--- a/vendor/github.com/sdboyer/gps/types.go
+++ b/vendor/github.com/sdboyer/gps/types.go
@@ -15,8 +15,8 @@
 // a project's manifest, and apply to all packages in a ProjectRoot's tree.
 // Solving itself mostly proceeds on a project-by-project basis.
 //
-// Aliasing string types is usually a bit of an anti-pattern. We do it here as a
-// means of clarifying API intent. This is important because Go's package
+// Aliasing string types is usually a bit of an anti-pattern. gps does it here
+// as a means of clarifying API intent. This is important because Go's package
 // management domain has lots of different path-ish strings floating around:
 //
 //  actual directories:
@@ -41,9 +41,9 @@
 // to, but differs in two keys ways from, an import path.
 //
 // First, ProjectIdentifiers do not identify a single package. Rather, they
-// encompasses the whole tree of packages that exist at or below their
+// encompasses the whole tree of packages rooted at and including their
 // ProjectRoot. In gps' current design, this ProjectRoot must correspond to the
-// root of a repository, though this may not always be the case.
+// root of a repository, though this may change in the future.
 //
 // Second, ProjectIdentifiers can optionally carry a NetworkName, which
 // identifies where the underlying source code can be located on the network.
@@ -57,7 +57,8 @@
 //
 // With plain import paths, network addresses are derived purely through an
 // algorithm. By having an explicit network name, it becomes possible to, for
-// example, transparently substitute a fork for an original upstream repository.
+// example, transparently substitute a fork for the original upstream source
+// repository.
 //
 // Note that gps makes no guarantees about the actual import paths contained in
 // a repository aligning with ImportRoot. If tools, or their users, specify an
@@ -126,7 +127,8 @@
 	return i
 }
 
-// ProjectProperties comprise the properties that can attached to a ProjectRoot.
+// ProjectProperties comprise the properties that can be attached to a
+// ProjectRoot.
 //
 // In general, these are declared in the context of a map of ProjectRoot to its
 // ProjectProperties; they make little sense without their corresponding