| // Copyright 2018 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. |
| |
| // Pseudo-versions |
| // |
| // Code authors are expected to tag the revisions they want users to use, |
| // including prereleases. However, not all authors tag versions at all, |
| // and not all commits a user might want to try will have tags. |
| // A pseudo-version is a version with a special form that allows us to |
| // address an untagged commit and order that version with respect to |
| // other versions we might encounter. |
| // |
| // A pseudo-version takes one of the general forms: |
| // |
| // (1) vX.0.0-yyyymmddhhmmss-abcdef123456 |
| // (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 |
| // (3) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible |
| // (4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 |
| // (5) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible |
| // |
| // If there is no recently tagged version with the right major version vX, |
| // then form (1) is used, creating a space of pseudo-versions at the bottom |
| // of the vX version range, less than any tagged version, including the unlikely v0.0.0. |
| // |
| // If the most recent tagged version before the target commit is vX.Y.Z or vX.Y.Z+incompatible, |
| // then the pseudo-version uses form (2) or (3), making it a prerelease for the next |
| // possible semantic version after vX.Y.Z. The leading 0 segment in the prerelease string |
| // ensures that the pseudo-version compares less than possible future explicit prereleases |
| // like vX.Y.(Z+1)-rc1 or vX.Y.(Z+1)-1. |
| // |
| // If the most recent tagged version before the target commit is vX.Y.Z-pre or vX.Y.Z-pre+incompatible, |
| // then the pseudo-version uses form (4) or (5), making it a slightly later prerelease. |
| |
| package module |
| |
| import ( |
| "errors" |
| "fmt" |
| "strings" |
| "time" |
| |
| "golang.org/x/mod/internal/lazyregexp" |
| "golang.org/x/mod/semver" |
| ) |
| |
| var pseudoVersionRE = lazyregexp.New(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`) |
| |
| const PseudoVersionTimestampFormat = "20060102150405" |
| |
| // PseudoVersion returns a pseudo-version for the given major version ("v1") |
| // preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time, |
| // and revision identifier (usually a 12-byte commit hash prefix). |
| func PseudoVersion(major, older string, t time.Time, rev string) string { |
| if major == "" { |
| major = "v0" |
| } |
| segment := fmt.Sprintf("%s-%s", t.UTC().Format(PseudoVersionTimestampFormat), rev) |
| build := semver.Build(older) |
| older = semver.Canonical(older) |
| if older == "" { |
| return major + ".0.0-" + segment // form (1) |
| } |
| if semver.Prerelease(older) != "" { |
| return older + ".0." + segment + build // form (4), (5) |
| } |
| |
| // Form (2), (3). |
| // Extract patch from vMAJOR.MINOR.PATCH |
| i := strings.LastIndex(older, ".") + 1 |
| v, patch := older[:i], older[i:] |
| |
| // Reassemble. |
| return v + incDecimal(patch) + "-0." + segment + build |
| } |
| |
| // ZeroPseudoVersion returns a pseudo-version with a zero timestamp and |
| // revision, which may be used as a placeholder. |
| func ZeroPseudoVersion(major string) string { |
| return PseudoVersion(major, "", time.Time{}, "000000000000") |
| } |
| |
| // incDecimal returns the decimal string incremented by 1. |
| func incDecimal(decimal string) string { |
| // Scan right to left turning 9s to 0s until you find a digit to increment. |
| digits := []byte(decimal) |
| i := len(digits) - 1 |
| for ; i >= 0 && digits[i] == '9'; i-- { |
| digits[i] = '0' |
| } |
| if i >= 0 { |
| digits[i]++ |
| } else { |
| // digits is all zeros |
| digits[0] = '1' |
| digits = append(digits, '0') |
| } |
| return string(digits) |
| } |
| |
| // decDecimal returns the decimal string decremented by 1, or the empty string |
| // if the decimal is all zeroes. |
| func decDecimal(decimal string) string { |
| // Scan right to left turning 0s to 9s until you find a digit to decrement. |
| digits := []byte(decimal) |
| i := len(digits) - 1 |
| for ; i >= 0 && digits[i] == '0'; i-- { |
| digits[i] = '9' |
| } |
| if i < 0 { |
| // decimal is all zeros |
| return "" |
| } |
| if i == 0 && digits[i] == '1' && len(digits) > 1 { |
| digits = digits[1:] |
| } else { |
| digits[i]-- |
| } |
| return string(digits) |
| } |
| |
| // IsPseudoVersion reports whether v is a pseudo-version. |
| func IsPseudoVersion(v string) bool { |
| return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v) |
| } |
| |
| // IsZeroPseudoVersion returns whether v is a pseudo-version with a zero base, |
| // timestamp, and revision, as returned by ZeroPseudoVersion. |
| func IsZeroPseudoVersion(v string) bool { |
| return v == ZeroPseudoVersion(semver.Major(v)) |
| } |
| |
| // PseudoVersionTime returns the time stamp of the pseudo-version v. |
| // It returns an error if v is not a pseudo-version or if the time stamp |
| // embedded in the pseudo-version is not a valid time. |
| func PseudoVersionTime(v string) (time.Time, error) { |
| _, timestamp, _, _, err := parsePseudoVersion(v) |
| if err != nil { |
| return time.Time{}, err |
| } |
| t, err := time.Parse("20060102150405", timestamp) |
| if err != nil { |
| return time.Time{}, &InvalidVersionError{ |
| Version: v, |
| Pseudo: true, |
| Err: fmt.Errorf("malformed time %q", timestamp), |
| } |
| } |
| return t, nil |
| } |
| |
| // PseudoVersionRev returns the revision identifier of the pseudo-version v. |
| // It returns an error if v is not a pseudo-version. |
| func PseudoVersionRev(v string) (rev string, err error) { |
| _, _, rev, _, err = parsePseudoVersion(v) |
| return |
| } |
| |
| // PseudoVersionBase returns the canonical parent version, if any, upon which |
| // the pseudo-version v is based. |
| // |
| // If v has no parent version (that is, if it is "vX.0.0-[…]"), |
| // PseudoVersionBase returns the empty string and a nil error. |
| func PseudoVersionBase(v string) (string, error) { |
| base, _, _, build, err := parsePseudoVersion(v) |
| if err != nil { |
| return "", err |
| } |
| |
| switch pre := semver.Prerelease(base); pre { |
| case "": |
| // vX.0.0-yyyymmddhhmmss-abcdef123456 → "" |
| if build != "" { |
| // Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible |
| // are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag, |
| // but the "+incompatible" suffix implies that the major version of |
| // the parent tag is not compatible with the module's import path. |
| // |
| // There are a few such entries in the index generated by proxy.golang.org, |
| // but we believe those entries were generated by the proxy itself. |
| return "", &InvalidVersionError{ |
| Version: v, |
| Pseudo: true, |
| Err: fmt.Errorf("lacks base version, but has build metadata %q", build), |
| } |
| } |
| return "", nil |
| |
| case "-0": |
| // vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z |
| // vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible |
| base = strings.TrimSuffix(base, pre) |
| i := strings.LastIndexByte(base, '.') |
| if i < 0 { |
| panic("base from parsePseudoVersion missing patch number: " + base) |
| } |
| patch := decDecimal(base[i+1:]) |
| if patch == "" { |
| // vX.0.0-0 is invalid, but has been observed in the wild in the index |
| // generated by requests to proxy.golang.org. |
| // |
| // NOTE(bcmills): I cannot find a historical bug that accounts for |
| // pseudo-versions of this form, nor have I seen such versions in any |
| // actual go.mod files. If we find actual examples of this form and a |
| // reasonable theory of how they came into existence, it seems fine to |
| // treat them as equivalent to vX.0.0 (especially since the invalid |
| // pseudo-versions have lower precedence than the real ones). For now, we |
| // reject them. |
| return "", &InvalidVersionError{ |
| Version: v, |
| Pseudo: true, |
| Err: fmt.Errorf("version before %s would have negative patch number", base), |
| } |
| } |
| return base[:i+1] + patch + build, nil |
| |
| default: |
| // vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre |
| // vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible |
| if !strings.HasSuffix(base, ".0") { |
| panic(`base from parsePseudoVersion missing ".0" before date: ` + base) |
| } |
| return strings.TrimSuffix(base, ".0") + build, nil |
| } |
| } |
| |
| var errPseudoSyntax = errors.New("syntax error") |
| |
| func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) { |
| if !IsPseudoVersion(v) { |
| return "", "", "", "", &InvalidVersionError{ |
| Version: v, |
| Pseudo: true, |
| Err: errPseudoSyntax, |
| } |
| } |
| build = semver.Build(v) |
| v = strings.TrimSuffix(v, build) |
| j := strings.LastIndex(v, "-") |
| v, rev = v[:j], v[j+1:] |
| i := strings.LastIndex(v, "-") |
| if j := strings.LastIndex(v, "."); j > i { |
| base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0" |
| timestamp = v[j+1:] |
| } else { |
| base = v[:i] // "vX.0.0" |
| timestamp = v[i+1:] |
| } |
| return base, timestamp, rev, build, nil |
| } |