diff --git a/CHANGELOG.md b/CHANGELOG.md index a3cd6e2e..cc7697a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # v0.3.3 (Unreleased) +NEW FEATURES: +* Add support for importing from [govendor](https://github.com/kardianos/govendor) + based projects (#815). + BUG FIXES: * Releases targeting Windows now have a `.exe` suffix (#1291). diff --git a/cmd/dep/govendor_importer.go b/cmd/dep/govendor_importer.go deleted file mode 100644 index 69d37939..00000000 --- a/cmd/dep/govendor_importer.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import ( - "encoding/json" - "io/ioutil" - "log" - "os" - "path/filepath" - "strings" - - "github.com/golang/dep" - fb "github.com/golang/dep/internal/feedback" - "github.com/golang/dep/internal/gps" - "github.com/pkg/errors" -) - -const govendorDir = "vendor" -const govendorName = "vendor.json" - -type govendorImporter struct { - file govendorFile - - logger *log.Logger - verbose bool - sm gps.SourceManager -} - -func newGovendorImporter(logger *log.Logger, verbose bool, sm gps.SourceManager) *govendorImporter { - return &govendorImporter{ - logger: logger, - verbose: verbose, - sm: sm, - } -} - -// File is the structure of the vendor file. -type govendorFile struct { - RootPath string // Import path of vendor folder - Ignore string - Package []*govendorPackage -} - -// Package represents each package. -type govendorPackage struct { - // See the vendor spec for definitions. - Origin string - Path string - Revision string - Version string -} - -func (g *govendorImporter) Name() string { - return "govendor" -} - -func (g *govendorImporter) HasDepMetadata(dir string) bool { - y := filepath.Join(dir, govendorDir, govendorName) - if _, err := os.Stat(y); err != nil { - return false - } - return true -} - -func (g *govendorImporter) Import(dir string, pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, error) { - err := g.load(dir) - if err != nil { - return nil, nil, err - } - return g.convert(pr) -} - -func (g *govendorImporter) load(projectDir string) error { - g.logger.Println("Detected govendor configuration file...") - v := filepath.Join(projectDir, govendorDir, govendorName) - if g.verbose { - g.logger.Printf(" Loading %s", v) - } - vb, err := ioutil.ReadFile(v) - if err != nil { - return errors.Wrapf(err, "Unable to read %s", v) - } - err = json.Unmarshal(vb, &g.file) - if err != nil { - return errors.Wrapf(err, "Unable to parse %s", v) - } - return nil -} - -func (g *govendorImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, error) { - g.logger.Println("Converting from vendor.json...") - - manifest := dep.NewManifest() - - if len(g.file.Ignore) > 0 { - // Govendor has three use cases here - // 1. 'test' - special case for ignoring test files - // 2. build tags - any string without a slash (/) in it - // 3. path and path prefix - any string with a slash (/) in it. - // The path case could be a full path or just a prefix. - // Dep doesn't support build tags right now: https://github.com/golang/dep/issues/120 - for _, i := range strings.Split(g.file.Ignore, " ") { - if !strings.Contains(i, "/") { - g.logger.Printf(" Govendor was configured to ignore the %s build tag, but that isn't supported by dep yet, and will be ignored. See https://github.com/golang/dep/issues/291.", i) - continue - } - _, err := g.sm.DeduceProjectRoot(i) - if err == nil { - manifest.Ignored = append(manifest.Ignored, i) - } else { - g.logger.Printf(" Govendor was configured to ignore the %s package prefix, but that isn't supported by dep yet, and will be ignored.", i) - } - } - } - - lock := &dep.Lock{} - for _, pkg := range g.file.Package { - // Path must not be empty - if pkg.Path == "" { - err := errors.New("Invalid govendor configuration, Path is required") - return nil, nil, err - } - - // Obtain ProjectRoot. Required for avoiding sub-package imports. - // Use Path instead of Origin since we are trying to group by project here - pr, err := g.sm.DeduceProjectRoot(pkg.Path) - if err != nil { - return nil, nil, err - } - pkg.Path = string(pr) - - // Check if it already existing in locked projects - if projectExistsInLock(lock, pr) { - continue - } - - // Revision must not be empty - if pkg.Revision == "" { - err := errors.New("Invalid govendor configuration, Revision is required") - return nil, nil, err - } - - if pkg.Version == "" { - // When no version is specified try to get the corresponding version - pi := gps.ProjectIdentifier{ - ProjectRoot: pr, - } - if pkg.Origin != "" { - pi.Source = pkg.Origin - } - revision := gps.Revision(pkg.Revision) - version, err := lookupVersionForLockedProject(pi, nil, revision, g.sm) - if err != nil { - // Only warn about the problem, it is not enough to warrant failing - g.logger.Println(err.Error()) - } else { - pp := getProjectPropertiesFromVersion(version) - if pp.Constraint != nil { - pkg.Version = pp.Constraint.String() - } - } - } - - // If there's a version, use it to create project constraint - pc, err := g.buildProjectConstraint(pkg) - if err != nil { - return nil, nil, err - } - manifest.Constraints[pc.Ident.ProjectRoot] = gps.ProjectProperties{Constraint: pc.Constraint} - - lp := g.buildLockedProject(pkg, manifest) - lock.P = append(lock.P, lp) - } - - return manifest, lock, nil -} - -func (g *govendorImporter) buildProjectConstraint(pkg *govendorPackage) (pc gps.ProjectConstraint, err error) { - pc.Ident = gps.ProjectIdentifier{ProjectRoot: gps.ProjectRoot(pkg.Path), Source: pkg.Path} - - if pkg.Version != "" { - pc.Constraint, err = g.sm.InferConstraint(pkg.Version, pc.Ident) - if err != nil { - return - } - } else { - pc.Constraint = gps.Any() - } - - f := fb.NewConstraintFeedback(pc, fb.DepTypeImported) - f.LogFeedback(g.logger) - - return -} - -func (g *govendorImporter) buildLockedProject(pkg *govendorPackage, manifest *dep.Manifest) gps.LockedProject { - pi := gps.ProjectIdentifier{ProjectRoot: gps.ProjectRoot(pkg.Path)} - revision := gps.Revision(pkg.Revision) - pp := manifest.Constraints[pi.ProjectRoot] - - version, err := lookupVersionForLockedProject(pi, pp.Constraint, revision, g.sm) - if err != nil { - // Only warn about the problem, it is not enough to warrant failing - g.logger.Println(err.Error()) - } - - lp := gps.NewLockedProject(pi, version, nil) - f := fb.NewLockedProjectFeedback(lp, fb.DepTypeImported) - f.LogFeedback(g.logger) - - return lp -} diff --git a/cmd/dep/govendor_importer_test.go b/cmd/dep/govendor_importer_test.go deleted file mode 100644 index eb92711a..00000000 --- a/cmd/dep/govendor_importer_test.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import ( - "bytes" - "log" - "path/filepath" - "strings" - "testing" - - "github.com/golang/dep/internal/gps" - "github.com/golang/dep/internal/test" - "github.com/pkg/errors" -) - -const testGovendorProjectRoot = "github.com/golang/notexist" - -func TestGovendorConfig_Import(t *testing.T) { - h := test.NewHelper(t) - defer h.Cleanup() - - ctx := newTestContext(h) - sm, err := ctx.SourceManager() - h.Must(err) - defer sm.Release() - - h.TempDir(filepath.Join("src", testGovendorProjectRoot)) - h.TempCopy(filepath.Join(testGovendorProjectRoot, govendorDir, govendorName), "govendor/vendor.json") - projectRoot := h.Path(testGovendorProjectRoot) - - // Capture stderr so we can verify output - verboseOutput := &bytes.Buffer{} - ctx.Err = log.New(verboseOutput, "", 0) - - g := newGovendorImporter(ctx.Err, false, sm) // Disable verbose so that we don't print values that change each test run - if !g.HasDepMetadata(projectRoot) { - t.Fatal("Expected the importer to detect the govendor configuration files") - } - - m, l, err := g.Import(projectRoot, testGovendorProjectRoot) - h.Must(err) - - if m == nil { - t.Fatal("Expected the manifest to be generated") - } - - if l == nil { - t.Fatal("Expected the lock to be generated") - } - - goldenFile := "govendor/golden.txt" - got := verboseOutput.String() - want := h.GetTestFileString(goldenFile) - if want != got { - if *test.UpdateGolden { - if err := h.WriteTestFile(goldenFile, got); err != nil { - t.Fatalf("%+v", errors.Wrapf(err, "Unable to write updated golden file %s", goldenFile)) - } - } else { - t.Fatalf("expected %s, got %s", want, got) - } - } -} - -func TestGovendorConfig_Convert_Project(t *testing.T) { - h := test.NewHelper(t) - defer h.Cleanup() - - ctx := newTestContext(h) - sm, err := ctx.SourceManager() - h.Must(err) - defer sm.Release() - - pkg := "github.com/sdboyer/deptest" - - g := newGovendorImporter(ctx.Err, true, sm) - g.file = govendorFile{ - Package: []*govendorPackage{ - { - Path: pkg, - Revision: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", - Version: "v1.0.0", - }, - }, - } - - manifest, lock, err := g.convert(testGovendorProjectRoot) - if err != nil { - t.Fatal(err) - } - - if manifest == nil { - t.Fatal("Expected the manifest to be generated") - } - - if lock == nil { - t.Fatal("Expected the lock file to be generated") - } - - d, ok := manifest.Constraints[gps.ProjectRoot(pkg)] - if !ok { - t.Fatal("Expected the manifest to have a dependency for 'github.com/sdboyer/deptest' but got none") - } - - wantC := "^1.0.0" - gotC := d.Constraint.String() - if gotC != wantC { - t.Fatalf("Expected manifest constraint to be %s, got %s", wantC, gotC) - } - - wantP := 1 - gotP := len(lock.P) - if gotP != 1 { - t.Fatalf("Expected the lock to contain %d project but got %d", wantP, gotP) - } - - p := lock.P[0] - gotPr := string(p.Ident().ProjectRoot) - if gotPr != pkg { - t.Fatalf("Expected the lock to have a project for %s but got '%s'", pkg, gotPr) - } - - lv := p.Version() - lpv, ok := lv.(gps.PairedVersion) - if !ok { - t.Fatalf("Expected locked version to be a PairedVersion but got %T", lv) - } - - wantRev := "ff2948a2ac8f538c4ecd55962e919d1e13e74baf" - gotRev := lpv.Revision().String() - if gotRev != wantRev { - t.Fatalf("Expected locked revision to be %s, got %s", wantRev, gotRev) - } - - wantV := "v1.0.0" - gotV := lpv.String() - if gotV != wantV { - t.Fatalf("Expected locked version to be %s, got %s", wantV, gotV) - } -} - -func TestGovendorConfig_Convert_TestProject(t *testing.T) { - h := test.NewHelper(t) - defer h.Cleanup() - - ctx := newTestContext(h) - sm, err := ctx.SourceManager() - h.Must(err) - defer sm.Release() - - pkg := "github.com/sdboyer/deptest" - - g := newGovendorImporter(ctx.Err, true, sm) - g.file = govendorFile{ - Package: []*govendorPackage{ - { - Path: pkg, - Revision: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", - Version: "v1.0.0", - }, - }, - } - - manifest, lock, err := g.convert(testGovendorProjectRoot) - if err != nil { - t.Fatal(err) - } - - if manifest == nil { - t.Fatal("Expected the manifest to be generated") - } - - if lock == nil { - t.Fatal("Expected the lock file to be generated") - } - - _, ok := manifest.Constraints[gps.ProjectRoot(pkg)] - if !ok { - t.Fatalf("Expected the manifest to have a dependency for %s but got none", pkg) - } - - if len(lock.P) != 1 { - t.Fatalf("Expected the lock to contain 1 project but got %d", len(lock.P)) - } - p := lock.P[0] - if p.Ident().ProjectRoot != gps.ProjectRoot(pkg) { - t.Fatalf("Expected the lock to have a project for %s but got '%s'", pkg, p.Ident().ProjectRoot) - } -} - -func TestGovendorConfig_Convert_Ignore(t *testing.T) { - h := test.NewHelper(t) - defer h.Cleanup() - - ctx := newTestContext(h) - sm, err := ctx.SourceManager() - h.Must(err) - defer sm.Release() - - pkg := "github.com/sdboyer/deptest" - - g := newGovendorImporter(ctx.Err, true, sm) - g.file = govendorFile{ - Ignore: strings.Join([]string{"test", pkg, "linux_amd64", "github.com/sdboyer/"}, " "), - } - - m, _, err := g.convert(testGovendorProjectRoot) - if err != nil { - t.Fatal(err) - } - - if len(m.Ignored) != 1 { - t.Fatalf("Expected the ignored list to contain 1 project but got %d", len(m.Ignored)) - } - - p := m.Ignored[0] - if p != pkg { - t.Fatalf("Expected the ignored list to have an element for %s but got '%s'", pkg, p) - } -} - -func TestGovendorConfig_Convert_BadInput_EmptyPackagePath(t *testing.T) { - h := test.NewHelper(t) - defer h.Cleanup() - - ctx := newTestContext(h) - sm, err := ctx.SourceManager() - h.Must(err) - defer sm.Release() - - g := newGovendorImporter(ctx.Err, true, sm) - g.file = govendorFile{ - Package: []*govendorPackage{{Path: ""}}, - } - - _, _, err = g.convert(testGovendorProjectRoot) - if err == nil { - t.Fatal("Expected conversion to fail because the package name is empty") - } -} diff --git a/cmd/dep/testdata/govendor/vendor-sub-pkg.json b/cmd/dep/testdata/govendor/vendor-sub-pkg.json deleted file mode 100644 index 4d47fc3d..00000000 --- a/cmd/dep/testdata/govendor/vendor-sub-pkg.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "comment": "", - "ignore": "test", - "package": [ - { - "checksumSHA1": "c+MCTe4HOVzNBjFpjmPYWFSs06U=", - "path": "github.com/sdboyer/deptest", - "revision": "e1b5acf7f78e07bc0a8e51121ca0c7e174975d7e", - "revisionTime": "2017-07-15T19:32:19Z" - }, - { - "checksumSHA1": "cp+gzxyVhoM8sZWLPrsigvpdDuc=", - "path": "github.com/sdboyer/deptest/floats", - "revision": "e1b5acf7f78e07bc0a8e51121ca0c7e174975d7e", - "revisionTime": "2017-07-15T19:32:19Z" - }, - { - "checksumSHA1": "5yiF57bPBKmLr6AFDDxfTmWYZJ4=", - "path": "github.com/sdboyer/deptest/strings", - "revision": "e1b5acf7f78e07bc0a8e51121ca0c7e174975d7e", - "revisionTime": "2017-07-15T19:32:19Z" - } - ], - "rootPath": "github.com/golang/notexist" -} diff --git a/cmd/dep/testdata/govendor/vendor-vendored-dep.json b/cmd/dep/testdata/govendor/vendor-vendored-dep.json deleted file mode 100644 index e8098657..00000000 --- a/cmd/dep/testdata/govendor/vendor-vendored-dep.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "comment": "", - "ignore": "test", - "package": [ - { - "checksumSHA1": "q6es/GCwizh2GbLTKWWj8bB6Lc8=", - "origin": "github.com/sdboyer/deptest/vendor/github.com/sdboyer/coolint", - "path": "github.com/sdboyer/coolint", - "revision": "a920fd05db2031ad1ba94f62035fab5def190141", - "revisionTime": "2017-07-15T19:22:11Z" - }, - { - "checksumSHA1": "c+MCTe4HOVzNBjFpjmPYWFSs06U=", - "path": "github.com/sdboyer/deptest", - "revision": "a920fd05db2031ad1ba94f62035fab5def190141", - "revisionTime": "2017-07-15T19:22:11Z" - } - ], - "rootPath": "github.com/golang/notexist" -} - diff --git a/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.lock b/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.lock index db52fe25..529e4889 100644 --- a/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.lock +++ b/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.lock @@ -1,12 +1,6 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. -[[projects]] - name = "github.com/carolynvs/go-dep-test" - packages = ["."] - revision = "6b35265ebb3003525cd3ad6498d7e81a89be106b" - version = "0.2.0" - [[projects]] name = "github.com/sdboyer/deptest" packages = ["."] @@ -22,6 +16,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f8e9ea65524b2cebafacf6c9b91441301c6b231bdaf408700a27ae7f9418a273" + inputs-digest = "9cc662f2e1b80c8df205d9d667fe2c47825a06961ceae378f44a8290d01dd359" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.toml b/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.toml index 130ae179..adef29b9 100644 --- a/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.toml +++ b/cmd/dep/testdata/harness_tests/init/govendor/case1/final/Gopkg.toml @@ -1,8 +1,4 @@ -ignored = ["github.com/sdboyer/dep-test"] - -[[constraint]] - name = "github.com/carolynvs/go-dep-test" - version = "0.2.0" +ignored = ["github.com/golang/notexist/samples*","github.com/sdboyer/dep-test*"] [[constraint]] name = "github.com/sdboyer/deptestdos" diff --git a/cmd/dep/testdata/harness_tests/init/govendor/case1/testcase.json b/cmd/dep/testdata/harness_tests/init/govendor/case1/testcase.json index 7a1a04de..e1197654 100644 --- a/cmd/dep/testdata/harness_tests/init/govendor/case1/testcase.json +++ b/cmd/dep/testdata/harness_tests/init/govendor/case1/testcase.json @@ -8,7 +8,6 @@ "github.com/sdboyer/deptestdos": "5c607206be5decd28e6263ffffdcee067266015e" }, "vendor-final": [ - "github.com/carolynvs/go-dep-test", "github.com/sdboyer/deptest", "github.com/sdboyer/deptestdos" ] diff --git a/internal/importers/base/importer.go b/internal/importers/base/importer.go index 8b3be146..9d17b072 100644 --- a/internal/importers/base/importer.go +++ b/internal/importers/base/importer.go @@ -6,6 +6,7 @@ package base import ( "log" + "strings" "github.com/golang/dep" "github.com/golang/dep/gps" @@ -16,28 +17,27 @@ import ( // Importer provides a common implementation for importing from other // dependency managers. type Importer struct { - sm gps.SourceManager - - Logger *log.Logger - Verbose bool - Manifest *dep.Manifest - Lock *dep.Lock + SourceManager gps.SourceManager + Logger *log.Logger + Verbose bool + Manifest *dep.Manifest + Lock *dep.Lock } // NewImporter creates a new Importer for embedding in an importer. func NewImporter(logger *log.Logger, verbose bool, sm gps.SourceManager) *Importer { return &Importer{ - Logger: logger, - Verbose: verbose, - Manifest: dep.NewManifest(), - Lock: &dep.Lock{}, - sm: sm, + Logger: logger, + Verbose: verbose, + Manifest: dep.NewManifest(), + Lock: &dep.Lock{}, + SourceManager: sm, } } // isTag determines if the specified value is a tag (plain or semver). func (i *Importer) isTag(pi gps.ProjectIdentifier, value string) (bool, gps.Version, error) { - versions, err := i.sm.ListVersions(pi) + versions, err := i.SourceManager.ListVersions(pi) if err != nil { return false, nil, errors.Wrapf(err, "unable to list versions for %s(%s)", pi.ProjectRoot, pi.Source) } @@ -61,7 +61,7 @@ func (i *Importer) isTag(pi gps.ProjectIdentifier, value string) (bool, gps.Vers // manifest, then finally the revision. func (i *Importer) lookupVersionForLockedProject(pi gps.ProjectIdentifier, c gps.Constraint, rev gps.Revision) (gps.Version, error) { // Find the version that goes with this revision, if any - versions, err := i.sm.ListVersions(pi) + versions, err := i.SourceManager.ListVersions(pi) if err != nil { return rev, errors.Wrapf(err, "Unable to lookup the version represented by %s in %s(%s). Falling back to locking the revision only.", rev, pi.ProjectRoot, pi.Source) } @@ -130,7 +130,7 @@ func (i *Importer) loadPackages(packages []ImportedPackage) ([]importedProject, projects := make(map[gps.ProjectRoot]*importedProject, len(packages)) for _, pkg := range packages { - pr, err := i.sm.DeduceProjectRoot(pkg.Name) + pr, err := i.SourceManager.DeduceProjectRoot(pkg.Name) if err != nil { return nil, errors.Wrapf(err, "Cannot determine the project root for %s", pkg.Name) } @@ -188,6 +188,9 @@ func (i *Importer) ImportPackages(packages []ImportedPackage, defaultConstraintF source = "" } else if isDefault { source = "" + } else if strings.Contains(source, "/vendor/") { + i.Logger.Printf(" Ignoring imported source %s for %s because vendored sources aren't supported", source, prj.Root) + source = "" } } @@ -198,7 +201,7 @@ func (i *Importer) ImportPackages(packages []ImportedPackage, defaultConstraintF }, } - pc.Constraint, err = i.sm.InferConstraint(prj.ConstraintHint, pc.Ident) + pc.Constraint, err = i.SourceManager.InferConstraint(prj.ConstraintHint, pc.Ident) if err != nil { pc.Constraint = gps.Any() } @@ -306,12 +309,12 @@ func (i *Importer) convertToConstraint(v gps.Version) gps.Constraint { func (i *Importer) isDefaultSource(projectRoot gps.ProjectRoot, sourceURL string) (bool, error) { // this condition is mainly for gopkg.in imports, // as some importers specify the repository url as https://gopkg.in/..., - // but sm.SourceURLsForPath() returns https://github.com/... urls for gopkg.in + // but SourceManager.SourceURLsForPath() returns https://github.com/... urls for gopkg.in if sourceURL == "https://"+string(projectRoot) { return true, nil } - sourceURLs, err := i.sm.SourceURLsForPath(string(projectRoot)) + sourceURLs, err := i.SourceManager.SourceURLsForPath(string(projectRoot)) if err != nil { return false, err } diff --git a/internal/importers/base/importer_test.go b/internal/importers/base/importer_test.go index be76ecde..8d38b816 100644 --- a/internal/importers/base/importer_test.go +++ b/internal/importers/base/importer_test.go @@ -382,7 +382,7 @@ func TestBaseImporter_ImportProjects(t *testing.T) { }, }, }, - "ignoring default source": { + "ignore default source": { importertest.TestCase{ WantConstraint: "*", WantSourceRepo: "", @@ -394,6 +394,19 @@ func TestBaseImporter_ImportProjects(t *testing.T) { }, }, }, + "ignore vendored source": { + importertest.TestCase{ + WantConstraint: "*", + WantSourceRepo: "", + WantWarning: "vendored sources aren't supported", + }, + []ImportedPackage{ + { + Name: importertest.Project, + Source: "example.com/vendor/" + importertest.Project, + }, + }, + }, } for name, tc := range testcases { diff --git a/internal/importers/govendor/importer.go b/internal/importers/govendor/importer.go new file mode 100644 index 00000000..f7a5d44d --- /dev/null +++ b/internal/importers/govendor/importer.go @@ -0,0 +1,150 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package govendor + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "path" + "path/filepath" + "strings" + + "github.com/golang/dep" + "github.com/golang/dep/gps" + "github.com/golang/dep/internal/importers/base" + "github.com/pkg/errors" +) + +const govendorDir = "vendor" +const govendorName = "vendor.json" + +// Importer imports govendor configuration into the dep configuration format. +type Importer struct { + *base.Importer + + file govendorFile +} + +// NewImporter for govendor. +func NewImporter(logger *log.Logger, verbose bool, sm gps.SourceManager) *Importer { + return &Importer{Importer: base.NewImporter(logger, verbose, sm)} +} + +// File is the structure of the vendor file. +type govendorFile struct { + RootPath string // Import path of vendor folder + Ignore string + Package []*govendorPackage +} + +// Package represents each package. +type govendorPackage struct { + // See the vendor spec for definitions. + Origin string + Path string + Revision string + Version string +} + +// Name of the importer. +func (g *Importer) Name() string { + return "govendor" +} + +// HasDepMetadata checks if a directory contains config that the importer can handle. +func (g *Importer) HasDepMetadata(dir string) bool { + y := filepath.Join(dir, govendorDir, govendorName) + if _, err := os.Stat(y); err != nil { + return false + } + return true +} + +// Import the config found in the directory. +func (g *Importer) Import(dir string, pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, error) { + err := g.load(dir) + if err != nil { + return nil, nil, err + } + return g.convert(pr) +} + +func (g *Importer) load(projectDir string) error { + g.Logger.Println("Detected govendor configuration file...") + v := filepath.Join(projectDir, govendorDir, govendorName) + if g.Verbose { + g.Logger.Printf(" Loading %s", v) + } + vb, err := ioutil.ReadFile(v) + if err != nil { + return errors.Wrapf(err, "unable to read %s", v) + } + err = json.Unmarshal(vb, &g.file) + if err != nil { + return errors.Wrapf(err, "unable to parse %s", v) + } + return nil +} + +func (g *Importer) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, error) { + g.Logger.Println("Converting from vendor.json...") + + packages := make([]base.ImportedPackage, 0, len(g.file.Package)) + for _, pkg := range g.file.Package { + // Path must not be empty + if pkg.Path == "" { + err := errors.New("invalid govendor configuration, Path is required") + return nil, nil, err + } + + // There are valid govendor configs in the wild that don't have a revision set + // so we are not requiring it to be set during import + + ip := base.ImportedPackage{ + Name: pkg.Path, + Source: pkg.Origin, + LockHint: pkg.Revision, + } + packages = append(packages, ip) + } + + err := g.ImportPackages(packages, true) + if err != nil { + return nil, nil, err + } + + if len(g.file.Ignore) > 0 { + // Govendor has three use cases here + // 1. 'test' - special case for ignoring test files + // 2. build tags - any string without a slash (/) in it + // 3. path and path prefix - any string with a slash (/) in it. + // The path case could be a full path or just a prefix. + // Dep doesn't support build tags right now: https://github.com/golang/dep/issues/120 + for _, i := range strings.Split(g.file.Ignore, " ") { + if !strings.Contains(i, "/") { + g.Logger.Printf(" Govendor was configured to ignore the %s build tag, but that isn't supported by dep yet, and will be ignored. See https://github.com/golang/dep/issues/291.", i) + continue + } + + var ignorePattern string + _, err := g.SourceManager.DeduceProjectRoot(i) + if err == nil { // external package + ignorePattern = i + } else { // relative package path in the current project + ignorePattern = path.Join(string(pr), i) + } + + // Convert to a a wildcard ignore + ignorePattern = strings.TrimRight(ignorePattern, "/") + ignorePattern += "*" + + g.Manifest.Ignored = append(g.Manifest.Ignored, ignorePattern) + } + } + + return g.Manifest, g.Lock, nil +} diff --git a/internal/importers/govendor/importer_test.go b/internal/importers/govendor/importer_test.go new file mode 100644 index 00000000..f0e28f70 --- /dev/null +++ b/internal/importers/govendor/importer_test.go @@ -0,0 +1,155 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package govendor + +import ( + "bytes" + "log" + "path/filepath" + "testing" + + "github.com/golang/dep" + "github.com/golang/dep/gps" + "github.com/golang/dep/internal/importers/importertest" + "github.com/golang/dep/internal/test" + "github.com/pkg/errors" +) + +const testGovendorProjectRoot = "github.com/golang/notexist" + +func TestGovendorConfig_Import(t *testing.T) { + h := test.NewHelper(t) + defer h.Cleanup() + + ctx := importertest.NewTestContext(h) + sm, err := ctx.SourceManager() + h.Must(err) + defer sm.Release() + + h.TempDir(filepath.Join("src", testGovendorProjectRoot)) + h.TempCopy(filepath.Join(testGovendorProjectRoot, govendorDir, govendorName), "vendor.json") + projectRoot := h.Path(testGovendorProjectRoot) + + // Capture stderr so we can verify output + verboseOutput := &bytes.Buffer{} + ctx.Err = log.New(verboseOutput, "", 0) + + g := NewImporter(ctx.Err, false, sm) // Disable verbose so that we don't print values that change each test run + if !g.HasDepMetadata(projectRoot) { + t.Fatal("Expected the importer to detect the govendor configuration files") + } + + m, l, err := g.Import(projectRoot, testGovendorProjectRoot) + h.Must(err) + + if m == nil { + t.Fatal("Expected the manifest to be generated") + } + + if l == nil { + t.Fatal("Expected the lock to be generated") + } + + goldenFile := "golden.txt" + got := verboseOutput.String() + want := h.GetTestFileString(goldenFile) + if want != got { + if *test.UpdateGolden { + if err := h.WriteTestFile(goldenFile, got); err != nil { + t.Fatalf("%+v", errors.Wrapf(err, "Unable to write updated golden file %s", goldenFile)) + } + } else { + t.Fatalf("expected %s, got %s", want, got) + } + } +} + +func TestGovendorConfig_Convert(t *testing.T) { + testCases := map[string]struct { + file govendorFile + importertest.TestCase + }{ + "project": { + govendorFile{ + Package: []*govendorPackage{ + { + Path: importertest.Project, + Origin: importertest.ProjectSrc, + Revision: importertest.V1Rev, + }, + }, + }, + importertest.TestCase{ + WantSourceRepo: importertest.ProjectSrc, + WantConstraint: importertest.V1Constraint, + WantRevision: importertest.V1Rev, + WantVersion: importertest.V1Tag, + }, + }, + "skipped build tags": { + govendorFile{ + Ignore: "test linux_amd64", + }, + importertest.TestCase{ + WantIgnored: nil, + }, + }, + "ignored external package": { + govendorFile{ + Ignore: "github.com/sdboyer/deptest k8s.io/apimachinery", + }, + importertest.TestCase{ + WantIgnored: []string{"github.com/sdboyer/deptest*", "k8s.io/apimachinery*"}, + }, + }, + "ignored internal package": { + govendorFile{ + Ignore: "samples/ foo/bar", + }, + importertest.TestCase{ + WantIgnored: []string{importertest.RootProject + "/samples*", importertest.RootProject + "/foo/bar*"}, + }, + }, + "missing package path": { + govendorFile{ + Package: []*govendorPackage{ + { + Revision: importertest.V2PatchRev, + }, + }, + }, + importertest.TestCase{ + WantConvertErr: true, + }, + }, + "missing package revision doesn't cause an error": { + govendorFile{ + Package: []*govendorPackage{ + { + Path: importertest.Project, + }, + }, + }, + importertest.TestCase{ + WantConstraint: "*", + }, + }, + } + + for name, testCase := range testCases { + name := name + testCase := testCase + t.Run(name, func(t *testing.T) { + err := testCase.Execute(t, func(logger *log.Logger, sm gps.SourceManager) (*dep.Manifest, *dep.Lock, error) { + g := NewImporter(logger, true, sm) + g.file = testCase.file + return g.convert(importertest.RootProject) + }) + if err != nil { + t.Fatalf("%#v", err) + } + }) + } +} diff --git a/cmd/dep/testdata/govendor/golden.txt b/internal/importers/govendor/testdata/golden.txt similarity index 100% rename from cmd/dep/testdata/govendor/golden.txt rename to internal/importers/govendor/testdata/golden.txt index 4fa314d1..51a348f7 100644 --- a/cmd/dep/testdata/govendor/golden.txt +++ b/internal/importers/govendor/testdata/golden.txt @@ -1,7 +1,7 @@ Detected govendor configuration file... Converting from vendor.json... - Govendor was configured to ignore the test build tag, but that isn't supported by dep yet, and will be ignored. See https://github.com/golang/dep/issues/291. Using ^0.8.1 as initial constraint for imported dep github.com/sdboyer/deptest Trying v0.8.1 (3f4c3be) as initial lock for imported dep github.com/sdboyer/deptest Using ^2.0.0 as initial constraint for imported dep github.com/sdboyer/deptestdos Trying v2.0.0 (5c60720) as initial lock for imported dep github.com/sdboyer/deptestdos + Govendor was configured to ignore the test build tag, but that isn't supported by dep yet, and will be ignored. See https://github.com/golang/dep/issues/291. diff --git a/cmd/dep/testdata/govendor/vendor.json b/internal/importers/govendor/testdata/vendor.json similarity index 100% rename from cmd/dep/testdata/govendor/vendor.json rename to internal/importers/govendor/testdata/vendor.json diff --git a/internal/importers/importers.go b/internal/importers/importers.go index 283effee..3f535e2c 100644 --- a/internal/importers/importers.go +++ b/internal/importers/importers.go @@ -12,6 +12,7 @@ import ( "github.com/golang/dep/internal/importers/glide" "github.com/golang/dep/internal/importers/godep" "github.com/golang/dep/internal/importers/govend" + "github.com/golang/dep/internal/importers/govendor" "github.com/golang/dep/internal/importers/gvt" "github.com/golang/dep/internal/importers/vndr" ) @@ -37,5 +38,6 @@ func BuildAll(logger *log.Logger, verbose bool, sm gps.SourceManager) []Importer vndr.NewImporter(logger, verbose, sm), govend.NewImporter(logger, verbose, sm), gvt.NewImporter(logger, verbose, sm), + govendor.NewImporter(logger, verbose, sm), } } diff --git a/internal/importers/importertest/testcase.go b/internal/importers/importertest/testcase.go index c3fa62ff..0d629531 100644 --- a/internal/importers/importertest/testcase.go +++ b/internal/importers/importertest/testcase.go @@ -79,7 +79,7 @@ func (tc TestCase) validate(manifest *dep.Manifest, lock *dep.Lock, convertErr e } if !equalSlice(manifest.Ignored, tc.WantIgnored) { - return errors.Errorf("unexpected set of ignored projects: \n\t(GOT) %v \n\t(WNT) %v", + return errors.Errorf("unexpected set of ignored projects: \n\t(GOT) %#v \n\t(WNT) %#v", manifest.Ignored, tc.WantIgnored) }