...

Source file src/pkg/cmd/go/internal/modfetch/pseudo.go

     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 modfetch
    36	
    37	import (
    38		"errors"
    39		"fmt"
    40		"strings"
    41		"time"
    42	
    43		"cmd/go/internal/module"
    44		"cmd/go/internal/semver"
    45		"internal/lazyregexp"
    46	)
    47	
    48	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-]+)*)?$`)
    49	
    50	// PseudoVersion returns a pseudo-version for the given major version ("v1")
    51	// preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time,
    52	// and revision identifier (usually a 12-byte commit hash prefix).
    53	func PseudoVersion(major, older string, t time.Time, rev string) string {
    54		if major == "" {
    55			major = "v0"
    56		}
    57		segment := fmt.Sprintf("%s-%s", t.UTC().Format("20060102150405"), rev)
    58		build := semver.Build(older)
    59		older = semver.Canonical(older)
    60		if older == "" {
    61			return major + ".0.0-" + segment // form (1)
    62		}
    63		if semver.Prerelease(older) != "" {
    64			return older + ".0." + segment + build // form (4), (5)
    65		}
    66	
    67		// Form (2), (3).
    68		// Extract patch from vMAJOR.MINOR.PATCH
    69		i := strings.LastIndex(older, ".") + 1
    70		v, patch := older[:i], older[i:]
    71	
    72		// Reassemble.
    73		return v + incDecimal(patch) + "-0." + segment + build
    74	}
    75	
    76	// incDecimal returns the decimal string incremented by 1.
    77	func incDecimal(decimal string) string {
    78		// Scan right to left turning 9s to 0s until you find a digit to increment.
    79		digits := []byte(decimal)
    80		i := len(digits) - 1
    81		for ; i >= 0 && digits[i] == '9'; i-- {
    82			digits[i] = '0'
    83		}
    84		if i >= 0 {
    85			digits[i]++
    86		} else {
    87			// digits is all zeros
    88			digits[0] = '1'
    89			digits = append(digits, '0')
    90		}
    91		return string(digits)
    92	}
    93	
    94	// decDecimal returns the decimal string decremented by 1, or the empty string
    95	// if the decimal is all zeroes.
    96	func decDecimal(decimal string) string {
    97		// Scan right to left turning 0s to 9s until you find a digit to decrement.
    98		digits := []byte(decimal)
    99		i := len(digits) - 1
   100		for ; i >= 0 && digits[i] == '0'; i-- {
   101			digits[i] = '9'
   102		}
   103		if i < 0 {
   104			// decimal is all zeros
   105			return ""
   106		}
   107		if i == 0 && digits[i] == '1' && len(digits) > 1 {
   108			digits = digits[1:]
   109		} else {
   110			digits[i]--
   111		}
   112		return string(digits)
   113	}
   114	
   115	// IsPseudoVersion reports whether v is a pseudo-version.
   116	func IsPseudoVersion(v string) bool {
   117		return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v)
   118	}
   119	
   120	// PseudoVersionTime returns the time stamp of the pseudo-version v.
   121	// It returns an error if v is not a pseudo-version or if the time stamp
   122	// embedded in the pseudo-version is not a valid time.
   123	func PseudoVersionTime(v string) (time.Time, error) {
   124		_, timestamp, _, _, err := parsePseudoVersion(v)
   125		if err != nil {
   126			return time.Time{}, err
   127		}
   128		t, err := time.Parse("20060102150405", timestamp)
   129		if err != nil {
   130			return time.Time{}, &module.InvalidVersionError{
   131				Version: v,
   132				Pseudo:  true,
   133				Err:     fmt.Errorf("malformed time %q", timestamp),
   134			}
   135		}
   136		return t, nil
   137	}
   138	
   139	// PseudoVersionRev returns the revision identifier of the pseudo-version v.
   140	// It returns an error if v is not a pseudo-version.
   141	func PseudoVersionRev(v string) (rev string, err error) {
   142		_, _, rev, _, err = parsePseudoVersion(v)
   143		return
   144	}
   145	
   146	// PseudoVersionBase returns the canonical parent version, if any, upon which
   147	// the pseudo-version v is based.
   148	//
   149	// If v has no parent version (that is, if it is "vX.0.0-[…]"),
   150	// PseudoVersionBase returns the empty string and a nil error.
   151	func PseudoVersionBase(v string) (string, error) {
   152		base, _, _, build, err := parsePseudoVersion(v)
   153		if err != nil {
   154			return "", err
   155		}
   156	
   157		switch pre := semver.Prerelease(base); pre {
   158		case "":
   159			// vX.0.0-yyyymmddhhmmss-abcdef123456 → ""
   160			if build != "" {
   161				// Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible
   162				// are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag,
   163				// but the "+incompatible" suffix implies that the major version of
   164				// the parent tag is not compatible with the module's import path.
   165				//
   166				// There are a few such entries in the index generated by proxy.golang.org,
   167				// but we believe those entries were generated by the proxy itself.
   168				return "", &module.InvalidVersionError{
   169					Version: v,
   170					Pseudo:  true,
   171					Err:     fmt.Errorf("lacks base version, but has build metadata %q", build),
   172				}
   173			}
   174			return "", nil
   175	
   176		case "-0":
   177			// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z
   178			// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible
   179			base = strings.TrimSuffix(base, pre)
   180			i := strings.LastIndexByte(base, '.')
   181			if i < 0 {
   182				panic("base from parsePseudoVersion missing patch number: " + base)
   183			}
   184			patch := decDecimal(base[i+1:])
   185			if patch == "" {
   186				// vX.0.0-0 is invalid, but has been observed in the wild in the index
   187				// generated by requests to proxy.golang.org.
   188				//
   189				// NOTE(bcmills): I cannot find a historical bug that accounts for
   190				// pseudo-versions of this form, nor have I seen such versions in any
   191				// actual go.mod files. If we find actual examples of this form and a
   192				// reasonable theory of how they came into existence, it seems fine to
   193				// treat them as equivalent to vX.0.0 (especially since the invalid
   194				// pseudo-versions have lower precedence than the real ones). For now, we
   195				// reject them.
   196				return "", &module.InvalidVersionError{
   197					Version: v,
   198					Pseudo:  true,
   199					Err:     fmt.Errorf("version before %s would have negative patch number", base),
   200				}
   201			}
   202			return base[:i+1] + patch + build, nil
   203	
   204		default:
   205			// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre
   206			// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible
   207			if !strings.HasSuffix(base, ".0") {
   208				panic(`base from parsePseudoVersion missing ".0" before date: ` + base)
   209			}
   210			return strings.TrimSuffix(base, ".0") + build, nil
   211		}
   212	}
   213	
   214	var errPseudoSyntax = errors.New("syntax error")
   215	
   216	func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) {
   217		if !IsPseudoVersion(v) {
   218			return "", "", "", "", &module.InvalidVersionError{
   219				Version: v,
   220				Pseudo:  true,
   221				Err:     errPseudoSyntax,
   222			}
   223		}
   224		build = semver.Build(v)
   225		v = strings.TrimSuffix(v, build)
   226		j := strings.LastIndex(v, "-")
   227		v, rev = v[:j], v[j+1:]
   228		i := strings.LastIndex(v, "-")
   229		if j := strings.LastIndex(v, "."); j > i {
   230			base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0"
   231			timestamp = v[j+1:]
   232		} else {
   233			base = v[:i] // "vX.0.0"
   234			timestamp = v[i+1:]
   235		}
   236		return base, timestamp, rev, build, nil
   237	}
   238	

View as plain text