pseudo.go 8.4 KB

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