| // Copyright 2023 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") | |
| ) | |
| const ( | |
| // Latest signifies the latest available version in requests to the | |
| // proxy client. | |
| Latest = "latest" | |
| // Main represents the main branch. | |
| Main = "main" | |
| // Master represents the master branch. | |
| Master = "master" | |
| ) | |
| 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, and both to pseudo-versions. | |
| func Later(v1, v2 string) bool { | |
| rel1 := semver.Prerelease(v1) == "" | |
| rel2 := semver.Prerelease(v2) == "" | |
| if rel1 && rel2 { | |
| return semver.Compare(v1, v2) > 0 | |
| } | |
| if rel1 != rel2 { | |
| return rel1 | |
| } | |
| // Both are pre-release. | |
| pseudo1 := IsPseudo(v1) | |
| pseudo2 := IsPseudo(v2) | |
| if pseudo1 == pseudo2 { | |
| return semver.Compare(v1, v2) > 0 | |
| } | |
| return !pseudo1 | |
| } | |
| // LatestVersion finds the latest version of a module using the same algorithm as the | |
| // Go command. It prefers tagged release versions to tagged pre-release | |
| // versions, and both of those to pseudo-versions. If versions is empty, LatestVersion | |
| // returns the empty string. | |
| // | |
| // hasGoMod should report whether the version it is given has a go.mod file. | |
| // LatestVersion returns the latest incompatible version only if the latest compatible | |
| // version does not have a go.mod file. | |
| // | |
| // The meaning of latest is defined at | |
| // https://golang.org/ref/mod#version-queries. That definition does not deal | |
| // with retractions, or with a subtlety involving incompatible versions. The | |
| // actual definition is embodied in the go command's queryMatcher.filterVersions | |
| // method. This function is a re-implementation and specialization of that | |
| // method at Go version 1.16 | |
| // (https://go.googlesource.com/go/+/refs/tags/go1.16/src/cmd/go/internal/modload/query.go#441). | |
| func LatestVersion(versions []string, hasGoMod func(v string) (bool, error)) (v string, err error) { | |
| latest := LatestOf(versions) | |
| if latest == "" { | |
| return "", nil | |
| } | |
| // If the latest is a compatible version, use it. | |
| if !IsIncompatible(latest) { | |
| return latest, nil | |
| } | |
| // The latest version is incompatible. If there is a go.mod file at the | |
| // latest compatible tagged version, assume the module author has adopted | |
| // proper versioning, and use that latest compatible version. Otherwise, use | |
| // this incompatible version. | |
| latestCompat := LatestOf(RemoveIf(versions, func(v string) bool { return IsIncompatible(v) || IsPseudo(v) })) | |
| if latestCompat == "" { | |
| // No compatible versions; use the latest (incompatible) version. | |
| return latest, nil | |
| } | |
| latestCompatHasGoMod, err := hasGoMod(latestCompat) | |
| if err != nil { | |
| return "", err | |
| } | |
| if latestCompatHasGoMod { | |
| return latestCompat, nil | |
| } | |
| return latest, nil | |
| } | |
| // LatestOf 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, and both are preferred to pseudo-versions. | |
| // If versions is empty, the empty string is returned. | |
| func LatestOf(versions []string) string { | |
| if len(versions) == 0 { | |
| return "" | |
| } | |
| latest := versions[0] | |
| for _, v := range versions[1:] { | |
| if Later(v, latest) { | |
| latest = v | |
| } | |
| } | |
| return latest | |
| } | |
| // RemoveIf returns a copy of s that omits all values for which f returns true. | |
| func RemoveIf(s []string, f func(string) bool) []string { | |
| var r []string | |
| for _, x := range s { | |
| if !f(x) { | |
| r = append(r, x) | |
| } | |
| } | |
| return r | |
| } |