-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathversion.go
300 lines (246 loc) · 7.93 KB
/
version.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
package comver
import (
"fmt"
"regexp"
"strconv"
"strings"
)
const (
classicalVersioningRegex = `(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?`
dateOnlyVersioningRegex = `(\d{4})(?:[.:-]?(\d{2}))(?:[.:-]?(\d{2}))?(?:\.(\d+))?`
modifierRegex = `[._-]?(?:(stable|beta|b|rc|alpha|a|patch|pl|p)((?:[.-]?\d+)+)?)?`
errEmptyString stringError = "version string is empty"
errInvalidVersionString stringError = "invalid version string"
errNotFixedVersion stringError = "not a fixed version"
errDateVersionWithFourBits stringError = "date versions with 4 bits"
)
var (
classicalVersioningRegexp = regexp.MustCompile("^" + classicalVersioningRegex + modifierRegex + "$")
dateOnlyVersioningRegexp = regexp.MustCompile("^" + dateOnlyVersioningRegex + modifierRegex + "$")
)
// Version represents a single composer version.
// The zero value for Version is v0.0.0.0 with empty original string.
type Version struct {
major, minor, patch, tweak uint64 `exhaustruct:"optional"`
modifier modifier `exhaustruct:"optional"`
preRelease string `exhaustruct:"optional"`
original string `exhaustruct:"optional"`
}
// Parse parses a given version string, attempts to coerce a version string into
// a [Version] object or return an error if unable to parse the version string.
//
// If there is a leading v or a version listed without all parts (e.g. v1.2.p5+foo) it
// attempt to coerce it into a valid composer version (e.g. 1.2.0.0-patch5). In both cases
// a [Version] object is returned that can be sorted, compared, and used in constraints.
//
// Due to implementation complexity, it only supports a subset of [composer versioning].
// Refer to the [version_test.go] for examples.
//
// [composer versioning]: https://github.com/composer/semver/
// [version_test.go]: https://github.com/typisttech/comver/blob/main/version_test.go
func Parse(v string) (Version, error) { //nolint:cyclop,funlen
original := v
// normalize to lowercase for easier pattern matching
v = strings.ToLower(v)
v = strings.TrimSpace(v)
if v == "" {
return Version{}, &ParseError{original, errEmptyString}
}
v = strings.TrimPrefix(v, "v")
if v == "" {
return Version{}, &ParseError{original, errInvalidVersionString}
}
if strings.Contains(v, " as ") {
return Version{}, &ParseError{original, errNotFixedVersion}
}
if hasSuffixAnyOf(v, "@stable", "@rc", "@beta", "@alpha", "@dev") {
return Version{}, &ParseError{original, errNotFixedVersion}
}
if containsAnyOf(v, "master", "trunk", "default") {
return Version{}, &ParseError{original, errNotFixedVersion}
}
if strings.HasPrefix(v, "dev-") {
return Version{}, &ParseError{original, errNotFixedVersion}
}
// strip off build metadata
v, metadata, _ := strings.Cut(v, "+")
if v == "" || strings.Contains(metadata, " ") {
return Version{}, &ParseError{original, errInvalidVersionString}
}
if strings.HasSuffix(v, "dev") {
return Version{}, &ParseError{original, errNotFixedVersion}
}
cv := Version{
original: original,
}
var match []string
if cm := classicalVersioningRegexp.FindStringSubmatch(v); cm != nil {
match = cm
} else if dm := dateOnlyVersioningRegexp.FindStringSubmatch(v); dm != nil {
match = dm
}
if match == nil || len(match) != 7 {
return Version{}, &ParseError{original, errInvalidVersionString}
}
var err error
if cv.major, err = strconv.ParseUint(match[1], 10, 64); err != nil {
return Version{}, &ParseError{original, err}
}
// CalVer (as MAJOR) must be in YYYYMMDDhhmm or YYYYMMDD formats
if s := strconv.FormatUint(cv.major, 10); len(s) > 12 || len(s) == 11 || len(s) == 9 || len(s) == 7 {
return Version{}, &ParseError{original, errInvalidVersionString}
}
if cv.minor, err = strconv.ParseUint(match[2], 10, 64); match[2] != "" && err != nil {
return Version{}, &ParseError{original, err}
}
if cv.patch, err = strconv.ParseUint(match[3], 10, 64); match[3] != "" && err != nil {
return Version{}, &ParseError{original, err}
}
if cv.major >= 1000_00 && match[4] != "" {
return Version{}, &ParseError{original, errDateVersionWithFourBits}
}
if cv.tweak, err = strconv.ParseUint(match[4], 10, 64); match[4] != "" && err != nil {
return Version{}, &ParseError{original, err}
}
if cv.modifier, err = newModifier(match[5]); err != nil {
return Version{}, &ParseError{original, err}
}
cv.preRelease = strings.TrimPrefix(strings.TrimPrefix(match[6], "-"), ".")
return cv, nil
}
// MustParse is like [Parse] but panics if the version string cannot be parsed.
func MustParse(v string) Version {
cv, err := Parse(v)
if err != nil {
panic(err)
}
return cv
}
func hasSuffixAnyOf(s string, suffixes ...string) bool {
for _, suffix := range suffixes {
if strings.HasSuffix(s, suffix) {
return true
}
}
return false
}
func containsAnyOf(s string, substrs ...string) bool {
for _, substr := range substrs {
if strings.Contains(s, substr) {
return true
}
}
return false
}
// String returns the normalized string representation of the version.
func (v Version) String() string {
s := fmt.Sprintf("%d.%d.%d.%d", v.major, v.minor, v.patch, v.tweak)
if v.modifier != modifierStable {
s += "-" + v.modifier.String() + v.preRelease
}
return s
}
// Short returns the shortest string representation of the version.
func (v Version) Short() string {
s := fmt.Sprintf("%d.%d.%d.%d", v.major, v.minor, v.patch, v.tweak)
s = strings.TrimSuffix(s, ".0")
s = strings.TrimSuffix(s, ".0")
s = strings.TrimSuffix(s, ".0")
if v.modifier != modifierStable {
s += "-" + v.modifier.String() + v.preRelease
}
return s
}
// Original returns the original version string passed into [Parse].
// Empty string is returned when [Version] is the zero value.
func (v Version) Original() string {
return v.original
}
// Compare returns an integer comparing two [Version] instances.
//
// Pre-release versions are compared according to [semantic version precedence].
// The result is 0 when v == w, -1 when v < w, or +1 when v > w.
//
// [semantic version precedence]: https://semver.org/#spec-item-11
func (v Version) Compare(w Version) int { //nolint:cyclop,funlen
switch {
case v.String() == w.String():
return 0
case v.major > w.major:
return +1
case v.major < w.major:
return -1
case v.minor > w.minor:
return +1
case v.minor < w.minor:
return -1
case v.patch > w.patch:
return +1
case v.patch < w.patch:
return -1
case v.tweak > w.tweak:
return +1
case v.tweak < w.tweak:
return -1
case v.modifier > w.modifier:
return +1
case v.modifier < w.modifier:
return -1
case v.preRelease != "" && w.preRelease == "":
return +1
case v.preRelease == "" && w.preRelease != "":
return -1
}
vPres := strings.Split(v.preRelease, ".")
wPres := strings.Split(w.preRelease, ".")
// comparing each dot separated identifier from ceiling to floor
for i := range vPres {
// a larger set of pre-release fields has a higher precedence than a smaller set
if i >= len(wPres) {
return +1
}
vi, wi := vPres[i], wPres[i]
if vi == wi {
continue
}
vid := isDigits(vi)
wid := isDigits(wi)
// identifiers consisting of only digits are compared numerically
if vid && wid {
vii, _ := strconv.ParseUint(vi, 10, 64)
wii, _ := strconv.ParseUint(wi, 10, 64)
if vii > wii {
return +1
}
return -1
}
//nolint:godox
// TODO: Find out whether composer/semver supports this
//
// identifiers with letters or hyphens are compared lexically in ASCII sort order
if !vid && !wid {
if vi > wi {
return +1
}
return -1
}
//nolint:godox
// TODO: Find out whether composer/semver supports this
//
// numeric identifiers always have floor precedence than non-numeric identifiers
if !vid && wid {
return +1
}
return -1
}
// a larger set of pre-release fields has a higher precedence than a smaller set
return -1
}
func isDigits(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}