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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ tmp/
dist/

semvertool

coverprofile.out
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ func init() {

// Add the sort subcommand to the root command
rootCmd.AddCommand(SortCmd)

rootCmd.AddCommand(scriptCmd)
}
110 changes: 110 additions & 0 deletions cmd/script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright © 2025 James Evans
*/
package cmd

import (
"fmt"
"os"

"github.com/Masterminds/semver/v3"
"github.com/spf13/cobra"
)

// scriptCmd represents the script command
var scriptCmd = &cobra.Command{
Use: "script",
Short: "Script utilities for semantic versioning",
Long: `Provides utilities for scripting with semantic versions.

These commands are designed to be used in shell scripts, returning exit codes
that can be used in conditionals.`,
}

// CompareVersions compares two semantic versions and returns:
// 0 if versions are equal
// 11 if v1 is greater than v2 (v1 is newer)
// 12 if v2 is greater than v1 (v2 is newer)
// Returns an error if either version is invalid
func CompareVersions(v1string, v2string string) (int, error) {
v1, err := semver.NewVersion(v1string)
if err != nil {
return 0, fmt.Errorf("invalid version: %s", v1string)
}

v2, err := semver.NewVersion(v2string)
if err != nil {
return 0, fmt.Errorf("invalid version: %s", v2string)
}

if v1.LessThan(v2) {
return 12, nil // v2 is newer
} else if v1.Equal(v2) {
return 0, nil // equal versions
} else {
return 11, nil // v1 is newer
}
}

// IsReleased checks if a version is a release version (no prerelease or metadata)
// Returns true for release versions, false for prerelease versions or those with metadata
// Returns an error if the version is invalid
func IsReleased(versionString string) (bool, error) {
v, err := semver.NewVersion(versionString)
if err != nil {
return false, fmt.Errorf("invalid version: %s", versionString)
}

return v.Prerelease() == "" && v.Metadata() == "", nil
}

// compareCmd represents the compare subcommand
var compareCmd = &cobra.Command{
Use: "compare <version1> <version2>",
Short: "Compare two semantic versions",
Long: `Compare two semantic versions and return an exit code based on the comparison:

0: version1 = version2
11: version1 > version2
12: version1 < version2

If there is an error, the command will return 1.`,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
result, err := CompareVersions(args[0], args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
os.Exit(result)
},
}

// releasedCmd represents the released subcommand
var releasedCmd = &cobra.Command{
Use: "released <version>",
Short: "Check if a version is a release version",
Long: `Check if a version is a release version (not a prerelease and has no metadata).

Returns exit code 0 if the version is a release version (X.Y.Z only),
Returns exit code 1 if the version is a prerelease or has metadata.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
isReleased, err := IsReleased(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}

if isReleased {
os.Exit(0)
} else {
os.Exit(1)
}
},
}

func init() {
scriptCmd.AddCommand(compareCmd)
scriptCmd.AddCommand(releasedCmd)
}
84 changes: 84 additions & 0 deletions cmd/script_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/assert"
)

// Tests for CompareVersions function
func TestCompareVersionsLessThan(t *testing.T) {
result, err := CompareVersions("1.0.0", "2.0.0")
assert.NoError(t, err)
assert.Equal(t, 12, result) // v2 is newer
}

func TestCompareVersionsEqual(t *testing.T) {
result, err := CompareVersions("1.0.0", "1.0.0")
assert.NoError(t, err)
assert.Equal(t, 0, result) // equal versions
}

func TestCompareVersionsGreaterThan(t *testing.T) {
result, err := CompareVersions("2.0.0", "1.0.0")
assert.NoError(t, err)
assert.Equal(t, 11, result) // v1 is newer
}

func TestCompareVersionsFirstInvalid(t *testing.T) {
_, err := CompareVersions("invalid", "1.0.0")
assert.Error(t, err)
}

func TestCompareVersionsSecondInvalid(t *testing.T) {
_, err := CompareVersions("1.0.0", "invalid")
assert.Error(t, err)
}

func TestCompareVersionsPatchVersions(t *testing.T) {
result, err := CompareVersions("1.0.1", "1.0.2")
assert.NoError(t, err)
assert.Equal(t, 12, result) // v2 is newer
}

func TestCompareVersionsPrereleaseVsRelease(t *testing.T) {
result, err := CompareVersions("1.0.0-alpha", "1.0.0")
assert.NoError(t, err)
assert.Equal(t, 12, result) // v2 is newer
}

// Tests for IsReleased function
func TestIsReleasedSimpleReleaseVersion(t *testing.T) {
result, err := IsReleased("1.0.0")
assert.NoError(t, err)
assert.True(t, result)
}

func TestIsReleasedPrereleaseVersion(t *testing.T) {
result, err := IsReleased("1.0.0-alpha.1")
assert.NoError(t, err)
assert.False(t, result)
}

func TestIsReleasedVersionWithMetadata(t *testing.T) {
result, err := IsReleased("1.0.0+20130313144700")
assert.NoError(t, err)
assert.False(t, result)
}

func TestIsReleasedPrereleaseWithMetadata(t *testing.T) {
result, err := IsReleased("1.0.0-beta.1+exp.sha.5114f85")
assert.NoError(t, err)
assert.False(t, result)
}

func TestIsReleasedInvalidVersion(t *testing.T) {
_, err := IsReleased("invalid")
assert.Error(t, err)
}

func TestIsReleasedComplexVersion(t *testing.T) {
result, err := IsReleased("2.1.0-rc.2")
assert.NoError(t, err)
assert.False(t, result)
}