Initial commit based on gb c057f8ce
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..abe0e88
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 constabulary
+Copyright (c) 2015 Filippo Valsorda
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..993b22d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,63 @@
+# gvt, the go vendoring tool
+
+`gvt` is a simple Go vendoring tool made for the GO15VENDOREXPERIMENT. It's based entirely on [gb-vendor](https://github.com/constabulary/gb).
+
+You run `gvt fetch` when you would run `go get`. gvt downloads dependencies to `./vendor/`. With `GO15VENDOREXPERIMENT=1` set the compiler will find and use those packages without need to do import path rewriting. `gvt` works recursively as you would expect, and lets you update vendored dependencies. It also writes a manifest to `./vendor/manifest`.
+
+Packages whose dependencies are vendored with gvt are `go build`-able and `go get`-able by Go 1.5 with `GO15VENDOREXPERIMENT=1` set.
+
+## Installation
+
+```
+go get -u github.com/FiloSottile/gvt
+```
+
+## Usage
+
+```
+$ gvt fetch github.com/fatih/color
+2015/09/05 02:38:06 fetching recursive dependency github.com/mattn/go-isatty
+2015/09/05 02:38:07 fetching recursive dependency github.com/shiena/ansicolor
+
+$ tree -d
+.
+└── vendor
+ └── github.com
+ ├── fatih
+ │ └── color
+ ├── mattn
+ │ └── go-isatty
+ └── shiena
+ └── ansicolor
+ └── ansicolor
+
+9 directories
+
+$ cat > main.go
+package main
+import "github.com/fatih/color"
+func main() {
+ color.Red("Hello, world!")
+}
+
+$ export GO15VENDOREXPERIMENT=1
+
+$ go build .
+
+$ ./hello
+Hello, world!
+
+$ gvt update github.com/fatih/color
+```
+
+[Full usage on godoc.](https://godoc.org/github.com/FiloSottile/gvt)
+
+## Why
+
+There are many Go vendoring tools, but they all have some subset of the following problems
+
+ * no GO15VENDOREXPERIMENT support: old tools are based on import path rewriting or GOPATH overrides
+ * requirement to run on clients: some require the user to install the tool and run it after cloning, which breaks `go get`
+ * **no real fetching support**: tools like Godep just copy source from your GOPATH, instead of pulling it from the Internet
+ * prominent metadata files: there's no need for the manifest to be in your repository root, or in its own empty folder
+ * entire different build stack: gb-vendor is awesome but it requires you to build your project with gb
diff --git a/alldocs.go b/alldocs.go
new file mode 100644
index 0000000..32da424
--- /dev/null
+++ b/alldocs.go
@@ -0,0 +1,92 @@
+// DO NOT EDIT THIS FILE.
+//go:generate gvt help documentation
+
+/*
+gvt, a tool to manage your vendored dependencies.
+
+Usage:
+ gvt command [arguments]
+
+The commands are:
+
+ fetch fetch a remote dependency
+ update update a local dependency
+ list lists dependencies, one per line
+ delete delete a local dependency
+
+Use "gvt help [command]" for more information about a command.
+
+
+## Fetch a remote dependency
+
+Usage:
+ gvt fetch [-branch branch | -revision rev | -tag tag] [-precaire] [-no-recurse] importpath
+
+fetch vendors an upstream import path.
+
+The import path may include a url scheme. This may be useful when fetching dependencies
+from private repositories that cannot be probed.
+
+Flags:
+ -branch branch
+ fetch from the name branch. If not supplied the default upstream
+ branch will be used.
+ -no-recurse
+ do not fetch recursively.
+ -tag tag
+ fetch the specified tag. If not supplied the default upstream
+ branch will be used.
+ -revision rev
+ fetch the specific revision from the branch (if supplied). If no
+ revision supplied, the latest available will be supplied.
+ -precaire
+ allow the use of insecure protocols.
+
+## Update a local dependency
+
+Usage:
+ gvt update [-all] import
+
+update will replaces the source with the latest available from the head of the master branch.
+
+Updating from one copy of a dependency to another comes with several restrictions.
+The first is you can only update to the head of the branch your dependency was vendored from, switching branches is not supported.
+The second restriction is if you have used -tag or -revision while vendoring a dependency, your dependency is "headless"
+(to borrow a term from git) and cannot be updated.
+
+To update across branches, or from one tag/revision to another, you must first use delete to remove the dependency, then
+fetch [-tag | -revision | -branch ] [-precaire] to replace it.
+
+Flags:
+ -all
+ will update all dependencies in the manifest, otherwise only the dependency supplied.
+ -precaire
+ allow the use of insecure protocols.
+
+## Lists dependencies, one per line
+
+Usage:
+ gvt list [-f format]
+
+list formats the contents of the manifest file.
+
+The output
+
+Flags:
+ -f
+ controls the template used for printing each manifest entry. If not supplied
+ the default value is "{{.Importpath}}\t{{.Repository}}{{.Path}}\t{{.Branch}}\t{{.Revision}}"
+
+## Delete a local dependency
+
+Usage:
+ gvt delete [-all] importpath
+
+delete removes a dependency from the vendor directory and the manifest
+
+Flags:
+ -all
+ remove all dependencies
+
+*/
+package main
diff --git a/delete.go b/delete.go
new file mode 100644
index 0000000..9a0476c
--- /dev/null
+++ b/delete.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "path/filepath"
+
+ "github.com/FiloSottile/gvt/gbvendor"
+)
+
+var (
+ deleteAll bool // delete all dependencies
+)
+
+func addDeleteFlags(fs *flag.FlagSet) {
+ fs.BoolVar(&deleteAll, "all", false, "delete all dependencies")
+}
+
+var cmdDelete = &Command{
+ Name: "delete",
+ UsageLine: "delete [-all] importpath",
+ Short: "delete a local dependency",
+ Long: `delete removes a dependency from the vendor directory and the manifest
+
+Flags:
+ -all
+ remove all dependencies
+
+`,
+ Run: func(args []string) error {
+ if len(args) != 1 && !deleteAll {
+ return fmt.Errorf("delete: import path or --all flag is missing")
+ } else if len(args) == 1 && deleteAll {
+ return fmt.Errorf("delete: you cannot specify path and --all flag at once")
+ }
+
+ m, err := vendor.ReadManifest(manifestFile())
+ if err != nil {
+ return fmt.Errorf("could not load manifest: %v", err)
+ }
+
+ var dependencies []vendor.Dependency
+ if deleteAll {
+ dependencies = make([]vendor.Dependency, len(m.Dependencies))
+ copy(dependencies, m.Dependencies)
+ } else {
+ p := args[0]
+ dependency, err := m.GetDependencyForImportpath(p)
+ if err != nil {
+ return fmt.Errorf("could not get dependency: %v", err)
+ }
+ dependencies = append(dependencies, dependency)
+ }
+
+ for _, d := range dependencies {
+ path := d.Importpath
+
+ if err := m.RemoveDependency(d); err != nil {
+ return fmt.Errorf("dependency could not be deleted: %v", err)
+ }
+
+ if err := vendor.RemoveAll(filepath.Join(vendorDir(), filepath.FromSlash(path))); err != nil {
+ // TODO(dfc) need to apply vendor.cleanpath here to remove indermediate directories.
+ return fmt.Errorf("dependency could not be deleted: %v", err)
+ }
+ }
+ return vendor.WriteManifest(manifestFile(), m)
+ },
+ AddFlags: addDeleteFlags,
+}
diff --git a/fetch.go b/fetch.go
new file mode 100644
index 0000000..831d620
--- /dev/null
+++ b/fetch.go
@@ -0,0 +1,285 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/url"
+ "path/filepath"
+ "runtime"
+ "sort"
+
+ "go/build"
+
+ "github.com/FiloSottile/gvt/gbvendor"
+)
+
+var (
+ branch string
+ revision string // revision (commit)
+ tag string
+ noRecurse bool
+ insecure bool // Allow the use of insecure protocols
+
+ recurse bool // should we fetch recursively
+)
+
+func addFetchFlags(fs *flag.FlagSet) {
+ fs.StringVar(&branch, "branch", "", "branch of the package")
+ fs.StringVar(&revision, "revision", "", "revision of the package")
+ fs.StringVar(&tag, "tag", "", "tag of the package")
+ fs.BoolVar(&noRecurse, "no-recurse", false, "do not fetch recursively")
+ fs.BoolVar(&insecure, "precaire", false, "allow the use of insecure protocols")
+}
+
+var cmdFetch = &Command{
+ Name: "fetch",
+ UsageLine: "fetch [-branch branch | -revision rev | -tag tag] [-precaire] [-no-recurse] importpath",
+ Short: "fetch a remote dependency",
+ Long: `fetch vendors an upstream import path.
+
+The import path may include a url scheme. This may be useful when fetching dependencies
+from private repositories that cannot be probed.
+
+Flags:
+ -branch branch
+ fetch from the name branch. If not supplied the default upstream
+ branch will be used.
+ -no-recurse
+ do not fetch recursively.
+ -tag tag
+ fetch the specified tag. If not supplied the default upstream
+ branch will be used.
+ -revision rev
+ fetch the specific revision from the branch (if supplied). If no
+ revision supplied, the latest available will be supplied.
+ -precaire
+ allow the use of insecure protocols.
+
+`,
+ Run: func(args []string) error {
+ switch len(args) {
+ case 0:
+ return fmt.Errorf("fetch: import path missing")
+ case 1:
+ path := args[0]
+ recurse = !noRecurse
+ return fetch(path, recurse)
+ default:
+ return fmt.Errorf("more than one import path supplied")
+ }
+ },
+ AddFlags: addFetchFlags,
+}
+
+func fetch(path string, recurse bool) error {
+ m, err := vendor.ReadManifest(manifestFile())
+ if err != nil {
+ return fmt.Errorf("could not load manifest: %v", err)
+ }
+
+ repo, extra, err := vendor.DeduceRemoteRepo(path, insecure)
+ if err != nil {
+ return err
+ }
+
+ // strip of any scheme portion from the path, it is already
+ // encoded in the repo.
+ path = stripscheme(path)
+
+ if m.HasImportpath(path) {
+ return fmt.Errorf("%s is already vendored", path)
+ }
+
+ var wc vendor.WorkingCopy
+
+ // if we are not recursing, then always fetch the HEAD of the master
+ if recurse {
+ wc, err = repo.Checkout(branch, tag, revision)
+ } else {
+ wc, err = repo.Checkout("", "", "")
+ }
+
+ if err != nil {
+ return err
+ }
+
+ rev, err := wc.Revision()
+ if err != nil {
+ return err
+ }
+
+ branch, err := wc.Branch()
+ if err != nil {
+ return err
+ }
+
+ dep := vendor.Dependency{
+ Importpath: path,
+ Repository: repo.URL(),
+ Revision: rev,
+ Branch: branch,
+ Path: extra,
+ }
+
+ if err := m.AddDependency(dep); err != nil {
+ return err
+ }
+
+ dst := filepath.Join(vendorDir(), dep.Importpath)
+ src := filepath.Join(wc.Dir(), dep.Path)
+
+ if err := vendor.Copypath(dst, src); err != nil {
+ return err
+ }
+
+ if err := vendor.WriteManifest(manifestFile(), m); err != nil {
+ return err
+ }
+
+ if err := wc.Destroy(); err != nil {
+ return err
+ }
+
+ if !recurse {
+ return nil
+ }
+
+ for done := false; !done; {
+
+ paths := []struct {
+ Root, Prefix string
+ }{
+ {filepath.Join(runtime.GOROOT(), "src"), ""},
+ }
+ m, err := vendor.ReadManifest(manifestFile())
+ if err != nil {
+ return err
+ }
+ for _, d := range m.Dependencies {
+ paths = append(paths, struct{ Root, Prefix string }{filepath.Join(vendorDir(), filepath.FromSlash(d.Importpath)), filepath.FromSlash(d.Importpath)})
+ }
+
+ dsm, err := vendor.LoadPaths(paths...)
+ if err != nil {
+ return err
+ }
+
+ is, ok := dsm[filepath.Join(vendorDir(), path)]
+ if !ok {
+ return fmt.Errorf("unable to locate depset for %q", path)
+ }
+
+ missing := findMissing(pkgs(is.Pkgs), dsm)
+ switch len(missing) {
+ case 0:
+ done = true
+ default:
+
+ // sort keys in ascending order, so the shortest missing import path
+ // with be fetched first.
+ keys := keys(missing)
+ sort.Strings(keys)
+ pkg := keys[0]
+ log.Printf("fetching recursive dependency %s", pkg)
+ if err := fetch(pkg, false); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func keys(m map[string]bool) []string {
+ var s []string
+ for k := range m {
+ s = append(s, k)
+ }
+ return s
+}
+
+func pkgs(m map[string]*vendor.Pkg) []*vendor.Pkg {
+ var p []*vendor.Pkg
+ for _, v := range m {
+ p = append(p, v)
+ }
+ return p
+}
+
+func findMissing(pkgs []*vendor.Pkg, dsm map[string]*vendor.Depset) map[string]bool {
+ missing := make(map[string]bool)
+ imports := make(map[string]*vendor.Pkg)
+ for _, s := range dsm {
+ for _, p := range s.Pkgs {
+ imports[p.ImportPath] = p
+ }
+ }
+
+ // make fake C package for cgo
+ imports["C"] = &vendor.Pkg{
+ Depset: nil, // probably a bad idea
+ Package: &build.Package{
+ Name: "C",
+ },
+ }
+ stk := make(map[string]bool)
+ push := func(v string) {
+ if stk[v] {
+ panic(fmt.Sprintln("import loop:", v, stk))
+ }
+ stk[v] = true
+ }
+ pop := func(v string) {
+ if !stk[v] {
+ panic(fmt.Sprintln("impossible pop:", v, stk))
+ }
+ delete(stk, v)
+ }
+
+ // checked records import paths who's dependencies are all present
+ checked := make(map[string]bool)
+
+ var fn func(string)
+ fn = func(importpath string) {
+ p, ok := imports[importpath]
+ if !ok {
+ missing[importpath] = true
+ return
+ }
+
+ // have we already walked this arm, if so, skip it
+ if checked[importpath] {
+ return
+ }
+
+ sz := len(missing)
+ push(importpath)
+ for _, i := range p.Imports {
+ if i == importpath {
+ continue
+ }
+ fn(i)
+ }
+
+ // if the size of the missing map has not changed
+ // this entire subtree is complete, mark it as such
+ if len(missing) == sz {
+ checked[importpath] = true
+ }
+ pop(importpath)
+ }
+ for _, pkg := range pkgs {
+ fn(pkg.ImportPath)
+ }
+ return missing
+}
+
+// stripscheme removes any scheme components from url like paths.
+func stripscheme(path string) string {
+ u, err := url.Parse(path)
+ if err != nil {
+ panic(err)
+ }
+ return u.Host + u.Path
+}
diff --git a/gbvendor/_testdata/copyfile/a/rick b/gbvendor/_testdata/copyfile/a/rick
new file mode 120000
index 0000000..0db35ec
--- /dev/null
+++ b/gbvendor/_testdata/copyfile/a/rick
@@ -0,0 +1 @@
+/never/going/to/give/you/up
\ No newline at end of file
diff --git a/gbvendor/_testdata/src/github.com/foo/bar/main.go b/gbvendor/_testdata/src/github.com/foo/bar/main.go
new file mode 100644
index 0000000..399afdd
--- /dev/null
+++ b/gbvendor/_testdata/src/github.com/foo/bar/main.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/quux/flobble"
+ // "bitbucket.org/fwoop/ftang" // commented out, this is deliberate
+ moo "github.com/lypo/moopo"
+)
+
+import "github.com/hoo/wuu"
+
+func main() {
+ fmt.Println(flobble.Q)
+ fmt.Prinln(moo.Q)
+ fmt.Println(wuu.Q)
+}
diff --git a/gbvendor/_testdata/vendor/src/bitbucket.org/fwoop/ftang/kthulu.go b/gbvendor/_testdata/vendor/src/bitbucket.org/fwoop/ftang/kthulu.go
new file mode 100644
index 0000000..baa83bb
--- /dev/null
+++ b/gbvendor/_testdata/vendor/src/bitbucket.org/fwoop/ftang/kthulu.go
@@ -0,0 +1,3 @@
+package ftang
+
+const CAT = "ack!"
diff --git a/gbvendor/_testdata/vendor/src/github.com/hoo/wuu/goo.go b/gbvendor/_testdata/vendor/src/github.com/hoo/wuu/goo.go
new file mode 100644
index 0000000..fd4d087
--- /dev/null
+++ b/gbvendor/_testdata/vendor/src/github.com/hoo/wuu/goo.go
@@ -0,0 +1,3 @@
+package wuu
+
+const Q = "hey"
diff --git a/gbvendor/_testdata/vendor/src/github.com/lypo/moopo/tropo.go b/gbvendor/_testdata/vendor/src/github.com/lypo/moopo/tropo.go
new file mode 100644
index 0000000..834ab54
--- /dev/null
+++ b/gbvendor/_testdata/vendor/src/github.com/lypo/moopo/tropo.go
@@ -0,0 +1,3 @@
+package moopo
+
+const Q = "hi"
diff --git a/gbvendor/_testdata/vendor/src/github.com/quux/flobble/wobble.go b/gbvendor/_testdata/vendor/src/github.com/quux/flobble/wobble.go
new file mode 100644
index 0000000..fa9e631
--- /dev/null
+++ b/gbvendor/_testdata/vendor/src/github.com/quux/flobble/wobble.go
@@ -0,0 +1,3 @@
+package flobble
+
+const Q = "hello"
diff --git a/gbvendor/copy.go b/gbvendor/copy.go
new file mode 100644
index 0000000..1d2f599
--- /dev/null
+++ b/gbvendor/copy.go
@@ -0,0 +1,69 @@
+package vendor
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+const debugCopypath = true
+const debugCopyfile = false
+
+// Copypath copies the contents of src to dst, excluding any file or
+// directory that starts with a period.
+func Copypath(dst string, src string) error {
+ err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if strings.HasPrefix(filepath.Base(path), ".") {
+ if info.IsDir() {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ if info.Mode()&os.ModeSymlink != 0 {
+ if debugCopypath {
+ fmt.Printf("skipping symlink: %v\n", path)
+ }
+ return nil
+ }
+
+ dst := filepath.Join(dst, path[len(src):])
+ return copyfile(dst, path)
+ })
+ if err != nil {
+ // if there was an error during copying, remove the partial copy.
+ RemoveAll(dst)
+ }
+ return err
+}
+
+func copyfile(dst, src string) error {
+ err := mkdir(filepath.Dir(dst))
+ if err != nil {
+ return fmt.Errorf("copyfile: mkdirall: %v", err)
+ }
+ r, err := os.Open(src)
+ if err != nil {
+ return fmt.Errorf("copyfile: open(%q): %v", src, err)
+ }
+ defer r.Close()
+ w, err := os.Create(dst)
+ if err != nil {
+ return fmt.Errorf("copyfile: create(%q): %v", dst, err)
+ }
+ if debugCopyfile {
+ fmt.Printf("copyfile(dst: %v, src: %v)\n", dst, src)
+ }
+ _, err = io.Copy(w, r)
+ return err
+}
diff --git a/gbvendor/copy_test.go b/gbvendor/copy_test.go
new file mode 100644
index 0000000..297d198
--- /dev/null
+++ b/gbvendor/copy_test.go
@@ -0,0 +1,19 @@
+package vendor
+
+import (
+ "path/filepath"
+ "runtime"
+ "testing"
+)
+
+func TestCopypathSkipsSymlinks(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("no symlinks on windows y'all")
+ }
+ dst := mktemp(t)
+ defer RemoveAll(dst)
+ src := filepath.Join("_testdata", "copyfile", "a")
+ if err := Copypath(dst, src); err != nil {
+ t.Fatalf("copypath(%s, %s): %v", dst, src, err)
+ }
+}
diff --git a/gbvendor/depset.go b/gbvendor/depset.go
new file mode 100644
index 0000000..b4c40d8
--- /dev/null
+++ b/gbvendor/depset.go
@@ -0,0 +1,114 @@
+package vendor
+
+import (
+ "fmt"
+ "go/build"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// Pkg describes a Go package.
+type Pkg struct {
+ *Depset
+ *build.Package
+}
+
+// Depset describes a set of related Go packages.
+type Depset struct {
+ Root string
+ Prefix string
+ Pkgs map[string]*Pkg
+}
+
+// LoadPaths returns a map of paths to Depsets.
+func LoadPaths(paths ...struct{ Root, Prefix string }) (map[string]*Depset, error) {
+ m := make(map[string]*Depset)
+ for _, p := range paths {
+ set, err := LoadTree(p.Root, p.Prefix)
+ if err != nil {
+ return nil, err
+ }
+ m[set.Root] = set
+ }
+ return m, nil
+}
+
+// LoadTree parses a tree of source files into a map of *pkgs.
+func LoadTree(root string, prefix string) (*Depset, error) {
+ d := Depset{
+ Root: root,
+ Prefix: prefix,
+ Pkgs: make(map[string]*Pkg),
+ }
+ fn := func(dir string, fi os.FileInfo) error {
+ importpath := filepath.Join(prefix, dir[len(root)+1:])
+
+ // if we're at the root of a tree, skip it
+ if importpath == "" {
+ return nil
+ }
+
+ p, err := loadPackage(&d, dir)
+ if err != nil {
+ if _, ok := err.(*build.NoGoError); ok {
+ return nil
+ }
+ return fmt.Errorf("loadPackage(%q, %q): %v", dir, importpath, err)
+ }
+ p.ImportPath = filepath.ToSlash(importpath)
+ if p != nil {
+ d.Pkgs[p.ImportPath] = p
+ }
+ return nil
+ }
+
+ // handle root of the tree
+ fi, err := os.Stat(root)
+ if err != nil {
+ return nil, err
+ }
+ if err := fn(root+string(filepath.Separator), fi); err != nil {
+ return nil, err
+ }
+
+ // walk sub directories
+ err = eachDir(root, fn)
+ return &d, err
+}
+
+func loadPackage(d *Depset, dir string) (*Pkg, error) {
+ p := Pkg{
+ Depset: d,
+ }
+ var err error
+
+ // expolit local import logic
+ p.Package, err = build.ImportDir(dir, build.ImportComment)
+ return &p, err
+}
+
+func eachDir(dir string, fn func(string, os.FileInfo) error) error {
+ f, err := os.Open(dir)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ files, err := f.Readdir(-1)
+ for _, fi := range files {
+ if !fi.IsDir() {
+ continue
+ }
+ if strings.HasPrefix(fi.Name(), "_") || strings.HasPrefix(fi.Name(), ".") || fi.Name() == "testdata" {
+ continue
+ }
+ path := filepath.Join(dir, fi.Name())
+ if err := fn(path, fi); err != nil {
+ return err
+ }
+ if err := eachDir(path, fn); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/gbvendor/discovery.go b/gbvendor/discovery.go
new file mode 100644
index 0000000..f504200
--- /dev/null
+++ b/gbvendor/discovery.go
@@ -0,0 +1,80 @@
+// Copyright 2012 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 vendor
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io"
+ "strings"
+)
+
+// charsetReader returns a reader for the given charset. Currently
+// it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful
+// error which is printed by go get, so the user can find why the package
+// wasn't downloaded if the encoding is not supported. Note that, in
+// order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters
+// greater than 0x7f are not rejected).
+func charsetReader(charset string, input io.Reader) (io.Reader, error) {
+ switch strings.ToLower(charset) {
+ case "ascii":
+ return input, nil
+ default:
+ return nil, fmt.Errorf("can't decode XML document using charset %q", charset)
+ }
+}
+
+type metaImport struct {
+ Prefix, VCS, RepoRoot string
+}
+
+// parseMetaGoImports returns meta imports from the HTML in r.
+// Parsing ends at the end of the <head> section or the beginning of the <body>.
+func parseMetaGoImports(r io.Reader) (imports []metaImport, err error) {
+ d := xml.NewDecoder(r)
+ d.CharsetReader = charsetReader
+ d.Strict = false
+ var t xml.Token
+ for {
+ t, err = d.Token()
+ if err != nil {
+ if err == io.EOF {
+ err = nil
+ }
+ return
+ }
+ if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
+ return
+ }
+ if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
+ return
+ }
+ e, ok := t.(xml.StartElement)
+ if !ok || !strings.EqualFold(e.Name.Local, "meta") {
+ continue
+ }
+ if attrValue(e.Attr, "name") != "go-import" {
+ continue
+ }
+ if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 {
+ imports = append(imports, metaImport{
+ Prefix: f[0],
+ VCS: f[1],
+ RepoRoot: f[2],
+ })
+ }
+ }
+}
+
+// attrValue returns the attribute value for the case-insensitive key
+// `name', or the empty string if nothing is found.
+func attrValue(attrs []xml.Attr, name string) string {
+ for _, a := range attrs {
+ if strings.EqualFold(a.Name.Local, name) {
+ return a.Value
+ }
+ }
+ return ""
+}
diff --git a/gbvendor/imports.go b/gbvendor/imports.go
new file mode 100644
index 0000000..93223cc
--- /dev/null
+++ b/gbvendor/imports.go
@@ -0,0 +1,110 @@
+package vendor
+
+import (
+ "fmt"
+ "go/parser"
+ "go/token"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/constabulary/gb"
+)
+
+// ParseImports parses Go packages from a specific root returning a set of import paths.
+func ParseImports(root string) (map[string]bool, error) {
+ pkgs := make(map[string]bool)
+
+ var walkFn = func(path string, info os.FileInfo, err error) error {
+ if info.IsDir() {
+ name := info.Name()
+ if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") || name == "testdata" {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+ if filepath.Ext(path) != ".go" { // Parse only go source files
+ return nil
+ }
+
+ fs := token.NewFileSet()
+ f, err := parser.ParseFile(fs, path, nil, parser.ImportsOnly)
+ if err != nil {
+ return err
+ }
+
+ for _, s := range f.Imports {
+ p := strings.Replace(s.Path.Value, "\"", "", -1)
+ if !contains(stdlib, p) {
+ pkgs[p] = true
+ }
+ }
+ return nil
+ }
+
+ err := filepath.Walk(root, walkFn)
+ return pkgs, err
+}
+
+// FetchMetadata fetchs the remote metadata for path.
+func FetchMetadata(path string, insecure bool) (io.ReadCloser, error) {
+ schemes := []string{"https", "http"}
+ for _, s := range schemes {
+ if r, err := fetchMetadata(s, path, insecure); err == nil {
+ return r, nil
+ }
+ }
+ return nil, fmt.Errorf("unable to determine remote metadata protocol")
+}
+
+func fetchMetadata(scheme, path string, insecure bool) (io.ReadCloser, error) {
+ url := fmt.Sprintf("%s://%s?go-get=1", scheme, path)
+ switch scheme {
+ case "https":
+ resp, err := http.Get(url)
+ if err == nil {
+ return resp.Body, nil
+ }
+ case "http":
+ if !insecure {
+ gb.Infof("skipping insecure protocol: %v", url)
+ } else {
+ resp, err := http.Get(url)
+ if err == nil {
+ return resp.Body, nil
+ }
+ }
+ }
+
+ return nil, fmt.Errorf("unknown remote protocol scheme: %q", scheme)
+}
+
+// ParseMetadata fetchs and decodes remote metadata for path.
+func ParseMetadata(path string, insecure bool) (string, string, string, error) {
+ rc, err := FetchMetadata(path, insecure)
+ if err != nil {
+ return "", "", "", err
+ }
+ defer rc.Close()
+
+ imports, err := parseMetaGoImports(rc)
+ if err != nil {
+ return "", "", "", err
+ }
+ match := -1
+ for i, im := range imports {
+ if !strings.HasPrefix(path, im.Prefix) {
+ continue
+ }
+ if match != -1 {
+ return "", "", "", fmt.Errorf("multiple meta tags match import path %q", path)
+ }
+ match = i
+ }
+ if match == -1 {
+ return "", "", "", fmt.Errorf("go-import metadata not found")
+ }
+ return imports[match].Prefix, imports[match].VCS, imports[match].RepoRoot, nil
+}
diff --git a/gbvendor/imports_test.go b/gbvendor/imports_test.go
new file mode 100644
index 0000000..2794292
--- /dev/null
+++ b/gbvendor/imports_test.go
@@ -0,0 +1,134 @@
+package vendor
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+)
+
+func TestParseImports(t *testing.T) {
+ root := filepath.Join(getwd(t), "_testdata", "src")
+
+ got, err := ParseImports(root)
+ if err != nil {
+ t.Fatalf("ParseImports(%q): %v", root, err)
+ }
+
+ want := set("github.com/quux/flobble", "github.com/lypo/moopo", "github.com/hoo/wuu")
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("ParseImports(%q): want: %v, got %v", root, want, got)
+ }
+}
+
+func TestFetchMetadata(t *testing.T) {
+ if testing.Short() {
+ t.Skipf("skipping network tests in -short mode")
+ }
+ tests := []struct {
+ path string
+ want string
+ insecure bool
+ }{{
+ path: "golang.org/x/tools/cmd/godoc",
+ want: `<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+<meta name="go-import" content="golang.org/x/tools git https://go.googlesource.com/tools">
+<meta name="go-source" content="golang.org/x/tools https://github.com/golang/tools/ https://github.com/golang/tools/tree/master{/dir} https://github.com/golang/tools/blob/master{/dir}/{file}#L{line}">
+<meta http-equiv="refresh" content="0; url=https://godoc.org/golang.org/x/tools/cmd/godoc">
+</head>
+<body>
+Nothing to see here; <a href="https://godoc.org/golang.org/x/tools/cmd/godoc">move along</a>.
+</body>
+</html>
+`,
+ }, {
+ path: "gopkg.in/check.v1",
+ want: `
+<html>
+<head>
+<meta name="go-import" content="gopkg.in/check.v1 git https://gopkg.in/check.v1">
+<meta name="go-source" content="gopkg.in/check.v1 _ https://github.com/go-check/check/tree/v1{/dir} https://github.com/go-check/check/blob/v1{/dir}/{file}#L{line}">
+</head>
+<body>
+go get gopkg.in/check.v1
+</body>
+</html>
+`,
+ }}
+
+ for _, tt := range tests {
+ r, err := FetchMetadata(tt.path, tt.insecure)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, r); err != nil {
+ t.Error(err)
+ r.Close()
+ continue
+ }
+ r.Close()
+ got := buf.String()
+ if got != tt.want {
+ t.Errorf("FetchMetadata(%q): want %q, got %q", tt.path, tt.want, got)
+ }
+ }
+}
+
+func TestParseMetadata(t *testing.T) {
+ if testing.Short() {
+ t.Skipf("skipping network tests in -short mode")
+ }
+ tests := []struct {
+ path string
+ importpath string
+ vcs string
+ reporoot string
+ insecure bool
+ err error
+ }{{
+ path: "golang.org/x/tools/cmd/godoc",
+ importpath: "golang.org/x/tools",
+ vcs: "git",
+ reporoot: "https://go.googlesource.com/tools",
+ }, {
+ path: "gopkg.in/check.v1",
+ importpath: "gopkg.in/check.v1",
+ vcs: "git",
+ reporoot: "https://gopkg.in/check.v1",
+ }, {
+ path: "gopkg.in/mgo.v2/bson",
+ importpath: "gopkg.in/mgo.v2",
+ vcs: "git",
+ reporoot: "https://gopkg.in/mgo.v2",
+ }, {
+ path: "speter.net/go/exp",
+ err: fmt.Errorf("go-import metadata not found"),
+ }}
+
+ for _, tt := range tests {
+ importpath, vcs, reporoot, err := ParseMetadata(tt.path, tt.insecure)
+ if !reflect.DeepEqual(err, tt.err) {
+ t.Error(err)
+ continue
+ }
+ if importpath != tt.importpath || vcs != tt.vcs || reporoot != tt.reporoot {
+ t.Errorf("ParseMetadata(%q): want %s %s %s, got %s %s %s ", tt.path, tt.importpath, tt.vcs, tt.reporoot, importpath, vcs, reporoot)
+ }
+ }
+}
+
+func getwd(t *testing.T) string {
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return cwd
+}
diff --git a/gbvendor/manifest.go b/gbvendor/manifest.go
new file mode 100644
index 0000000..f4d973e
--- /dev/null
+++ b/gbvendor/manifest.go
@@ -0,0 +1,149 @@
+package vendor
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "reflect"
+ "sort"
+)
+
+// gb-vendor manifest support
+
+// Manifest describes the layout of $PROJECT/vendor/manifest.
+type Manifest struct {
+ // Manifest version. Current manifest version is 0.
+ Version int `json:"version"`
+
+ // Depenencies is a list of vendored dependencies.
+ Dependencies []Dependency `json:"dependencies"`
+}
+
+// AddDependency adds a Dependency to the current Manifest.
+// If the dependency exists already then it returns and error.
+func (m *Manifest) AddDependency(dep Dependency) error {
+ if m.HasImportpath(dep.Importpath) {
+ return fmt.Errorf("already registered")
+ }
+ m.Dependencies = append(m.Dependencies, dep)
+ return nil
+}
+
+// RemoveDependency removes a Dependency from the current Manifest.
+// If the dependency does not exist then it returns an error.
+func (m *Manifest) RemoveDependency(dep Dependency) error {
+ for i, d := range m.Dependencies {
+ if reflect.DeepEqual(d, dep) {
+ m.Dependencies = append(m.Dependencies[:i], m.Dependencies[i+1:]...)
+ return nil
+ }
+ }
+ return fmt.Errorf("dependency does not exist")
+}
+
+// HasImportpath reports whether the Manifest contains the import path.
+func (m *Manifest) HasImportpath(path string) bool {
+ _, err := m.GetDependencyForImportpath(path)
+ return err == nil
+}
+
+// GetDependencyForRepository return a dependency for specified URL
+// If the dependency does not exist it returns an error
+func (m *Manifest) GetDependencyForImportpath(path string) (Dependency, error) {
+ for _, d := range m.Dependencies {
+ if d.Importpath == path {
+ return d, nil
+ }
+ }
+ return Dependency{}, fmt.Errorf("dependency for %s does not exist", path)
+}
+
+// Dependency describes one vendored import path of code
+// A Dependency is an Importpath sources from a Respository
+// at Revision from Path.
+type Dependency struct {
+ // Importpath is name by which this dependency is known.
+ Importpath string `json:"importpath"`
+
+ // Repository is the remote DVCS location that this
+ // dependency was fetched from.
+ Repository string `json:"repository"`
+
+ // Revision is the revision that describes the dependency's
+ // remote revision.
+ Revision string `json:"revision"`
+
+ // Branch is the branch the Revision was located on.
+ // Can be blank if not needed.
+ Branch string `json:"branch"`
+
+ // Path is the path inside the Repository where the
+ // dependency was fetched from.
+ Path string `json:"path,omitempty"`
+}
+
+// WriteManifest writes a Manifest to the path. If the manifest does
+// not exist, it is created. If it does exist, it will be overwritten.
+// If the manifest file is empty (0 dependencies) it will be deleted.
+// The dependencies will be ordered by import path to reduce churn when making
+// changes.
+// TODO(dfc) write to temporary file and move atomically to avoid
+// destroying a working vendorfile.
+func WriteManifest(path string, m *Manifest) error {
+ if len(m.Dependencies) == 0 {
+ err := os.Remove(path)
+ if !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+ }
+
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ if err := writeManifest(f, m); err != nil {
+ f.Close()
+ return err
+ }
+ return f.Close()
+}
+
+func writeManifest(w io.Writer, m *Manifest) error {
+ sort.Sort(byImportpath(m.Dependencies))
+ buf, err := json.MarshalIndent(m, "", "\t")
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, bytes.NewReader(buf))
+ return err
+}
+
+// ReadManifest reads a Manifest from path. If the Manifest is not
+// found, a blank Manifest will be returned.
+func ReadManifest(path string) (*Manifest, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return new(Manifest), nil
+ }
+ return nil, err
+ }
+ defer f.Close()
+ return readManifest(f)
+}
+
+func readManifest(r io.Reader) (*Manifest, error) {
+ var m Manifest
+ d := json.NewDecoder(r)
+ err := d.Decode(&m)
+ return &m, err
+}
+
+type byImportpath []Dependency
+
+func (s byImportpath) Len() int { return len(s) }
+func (s byImportpath) Less(i, j int) bool { return s[i].Importpath < s[j].Importpath }
+func (s byImportpath) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
diff --git a/gbvendor/manifest_test.go b/gbvendor/manifest_test.go
new file mode 100644
index 0000000..479f588
--- /dev/null
+++ b/gbvendor/manifest_test.go
@@ -0,0 +1,104 @@
+package vendor
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func mktemp(t *testing.T) string {
+ s, err := mktmp()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return s
+}
+
+func assertNotExists(t *testing.T, path string) {
+ _, err := os.Stat(path)
+ if err == nil || !os.IsNotExist(err) {
+ t.Fatalf("expected %q to be not found, got %v", path, err)
+ }
+}
+
+func assertExists(t *testing.T, path string) {
+ _, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("expected %q to be found, got %v", path, err)
+ }
+}
+
+func TestManifest(t *testing.T) {
+ root := mktemp(t)
+ defer RemoveAll(root)
+
+ mf := filepath.Join(root, "vendor")
+
+ // check that reading an non existant manifest
+ // does not return an error
+ m, err := ReadManifest(mf)
+ if err != nil {
+ t.Fatalf("reading a non existant manifest should not fail: %v", err)
+ }
+
+ // check that no manifest file was created
+ assertNotExists(t, mf)
+
+ // add a dep
+ m.Dependencies = append(m.Dependencies, Dependency{
+ Importpath: "github.com/foo/bar/baz",
+ Repository: "https://github.com/foo/bar",
+ Revision: "cafebad",
+ Branch: "master",
+ Path: "/baz",
+ })
+
+ // write it back
+ if err := WriteManifest(mf, m); err != nil {
+ t.Fatalf("WriteManifest failed: %v", err)
+ }
+
+ // check the manifest was written
+ assertExists(t, mf)
+
+ // remove it
+ m.Dependencies = nil
+ if err := WriteManifest(mf, m); err != nil {
+ t.Fatalf("WriteManifest failed: %v", err)
+ }
+
+ // check that no manifest file was removed
+ assertNotExists(t, mf)
+}
+
+func TestEmptyPathIsNotWritten(t *testing.T) {
+ m := Manifest{
+ Version: 0,
+ Dependencies: []Dependency{{
+ Importpath: "github.com/foo/bar",
+ Repository: "https://github.com/foo/bar",
+ Revision: "abcdef",
+ Branch: "master",
+ }},
+ }
+ var buf bytes.Buffer
+ if err := writeManifest(&buf, &m); err != nil {
+ t.Fatal(err)
+ }
+ want := `{
+ "version": 0,
+ "dependencies": [
+ {
+ "importpath": "github.com/foo/bar",
+ "repository": "https://github.com/foo/bar",
+ "revision": "abcdef",
+ "branch": "master"
+ }
+ ]
+}`
+ got := buf.String()
+ if want != got {
+ t.Fatalf("want: %s, got %s", want, got)
+ }
+}
diff --git a/gbvendor/remove.go b/gbvendor/remove.go
new file mode 100644
index 0000000..fdc155b
--- /dev/null
+++ b/gbvendor/remove.go
@@ -0,0 +1,23 @@
+package vendor
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+)
+
+// RemoveAll removes path and any children it contains. Unlike os.RemoveAll it
+// deletes read only files on Windows.
+func RemoveAll(path string) error {
+ if runtime.GOOS == "windows" {
+ // make sure all files are writable so we can delete them
+ filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
+ mode := info.Mode()
+ if mode|0200 == mode {
+ return nil
+ }
+ return os.Chmod(path, mode|0200)
+ })
+ }
+ return os.RemoveAll(path)
+}
diff --git a/gbvendor/repo.go b/gbvendor/repo.go
new file mode 100644
index 0000000..521054b
--- /dev/null
+++ b/gbvendor/repo.go
@@ -0,0 +1,542 @@
+package vendor
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/constabulary/gb"
+)
+
+// RemoteRepo describes a remote dvcs repository.
+type RemoteRepo interface {
+
+ // Checkout checks out a specific branch, tag, or revision.
+ // The interpretation of these three values is impementation
+ // specific.
+ Checkout(branch, tag, revision string) (WorkingCopy, error)
+
+ // URL returns the URL the clone was taken from. It should
+ // only be called after Clone.
+ URL() string
+}
+
+// WorkingCopy represents a local copy of a remote dvcs repository.
+type WorkingCopy interface {
+
+ // Dir is the root of this working copy.
+ Dir() string
+
+ // Revision returns the revision of this working copy.
+ Revision() (string, error)
+
+ // Branch returns the branch to which this working copy belongs.
+ Branch() (string, error)
+
+ // Destroy removes the working copy and cleans path to the working copy.
+ Destroy() error
+}
+
+var (
+ ghregex = regexp.MustCompile(`^(?P<root>github\.com/([A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`)
+ bbregex = regexp.MustCompile(`^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`)
+ lpregex = regexp.MustCompile(`^launchpad.net/([A-Za-z0-9-._]+)(/[A-Za-z0-9-._]+)?(/.+)?`)
+ gcregex = regexp.MustCompile(`^(?P<root>code\.google\.com/[pr]/(?P<project>[a-z0-9\-]+)(\.(?P<subrepo>[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`)
+ genericre = regexp.MustCompile(`^(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/]*?)\.(?P<vcs>bzr|git|hg|svn))([/A-Za-z0-9_.\-]+)*$`)
+)
+
+// DeduceRemoteRepo takes a potential import path and returns a RemoteRepo
+// representing the remote location of the source of an import path.
+// Remote repositories can be bare import paths, or urls including a checkout scheme.
+// If deduction would cause traversal of an insecure host, a message will be
+// printed and the travelsal path will be ignored.
+func DeduceRemoteRepo(path string, insecure bool) (RemoteRepo, string, error) {
+ u, err := url.Parse(path)
+ if err != nil {
+ return nil, "", fmt.Errorf("%q is not a valid import path", path)
+ }
+
+ var schemes []string
+ if u.Scheme != "" {
+ schemes = append(schemes, u.Scheme)
+ }
+
+ path = u.Host + u.Path
+ if !regexp.MustCompile(`^([A-Za-z0-9-]+)(.[A-Za-z0-9-]+)+(/[A-Za-z0-9-_.]+)+$`).MatchString(path) {
+ return nil, "", fmt.Errorf("%q is not a valid import path", path)
+ }
+
+ switch {
+ case ghregex.MatchString(path):
+ v := ghregex.FindStringSubmatch(path)
+ url := &url.URL{
+ Host: "github.com",
+ Path: v[2],
+ }
+ repo, err := Gitrepo(url, insecure, schemes...)
+ return repo, v[0][len(v[1]):], err
+ case bbregex.MatchString(path):
+ v := bbregex.FindStringSubmatch(path)
+ url := &url.URL{
+ Host: "bitbucket.org",
+ Path: v[2],
+ }
+ repo, err := Gitrepo(url, insecure, schemes...)
+ if err == nil {
+ return repo, v[0][len(v[1]):], nil
+ }
+ repo, err = Hgrepo(url, insecure)
+ if err == nil {
+ return repo, v[0][len(v[1]):], nil
+ }
+ return nil, "", fmt.Errorf("unknown repository type")
+ case gcregex.MatchString(path):
+ v := gcregex.FindStringSubmatch(path)
+ url := &url.URL{
+ Host: "code.google.com",
+ Path: "p/" + v[2],
+ }
+ repo, err := Hgrepo(url, insecure, schemes...)
+ if err == nil {
+ return repo, v[0][len(v[1]):], nil
+ }
+ repo, err = Gitrepo(url, insecure, schemes...)
+ if err == nil {
+ return repo, v[0][len(v[1]):], nil
+ }
+ return nil, "", fmt.Errorf("unknown repository type")
+ case lpregex.MatchString(path):
+ v := lpregex.FindStringSubmatch(path)
+ v = append(v, "", "")
+ if v[2] == "" {
+ // launchpad.net/project"
+ repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%v", v[1]))
+ return repo, "", err
+ }
+ // launchpad.net/project/series"
+ repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%s/%s", v[1], v[2]))
+ return repo, v[3], err
+ }
+
+ // try the general syntax
+ if genericre.MatchString(path) {
+ v := genericre.FindStringSubmatch(path)
+ switch v[5] {
+ case "git":
+ x := strings.SplitN(v[1], "/", 2)
+ url := &url.URL{
+ Host: x[0],
+ Path: x[1],
+ }
+ repo, err := Gitrepo(url, insecure, schemes...)
+ return repo, v[6], err
+ case "hg":
+ x := strings.SplitN(v[1], "/", 2)
+ url := &url.URL{
+ Host: x[0],
+ Path: x[1],
+ }
+ repo, err := Hgrepo(url, insecure, schemes...)
+ return repo, v[6], err
+ case "bzr":
+ repo, err := Bzrrepo("https://" + v[1])
+ return repo, v[6], err
+ default:
+ return nil, "", fmt.Errorf("unknown repository type: %q", v[5])
+
+ }
+ }
+
+ // no idea, try to resolve as a vanity import
+ importpath, vcs, reporoot, err := ParseMetadata(path, insecure)
+ if err != nil {
+ return nil, "", err
+ }
+ u, err = url.Parse(reporoot)
+ if err != nil {
+ return nil, "", err
+ }
+ extra := path[len(importpath):]
+ switch vcs {
+ case "git":
+ u.Path = u.Path[1:]
+ repo, err := Gitrepo(u, insecure, u.Scheme)
+ return repo, extra, err
+ case "hg":
+ u.Path = u.Path[1:]
+ repo, err := Hgrepo(u, insecure, u.Scheme)
+ return repo, extra, err
+ case "bzr":
+ repo, err := Bzrrepo(reporoot)
+ return repo, extra, err
+ default:
+ return nil, "", fmt.Errorf("unknown repository type: %q", vcs)
+ }
+}
+
+// Gitrepo returns a RemoteRepo representing a remote git repository.
+func Gitrepo(url *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) {
+ if len(schemes) == 0 {
+ schemes = []string{"https", "git", "ssh", "http"}
+ }
+ u, err := probeGitUrl(url, insecure, schemes)
+ if err != nil {
+ return nil, err
+ }
+ return &gitrepo{
+ url: u,
+ }, nil
+}
+
+func probeGitUrl(u *url.URL, insecure bool, schemes []string) (string, error) {
+ git := func(url *url.URL) error {
+ out, err := run("git", "ls-remote", url.String(), "HEAD")
+ if err != nil {
+ return err
+ }
+
+ if !bytes.Contains(out, []byte("HEAD")) {
+ return fmt.Errorf("not a git repo")
+ }
+ return nil
+ }
+ return probe(git, u, insecure, schemes...)
+}
+
+func probeHgUrl(u *url.URL, insecure bool, schemes []string) (string, error) {
+ hg := func(url *url.URL) error {
+ _, err := run("hg", "identify", url.String())
+ return err
+ }
+ return probe(hg, u, insecure, schemes...)
+}
+
+func probeBzrUrl(u string) error {
+ bzr := func(url *url.URL) error {
+ _, err := run("bzr", "info", url.String())
+ return err
+ }
+ url, err := url.Parse(u)
+ if err != nil {
+ return err
+ }
+ _, err = probe(bzr, url, false, "https")
+ return err
+}
+
+// probe calls the supplied vcs function to probe a variety of url constructions.
+// If vcs returns non nil, it is assumed that the url is not a valid repo.
+func probe(vcs func(*url.URL) error, url *url.URL, insecure bool, schemes ...string) (string, error) {
+ var unsuccessful []string
+ for _, scheme := range schemes {
+
+ // make copy of url and apply scheme
+ url := *url
+ url.Scheme = scheme
+
+ switch url.Scheme {
+ case "https", "ssh":
+ if err := vcs(&url); err == nil {
+ return url.String(), nil
+ }
+ case "http", "git":
+ if !insecure {
+ gb.Infof("skipping insecure protocol: %s", url.String())
+ continue
+ }
+ if err := vcs(&url); err == nil {
+ return url.String(), nil
+ }
+ default:
+ return "", fmt.Errorf("unsupported scheme: %v", url.Scheme)
+ }
+ unsuccessful = append(unsuccessful, url.String())
+ }
+ return "", fmt.Errorf("vcs probe failed, tried: %s", strings.Join(unsuccessful, ","))
+}
+
+// gitrepo is a git RemoteRepo.
+type gitrepo struct {
+
+ // remote repository url, see man 1 git-clone
+ url string
+}
+
+func (g *gitrepo) URL() string {
+ return g.url
+}
+
+// Checkout fetchs the remote branch, tag, or revision. If more than one is
+// supplied, an error is returned. If the branch is blank,
+// then the default remote branch will be used. If the branch is "HEAD", an
+// error will be returned.
+func (g *gitrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
+ if branch == "HEAD" {
+ return nil, fmt.Errorf("cannot update %q as it has been previously fetched with -tag or -revision. Please use gb vendor delete then fetch again.", g.url)
+ }
+ if !atMostOne(branch, tag, revision) {
+ return nil, fmt.Errorf("only one of branch, tag or revision may be supplied")
+ }
+ dir, err := mktmp()
+ if err != nil {
+ return nil, err
+ }
+ wc := workingcopy{
+ path: dir,
+ }
+
+ args := []string{
+ "clone",
+ "-q", // silence progress report to stderr
+ g.url,
+ dir,
+ }
+ if branch != "" {
+ args = append(args, "--branch", branch)
+ }
+
+ if _, err := run("git", args...); err != nil {
+ wc.Destroy()
+ return nil, err
+ }
+
+ if revision != "" || tag != "" {
+ if err := runOutPath(os.Stderr, dir, "git", "checkout", "-q", oneOf(revision, tag)); err != nil {
+ wc.Destroy()
+ return nil, err
+ }
+ }
+
+ return &GitClone{wc}, nil
+}
+
+type workingcopy struct {
+ path string
+}
+
+func (w workingcopy) Dir() string { return w.path }
+
+func (w workingcopy) Destroy() error {
+ if err := RemoveAll(w.path); err != nil {
+ return err
+ }
+ parent := filepath.Dir(w.path)
+ return cleanPath(parent)
+}
+
+// GitClone is a git WorkingCopy.
+type GitClone struct {
+ workingcopy
+}
+
+func (g *GitClone) Revision() (string, error) {
+ rev, err := runPath(g.path, "git", "rev-parse", "HEAD")
+ return strings.TrimSpace(string(rev)), err
+}
+
+func (g *GitClone) Branch() (string, error) {
+ rev, err := runPath(g.path, "git", "rev-parse", "--abbrev-ref", "HEAD")
+ return strings.TrimSpace(string(rev)), err
+}
+
+// Hgrepo returns a RemoteRepo representing a remote git repository.
+func Hgrepo(u *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) {
+ if len(schemes) == 0 {
+ schemes = []string{"https", "http"}
+ }
+ url, err := probeHgUrl(u, insecure, schemes)
+ if err != nil {
+ return nil, err
+ }
+ return &hgrepo{
+ url: url,
+ }, nil
+}
+
+// hgrepo is a Mercurial repo.
+type hgrepo struct {
+
+ // remote repository url, see man 1 hg
+ url string
+}
+
+func (h *hgrepo) URL() string { return h.url }
+
+func (h *hgrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
+ if !atMostOne(tag, revision) {
+ return nil, fmt.Errorf("only one of tag or revision may be supplied")
+ }
+ dir, err := mktmp()
+ if err != nil {
+ return nil, err
+ }
+ args := []string{
+ "clone",
+ h.url,
+ dir,
+ }
+
+ if branch != "" {
+ args = append(args, "--branch", branch)
+ }
+ if err := runOut(os.Stderr, "hg", args...); err != nil {
+ RemoveAll(dir)
+ return nil, err
+ }
+ if revision != "" {
+ if err := runOut(os.Stderr, "hg", "--cwd", dir, "update", "-r", revision); err != nil {
+ RemoveAll(dir)
+ return nil, err
+ }
+ }
+
+ return &HgClone{
+ workingcopy{
+ path: dir,
+ },
+ }, nil
+}
+
+// HgClone is a mercurial WorkingCopy.
+type HgClone struct {
+ workingcopy
+}
+
+func (h *HgClone) Revision() (string, error) {
+ rev, err := run("hg", "--cwd", h.path, "id", "-i")
+ return strings.TrimSpace(string(rev)), err
+}
+
+func (h *HgClone) Branch() (string, error) {
+ rev, err := run("hg", "--cwd", h.path, "branch")
+ return strings.TrimSpace(string(rev)), err
+}
+
+// Bzrrepo returns a RemoteRepo representing a remote bzr repository.
+func Bzrrepo(url string) (RemoteRepo, error) {
+ if err := probeBzrUrl(url); err != nil {
+ return nil, err
+ }
+ return &bzrrepo{
+ url: url,
+ }, nil
+}
+
+// bzrrepo is a bzr RemoteRepo.
+type bzrrepo struct {
+
+ // remote repository url
+ url string
+}
+
+func (b *bzrrepo) URL() string {
+ return b.url
+}
+
+func (b *bzrrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) {
+ if !atMostOne(tag, revision) {
+ return nil, fmt.Errorf("only one of tag or revision may be supplied")
+ }
+ dir, err := mktmp()
+ if err != nil {
+ return nil, err
+ }
+ wc := filepath.Join(dir, "wc")
+ if err := runOut(os.Stderr, "bzr", "branch", b.url, wc); err != nil {
+ RemoveAll(dir)
+ return nil, err
+ }
+
+ return &BzrClone{
+ workingcopy{
+ path: wc,
+ },
+ }, nil
+}
+
+// BzrClone is a bazaar WorkingCopy.
+type BzrClone struct {
+ workingcopy
+}
+
+func (b *BzrClone) Revision() (string, error) {
+ return "1", nil
+}
+
+func (b *BzrClone) Branch() (string, error) {
+ return "master", nil
+}
+
+func cleanPath(path string) error {
+ if files, _ := ioutil.ReadDir(path); len(files) > 0 || filepath.Base(path) == "src" {
+ return nil
+ }
+ parent := filepath.Dir(path)
+ if err := RemoveAll(path); err != nil {
+ return err
+ }
+ return cleanPath(parent)
+}
+
+func mkdir(path string) error {
+ return os.MkdirAll(path, 0755)
+}
+
+func mktmp() (string, error) {
+ return ioutil.TempDir("", "gb-vendor-")
+}
+
+func run(c string, args ...string) ([]byte, error) {
+ var buf bytes.Buffer
+ err := runOut(&buf, c, args...)
+ return buf.Bytes(), err
+}
+
+func runOut(w io.Writer, c string, args ...string) error {
+ cmd := exec.Command(c, args...)
+ cmd.Stdout = w
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+func runPath(path string, c string, args ...string) ([]byte, error) {
+ var buf bytes.Buffer
+ err := runOutPath(&buf, path, c, args...)
+ return buf.Bytes(), err
+}
+
+func runOutPath(w io.Writer, path string, c string, args ...string) error {
+ cmd := exec.Command(c, args...)
+ cmd.Dir = path
+ cmd.Stdout = w
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+// atMostOne returns true if no more than one string supplied is not empty.
+func atMostOne(args ...string) bool {
+ var c int
+ for _, arg := range args {
+ if arg != "" {
+ c++
+ }
+ }
+ return c < 2
+}
+
+// oneof returns the first non empty string
+func oneOf(args ...string) string {
+ for _, arg := range args {
+ if arg != "" {
+ return arg
+ }
+ }
+ return ""
+}
diff --git a/gbvendor/repo_test.go b/gbvendor/repo_test.go
new file mode 100644
index 0000000..03c5107
--- /dev/null
+++ b/gbvendor/repo_test.go
@@ -0,0 +1,139 @@
+package vendor
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+)
+
+func TestDeduceRemoteRepo(t *testing.T) {
+ if testing.Short() {
+ t.Skipf("skipping network tests in -short mode")
+ }
+ tests := []struct {
+ path string
+ want RemoteRepo
+ extra string
+ err error
+ insecure bool
+ }{{
+ path: "",
+ err: fmt.Errorf(`"" is not a valid import path`),
+ }, {
+ path: "corporate",
+ err: fmt.Errorf(`"corporate" is not a valid import path`),
+ }, {
+ path: "github.com/cznic/b",
+ want: &gitrepo{
+ url: "https://github.com/cznic/b",
+ },
+ }, {
+ path: "github.com/pkg/sftp",
+ want: &gitrepo{
+ url: "https://github.com/pkg/sftp",
+ },
+ }, {
+ path: "github.com/pkg/sftp/examples/gsftp",
+ want: &gitrepo{
+ url: "https://github.com/pkg/sftp",
+ },
+ extra: "/examples/gsftp",
+ }, {
+ path: "github.com/coreos/go-etcd",
+ want: &gitrepo{
+ url: "https://github.com/coreos/go-etcd",
+ },
+ }, {
+ path: "bitbucket.org/davecheney/gitrepo/cmd/main",
+ want: &gitrepo{
+ url: "https://bitbucket.org/davecheney/gitrepo",
+ },
+ extra: "/cmd/main",
+ }, {
+ path: "bitbucket.org/davecheney/hgrepo/cmd/main",
+ want: &hgrepo{
+ url: "https://bitbucket.org/davecheney/hgrepo",
+ },
+ extra: "/cmd/main",
+ }, {
+ path: "code.google.com/p/goauth2/oauth",
+ want: &hgrepo{
+ url: "https://code.google.com/p/goauth2",
+ },
+ extra: "/oauth",
+ }, {
+ path: "code.google.com/p/gami",
+ want: &gitrepo{
+ url: "https://code.google.com/p/gami",
+ },
+ }, {
+ path: "git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.golang.git",
+ want: &gitrepo{
+ url: "https://git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.golang.git",
+ },
+ }, {
+ path: "git.apache.org/thrift.git/lib/go/thrift",
+ want: &gitrepo{
+ url: "https://git.apache.org/thrift.git",
+ },
+ extra: "/lib/go/thrift",
+ }, {
+ path: "gopkg.in/check.v1",
+ want: &gitrepo{
+ url: "https://gopkg.in/check.v1",
+ },
+ extra: "",
+ }, {
+ path: "golang.org/x/tools/go/vcs",
+ want: &gitrepo{
+ url: "https://go.googlesource.com/tools",
+ },
+ extra: "/go/vcs",
+ }, {
+ path: "labix.org/v2/mgo",
+ want: &bzrrepo{
+ url: "https://launchpad.net/mgo/v2",
+ },
+ insecure: true,
+ }, {
+ path: "launchpad.net/gnuflag",
+ want: &bzrrepo{
+ url: "https://launchpad.net/gnuflag",
+ },
+ }, {
+ path: "https://github.com/pkg/sftp",
+ want: &gitrepo{
+ url: "https://github.com/pkg/sftp",
+ },
+ }, {
+ path: "git://github.com/pkg/sftp",
+ want: &gitrepo{
+ url: "git://github.com/pkg/sftp",
+ },
+ insecure: true,
+ }, {
+ path: "code.google.com/p/google-api-go-client/bigquery/v2",
+ want: &hgrepo{
+ url: "https://code.google.com/p/google-api-go-client",
+ },
+ extra: "/bigquery/v2",
+ }, {
+ path: "code.google.com/p/go-sqlite/go1/sqlite3",
+ want: &hgrepo{
+ url: "https://code.google.com/p/go-sqlite",
+ },
+ extra: "/go1/sqlite3",
+ }}
+
+ for _, tt := range tests {
+ t.Logf("DeduceRemoteRepo(%q, %v)", tt.path, tt.insecure)
+ got, extra, err := DeduceRemoteRepo(tt.path, tt.insecure)
+ if !reflect.DeepEqual(err, tt.err) {
+ t.Errorf("DeduceRemoteRepo(%q): want err: %v, got err: %v", tt.path, tt.err, err)
+ continue
+ }
+ if !reflect.DeepEqual(got, tt.want) || extra != tt.extra {
+ t.Errorf("DeduceRemoteRepo(%q): want %#v, %v, got %#v, %v", tt.path, tt.want, tt.extra, got, extra)
+ }
+ }
+}
diff --git a/gbvendor/stdlib.go b/gbvendor/stdlib.go
new file mode 100644
index 0000000..f297561
--- /dev/null
+++ b/gbvendor/stdlib.go
@@ -0,0 +1,138 @@
+package vendor
+
+// packages from the standard lib. They are excluded
+// from the package map.
+var stdlib = map[string]bool{
+ "C": true,
+ "archive/tar": true,
+ "archive/zip": true,
+ "bufio": true,
+ "bytes": true,
+ "compress/bzip2": true,
+ "compress/flate": true,
+ "compress/gzip": true,
+ "compress/lzw": true,
+ "compress/zlib": true,
+ "container/heap": true,
+ "container/list": true,
+ "container/ring": true,
+ "crypto": true,
+ "crypto/aes": true,
+ "crypto/cipher": true,
+ "crypto/des": true,
+ "crypto/dsa": true,
+ "crypto/ecdsa": true,
+ "crypto/elliptic": true,
+ "crypto/hmac": true,
+ "crypto/md5": true,
+ "crypto/rand": true,
+ "crypto/rc4": true,
+ "crypto/rsa": true,
+ "crypto/sha1": true,
+ "crypto/sha256": true,
+ "crypto/sha512": true,
+ "crypto/subtle": true,
+ "crypto/tls": true,
+ "crypto/x509": true,
+ "crypto/x509/pkix": true,
+ "database/sql": true,
+ "database/sql/driver": true,
+ "debug/dwarf": true,
+ "debug/elf": true,
+ "debug/gosym": true,
+ "debug/macho": true,
+ "debug/pe": true,
+ "encoding": true,
+ "encoding/ascii85": true,
+ "encoding/asn1": true,
+ "encoding/base32": true,
+ "encoding/base64": true,
+ "encoding/binary": true,
+ "encoding/csv": true,
+ "encoding/gob": true,
+ "encoding/hex": true,
+ "encoding/json": true,
+ "encoding/pem": true,
+ "encoding/xml": true,
+ "errors": true,
+ "expvar": true,
+ "flag": true,
+ "fmt": true,
+ "go/ast": true,
+ "go/build": true,
+ "go/doc": true,
+ "go/format": true,
+ "go/parser": true,
+ "go/printer": true,
+ "go/scanner": true,
+ "go/token": true,
+ "hash": true,
+ "hash/adler32": true,
+ "hash/crc32": true,
+ "hash/crc64": true,
+ "hash/fnv": true,
+ "html": true,
+ "html/template": true,
+ "image": true,
+ "image/color": true,
+ "image/draw": true,
+ "image/gif": true,
+ "image/jpeg": true,
+ "image/png": true,
+ "index/suffixarray": true,
+ "io": true,
+ "io/ioutil": true,
+ "log": true,
+ "log/syslog": true,
+ "math": true,
+ "math/big": true,
+ "math/cmplx": true,
+ "math/rand": true,
+ "mime": true,
+ "mime/multipart": true,
+ "net": true,
+ "net/http": true,
+ "net/http/cgi": true,
+ "net/http/cookiejar": true,
+ "net/http/fcgi": true,
+ "net/http/httptest": true,
+ "net/http/httputil": true,
+ "net/http/pprof": true,
+ "net/mail": true,
+ "net/rpc": true,
+ "net/rpc/jsonrpc": true,
+ "net/smtp": true,
+ "net/textproto": true,
+ "net/url": true,
+ "os": true,
+ "os/exec": true,
+ "os/signal": true,
+ "os/user": true,
+ "path": true,
+ "path/filepath": true,
+ "reflect": true,
+ "regexp": true,
+ "regexp/syntax": true,
+ "runtime": true,
+ "runtime/cgo": true,
+ "runtime/debug": true,
+ "runtime/pprof": true,
+ "sort": true,
+ "strconv": true,
+ "strings": true,
+ "sync": true,
+ "sync/atomic": true,
+ "syscall": true,
+ "testing": true,
+ "testing/iotest": true,
+ "testing/quick": true,
+ "text/scanner": true,
+ "text/tabwriter": true,
+ "text/template": true,
+ "text/template/parse": true,
+ "time": true,
+ "unicode": true,
+ "unicode/utf16": true,
+ "unicode/utf8": true,
+ "unsafe": true,
+}
diff --git a/gbvendor/stringset.go b/gbvendor/stringset.go
new file mode 100644
index 0000000..007eae5
--- /dev/null
+++ b/gbvendor/stringset.go
@@ -0,0 +1,52 @@
+package vendor
+
+// union returns the union of a and b.
+func union(a, b map[string]bool) map[string]bool {
+ r := make(map[string]bool)
+ for k := range a {
+ r[k] = true
+ }
+ for k := range b {
+ r[k] = true
+ }
+ return r
+}
+
+// intersection returns the intersection of a and b.
+func intersection(a, b map[string]bool) map[string]bool {
+ r := make(map[string]bool)
+ for k := range a {
+ if b[k] {
+ r[k] = true
+ }
+ }
+ return r
+}
+
+// difference returns the symetric difference of a and b.
+func difference(a, b map[string]bool) map[string]bool {
+ r := make(map[string]bool)
+ for k := range a {
+ if !b[k] {
+ r[k] = true
+ }
+ }
+ for k := range b {
+ if !a[k] {
+ r[k] = true
+ }
+ }
+ return r
+}
+
+// contains returns true if a contains all the elements in s.
+func contains(a map[string]bool, s ...string) bool {
+ var r bool
+ for _, e := range s {
+ if !a[e] {
+ return false
+ }
+ r = true
+ }
+ return r
+}
diff --git a/gbvendor/stringset_test.go b/gbvendor/stringset_test.go
new file mode 100644
index 0000000..d8781a4
--- /dev/null
+++ b/gbvendor/stringset_test.go
@@ -0,0 +1,147 @@
+package vendor
+
+import "testing"
+import "reflect"
+
+func set(args ...string) map[string]bool {
+ r := make(map[string]bool)
+ for _, a := range args {
+ r[a] = true
+ }
+ return r
+}
+
+func TestUnion(t *testing.T) {
+ tests := []struct {
+ a, b map[string]bool
+ want map[string]bool
+ }{{
+ a: nil, b: nil,
+ want: set(),
+ }, {
+ a: nil, b: set("b"),
+ want: set("b"),
+ }, {
+ a: set("a"), b: nil,
+ want: set("a"),
+ }, {
+ a: set("a"), b: set("b"),
+ want: set("b", "a"),
+ }, {
+ a: set("c"), b: set("c"),
+ want: set("c"),
+ }}
+
+ for _, tt := range tests {
+ got := union(tt.a, tt.b)
+ if !reflect.DeepEqual(tt.want, got) {
+ t.Errorf("union(%v, %v) want: %v, got %v", tt.a, tt.b, tt.want, got)
+ }
+ }
+}
+
+func TestIntersection(t *testing.T) {
+ tests := []struct {
+ a, b map[string]bool
+ want map[string]bool
+ }{{
+ a: nil, b: nil,
+ want: set(),
+ }, {
+ a: nil, b: set("b"),
+ want: set(),
+ }, {
+ a: set("a"), b: nil,
+ want: set(),
+ }, {
+ a: set("a"), b: set("b"),
+ want: set(),
+ }, {
+ a: set("c"), b: set("c"),
+ want: set("c"),
+ }, {
+ a: set("a", "c"), b: set("b", "c"),
+ want: set("c"),
+ }}
+
+ for _, tt := range tests {
+ got := intersection(tt.a, tt.b)
+ if !reflect.DeepEqual(tt.want, got) {
+ t.Errorf("intersection(%v, %v) want: %v, got %v", tt.a, tt.b, tt.want, got)
+ }
+ }
+}
+
+func TestDifference(t *testing.T) {
+ tests := []struct {
+ a, b map[string]bool
+ want map[string]bool
+ }{{
+ a: nil, b: nil,
+ want: set(),
+ }, {
+ a: nil, b: set("b"),
+ want: set("b"),
+ }, {
+ a: set("a"), b: nil,
+ want: set("a"),
+ }, {
+ a: set("a"), b: set("b"),
+ want: set("a", "b"),
+ }, {
+ a: set("c"), b: set("c"),
+ want: set(),
+ }, {
+ a: set("a", "c"), b: set("b", "c"),
+ want: set("a", "b"),
+ }}
+
+ for _, tt := range tests {
+ got := difference(tt.a, tt.b)
+ if !reflect.DeepEqual(tt.want, got) {
+ t.Errorf("difference(%v, %v) want: %v, got %v", tt.a, tt.b, tt.want, got)
+ }
+ }
+}
+
+func TestContains(t *testing.T) {
+ tests := []struct {
+ a map[string]bool
+ s []string
+ want bool
+ }{{
+ a: nil, s: nil,
+ want: false,
+ }, {
+ a: set("a"), s: nil,
+ want: false,
+ }, {
+ a: set("a"), s: []string{"a"},
+ want: true,
+ }, {
+ a: set("a"), s: []string{"b"},
+ want: false,
+ }, {
+ a: set("a", "b"), s: []string{"b"},
+ want: true,
+ }, {
+ a: set("a"), s: []string{"a", "b"},
+ want: false,
+ }, {
+ a: set("a", "b", "c"), s: []string{"a", "b"},
+ want: true,
+ }, {
+ a: set("a", "b", "c"), s: []string{"x", "b"},
+ want: false,
+ }, {
+ a: set("a", "b", "c"), s: []string{"b", "c", "d"},
+ want: false,
+ }}
+
+ for _, tt := range tests {
+ got := contains(tt.a, tt.s...)
+ if !reflect.DeepEqual(tt.want, got) {
+ t.Errorf("contains(%v, %v) want: %v, got %v", tt.a, tt.s, tt.want, got)
+ }
+ }
+}
diff --git a/help.go b/help.go
new file mode 100644
index 0000000..a0c8856
--- /dev/null
+++ b/help.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "text/template"
+ "unicode"
+ "unicode/utf8"
+)
+
+var helpTemplate = `usage: gvt {{.UsageLine}}
+
+{{.Long | trim}}
+`
+
+// help implements the 'help' command.
+func help(args []string) {
+ if len(args) == 0 {
+ printUsage(os.Stdout)
+ return
+ }
+ if len(args) != 1 {
+ fmt.Fprintf(os.Stderr, "usage: gvt help command\n\nToo many arguments given.\n")
+ os.Exit(2)
+ }
+
+ arg := args[0]
+
+ // 'gvt help documentation' generates alldocs.go.
+ if arg == "documentation" {
+ var u bytes.Buffer
+ printUsage(&u)
+ f, _ := os.Create("alldocs.go")
+ tmpl(f, documentationTemplate, struct {
+ Usage string
+ Commands []*Command
+ }{
+ u.String(),
+ commands,
+ })
+ f.Close()
+ return
+ }
+
+ for _, cmd := range commands {
+ if cmd.Name == arg {
+ tmpl(os.Stdout, helpTemplate, cmd)
+ // not exit 2: succeeded at 'gb help cmd'.
+ return
+ }
+ }
+
+ fmt.Fprintf(os.Stderr, "Unknown help topic %#q. Run 'gb help'.\n", arg)
+ os.Exit(2) // failed at 'gb help cmd'
+}
+
+var usageTemplate = `gvt, a simple go vendoring tool based on gb-vendor.
+
+Usage:
+ gvt command [arguments]
+
+The commands are:
+{{range .}}
+ {{.Name | printf "%-11s"}} {{.Short}}{{end}}
+
+Use "gvt help [command]" for more information about a command.
+`
+
+var documentationTemplate = `// DO NOT EDIT THIS FILE.
+//go:generate gvt help documentation
+
+/*
+{{ .Usage }}
+
+{{range .Commands}}{{if .Short}}## {{.Short | capitalize}}
+{{end}}
+Usage:
+ gvt {{.UsageLine}}
+
+{{.Long | trim}}
+
+{{end}}*/
+package main
+`
+
+// tmpl executes the given template text on data, writing the result to w.
+func tmpl(w io.Writer, text string, data interface{}) {
+ t := template.New("top")
+ t.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize})
+ template.Must(t.Parse(text))
+ if err := t.Execute(w, data); err != nil {
+ panic(err)
+ }
+}
+
+func capitalize(s string) string {
+ if s == "" {
+ return s
+ }
+ r, n := utf8.DecodeRuneInString(s)
+ return string(unicode.ToTitle(r)) + s[n:]
+}
+
+func printUsage(w io.Writer) {
+ bw := bufio.NewWriter(w)
+ tmpl(bw, usageTemplate, commands)
+ bw.Flush()
+}
+
+func usage() {
+ printUsage(os.Stderr)
+ os.Exit(2)
+}
diff --git a/list.go b/list.go
new file mode 100644
index 0000000..e3113d9
--- /dev/null
+++ b/list.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "html/template"
+ "os"
+ "text/tabwriter"
+
+ "github.com/FiloSottile/gvt/gbvendor"
+)
+
+var (
+ format string
+)
+
+func addListFlags(fs *flag.FlagSet) {
+ fs.StringVar(&format, "f", "{{.Importpath}}\t{{.Repository}}{{.Path}}\t{{.Branch}}\t{{.Revision}}", "format template")
+}
+
+var cmdList = &Command{
+ Name: "list",
+ UsageLine: "list [-f format]",
+ Short: "lists dependencies, one per line",
+ Long: `list formats the contents of the manifest file.
+
+The output
+
+Flags:
+ -f
+ controls the template used for printing each manifest entry. If not supplied
+ the default value is "{{.Importpath}}\t{{.Repository}}{{.Path}}\t{{.Branch}}\t{{.Revision}}"
+
+`,
+ Run: func(args []string) error {
+ m, err := vendor.ReadManifest(manifestFile())
+ if err != nil {
+ return fmt.Errorf("could not load manifest: %v", err)
+ }
+ tmpl, err := template.New("list").Parse(format)
+ if err != nil {
+ return fmt.Errorf("unable to parse template %q: %v", format, err)
+ }
+ w := tabwriter.NewWriter(os.Stdout, 1, 2, 1, ' ', 0)
+ for _, dep := range m.Dependencies {
+ if err := tmpl.Execute(w, dep); err != nil {
+ return fmt.Errorf("unable to execute template: %v", err)
+ }
+ fmt.Fprintln(w)
+ }
+ return w.Flush()
+ },
+ AddFlags: addListFlags,
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..89a8dc6
--- /dev/null
+++ b/main.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "path/filepath"
+)
+
+var fs = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
+
+func init() {
+ fs.Usage = func() {
+ printUsage(os.Stderr)
+ os.Exit(2)
+ }
+}
+
+type Command struct {
+ Name string
+ UsageLine string
+ Short string
+ Long string
+ Run func(args []string) error
+ AddFlags func(fs *flag.FlagSet)
+}
+
+var commands = []*Command{
+ cmdFetch,
+ cmdUpdate,
+ cmdList,
+ cmdDelete,
+}
+
+func main() {
+ args := os.Args[1:]
+
+ switch {
+ case len(args) < 1, args[0] == "-h", args[0] == "-help":
+ fs.Usage()
+ os.Exit(1)
+ case args[0] == "help":
+ help(args[1:])
+ return
+ }
+
+ for _, command := range commands {
+ if command.Name == args[0] {
+
+ // add extra flags if necessary
+ if command.AddFlags != nil {
+ command.AddFlags(fs)
+ }
+
+ if err := fs.Parse(args[1:]); err != nil {
+ log.Fatalf("could not parse flags: %v", err)
+ }
+ args = fs.Args() // reset args to the leftovers from fs.Parse
+
+ if err := command.Run(args); err != nil {
+ log.Fatalf("command %q failed: %v", command.Name, err)
+ }
+ return
+ }
+ }
+ log.Fatalf("unknown command %q ", args[0])
+}
+
+const manifestfile = "manifest"
+
+func vendorDir() string {
+ wd, err := os.Getwd()
+ if err != nil {
+ log.Fatal(err)
+ }
+ return filepath.Join(wd, "vendor")
+}
+
+func manifestFile() string {
+ return filepath.Join(vendorDir(), manifestfile)
+}
diff --git a/update.go b/update.go
new file mode 100644
index 0000000..86d0fb1
--- /dev/null
+++ b/update.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "path/filepath"
+
+ "github.com/FiloSottile/gvt/gbvendor"
+)
+
+var (
+ updateAll bool // update all dependencies
+)
+
+func addUpdateFlags(fs *flag.FlagSet) {
+ fs.BoolVar(&updateAll, "all", false, "update all dependencies")
+ fs.BoolVar(&insecure, "precaire", false, "allow the use of insecure protocols")
+}
+
+var cmdUpdate = &Command{
+ Name: "update",
+ UsageLine: "update [-all] import",
+ Short: "update a local dependency",
+ Long: `update will replaces the source with the latest available from the head of the master branch.
+
+Updating from one copy of a dependency to another comes with several restrictions.
+The first is you can only update to the head of the branch your dependency was vendored from, switching branches is not supported.
+The second restriction is if you have used -tag or -revision while vendoring a dependency, your dependency is "headless"
+(to borrow a term from git) and cannot be updated.
+
+To update across branches, or from one tag/revision to another, you must first use delete to remove the dependency, then
+fetch [-tag | -revision | -branch ] [-precaire] to replace it.
+
+Flags:
+ -all
+ will update all dependencies in the manifest, otherwise only the dependency supplied.
+ -precaire
+ allow the use of insecure protocols.
+
+`,
+ Run: func(args []string) error {
+ if len(args) != 1 && !updateAll {
+ return fmt.Errorf("update: import path or --all flag is missing")
+ } else if len(args) == 1 && updateAll {
+ return fmt.Errorf("update: you cannot specify path and --all flag at once")
+ }
+
+ m, err := vendor.ReadManifest(manifestFile())
+ if err != nil {
+ return fmt.Errorf("could not load manifest: %v", err)
+ }
+
+ var dependencies []vendor.Dependency
+ if updateAll {
+ dependencies = make([]vendor.Dependency, len(m.Dependencies))
+ copy(dependencies, m.Dependencies)
+ } else {
+ p := args[0]
+ dependency, err := m.GetDependencyForImportpath(p)
+ if err != nil {
+ return fmt.Errorf("could not get dependency: %v", err)
+ }
+ dependencies = append(dependencies, dependency)
+ }
+
+ for _, d := range dependencies {
+ err = m.RemoveDependency(d)
+ if err != nil {
+ return fmt.Errorf("dependency could not be deleted from manifest: %v", err)
+ }
+
+ repo, extra, err := vendor.DeduceRemoteRepo(d.Importpath, insecure)
+ if err != nil {
+ return fmt.Errorf("could not determine repository for import %q", d.Importpath)
+ }
+
+ wc, err := repo.Checkout(d.Branch, "", "")
+ if err != nil {
+ return err
+ }
+
+ rev, err := wc.Revision()
+ if err != nil {
+ return err
+ }
+
+ branch, err := wc.Branch()
+ if err != nil {
+ return err
+ }
+
+ dep := vendor.Dependency{
+ Importpath: d.Importpath,
+ Repository: repo.URL(),
+ Revision: rev,
+ Branch: branch,
+ Path: extra,
+ }
+
+ if err := vendor.RemoveAll(filepath.Join(vendorDir(), filepath.FromSlash(d.Importpath))); err != nil {
+ // TODO(dfc) need to apply vendor.cleanpath here to remove intermediate directories.
+ return fmt.Errorf("dependency could not be deleted: %v", err)
+ }
+
+ dst := filepath.Join(vendorDir(), filepath.FromSlash(dep.Importpath))
+ src := filepath.Join(wc.Dir(), dep.Path)
+
+ if err := vendor.Copypath(dst, src); err != nil {
+ return err
+ }
+
+ if err := m.AddDependency(dep); err != nil {
+ return err
+ }
+
+ if err := vendor.WriteManifest(manifestFile(), m); err != nil {
+ return err
+ }
+
+ if err := wc.Destroy(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ },
+ AddFlags: addUpdateFlags,
+}