internal/version/order.go
Ref: Size: 4.7 KiB History
package version
import (
"strconv"
"strings"
)
// Less reports whether version a orders strictly before b. Versions are
// eitri's own tags — releases ("v0.0.3"), pre-releases of them ("v0.0.4-pre.1")
// — and the git-describe builds derived from either ("v0.0.3-5-gabc1234",
// "v0.0.4-pre.1-3-gabc1234", N commits past that tag). The whole chain orders
// the way it reads:
//
// v0.0.4-pre.1 < v0.0.4-pre.1-3-gabc1234 < v0.0.4-pre.2 < v0.0.4 < v0.0.4-2-gabc1234
//
// So a hand-deployed "v0.0.2-2-g68f804d" orders before the "v0.0.3" release and
// takes the upgrade; a build ahead of the latest release never orders before it
// and is never offered a downgrade; and an agent on a pre-release takes the next
// pre-release and then the release itself, which is what lets a staging plane
// rehearse the upgrade path on the same tags a release cycle produces.
//
// Anything unparsable — "dev", a "-dirty" working tree, a malformed tag — never
// orders before anything, so an unstamped build never sees an upgrade, and an
// unstamped agent is never held below a version floor.
//
// Ordering lives here, beside the stamp it orders, because both planes need it:
// the control plane offers upgrades against it, and the agent measures itself
// against a snapshot's floor. Neither plane may import the other (arch R1), and
// this package is the shared leaf that already carries Version.
func Less(a, b string) bool {
pa, oka := parse(a)
pb, okb := parse(b)
if !oka || !okb {
return false
}
return before(pa, pb)
}
// Ordered reports whether v has a shape this ordering understands. It is the
// predicate behind Less's "unparsable orders before nothing" rule, exported so
// that a constant meant to BE a version — a feature floor, a release tag — can
// be asserted well-formed at the point it is written down rather than
// discovered wrong by a fleet that silently refuses or admits.
func Ordered(v string) bool {
_, ok := parse(v)
return ok
}
// before compares two ordering tuples component by component.
func before(a, b [6]int) bool {
for i := range a {
if a[i] != b[i] {
return a[i] < b[i]
}
}
return false
}
// parse reads a version into its ordering tuple. Four shapes are accepted:
//
// vX.Y.Z a release
// vX.Y.Z-pre.N the Nth pre-release leading up to it
// vX.Y.Z-C-g<hex> C commits past the release (git describe)
// vX.Y.Z-pre.N-C-g<hex> C commits past the pre-release
//
// The tuple is (X, Y, Z, final, pre, count). `final` is 1 for a release and 0
// for a pre-release of it, which is the whole trick: it sinks every
// vX.Y.Z-pre.N below vX.Y.Z without disturbing how anything else sorts, and
// leaves builds derived from either in their own place. Both counters must be
// exactly a non-negative decimal, and a describe suffix must carry a
// "g"-prefixed non-empty hex abbrev — a "-dirty" marker or any other trailing
// text makes the version unparsable.
func parse(v string) ([6]int, bool) {
var out [6]int
core, suffix, hasSuffix := strings.Cut(strings.TrimPrefix(v, "v"), "-")
out[3] = 1 // a release outranks every pre-release of itself
if hasSuffix {
if pre, isPre := strings.CutPrefix(suffix, "pre."); isPre {
out[3] = 0
num, rest, hasRest := strings.Cut(pre, "-")
n, ok := decimal(num)
if !ok {
return out, false
}
out[4] = n
suffix, hasSuffix = rest, hasRest
}
}
if hasSuffix {
count, abbrev, found := strings.Cut(suffix, "-")
if !found || !isGitAbbrev(abbrev) {
return out, false
}
n, ok := decimal(count)
if !ok {
return out, false
}
out[5] = n
}
parts := strings.SplitN(core, ".", 3)
if len(parts) != 3 {
return out, false
}
for i, p := range parts {
n, ok := decimal(p)
if !ok {
return out, false
}
out[i] = n
}
return out, true
}
// decimal parses one non-negative decimal component, rejecting the sign Atoi
// would otherwise accept and any value too large to hold.
func decimal(s string) (int, bool) {
if !isDecimal(s) {
return 0, false
}
n, err := strconv.Atoi(s)
if err != nil {
return 0, false
}
return n, true
}
// isDecimal reports whether s is a non-empty run of decimal digits — the sign
// Atoi would otherwise accept is not part of a version component.
func isDecimal(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// isGitAbbrev reports whether s is a "g"-prefixed non-empty hex object abbrev,
// the shape git describe appends after the commit count.
func isGitAbbrev(s string) bool {
hex, ok := strings.CutPrefix(s, "g")
if !ok || hex == "" {
return false
}
for _, r := range hex {
switch {
case r >= '0' && r <= '9', r >= 'a' && r <= 'f', r >= 'A' && r <= 'F':
default:
return false
}
}
return true
}