Files
forge-tools-lexicomai/internal/lexicomai/version.go
T
2026-10-01 09:50:59 +07:00

145 lines
4.6 KiB
Go

package lexicomai
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
)
// distroNameRe — имя дистрибутива в репозитории: <product>-<version>-<branch>.sh
// (например ups-7.012-release.sh, ses-7.045-beta.sh). Версия может содержать
// точки, ветка — release|beta.
var distroNameRe = regexp.MustCompile(`^(?P<product>[A-Za-z0-9_-]+)-(?P<version>[0-9][0-9.]*)-(?P<branch>release|beta)\.sh$`)
// Version — семантическая версия (разбивка по точкам). Сравнение числовое,
// поэтому 7.12 > 7.9 (в отличие от лексикографического).
type Version []int
// ParseVersion разбирает строку версии "7.012" в числовые сегменты.
// Нечисловой сегмент обрезается (считаем по корректному префиксу).
func ParseVersion(s string) Version {
parts := strings.Split(s, ".")
v := make(Version, 0, len(parts))
for _, p := range parts {
n, err := strconv.Atoi(strings.TrimSpace(p))
if err != nil {
break
}
v = append(v, n)
}
return v
}
// String возвращает канонический вид версии (сегменты через точку).
func (v Version) String() string {
s := make([]string, len(v))
for i, n := range v {
s[i] = strconv.Itoa(n)
}
return strings.Join(s, ".")
}
// Compare сравнивает две версии семантически.
func CompareVersions(a, b string) int {
return ParseVersion(a).Compare(ParseVersion(b))
}
// Compare сравнивает две версии посегментно (короткая версия считается меньшей
// при равенстве общих сегментов, что соответствует общепринятой семантике).
func (v Version) Compare(o Version) int {
n := len(v)
if len(o) < n {
n = len(o)
}
for i := 0; i < n; i++ {
if v[i] < o[i] {
return -1
}
if v[i] > o[i] {
return 1
}
}
switch {
case len(v) < len(o):
return -1
case len(v) > len(o):
return 1
default:
return 0
}
}
// distroParts — разобранное имя дистрибутива.
type distroParts struct {
Product string
Version string
Branch string
}
// parseDistroName разбирает имя файла дистрибутива.
func parseDistroName(name string) (distroParts, error) {
m := distroNameRe.FindStringSubmatch(name)
if m == nil {
return distroParts{}, fmt.Errorf("not a distro name: %q", name)
}
return distroParts{
Product: m[1],
Version: m[2],
Branch: m[3],
}, nil
}
// ResolveDistro выбирает имя файла дистрибутива из списка кандидатов.
//
// product — продукт; version — требуемая версия (пустая = последняя в ветке);
// branch — release|beta (по умолчанию release). Возвращает имя файла.
// Если по условиям кандидатов нет — доменная ошибка (модель может исправить).
func ResolveDistro(names []string, product, version, branch string) (string, error) {
if branch == "" {
branch = "release"
}
// Собираем кандидаты: продукт + ветка, отсортированные по убыванию версии.
var candidates []distroParts
for _, n := range names {
p, err := parseDistroName(n)
if err != nil {
continue
}
if p.Product == product && p.Branch == branch {
candidates = append(candidates, p)
}
}
if len(candidates) == 0 {
return "", fmt.Errorf("no %s distro in %s for %q", product, branch, product)
}
sort.SliceStable(candidates, func(i, j int) bool {
return CompareVersions(candidates[i].Version, candidates[j].Version) > 0
})
if version != "" {
for _, c := range candidates {
if c.Version == version {
return product + "-" + c.Version + "-" + c.Branch + ".sh", nil
}
}
return "", fmt.Errorf("version %q of %s not found in %s", version, product, branch)
}
top := candidates[0]
return product + "-" + top.Version + "-" + top.Branch + ".sh", nil
}
// normalizeBranch приводит значение ветки к каноническому виду (release/beta).
func normalizeBranch(branch string) (string, error) {
branch = strings.ToLower(strings.TrimSpace(branch))
switch branch {
case "", "release":
return "release", nil
case "beta":
return "beta", nil
default:
return "", fmt.Errorf("unknown branch %q (expected release or beta)", branch)
}
}