src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

pseudo.go (8630B)


      1 // Copyright 2018 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 // Pseudo-versions
      6 //
      7 // Code authors are expected to tag the revisions they want users to use,
      8 // including prereleases. However, not all authors tag versions at all,
      9 // and not all commits a user might want to try will have tags.
     10 // A pseudo-version is a version with a special form that allows us to
     11 // address an untagged commit and order that version with respect to
     12 // other versions we might encounter.
     13 //
     14 // A pseudo-version takes one of the general forms:
     15 //
     16 //	(1) vX.0.0-yyyymmddhhmmss-abcdef123456
     17 //	(2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456
     18 //	(3) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible
     19 //	(4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456
     20 //	(5) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible
     21 //
     22 // If there is no recently tagged version with the right major version vX,
     23 // then form (1) is used, creating a space of pseudo-versions at the bottom
     24 // of the vX version range, less than any tagged version, including the unlikely v0.0.0.
     25 //
     26 // If the most recent tagged version before the target commit is vX.Y.Z or vX.Y.Z+incompatible,
     27 // then the pseudo-version uses form (2) or (3), making it a prerelease for the next
     28 // possible semantic version after vX.Y.Z. The leading 0 segment in the prerelease string
     29 // ensures that the pseudo-version compares less than possible future explicit prereleases
     30 // like vX.Y.(Z+1)-rc1 or vX.Y.(Z+1)-1.
     31 //
     32 // If the most recent tagged version before the target commit is vX.Y.Z-pre or vX.Y.Z-pre+incompatible,
     33 // then the pseudo-version uses form (4) or (5), making it a slightly later prerelease.
     34 
     35 package module
     36 
     37 import (
     38 	"errors"
     39 	"fmt"
     40 	"strings"
     41 	"time"
     42 
     43 	"golang.org/x/mod/internal/lazyregexp"
     44 	"golang.org/x/mod/semver"
     45 )
     46 
     47 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-]+)*)?$`)
     48 
     49 const PseudoVersionTimestampFormat = "20060102150405"
     50 
     51 // PseudoVersion returns a pseudo-version for the given major version ("v1")
     52 // preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time,
     53 // and revision identifier (usually a 12-byte commit hash prefix).
     54 func PseudoVersion(major, older string, t time.Time, rev string) string {
     55 	if major == "" {
     56 		major = "v0"
     57 	}
     58 	segment := fmt.Sprintf("%s-%s", t.UTC().Format(PseudoVersionTimestampFormat), rev)
     59 	build := semver.Build(older)
     60 	older = semver.Canonical(older)
     61 	if older == "" {
     62 		return major + ".0.0-" + segment // form (1)
     63 	}
     64 	if semver.Prerelease(older) != "" {
     65 		return older + ".0." + segment + build // form (4), (5)
     66 	}
     67 
     68 	// Form (2), (3).
     69 	// Extract patch from vMAJOR.MINOR.PATCH
     70 	i := strings.LastIndex(older, ".") + 1
     71 	v, patch := older[:i], older[i:]
     72 
     73 	// Reassemble.
     74 	return v + incDecimal(patch) + "-0." + segment + build
     75 }
     76 
     77 // ZeroPseudoVersion returns a pseudo-version with a zero timestamp and
     78 // revision, which may be used as a placeholder.
     79 func ZeroPseudoVersion(major string) string {
     80 	return PseudoVersion(major, "", time.Time{}, "000000000000")
     81 }
     82 
     83 // incDecimal returns the decimal string incremented by 1.
     84 func incDecimal(decimal string) string {
     85 	// Scan right to left turning 9s to 0s until you find a digit to increment.
     86 	digits := []byte(decimal)
     87 	i := len(digits) - 1
     88 	for ; i >= 0 && digits[i] == '9'; i-- {
     89 		digits[i] = '0'
     90 	}
     91 	if i >= 0 {
     92 		digits[i]++
     93 	} else {
     94 		// digits is all zeros
     95 		digits[0] = '1'
     96 		digits = append(digits, '0')
     97 	}
     98 	return string(digits)
     99 }
    100 
    101 // decDecimal returns the decimal string decremented by 1, or the empty string
    102 // if the decimal is all zeroes.
    103 func decDecimal(decimal string) string {
    104 	// Scan right to left turning 0s to 9s until you find a digit to decrement.
    105 	digits := []byte(decimal)
    106 	i := len(digits) - 1
    107 	for ; i >= 0 && digits[i] == '0'; i-- {
    108 		digits[i] = '9'
    109 	}
    110 	if i < 0 {
    111 		// decimal is all zeros
    112 		return ""
    113 	}
    114 	if i == 0 && digits[i] == '1' && len(digits) > 1 {
    115 		digits = digits[1:]
    116 	} else {
    117 		digits[i]--
    118 	}
    119 	return string(digits)
    120 }
    121 
    122 // IsPseudoVersion reports whether v is a pseudo-version.
    123 func IsPseudoVersion(v string) bool {
    124 	return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v)
    125 }
    126 
    127 // IsZeroPseudoVersion returns whether v is a pseudo-version with a zero base,
    128 // timestamp, and revision, as returned by [ZeroPseudoVersion].
    129 func IsZeroPseudoVersion(v string) bool {
    130 	return v == ZeroPseudoVersion(semver.Major(v))
    131 }
    132 
    133 // PseudoVersionTime returns the time stamp of the pseudo-version v.
    134 // It returns an error if v is not a pseudo-version or if the time stamp
    135 // embedded in the pseudo-version is not a valid time.
    136 func PseudoVersionTime(v string) (time.Time, error) {
    137 	_, timestamp, _, _, err := parsePseudoVersion(v)
    138 	if err != nil {
    139 		return time.Time{}, err
    140 	}
    141 	t, err := time.Parse("20060102150405", timestamp)
    142 	if err != nil {
    143 		return time.Time{}, &InvalidVersionError{
    144 			Version: v,
    145 			Pseudo:  true,
    146 			Err:     fmt.Errorf("malformed time %q", timestamp),
    147 		}
    148 	}
    149 	return t, nil
    150 }
    151 
    152 // PseudoVersionRev returns the revision identifier of the pseudo-version v.
    153 // It returns an error if v is not a pseudo-version.
    154 func PseudoVersionRev(v string) (rev string, err error) {
    155 	_, _, rev, _, err = parsePseudoVersion(v)
    156 	return
    157 }
    158 
    159 // PseudoVersionBase returns the canonical parent version, if any, upon which
    160 // the pseudo-version v is based.
    161 //
    162 // If v has no parent version (that is, if it is "vX.0.0-[…]"),
    163 // PseudoVersionBase returns the empty string and a nil error.
    164 func PseudoVersionBase(v string) (string, error) {
    165 	base, _, _, build, err := parsePseudoVersion(v)
    166 	if err != nil {
    167 		return "", err
    168 	}
    169 
    170 	switch pre := semver.Prerelease(base); pre {
    171 	case "":
    172 		// vX.0.0-yyyymmddhhmmss-abcdef123456 → ""
    173 		if build != "" {
    174 			// Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible
    175 			// are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag,
    176 			// but the "+incompatible" suffix implies that the major version of
    177 			// the parent tag is not compatible with the module's import path.
    178 			//
    179 			// There are a few such entries in the index generated by proxy.golang.org,
    180 			// but we believe those entries were generated by the proxy itself.
    181 			return "", &InvalidVersionError{
    182 				Version: v,
    183 				Pseudo:  true,
    184 				Err:     fmt.Errorf("lacks base version, but has build metadata %q", build),
    185 			}
    186 		}
    187 		return "", nil
    188 
    189 	case "-0":
    190 		// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z
    191 		// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible
    192 		base = strings.TrimSuffix(base, pre)
    193 		i := strings.LastIndexByte(base, '.')
    194 		if i < 0 {
    195 			panic("base from parsePseudoVersion missing patch number: " + base)
    196 		}
    197 		patch := decDecimal(base[i+1:])
    198 		if patch == "" {
    199 			// vX.0.0-0 is invalid, but has been observed in the wild in the index
    200 			// generated by requests to proxy.golang.org.
    201 			//
    202 			// NOTE(bcmills): I cannot find a historical bug that accounts for
    203 			// pseudo-versions of this form, nor have I seen such versions in any
    204 			// actual go.mod files. If we find actual examples of this form and a
    205 			// reasonable theory of how they came into existence, it seems fine to
    206 			// treat them as equivalent to vX.0.0 (especially since the invalid
    207 			// pseudo-versions have lower precedence than the real ones). For now, we
    208 			// reject them.
    209 			return "", &InvalidVersionError{
    210 				Version: v,
    211 				Pseudo:  true,
    212 				Err:     fmt.Errorf("version before %s would have negative patch number", base),
    213 			}
    214 		}
    215 		return base[:i+1] + patch + build, nil
    216 
    217 	default:
    218 		// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre
    219 		// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible
    220 		if !strings.HasSuffix(base, ".0") {
    221 			panic(`base from parsePseudoVersion missing ".0" before date: ` + base)
    222 		}
    223 		return strings.TrimSuffix(base, ".0") + build, nil
    224 	}
    225 }
    226 
    227 var errPseudoSyntax = errors.New("syntax error")
    228 
    229 func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) {
    230 	if !IsPseudoVersion(v) {
    231 		return "", "", "", "", &InvalidVersionError{
    232 			Version: v,
    233 			Pseudo:  true,
    234 			Err:     errPseudoSyntax,
    235 		}
    236 	}
    237 	build = semver.Build(v)
    238 	v = strings.TrimSuffix(v, build)
    239 	j := strings.LastIndex(v, "-")
    240 	v, rev = v[:j], v[j+1:]
    241 	i := strings.LastIndex(v, "-")
    242 	if j := strings.LastIndex(v, "."); j > i {
    243 		base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0"
    244 		timestamp = v[j+1:]
    245 	} else {
    246 		base = v[:i] // "vX.0.0"
    247 		timestamp = v[i+1:]
    248 	}
    249 	return base, timestamp, rev, build, nil
    250 }