fix(cli): use numeric comparison for pre-release version segments (#201)

isNewer compares pre-release suffixes lexicographically, which gives
wrong results for multi-digit numbers: "beta.10" < "beta.2" because
'1' < '2' in ASCII. This causes the update checker to miss newer
pre-release versions (e.g. v1.2.3-beta.10 not recognized as newer
than v1.2.3-beta.2).

Split pre-release strings on "." and compare numeric segments as
integers. Add unit tests covering stable, pre-release, and dev
version comparisons.
This commit is contained in:
Shawn authored and GitHub committed 2026-03-18 07:48:53 +08:00
1 parent 84ce254bbe
commit d475173477
2 files changed
+88 -2

No files matched your search

+47 -2
View File
@@ -537,10 +537,55 @@ func isNewer(latest, current string) bool {
if cPre == "" && lPre != "" {
return false
}
// Both have pre-release: compare lexicographically (beta.2 > beta.1, rc.1 > beta.9)
// Both have pre-release: split on "." and compare each segment
// numerically where possible so beta.10 > beta.2.
if lPre != "" && cPre != "" {
return lPre > cPre
return comparePreRelease(lPre, cPre) > 0
}
return false
}
// comparePreRelease compares two pre-release strings segment by segment.
// Numeric segments are compared as integers; non-numeric segments are
// compared lexicographically. Returns >0 if a is greater, <0 if b is
// greater, 0 if equal.
func comparePreRelease(a, b string) int {
aParts := strings.Split(a, ".")
bParts := strings.Split(b, ".")
max := len(aParts)
if len(bParts) > max {
max = len(bParts)
}
for i := 0; i < max; i++ {
var ap, bp string
if i < len(aParts) {
ap = aParts[i]
}
if i < len(bParts) {
bp = bParts[i]
}
var an, bn int
aN, _ := fmt.Sscanf(ap, "%d", &an)
bN, _ := fmt.Sscanf(bp, "%d", &bn)
aIsNum := aN == 1 && fmt.Sprintf("%d", an) == ap
bIsNum := bN == 1 && fmt.Sprintf("%d", bn) == bp
if aIsNum && bIsNum {
if an != bn {
return an - bn
}
continue
}
// Non-numeric: lexicographic
if ap < bp {
return -1
}
if ap > bp {
return 1
}
}
return 0
}
+41
View File
@@ -0,0 +1,41 @@
package main
import "testing"
func TestIsNewer(t *testing.T) {
tests := []struct {
latest, current string
want bool
}{
// Basic semver
{"v1.2.3", "v1.2.2", true},
{"v1.2.2", "v1.2.3", false},
{"v1.2.3", "v1.2.3", false},
{"v2.0.0", "v1.9.9", true},
// Pre-release vs stable
{"v1.2.3", "v1.2.3-beta.1", true},
{"v1.2.3-beta.1", "v1.2.3", false},
// Pre-release numeric ordering
{"v1.2.3-beta.10", "v1.2.3-beta.2", true},
{"v1.2.3-beta.2", "v1.2.3-beta.10", false},
{"v1.2.3-beta.2", "v1.2.3-beta.2", false},
// rc > beta lexicographically
{"v1.2.3-rc.1", "v1.2.3-beta.9", true},
// Dev builds always upgradeable
{"v1.0.0", "dev", true},
// Empty
{"", "v1.0.0", false},
{"v1.0.0", "", false},
}
for _, tt := range tests {
got := isNewer(tt.latest, tt.current)
if got != tt.want {
t.Errorf("isNewer(%q, %q) = %v, want %v", tt.latest, tt.current, got, tt.want)
}
}
}