17863fe8bc
gates / gates (push) Successful in 47s
MinAgent 0.155.0 (unchanged). Tests red-proved by mutation (A: 10, B: 7, C: 1, C2: 2). Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
151 lines
4.2 KiB
Go
151 lines
4.2 KiB
Go
package util
|
|
|
|
// Semantic-version parsing + comparison — THE one comparator in this repo (house rule: never a
|
|
// second one). Moved verbatim from internal/selfupdate/version.go (v0.115.0) so that BOTH
|
|
// selfupdate (floor/update decisions) and agentapi (the per-feature MinAgent capability
|
|
// comparison) share it without an import cycle (selfupdate imports agentapi). selfupdate keeps
|
|
// aliases, so its call sites and tests are unchanged.
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Version represents a semantic version (Major.Minor.Patch).
|
|
type Version struct {
|
|
Major int
|
|
Minor int
|
|
Patch int
|
|
Raw string
|
|
// Pre (v0.314.0, R-1053) is a pre-release suffix — „rc.4" in 0.313.0-rc.4 — set ONLY by ParseVersionPre.
|
|
// ParseVersion refuses a suffix, so every caller that lists or offers tags still never sees a test build.
|
|
Pre string
|
|
}
|
|
|
|
// ParseVersion parses "X.Y.Z" or "vX.Y.Z". Returns error for "dev", "latest", or invalid formats.
|
|
func ParseVersion(s string) (Version, error) {
|
|
s = strings.TrimPrefix(s, "v")
|
|
if s == "dev" || s == "latest" || s == "" {
|
|
return Version{}, fmt.Errorf("invalid version: %q", s)
|
|
}
|
|
parts := strings.SplitN(s, ".", 3)
|
|
if len(parts) != 3 {
|
|
return Version{}, fmt.Errorf("invalid version format: %q (expected X.Y.Z)", s)
|
|
}
|
|
major, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return Version{}, fmt.Errorf("invalid major version: %w", err)
|
|
}
|
|
minor, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return Version{}, fmt.Errorf("invalid minor version: %w", err)
|
|
}
|
|
patch, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return Version{}, fmt.Errorf("invalid patch version: %w", err)
|
|
}
|
|
return Version{Major: major, Minor: minor, Patch: patch, Raw: s}, nil
|
|
}
|
|
|
|
// ParseVersionPre is ParseVersion that also accepts a pre-release suffix (semver §9): „0.313.0-rc.4" is 0.313.0 with
|
|
// Pre „rc.4", which Compare orders BELOW 0.313.0. A build suffix („+abc") is dropped. Used where the question is „is
|
|
// this box below the floor?" (R-1053: a box on a test build was never moved by the floor) — never to pick a tag to offer.
|
|
func ParseVersionPre(s string) (Version, error) {
|
|
s = strings.TrimPrefix(s, "v")
|
|
if i := strings.IndexByte(s, '+'); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
base, pre, hasPre := strings.Cut(s, "-")
|
|
v, err := ParseVersion(base)
|
|
if err != nil {
|
|
return Version{}, err
|
|
}
|
|
if hasPre {
|
|
if pre == "" {
|
|
return Version{}, fmt.Errorf("invalid version: %q (empty pre-release)", s)
|
|
}
|
|
for _, id := range strings.Split(pre, ".") {
|
|
if id == "" || strings.Trim(id, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-") != "" {
|
|
return Version{}, fmt.Errorf("invalid pre-release in %q", s)
|
|
}
|
|
}
|
|
v.Pre = pre
|
|
}
|
|
v.Raw = s
|
|
return v, nil
|
|
}
|
|
|
|
// comparePre orders pre-release suffixes per semver §11: none > any; identifiers left to right, numeric ones
|
|
// numerically and below alphanumeric ones; a shorter list below a longer one with the same prefix.
|
|
func comparePre(a, b string) int {
|
|
switch {
|
|
case a == b:
|
|
return 0
|
|
case a == "":
|
|
return 1
|
|
case b == "":
|
|
return -1
|
|
}
|
|
as, bs := strings.Split(a, "."), strings.Split(b, ".")
|
|
for i := 0; i < len(as) && i < len(bs); i++ {
|
|
an, aerr := strconv.Atoi(as[i])
|
|
bn, berr := strconv.Atoi(bs[i])
|
|
switch {
|
|
case aerr == nil && berr == nil:
|
|
if an != bn {
|
|
if an < bn {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
case aerr == nil:
|
|
return -1
|
|
case berr == nil:
|
|
return 1
|
|
default:
|
|
if c := strings.Compare(as[i], bs[i]); c != 0 {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
switch {
|
|
case len(as) < len(bs):
|
|
return -1
|
|
case len(as) > len(bs):
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Compare returns -1 if a < b, 0 if a == b, 1 if a > b.
|
|
func (a Version) Compare(b Version) int {
|
|
if a.Major != b.Major {
|
|
if a.Major < b.Major {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
if a.Minor != b.Minor {
|
|
if a.Minor < b.Minor {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
if a.Patch != b.Patch {
|
|
if a.Patch < b.Patch {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
return comparePre(a.Pre, b.Pre)
|
|
}
|
|
|
|
// String returns the version as "X.Y.Z" (or "X.Y.Z-pre" for a ParseVersionPre result).
|
|
func (v Version) String() string {
|
|
if v.Pre != "" {
|
|
return fmt.Sprintf("%d.%d.%d-%s", v.Major, v.Minor, v.Patch, v.Pre)
|
|
}
|
|
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
|
}
|