package lexicomai import ( "fmt" "regexp" "sort" "strconv" "strings" ) // distroNameRe — имя дистрибутива в репозитории: --.sh // (например ups-7.012-release.sh, ses-7.045-beta.sh). Версия может содержать // точки, ветка — release|beta. var distroNameRe = regexp.MustCompile(`^(?P[A-Za-z0-9_-]+)-(?P[0-9][0-9.]*)-(?Prelease|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) } }