Skip to content

Commit 9c61e4e

Browse files
committed
fix: alpha version
1 parent e66a261 commit 9c61e4e

2 files changed

Lines changed: 120 additions & 21 deletions

File tree

internal/update/update.go

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ func NeedsUpdate(current, latest string) bool {
9595
return true
9696
}
9797

98-
curParts := parseSemver(current)
99-
latParts := parseSemver(latest)
98+
curParts, curPre := parseSemver(current)
99+
latParts, latPre := parseSemver(latest)
100100

101101
if curParts == nil || latParts == nil {
102102
// Fall back to string comparison if parsing fails.
@@ -111,7 +111,20 @@ func NeedsUpdate(current, latest string) bool {
111111
return false
112112
}
113113
}
114-
return false
114+
115+
// Major.minor.patch are equal — compare pre-release.
116+
// Per SemVer: release (no pre-release) > any pre-release.
117+
if curPre != "" && latPre == "" {
118+
return true // current is pre-release, latest is release
119+
}
120+
if curPre == "" && latPre != "" {
121+
return false // current is release, latest is pre-release
122+
}
123+
if curPre == "" && latPre == "" {
124+
return false // both are releases, identical
125+
}
126+
127+
return comparePreRelease(curPre, latPre) < 0
115128
}
116129

117130
// stripV removes the leading "v" prefix from a version string.
@@ -120,28 +133,79 @@ func stripV(v string) string {
120133
}
121134

