getproviders: Normalize versions before dedupe

When rendering a set of version constraints to a string, we normalize
partially-constrained versions. This means converting a version
like 2.68.* to 2.68.0.

Prior to this commit, this normalization was done after deduplication.
This could result in a version constraints string with duplicate
entries, if multiple partially-constrained versions are equivalent. This
commit fixes this by normalizing before deduplicating and sorting.
This commit is contained in:
Alisdair McDiarmid 2020-11-02 10:01:08 -05:00
parent 3ed08e3566
commit b1bc0e5d92
2 changed files with 24 additions and 17 deletions

View File

@ -407,7 +407,18 @@ func VersionConstraintsString(spec VersionConstraints) string {
// and sort them into a consistent order.
sels := make(map[constraints.SelectionSpec]struct{})
for _, sel := range spec {
sels[sel] = struct{}{}
// The parser allows writing abbreviated version (such as 2) which
// end up being represented in memory with trailing unconstrained parts
// (for example 2.*.*). For the purpose of serialization with Ruby
// style syntax, these unconstrained parts can all be represented as 0
// with no loss of meaning, so we make that conversion here. Doing so
// allows us to deduplicate equivalent constraints, such as >= 2.0 and
// >= 2.0.0.
normalizedSel := constraints.SelectionSpec{
Operator: sel.Operator,
Boundary: sel.Boundary.ConstrainToZero(),
}
sels[normalizedSel] = struct{}{}
}
selsOrder := make([]constraints.SelectionSpec, 0, len(sels))
for sel := range sels {
@ -450,34 +461,26 @@ func VersionConstraintsString(spec VersionConstraints) string {
b.WriteString("??? ")
}
// The parser allows writing abbreviated version (such as 2) which
// end up being represented in memory with trailing unconstrained parts
// (for example 2.*.*). For the purpose of serialization with Ruby
// style syntax, these unconstrained parts can all be represented as 0
// with no loss of meaning, so we make that conversion here.
//
// This is possible because we use a different constraint operator to
// distinguish between the two types of pessimistic constraint:
// minor-only and patch-only. For minor-only constraints, we always
// want to display only the major and minor version components, so we
// special-case that operator below.
// We use a different constraint operator to distinguish between the
// two types of pessimistic constraint: minor-only and patch-only. For
// minor-only constraints, we always want to display only the major and
// minor version components, so we special-case that operator below.
//
// One final edge case is a minor-only constraint specified with only
// the major version, such as ~> 2. We treat this the same as ~> 2.0,
// because a major-only pessimistic constraint does not exist: it is
// logically identical to >= 2.0.0.
boundary := sel.Boundary.ConstrainToZero()
if sel.Operator == constraints.OpGreaterThanOrEqualMinorOnly {
// The minor-pessimistic syntax uses only two version components.
fmt.Fprintf(&b, "%s.%s", boundary.Major, boundary.Minor)
fmt.Fprintf(&b, "%s.%s", sel.Boundary.Major, sel.Boundary.Minor)
} else {
fmt.Fprintf(&b, "%s.%s.%s", boundary.Major, boundary.Minor, boundary.Patch)
fmt.Fprintf(&b, "%s.%s.%s", sel.Boundary.Major, sel.Boundary.Minor, sel.Boundary.Patch)
}
if sel.Boundary.Prerelease != "" {
b.WriteString("-" + boundary.Prerelease)
b.WriteString("-" + sel.Boundary.Prerelease)
}
if sel.Boundary.Metadata != "" {
b.WriteString("+" + boundary.Metadata)
b.WriteString("+" + sel.Boundary.Metadata)
}
}
return b.String()

View File

@ -53,6 +53,10 @@ func TestVersionConstraintsString(t *testing.T) {
MustParseVersionConstraints(">= 1.2.3, 1.2.3, ~> 1.2, 1.2.3"),
"~> 1.2, >= 1.2.3, 1.2.3",
},
"equivalent duplicates removed": {
MustParseVersionConstraints(">= 2.68, >= 2.68.0"),
">= 2.68.0",
},
"consistent ordering, exhaustive": {
// This weird jumble is just to exercise the different sort
// ordering codepaths. Hopefully nothing quite this horrific