diff --git a/cmd/dep/gb_importer.go b/cmd/dep/gb_importer.go new file mode 100644 index 0000000000..ae120305d1 --- /dev/null +++ b/cmd/dep/gb_importer.go @@ -0,0 +1,169 @@ +// Copyright 2017 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" + + "github.com/golang/dep" + fb "github.com/golang/dep/internal/feedback" + "github.com/golang/dep/internal/gps" + "github.com/pkg/errors" +) + +// gbImporter imports gb configuration into the dep configuration format. +type gbImporter struct { + manifest gbManifest + logger *log.Logger + verbose bool + sm gps.SourceManager +} + +func newGbImporter(logger *log.Logger, verbose bool, sm gps.SourceManager) *gbImporter { + return &gbImporter{ + logger: logger, + verbose: verbose, + sm: sm, + } +} + +// gbManifest represents the manifest file for GB projects +type gbManifest struct { + Version int `json:"version"` + Dependencies []gbDependency `json:"dependencies"` +} + +type gbDependency struct { + Importpath string `json:"importpath"` + Repository string `json:"repository"` + + // All gb vendored dependencies have a specific revision + Revision string `json:"revision"` + + // Branch may be HEAD or an actual branch. In the case of HEAD, that means + // the user vendored a dependency by specifying a tag or a specific revision + // which results in a detached HEAD + Branch string `json:"branch"` + + // Path is used when the actual used package exists in a subpath + // For importing purposes, this isn't necessary as dep will figure + // out which packages are actually being used during resolving + Path string `json:"path,omitempty"` +} + +func (i *gbImporter) Name() string { + return "gb" +} + +func (i *gbImporter) HasDepMetadata(dir string) bool { + // gb stores the manifest in the vendor tree + var m = filepath.Join(dir, "vendor", "manifest") + if _, err := os.Stat(m); err != nil { + return false + } + + return true +} + +func (i *gbImporter) Import(dir string, pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, error) { + err := i.load(dir) + if err != nil { + return nil, nil, err + } + + return i.convert(pr) +} + +// load the gb manifest +func (i *gbImporter) load(projectDir string) error { + i.logger.Println("Detected gb manifest file...") + var err error + var mf = filepath.Join(projectDir, "vendor", "manifest") + if i.verbose { + i.logger.Printf(" Loading %s", mf) + } + + var buf []byte + if buf, err = ioutil.ReadFile(mf); err != nil { + return errors.Wrapf(err, "Unable to read %s", mf) + } + if err = json.Unmarshal(buf, &i.manifest); err != nil { + return errors.Wrapf(err, "Unable to parse %s", mf) + } + + return nil +} + +// convert the gb manifest into dep configuration files. +func (i *gbImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, error) { + i.logger.Println("Converting from gb manifest...") + + manifest := &dep.Manifest{ + Constraints: make(gps.ProjectConstraints), + } + + lock := &dep.Lock{} + + for _, pkg := range i.manifest.Dependencies { + pc, lp, err := i.convertOne(pkg) + if err != nil { + return nil, nil, err + } + manifest.Constraints[pc.Ident.ProjectRoot] = gps.ProjectProperties{Source: pc.Ident.Source, Constraint: pc.Constraint} + lock.P = append(lock.P, lp) + } + + return manifest, lock, nil +} + +func (i *gbImporter) convertOne(pkg gbDependency) (pc gps.ProjectConstraint, lp gps.LockedProject, err error) { + if pkg.Importpath == "" { + err = errors.New("Invalid gb configuration, package import path is required") + return + } + + /* + gb's vendor plugin (gb vendor), which manages the vendor tree and manifest + file, supports fetching by a specific tag or revision, but if you specify + either of those it's a detached checkout and the "branch" field is HEAD. + The only time the "branch" field is not "HEAD" is if you do not specify a + tag or revision, otherwise it's either "master" or the value of the -branch + flag + + This means that, generally, the only possible "constraint" we can really specify is + the branch name if the branch name is not HEAD. Otherwise, it's a specific revision. + + However, if we can infer a tag that points to the revision or the branch, we may be able + to use that as the constraint + */ + + pc.Ident = gps.ProjectIdentifier{ProjectRoot: gps.ProjectRoot(pkg.Importpath), Source: pkg.Repository} + + // The default constraint is just a revision + var revision = gps.Revision(pkg.Revision) + + // See if we can get a version from that constraint + version, err := lookupVersionForLockedProject(pc.Ident, revision, revision, i.sm) + if err != nil { + return + } + + // And now try to infer a constraint from the returned version + pc.Constraint, err = i.sm.InferConstraint(version.String(), pc.Ident) + if err != nil { + return + } + + lp = gps.NewLockedProject(pc.Ident, version, nil) + + fb.NewLockedProjectFeedback(lp, fb.DepTypeImported).LogFeedback(i.logger) + fb.NewConstraintFeedback(pc, fb.DepTypeImported).LogFeedback(i.logger) + + return +} diff --git a/cmd/dep/gb_importer_test.go b/cmd/dep/gb_importer_test.go new file mode 100644 index 0000000000..9ab7d3d3f5 --- /dev/null +++ b/cmd/dep/gb_importer_test.go @@ -0,0 +1,165 @@ +// Copyright 2017 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" + "testing" + + "github.com/golang/dep/internal/gps" + "github.com/golang/dep/internal/test" + "github.com/pkg/errors" +) + +const testGbProjectRoot = "github.com/golang/notexist" + +func TestGbConfig_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", testGbProjectRoot, "vendor")) + h.TempCopy(filepath.Join(testGbProjectRoot, "vendor", "manifest"), "gb/manifest") + projectRoot := h.Path(testGbProjectRoot) + + // Capture stderr so we can verify output + verboseOutput := &bytes.Buffer{} + ctx.Err = log.New(verboseOutput, "", 0) + + g := newGbImporter(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 gb manifest file") + } + + m, l, err := g.Import(projectRoot, testGbProjectRoot) + 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 := "gb/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 TestGbConfig_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" + repo := "https://github.com/sdboyer/deptest.git" + + g := newGbImporter(ctx.Err, true, sm) + g.manifest = gbManifest{ + Dependencies: []gbDependency{ + { + Importpath: pkg, + Repository: repo, + Revision: "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", + }, + }, + } + + manifest, lock, err := g.convert(testGbProjectRoot) + if err != nil { + t.Fatal(err) + } + + 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) + } + + gotS := d.Source + if gotS != repo { + t.Fatalf("Expected manifest source to be %s, got %s", repo, gotS) + } + + 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) + } + + gotS = p.Ident().Source + if gotS != repo { + t.Fatalf("Expected locked source to be %s, got '%s'", repo, gotS) + } + + 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 TestGbConfig_Convert_BadInput_EmptyPackageName(t *testing.T) { + h := test.NewHelper(t) + defer h.Cleanup() + + ctx := newTestContext(h) + sm, err := ctx.SourceManager() + h.Must(err) + defer sm.Release() + + g := newGbImporter(ctx.Err, true, sm) + g.manifest = gbManifest{ + Dependencies: []gbDependency{{Importpath: ""}}, + } + + _, _, err = g.convert(testGbProjectRoot) + if err == nil { + t.Fatal("Expected conversion to fail because the package name is empty") + } +} diff --git a/cmd/dep/root_analyzer.go b/cmd/dep/root_analyzer.go index 9f93b7eb22..c144085c60 100644 --- a/cmd/dep/root_analyzer.go +++ b/cmd/dep/root_analyzer.go @@ -73,6 +73,7 @@ func (a *rootAnalyzer) importManifestAndLock(dir string, pr gps.ProjectRoot, sup importers := []importer{ newGlideImporter(logger, a.ctx.Verbose, a.sm), newGodepImporter(logger, a.ctx.Verbose, a.sm), + newGbImporter(logger, a.ctx.Verbose, a.sm), } for _, i := range importers { diff --git a/cmd/dep/testdata/gb/golden.txt b/cmd/dep/testdata/gb/golden.txt new file mode 100644 index 0000000000..09d438e7dd --- /dev/null +++ b/cmd/dep/testdata/gb/golden.txt @@ -0,0 +1,7 @@ +Detected gb manifest file... +Converting from gb manifest... + Trying master (2788f0d) as initial lock for imported dep github.com/kr/fs + Using master as initial constraint for imported dep github.com/kr/fs + Using f234c3c6540c0358b1802f7fd90c0879af9232eb as initial hint for imported dep github.com/pkg/sftp + Trying v1.0.0 (ff2948a) as initial lock for imported dep github.com/sdboyer/deptest + Using ^1.0.0 as initial constraint for imported dep github.com/sdboyer/deptest diff --git a/cmd/dep/testdata/gb/manifest b/cmd/dep/testdata/gb/manifest new file mode 100644 index 0000000000..68c1d7b4cc --- /dev/null +++ b/cmd/dep/testdata/gb/manifest @@ -0,0 +1,25 @@ +{ + "version": 0, + "dependencies": [ + { + "importpath": "github.com/kr/fs", + "repository": "https://github.com/kr/fs", + "revision": "2788f0dbd16903de03cb8186e5c7d97b69ad387b", + "branch": "master", + "path": "" + }, + { + "importpath": "github.com/pkg/sftp", + "repository": "https://github.com/pkg/sftp", + "revision": "f234c3c6540c0358b1802f7fd90c0879af9232eb", + "branch": "master", + "path": "" + }, + { + "importpath": "github.com/sdboyer/deptest", + "repository": "https://github.com/sdboyer/deptest", + "revision": "ff2948a2ac8f538c4ecd55962e919d1e13e74baf", + "branch": "HEAD" + } + ] +}