122135
// parseSemver splits a version string like "1.2.3" or "1.2.3-beta.1" into
123-
// [major, minor, patch]. Pre-release suffixes are stripped before parsing.
136+
// [major, minor, patch] and an optional pre-release string.
124137
// Returns nil if parsing fails.
125-
func parseSemver(v string) []int {
126-
// Strip pre-release suffix for comparison purposes.
138+
func parseSemver(v string) ([]int, string) {
139+
var preRelease string
127140
if idx := strings.Index(v, "-"); idx != -1 {
141+
preRelease = v[idx+1:]
128142
v = v[:idx]
129143
}
130144

131145
parts := strings.Split(v, ".")
132146
if len(parts) != 3 {
133-
return nil
147+
return nil, ""
134148
}
135149

136150
result := make([]int, 3)
137151
for i, p := range parts {
138152
n, err := strconv.Atoi(p)
139153
if err != nil {
140-
return nil
154+
return nil, ""
141155
}
142156
result[i] = n
143157
}
144-
return result
158+
return result, preRelease
159+
}
160+
161+
// comparePreRelease compares two pre-release strings per SemVer 2.0.0 §11.
162+
// Returns -1 if a < b, 0 if a == b, +1 if a > b.
163+
func comparePreRelease(a, b string) int {
164+
aParts := strings.Split(a, ".")
165+
bParts := strings.Split(b, ".")
166+
167+
n := len(aParts)
168+
if len(bParts) < n {
169+
n = len(bParts)
170+
}
171+
172+
for i := range n {
173+
aNum, aErr := strconv.Atoi(aParts[i])
174+
bNum, bErr := strconv.Atoi(bParts[i])
175+
176+
switch {
177+
case aErr == nil && bErr == nil:
178+
// Both numeric: compare as integers.
179+
if aNum < bNum {
180+
return -1
181+
}
182+
if aNum > bNum {
183+
return 1
184+
}
185+
case aErr == nil && bErr != nil:
186+
// Numeric < alphanumeric per SemVer.
187+
return -1
188+
case aErr != nil && bErr == nil:
189+
return 1
190+
default:
191+
// Both alphanumeric: compare lexically.
192+
if aParts[i] < bParts[i] {
193+
return -1
194+
}
195+
if aParts[i] > bParts[i] {
196+
return 1
197+
}
198+
}
199+
}
200+
201+
// All compared identifiers are equal — shorter set has lower precedence.
202+
if len(aParts) < len(bParts) {
203+
return -1
204+
}
205+
if len(aParts) > len(bParts) {
206+
return 1
207+
}
208+
return 0
145209
}
146210

147211
// ArchiveName returns the expected archive filename for the current platform.

internal/update/update_test.go

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ func TestNeedsUpdate(t *testing.T) {
3737
{"no v prefix current", "1.0.0", "v1.1.0", true},
3838
{"no v prefix latest", "v1.0.0", "1.1.0", true},
3939
{"no v prefix both", "1.0.0", "1.1.0", true},
40-
{"pre-release stripped", "v1.0.0-alpha.1", "v1.0.0", false},
41-
{"pre-release to release", "v0.9.0-beta.1", "v1.0.0", true},
40+
{"pre-release to release", "v1.0.0-alpha.1", "v1.0.0", true},
41+
{"pre-release major update", "v0.9.0-beta.1", "v1.0.0", true},
42+
{"same pre-release", "v0.0.1-alpha.7", "v0.0.1-alpha.7", false},
43+
{"pre-release bump", "v0.0.1-alpha.7", "v0.0.1-alpha.8", true},
44+
{"pre-release newer current", "v0.0.1-alpha.8", "v0.0.1-alpha.7", false},
45+
{"release to pre-release", "v1.0.0", "v1.0.0-alpha.1", false},
46+
{"alpha to beta", "v1.0.0-alpha.1", "v1.0.0-beta.1", true},
4247
}
4348

4449
for _, tt := range tests {
@@ -255,21 +260,23 @@ func TestArchiveName(t *testing.T) {
255260

256261
func TestParseSemver(t *testing.T) {
257262
tests := []struct {
258-
input string
259-
want []int
263+
input string
264+
want []int
265+
wantPre string
260266
}{
261-
{"1.2.3", []int{1, 2, 3}},
262-
{"0.0.1", []int{0, 0, 1}},
263-
{"10.20.30", []int{10, 20, 30}},
264-
{"1.2.3-alpha.1", []int{1, 2, 3}},
265-
{"invalid", nil},
266-
{"1.2", nil},
267-
{"1.2.x", nil},
267+
{"1.2.3", []int{1, 2, 3}, ""},
268+
{"0.0.1", []int{0, 0, 1}, ""},
269+
{"10.20.30", []int{10, 20, 30}, ""},
270+
{"1.2.3-alpha.1", []int{1, 2, 3}, "alpha.1"},
271+
{"0.0.1-beta.2", []int{0, 0, 1}, "beta.2"},
272+
{"invalid", nil, ""},
273+
{"1.2", nil, ""},
274+
{"1.2.x", nil, ""},
268275
}
269276

270277
for _, tt := range tests {
271278
t.Run(tt.input, func(t *testing.T) {
272-
got := parseSemver(tt.input)
279+
got, gotPre := parseSemver(tt.input)
273280
if tt.want == nil {
274281
if got != nil {
275282
t.Errorf("parseSemver(%q) = %v, want nil", tt.input, got)
@@ -284,6 +291,34 @@ func TestParseSemver(t *testing.T) {
284291
t.Errorf("parseSemver(%q)[%d] = %d, want %d", tt.input, i, got[i], tt.want[i])
285292
}
286293
}
294+
if gotPre != tt.wantPre {
295+
t.Errorf("parseSemver(%q) pre-release = %q, want %q", tt.input, gotPre, tt.wantPre)
296+
}
297+
})
298+
}
299+
}
300+
301+
func TestComparePreRelease(t *testing.T) {
302+
tests := []struct {
303+
name string
304+
a, b string
305+
want int
306+
}{
307+
{"equal", "alpha.1", "alpha.1", 0},
308+
{"numeric bump", "alpha.7", "alpha.8", -1},
309+
{"numeric reverse", "alpha.8", "alpha.7", 1},
310+
{"alpha vs beta", "alpha.1", "beta.1", -1},
311+
{"numeric vs alpha", "1", "alpha", -1},
312+
{"shorter is less", "alpha", "alpha.1", -1},
313+
{"longer is more", "alpha.1", "alpha", 1},
314+
}
315+
316+
for _, tt := range tests {
317+
t.Run(tt.name, func(t *testing.T) {
318+
got := comparePreRelease(tt.a, tt.b)
319+
if got != tt.want {
320+
t.Errorf("comparePreRelease(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
321+
}
287322
})
288323
}
289324
}

0 commit comments

Comments
 (0)