Skip to content
This repository has been archived by the owner on Sep 9, 2020. It is now read-only.

Commit

Permalink
Interpret strings as branches > semver constraints
Browse files Browse the repository at this point in the history
When a user supplied string in an imported config file, or specified to
dep ensure, can be interpreted multiple ways, prefer the branch over a
semver constraint.

In #710, glide.yaml specified v2 for https://github.com/go-mgo/mgo.
When we assume that is a semver constraint, solve fails because the
hinted revision in the lock (a commit on the v2 branch) doesn't satisfy
the assumed constraint of ^2.0.0.

The new preferred match order for the user string is:

* revision
* branch
* semver constraint
* tag

I am giving preference of a semver constraint over a tag so that a bare
version, 1.0.0, is interpreted more loosely with an implied caret,
^1.0.0, instead of the stricter exact match.
  • Loading branch information
carolynvs committed Jun 11, 2017
1 parent c3259a4 commit 74056c0
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 49 deletions.
62 changes: 32 additions & 30 deletions cmd/dep/ensure.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,39 +304,19 @@ func getProjectConstraint(arg string, sm gps.SourceManager) (gps.ProjectConstrai
return gps.ProjectConstraint{Ident: pi, Constraint: c}, nil
}

// deduceConstraint tries to puzzle out what kind of version is given in a string -
// semver, a revision, or as a fallback, a plain tag
// deduceConstraint tries to puzzle out what kind of version is given in a string.
// Preference is given first for revisions, then branches, then semver constraints, then plain tags.
func deduceConstraint(s string, pi gps.ProjectIdentifier, sm gps.SourceManager) (gps.Constraint, error) {
if s == "" {
// Find the default branch
versions, err := sm.ListVersions(pi)
if err != nil {
return nil, errors.Wrapf(err, "list versions for %s(%s)", pi.ProjectRoot, pi.Source) // means repo does not exist
}

gps.SortPairedForUpgrade(versions)
for _, v := range versions {
if v.Type() == gps.IsBranch {
return v.Unpair(), nil
}
}
}

// always semver if we can
c, err := gps.NewSemverConstraintIC(s)
if err == nil {
return c, nil
}

slen := len(s)
if slen == 40 {
if _, err = hex.DecodeString(s); err == nil {
if _, err := hex.DecodeString(s); err == nil {
// Whether or not it's intended to be a SHA1 digest, this is a
// valid byte sequence for that, so go with Revision. This
// covers git and hg
return gps.Revision(s), nil
}
}

// Next, try for bzr, which has a three-component GUID separated by
// dashes. There should be two, but the email part could contain
// internal dashes
Expand All @@ -349,24 +329,46 @@ func deduceConstraint(s string, pi gps.ProjectIdentifier, sm gps.SourceManager)
}

i2 := strings.LastIndex(s[:i3], "-")
if _, err = strconv.ParseUint(s[i2+1:i3], 10, 64); err == nil {
if _, err := strconv.ParseUint(s[i2+1:i3], 10, 64); err == nil {
// Getting this far means it'd pretty much be nuts if it's not a
// bzr rev, so don't bother parsing the email.
return gps.Revision(s), nil
}
}

// call out to network and get the package's versions
// Lookup the string in the repository
var version gps.PairedVersion
versions, err := sm.ListVersions(pi)
if err != nil {
return nil, errors.Wrapf(err, "list versions for %s(%s)", pi.ProjectRoot, pi.Source) // means repo does not exist
}

for _, version := range versions {
if s == version.String() {
return version.Unpair(), nil
gps.SortPairedForUpgrade(versions)
for _, v := range versions {
// Pick the default branch if no constraint is given
if s == "" || s == v.String() {
version = v
break
}
}

// Try to interpret as a semver constraint
c, err := gps.NewSemverConstraintIC(s)

// Branch
if version != nil && version.Type() == gps.IsBranch {
return version.Unpair(), nil
}

// Semver Constraint
if c != nil && err == nil {
return c, nil
}

// Tag
if version != nil {
return version.Unpair(), nil
}

return nil, errors.Errorf("%s is not a valid version for the package %s(%s)", s, pi.ProjectRoot, pi.Source)
}

Expand Down
17 changes: 8 additions & 9 deletions cmd/dep/glide_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ func (g *glideImporter) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, e
lock = &dep.Lock{}

for _, pkg := range g.lock.Imports {
lp := g.buildLockedProject(pkg)
lp := g.buildLockedProject(pkg, manifest)
lock.P = append(lock.P, lp)
}
for _, pkg := range g.lock.TestImports {
lp := g.buildLockedProject(pkg)
lp := g.buildLockedProject(pkg, manifest)
lock.P = append(lock.P, lp)
}
}
Expand Down Expand Up @@ -213,19 +213,18 @@ func (g *glideImporter) buildProjectConstraint(pkg glidePackage) (pc gps.Project
return
}

func (g *glideImporter) buildLockedProject(pkg glideLockedPackage) gps.LockedProject {
func (g *glideImporter) buildLockedProject(pkg glideLockedPackage, manifest *dep.Manifest) gps.LockedProject {
pi := gps.ProjectIdentifier{
ProjectRoot: gps.ProjectRoot(pkg.Name),
Source: pkg.Repository,
}
revision := gps.Revision(pkg.Reference)
pp := manifest.Constraints[pi.ProjectRoot]

version, err := lookupVersionForRevision(revision, pi, g.sm)
if err != nil {
// Warn about the problem, it is not enough to warrant failing
warn := errors.Wrapf(err, "Unable to lookup the version represented by %s in %s(%s). Falling back to locking the revision only.", revision, pi.ProjectRoot, pi.Source)
g.logger.Printf(warn.Error())
version = revision
version, warning := lookupVersionForLockedProject(pi, pp.Constraint, revision, g.sm)
if warning != nil {
// Only warn about the problem, it is not enough to warrant failing
g.logger.Println(warning.Error())
}

f := fb.NewLockedProjectFeedback(version, pi.ProjectRoot, fb.DepTypeImported)
Expand Down
60 changes: 54 additions & 6 deletions cmd/dep/glide_importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ func TestGlideConfig_Import(t *testing.T) {
defer sm.Release()

h.TempDir(filepath.Join("src", testGlideProjectRoot))
h.TempCopy(filepath.Join(testGlideProjectRoot, glideYamlName), "glide/glide.yaml")
h.TempCopy(filepath.Join(testGlideProjectRoot, glideLockName), "glide/glide.lock")
h.TempCopy(filepath.Join(testGlideProjectRoot, glideYamlName), "glide/glide0.yaml")
h.TempCopy(filepath.Join(testGlideProjectRoot, glideLockName), "glide/glide0.lock")
projectRoot := h.Path(testGlideProjectRoot)

// Capture stderr so we can verify output
Expand All @@ -68,7 +68,55 @@ func TestGlideConfig_Import(t *testing.T) {
t.Fatal("Expected the lock to be generated")
}

goldenFile := "glide/expected_import_output.txt"
goldenFile := "glide/golden0.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 TestGlideConfig_Import_BranchWithSemverName(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", testGlideProjectRoot))
h.TempCopy(filepath.Join(testGlideProjectRoot, glideYamlName), "glide/glide1.yaml")
h.TempCopy(filepath.Join(testGlideProjectRoot, glideLockName), "glide/glide1.lock")
projectRoot := h.Path(testGlideProjectRoot)

// Capture stderr so we can verify output
verboseOutput := &bytes.Buffer{}
ctx.Err = log.New(verboseOutput, "", 0)

g := newGlideImporter(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 glide configuration files")
}

m, l, err := g.Import(projectRoot, testGlideProjectRoot)
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 := "glide/golden1.txt"
got := verboseOutput.String()
want := h.GetTestFileString(goldenFile)
if want != got {
Expand All @@ -91,9 +139,9 @@ func TestGlideConfig_Import_MissingLockFile(t *testing.T) {
h.Must(err)
defer sm.Release()

h.TempDir(filepath.Join("src", "glidetest"))
h.TempCopy(filepath.Join("glidetest", glideYamlName), "glide/glide.yaml")
projectRoot := h.Path("glidetest")
h.TempDir(filepath.Join("src", testGlideProjectRoot))
h.TempCopy(filepath.Join(testGlideProjectRoot, glideYamlName), "glide/glide0.yaml")
projectRoot := h.Path(testGlideProjectRoot)

g := newGlideImporter(ctx.Err, true, sm)
if !g.HasDepMetadata(projectRoot) {
Expand Down
22 changes: 18 additions & 4 deletions cmd/dep/root_analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,33 @@ func (a *rootAnalyzer) Info() (string, int) {
return name, version
}

func lookupVersionForRevision(rev gps.Revision, pi gps.ProjectIdentifier, sm gps.SourceManager) (gps.Version, error) {
func lookupVersionForLockedProject(pi gps.ProjectIdentifier, c gps.Constraint, rev gps.Revision, sm gps.SourceManager) (version gps.Version, warning error) {
// Find the version that goes with this revision, if any
versions, err := sm.ListVersions(pi)
if err != nil {
return nil, errors.Wrapf(err, "Unable to list versions for %s(%s)", pi.ProjectRoot, pi.Source)
warning = 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)
return
}

gps.SortPairedForUpgrade(versions) // Sort versions in asc order
for _, v := range versions {
if v.Underlying() == rev {
return v, nil
version = v
return
}
}

return rev, nil
// Use the version from the manifest as long as it wasn't a range
switch tv := c.(type) {
case gps.PairedVersion:
version = tv.Unpair().Is(rev)
return
case gps.UnpairedVersion:
version = tv.Is(rev)
return
}

// Give up and lock only to a revision
version = rev
return
}
File renamed without changes.
File renamed without changes.
10 changes: 10 additions & 0 deletions cmd/dep/testdata/glide/glide1.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
hash: 3b7d5be7d68cd94765ac91ea2f58b554151f67f5d0bf499ea8cadb600538a0e5
updated: 2017-05-03T15:13:27.629768623+02:00
imports:
- name: gopkg.in/mgo.v2
version: 29cc868a5ca65f401ff318143f9408d02f4799cc
subpackages:
- bson
- internal/sasl
- internal/scram
devImports: []
4 changes: 4 additions & 0 deletions cmd/dep/testdata/glide/glide1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package: github.com/golang/notexist
import:
- package: gopkg.in/mgo.v2
version: v2
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Converting from glide.yaml and glide.lock...
Using master as initial constraint for imported dep github.com/golang/lint
Trying v0.8.1 (3f4c3be) as initial lock for imported dep github.com/sdboyer/deptest
Trying v2.0.0 (5c60720) as initial lock for imported dep github.com/sdboyer/deptestdos
Trying master (cb00e56) as initial lock for imported dep github.com/golang/lint
4 changes: 4 additions & 0 deletions cmd/dep/testdata/glide/golden1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Detected glide configuration files...
Converting from glide.yaml and glide.lock...
Using v2 as initial constraint for imported dep gopkg.in/mgo.v2
Trying v2 (29cc868) as initial lock for imported dep gopkg.in/mgo.v2
4 changes: 4 additions & 0 deletions internal/feedback/feedback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func TestGetConstraintString(t *testing.T) {
feedback: GetLockingFeedback("master", "436f39d", DepTypeTransitive, "github.com/baz/qux"),
want: "Locking in master (436f39d) for transitive dep github.com/baz/qux",
},
{
feedback: GetLockingFeedback("master", "436f39d", DepTypeImported, "github.com/baz/qux"),
want: "Trying master (436f39d) as initial lock for imported dep github.com/baz/qux",
},
}

for _, c := range cases {
Expand Down

0 comments on commit 74056c0

Please sign in to comment.