Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 148 additions & 42 deletions internal/operations/bump.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
BumpMajor BumpType = "major"
BumpRelease BumpType = "release"
BumpAuto BumpType = "auto"
BumpPre BumpType = "pre"
)

// BumpOperation performs a version bump on a module.
Expand Down Expand Up @@ -59,69 +60,174 @@ func (op *BumpOperation) Execute(ctx context.Context, mod *workspace.Module) err
return fmt.Errorf("failed to read version from %s: %w", mod.Path, err)
}

// Perform the bump based on type
var newVer semver.SemVersion
// Calculate the new version
newVer, err := op.calculateNewVersion(currentVer)
if err != nil {
return err
}

// BumpPre handles its own metadata, others use common logic
if op.bumpType != BumpPre {
op.applyPreReleaseAndMetadata(&newVer, currentVer)
}

// Write the new version
if err := vm.Save(ctx, mod.Path, newVer); err != nil {
return fmt.Errorf("failed to write version to %s: %w", mod.Path, err)
}

// Update module's current version for display
mod.CurrentVersion = newVer.String()

return nil
}

// calculateNewVersion computes the new version based on bump type.
func (op *BumpOperation) calculateNewVersion(currentVer semver.SemVersion) (semver.SemVersion, error) {
switch op.bumpType {
case BumpPatch:
newVer = semver.SemVersion{
Major: currentVer.Major,
Minor: currentVer.Minor,
Patch: currentVer.Patch + 1,
}
return op.bumpPatch(currentVer), nil
case BumpMinor:
newVer = semver.SemVersion{
Major: currentVer.Major,
Minor: currentVer.Minor + 1,
Patch: 0,
}
return op.bumpMinor(currentVer), nil
case BumpMajor:
newVer = semver.SemVersion{
Major: currentVer.Major + 1,
Minor: 0,
Patch: 0,
}
return op.bumpMajor(currentVer), nil
case BumpRelease:
// Release removes pre-release and build metadata
newVer = semver.SemVersion{
Major: currentVer.Major,
Minor: currentVer.Minor,
Patch: currentVer.Patch,
}
return op.bumpRelease(currentVer), nil
case BumpAuto:
// Auto bump uses heuristic-based logic
autoVer, autoErr := semver.BumpNextFunc(currentVer)
if autoErr != nil {
return fmt.Errorf("auto bump failed: %w", autoErr)
}
newVer = autoVer
return op.bumpAuto(currentVer)
case BumpPre:
return op.bumpPre(currentVer)
default:
return fmt.Errorf("unknown bump type: %s", op.bumpType)
return semver.SemVersion{}, fmt.Errorf("unknown bump type: %s", op.bumpType)
}
}

func (op *BumpOperation) bumpPatch(current semver.SemVersion) semver.SemVersion {
return semver.SemVersion{
Major: current.Major,
Minor: current.Minor,
Patch: current.Patch + 1,
}
}

func (op *BumpOperation) bumpMinor(current semver.SemVersion) semver.SemVersion {
return semver.SemVersion{
Major: current.Major,
Minor: current.Minor + 1,
Patch: 0,
}
}

// Apply pre-release label if provided
func (op *BumpOperation) bumpMajor(current semver.SemVersion) semver.SemVersion {
return semver.SemVersion{
Major: current.Major + 1,
Minor: 0,
Patch: 0,
}
}

func (op *BumpOperation) bumpRelease(current semver.SemVersion) semver.SemVersion {
return semver.SemVersion{
Major: current.Major,
Minor: current.Minor,
Patch: current.Patch,
}
}

func (op *BumpOperation) bumpAuto(current semver.SemVersion) (semver.SemVersion, error) {
newVer, err := semver.BumpNextFunc(current)
if err != nil {
return semver.SemVersion{}, fmt.Errorf("auto bump failed: %w", err)
}
return newVer, nil
}

func (op *BumpOperation) bumpPre(current semver.SemVersion) (semver.SemVersion, error) {
newVer := semver.SemVersion{
Major: current.Major,
Minor: current.Minor,
Patch: current.Patch,
}

// Determine the pre-release value
preRelease, err := op.calculatePreRelease(current)
if err != nil {
return semver.SemVersion{}, err
}
newVer.PreRelease = preRelease

// Apply metadata for pre-release bump
op.applyMetadata(&newVer, current)

return newVer, nil
}

// calculatePreRelease determines the pre-release string for BumpPre.
func (op *BumpOperation) calculatePreRelease(current semver.SemVersion) (string, error) {
if op.preRelease != "" {
return semver.IncrementPreRelease(current.PreRelease, op.preRelease), nil
}
if current.PreRelease != "" {
base := extractPreReleaseBase(current.PreRelease)
return semver.IncrementPreRelease(current.PreRelease, base), nil
}
return "", fmt.Errorf("current version has no pre-release; use --label to specify one")
}

// applyPreReleaseAndMetadata applies pre-release and metadata to the version.
func (op *BumpOperation) applyPreReleaseAndMetadata(newVer *semver.SemVersion, currentVer semver.SemVersion) {
if op.preRelease != "" {
newVer.PreRelease = op.preRelease
}
op.applyMetadata(newVer, currentVer)
}

// Apply metadata
// applyMetadata applies build metadata to the version.
func (op *BumpOperation) applyMetadata(newVer *semver.SemVersion, currentVer semver.SemVersion) {
if op.metadata != "" {
newVer.Build = op.metadata
} else if op.preserveMetadata && currentVer.Build != "" {
newVer.Build = currentVer.Build
}

// Write the new version
if err := vm.Save(ctx, mod.Path, newVer); err != nil {
return fmt.Errorf("failed to write version to %s: %w", mod.Path, err)
}

// Update module's current version for display
mod.CurrentVersion = newVer.String()

return nil
}

// Name returns the name of this operation.
func (op *BumpOperation) Name() string {
return fmt.Sprintf("bump %s", op.bumpType)
}

// extractPreReleaseBase extracts the base label from a pre-release string.
// e.g., "rc.1" -> "rc", "beta.2" -> "beta", "alpha" -> "alpha", "rc1" -> "rc"
func extractPreReleaseBase(pre string) string {
// First, check for dot followed by a number
for i := len(pre) - 1; i >= 0; i-- {
if pre[i] == '.' {
// Check if everything after the dot is numeric
suffix := pre[i+1:]
isNumeric := true
for _, c := range suffix {
if c < '0' || c > '9' {
isNumeric = false
break
}
}
if isNumeric && len(suffix) > 0 {
return pre[:i]
}
}
}

// Check for trailing digits without dot (e.g., "rc1" -> "rc")
lastNonDigit := -1
for i := len(pre) - 1; i >= 0; i-- {
if pre[i] < '0' || pre[i] > '9' {
lastNonDigit = i
break
}
}
if lastNonDigit >= 0 && lastNonDigit < len(pre)-1 {
return pre[:lastNonDigit+1]
}

return pre
}
Loading