blob: 204fda9b49a52f0b20a5cfb7d8908574e9492052 [file] [log] [blame]
// Copyright 2019 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 version handles version types.
package version
import (
"fmt"
"regexp"
"strings"
"golang.org/x/mod/semver"
)
// Type defines the version types a module can have.
// This must be kept in sync with the 'version_type' database enum.
type Type string
const (
// TypeRelease is a normal release.
TypeRelease = Type("release")
// TypePrerelease is a version with a prerelease.
TypePrerelease = Type("prerelease")
// TypePseudo appears to have a prerelease of the
// form <commit date>-<commit hash>.
TypePseudo = Type("pseudo")
)
func (t Type) String() string {
return string(t)
}
var pseudoVersionRE = regexp.MustCompile(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+incompatible)?$`)
// IsPseudo reports whether a valid version v is a pseudo-version.
// Modified from src/cmd/go/internal/modfetch.
func IsPseudo(v string) bool {
return strings.Count(v, "-") >= 2 && pseudoVersionRE.MatchString(v)
}
// IsIncompatible reports whether a valid version v is an incompatible version.
func IsIncompatible(v string) bool {
return strings.HasSuffix(v, "+incompatible")
}
// ParseType returns the Type of a given a version.
func ParseType(version string) (Type, error) {
if !semver.IsValid(version) {
return "", fmt.Errorf("ParseType(%q): invalid semver", version)
}
switch {
case IsPseudo(version):
return TypePseudo, nil
case semver.Prerelease(version) != "":
return TypePrerelease, nil
default:
return TypeRelease, nil
}
}
// ForSorting returns a string that encodes version, so that comparing two such
// strings follows SemVer precedence, https://semver.org clause 11. It assumes
// version is valid. The returned string ends in '~' if and only if the version
// does not have a prerelease.
//
// For examples, see TestForSorting.
func ForSorting(version string) string {
bytes := make([]byte, 0, len(version))
prerelease := false // we are in the prerelease part
nondigit := false // this part has a non-digit character
start := 1 // skip 'v'
last := len(version)
// Add the semver component version[start:end] to the result.
addPart := func(end int) {
if len(bytes) > 0 {
// ',' comes before '-' and all letters and digits, so it correctly
// imposes lexicographic ordering on the parts of the version.
bytes = append(bytes, ',')
}
if nondigit {
// Prepending the largest printable character '~' to a non-numeric
// part, along with the fact that encoded numbers never begin with a
// '~', (see appendNumericPrefix), ensures the semver requirement
// that numeric identifiers always have lower precedence than
// non-numeric ones.
bytes = append(bytes, '~')
} else {
bytes = appendNumericPrefix(bytes, end-start)
}
bytes = append(bytes, version[start:end]...)
start = end + 1 // skip over separator character
nondigit = false
}
loop:
for i, c := range version[start:] {
p := i + 1
switch {
case c == '.': // end of a part
addPart(p)
case c == '-': // first one is start of prerelease
if !prerelease {
prerelease = true
addPart(p)
} else {
nondigit = true
}
case c == '+': // start of build; nothing after this matters
last = p
break loop
case c < '0' || c > '9':
nondigit = true
}
}
if start < last {
addPart(last)
}
if !prerelease {
// Make sure prereleases appear first.
bytes = append(bytes, '~')
}
return string(bytes)
}
// appendNumericPrefix appends a string representing n to dst.
// n is the length of a digit string; the value we append is a prefix for the
// digit string s such that
// prefix1 + s1 < prefix2 + s2
// if and only if the integer denoted by s1 is less than the one denoted by s2.
// In other words, prefix + s is a string that can be compared with other such
// strings while preserving the ordering of the numbers.
//
// If n==1, there is no prefix. (Single-digit numbers are unchanged.)
// Otherwise, the prefix is a sequence of lower-case letters encoding n.
// Examples:
// n prefix
// 1 <none>
// 2 a
// 27 z
// 28 za
// 53 zz
// 54 zza
// This encoding depends on the ASCII properties that:
// - digits are ordered numerically
// - letters are ordered alphabetically
// - digits order before letters (so "1" < "a10")
func appendNumericPrefix(dst []byte, n int) []byte {
n--
for i := 0; i < n/26; i++ {
dst = append(dst, 'z')
}
if rem := n % 26; rem > 0 {
dst = append(dst, byte('a'+rem-1))
}
return dst
}
// Later reports whether v1 is later than v2, using semver but preferring
// release versions to pre-release versions.
func Later(v1, v2 string) bool {
pre1 := semver.Prerelease(v1) != ""
pre2 := semver.Prerelease(v2) != ""
if pre1 == pre2 {
return semver.Compare(v1, v2) > 0
}
return !pre1
}
// Latest returns the latest version of a module from a list of versions, using
// the go command's definition of latest: semver is observed, except that
// release versions are preferred to prerelease.
// If allow is non-nil, then only versions for which allow returns true will be
// considered. If no versions are allowed, the empty string is returned.
func Latest(versions []string, allow func(string) bool) string {
var latest string
for _, v := range versions {
if allow != nil && !allow(v) {
continue
}
if latest == "" || Later(v, latest) {
latest = v
}
}
return latest
}