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, +}