| // 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 |
| } |