diff --git a/.gitignore b/.gitignore index 1e8445d..ecbef58 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ tmp/ dist/ semvertool + +coverprofile.out diff --git a/cmd/root.go b/cmd/root.go index 27a614d..6290db6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,4 +43,6 @@ func init() { // Add the sort subcommand to the root command rootCmd.AddCommand(SortCmd) + + rootCmd.AddCommand(scriptCmd) } diff --git a/cmd/script.go b/cmd/script.go new file mode 100644 index 0000000..25a6235 --- /dev/null +++ b/cmd/script.go @@ -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 ", + 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 ", + 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) +} diff --git a/cmd/script_test.go b/cmd/script_test.go new file mode 100644 index 0000000..6cd63ea --- /dev/null +++ b/cmd/script_test.go @@ -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) +}