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