Files
stamp/internal/semver/semver.go
2026-03-12 23:41:12 +01:00

149 lines
4.3 KiB
Go

package semver
import (
"fmt"
"strings"
goSemver "github.com/Masterminds/semver/v3"
"git.thokra.dev/thokra/stamp/internal/changeset"
)
// PreReleaseBump converts a regular bump type to its pre-release equivalent,
// taking into account whether the current version is already a pre-release.
// If it is, the bump is always BumpPreRelease (just increment the pre number).
// Otherwise the highest regular bump is promoted to its pre-release variant.
func PreReleaseBump(currentVersion string, highest changeset.BumpType) changeset.BumpType {
v, err := goSemver.NewVersion(currentVersion)
if err == nil && v.Prerelease() != "" {
return changeset.BumpPreRelease
}
switch highest {
case changeset.BumpMajor:
return changeset.BumpPreMajor
case changeset.BumpMinor:
return changeset.BumpPreMinor
default:
return changeset.BumpPrePatch
}
}
// CurrentVersion parses a version string and returns the semver object.
// It is exported so callers can inspect pre-release state without re-importing the semver library.
func CurrentVersion(version string) (*goSemver.Version, error) {
return goSemver.NewVersion(version)
}
// Bump computes the next version given the current version string and a bump type.
// preID is the pre-release identifier (e.g. "alpha", "beta", "rc") used for pre-* bumps.
func Bump(current string, bump changeset.BumpType, preID string) (string, error) {
v, err := goSemver.NewVersion(current)
if err != nil {
return "", fmt.Errorf("invalid version %q: %w", current, err)
}
if preID == "" {
preID = "0"
}
switch bump {
case changeset.BumpMajor:
next := v.IncMajor()
return next.Original(), nil
case changeset.BumpMinor:
next := v.IncMinor()
return next.Original(), nil
case changeset.BumpPatch:
next := v.IncPatch()
return next.Original(), nil
case changeset.BumpPreMajor:
next := v.IncMajor()
pre, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0", next.Major(), next.Minor(), next.Patch(), preID))
if err != nil {
return "", err
}
return pre.Original(), nil
case changeset.BumpPreMinor:
next := v.IncMinor()
pre, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0", next.Major(), next.Minor(), next.Patch(), preID))
if err != nil {
return "", err
}
return pre.Original(), nil
case changeset.BumpPrePatch:
next := v.IncPatch()
pre, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0", next.Major(), next.Minor(), next.Patch(), preID))
if err != nil {
return "", err
}
return pre.Original(), nil
case changeset.BumpPreRelease:
// If already a pre-release with matching identifier, increment the pre-release number.
// Otherwise start a fresh pre-release on current patch.
preStr := v.Prerelease()
if preStr != "" && strings.HasPrefix(preStr, preID+".") {
parts := strings.SplitN(preStr, ".", 2)
var num int
fmt.Sscanf(parts[1], "%d", &num)
next, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.%d",
v.Major(), v.Minor(), v.Patch(), preID, num+1))
if err != nil {
return "", err
}
return next.Original(), nil
}
// Start a new pre-release on next patch.
patch := v.IncPatch()
next, err := goSemver.NewVersion(fmt.Sprintf("%d.%d.%d-%s.0",
patch.Major(), patch.Minor(), patch.Patch(), preID))
if err != nil {
return "", err
}
return next.Original(), nil
default:
return "", fmt.Errorf("unknown bump type %q", bump)
}
}
// HighestBump returns the highest-priority bump type from a set of bumps.
// Priority: major > minor > patch > premajor > preminor > prepatch > prerelease.
func HighestBump(bumps []changeset.BumpType) changeset.BumpType {
priority := map[changeset.BumpType]int{
changeset.BumpMajor: 6,
changeset.BumpMinor: 5,
changeset.BumpPatch: 4,
changeset.BumpPreMajor: 3,
changeset.BumpPreMinor: 2,
changeset.BumpPrePatch: 1,
changeset.BumpPreRelease: 0,
}
best := changeset.BumpPreRelease
bestP := -1
for _, b := range bumps {
if p, ok := priority[b]; ok && p > bestP {
best = b
bestP = p
}
}
return best
}
// ProjectBumps aggregates bump types per project from a list of changesets.
func ProjectBumps(changesets []*changeset.Changeset) map[string][]changeset.BumpType {
result := map[string][]changeset.BumpType{}
for _, cs := range changesets {
for project, bump := range cs.Bumps {
result[project] = append(result[project], bump)
}
}
return result
}