Skip to content

Commit

Permalink
Replace version comparison package
Browse files Browse the repository at this point in the history
  • Loading branch information
pjcdawkins committed Jan 3, 2025
1 parent 8de3af6 commit 452145e
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 241 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22
toolchain go1.22.4

require (
github.com/Masterminds/semver/v3 v3.3.1
github.com/fatih/color v1.17.0
github.com/go-chi/chi/v5 v5.1.0
github.com/go-playground/validator/v10 v10.20.0
Expand All @@ -17,14 +18,12 @@ require (
github.com/stretchr/testify v1.9.0
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/crypto v0.31.0
golang.org/x/mod v0.18.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1r
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
Expand Down Expand Up @@ -160,8 +160,6 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
Expand Down
17 changes: 10 additions & 7 deletions internal/config/alt/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"os"
"time"

"golang.org/x/mod/semver"
"gopkg.in/yaml.v3"

"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/state"
"github.com/platformsh/cli/internal/version"
)

// ShouldUpdate returns whether the Update function may be run on configuration.
Expand Down Expand Up @@ -72,12 +72,15 @@ func Update(ctx context.Context, cnf *config.Config, debugLog func(fmt string, i
debugLog("Config is already up to date (updated at %v)", cnf.Metadata.UpdatedAt)
return nil
}
if newCnfStruct.Metadata.Version != "" &&
(cnf.Metadata.Version == newCnfStruct.Metadata.Version ||
// TODO find a version comparison tool that does not require a "v" prefix
semver.Compare(cnf.Metadata.Version, newCnfStruct.Metadata.Version) > 0) {
debugLog("Config is already up to date (version %s)", cnf.Metadata.Version)
return nil
if newCnfStruct.Metadata.Version != "" {
cmp, err := version.Compare(cnf.Metadata.Version, newCnfStruct.Metadata.Version)
if err != nil {
return fmt.Errorf("could not compare config versions: %w", err)
}
if cmp >= 0 {
debugLog("Config is already up to date (version %s)", cnf.Metadata.Version)
return nil
}
}
b, err := yaml.Marshal(newCnfNode)
if err != nil {
Expand Down
10 changes: 7 additions & 3 deletions internal/config/alt/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,15 @@ func TestUpdate(t *testing.T) {
}
resetTimes()

cnf.Metadata.Version = "v1.0.1"
remoteConfig = append(remoteConfig, []byte("\nmetadata: {version: v1.0.1}")...)
remoteConfig = append(remoteConfig, []byte("\nmetadata: {version: 1.0.1}")...)
cnf.Metadata.Version = "invalid"
err = alt.Update(ctx, cnf, logger)
assert.ErrorContains(t, err, "could not compare config versions")
resetTimes()
cnf.Metadata.Version = "1.0.1"
err = alt.Update(ctx, cnf, logger)
assert.NoError(t, err)
assert.Contains(t, lastLogged, "Config is already up to date (version v1.0.1)")
assert.Contains(t, lastLogged, "Config is already up to date (version 1.0.1)")

resetTimes()

Expand Down
4 changes: 1 addition & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"os"

"github.com/go-playground/validator/v10"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -39,8 +38,7 @@ func FromYAML(b []byte) (*Config, error) {
if err := yaml.Unmarshal(b, c); err != nil {
return nil, fmt.Errorf("invalid config YAML: %w", err)
}
v := validator.New()
if err := v.Struct(c); err != nil {
if err := getValidator().Struct(c); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
c.applyDynamicDefaults()
Expand Down
2 changes: 1 addition & 1 deletion internal/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type Config struct {

// Metadata defines information about the config itself.
type Metadata struct {
Version string `validate:"omitempty" yaml:"version,omitempty"`
Version string `validate:"omitempty,version" yaml:"version,omitempty"`
UpdatedAt time.Time `validate:"omitempty" yaml:"updated_at,omitempty"`
DownloadedAt time.Time `validate:"omitempty" yaml:"downloaded_at,omitempty"`
URL string `validate:"omitempty,url" yaml:"url,omitempty"`
Expand Down
25 changes: 25 additions & 0 deletions internal/config/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package config

import (
"reflect"

"github.com/go-playground/validator/v10"

"github.com/platformsh/cli/internal/version"
)

var _validator *validator.Validate

func getValidator() *validator.Validate {
if _validator == nil {
_validator = validator.New()
initCustomValidators(_validator)
}
return _validator
}

func initCustomValidators(v *validator.Validate) {
_ = v.RegisterValidation("version", func(fl validator.FieldLevel) bool {
return fl.Field().Kind() == reflect.String && version.Validate(fl.Field().String())
})
}
97 changes: 4 additions & 93 deletions internal/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,106 +6,22 @@ import (
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/mattn/go-isatty"

"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/state"
"github.com/platformsh/cli/internal/version"
)

var versionRegex = regexp.MustCompile(`^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<preRelease>.+))?$`)

// ReleaseInfo stores information about a release
type ReleaseInfo struct {
Version string `json:"tag_name"`
URL string `json:"html_url"`
PublishedAt time.Time `json:"published_at"`
}

// Version contains parsed information about a SemVer version
type Version struct {
VersionParts [3]int
PreReleaseParts []string
}

// CompareVersions and see which version is greater
func CompareVersions(a, b *Version) int {
// Compare Major, Minor and Patch versions
for i := 0; i < 3; i++ {
if a.VersionParts[i] > b.VersionParts[i] {
return 1
}
if a.VersionParts[i] < b.VersionParts[i] {
return -1
}
}

// Start comparing identifiers
for i := 0; ; i++ {
// Check that there are identifiers left
if len(a.PreReleaseParts) <= i && len(b.PreReleaseParts) <= i {
return 0
}

// Shorter takes precedence
if len(b.PreReleaseParts) <= i {
return 1
}
if len(a.PreReleaseParts) <= i {
return -1
}

aPart := a.PreReleaseParts[i]
bPart := b.PreReleaseParts[i]
aInt, aErr := strconv.Atoi(aPart)
bInt, bErr := strconv.Atoi(bPart)

// Try comparing integers first
if aErr == nil && bErr == nil {
if aInt > bInt {
return 1
}
if aInt < bInt {
return -1
}
// Integer wins string
} else if aErr == nil {
return 1
} else if bErr == nil {
return -1
// Compare strings
} else if cmp := strings.Compare(aPart, bPart); cmp != 0 {
return cmp
}
}
}

// ParseVersion from a string, returning a Version or error if it's not SemVer
func ParseVersion(version string) (*Version, error) {
if !versionRegex.MatchString(version) {
return nil, fmt.Errorf("version does not match SemVer: %s", version)
}

result := versionRegex.FindStringSubmatch(version)
major, _ := strconv.Atoi(result[versionRegex.SubexpIndex("major")])
minor, _ := strconv.Atoi(result[versionRegex.SubexpIndex("minor")])
patch, _ := strconv.Atoi(result[versionRegex.SubexpIndex("patch")])
preRelease := result[versionRegex.SubexpIndex("preRelease")]
var preReleaseParts []string
if preRelease != "" {
preReleaseParts = strings.Split(preRelease, ".")
}

return &Version{
VersionParts: [3]int{major, minor, patch},
PreReleaseParts: preReleaseParts,
}, nil
}

// CheckForUpdate checks whether this software has had a newer release on GitHub
func CheckForUpdate(cnf *config.Config, currentVersion string) (*ReleaseInfo, error) {
if !shouldCheckForUpdate(cnf) {
Expand All @@ -130,16 +46,11 @@ func CheckForUpdate(cnf *config.Config, currentVersion string) (*ReleaseInfo, er
return nil, fmt.Errorf("could not determine latest release: %w", err)
}

currentVersionParsed, err := ParseVersion(currentVersion)
cmp, err := version.Compare(releaseInfo.Version, currentVersion)
if err != nil {
return nil, err
}

latestVersionParsed, err := ParseVersion(releaseInfo.Version)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not compare versions: %w", err)
}
if CompareVersions(latestVersionParsed, currentVersionParsed) == 1 {
if cmp > 0 {
return releaseInfo, nil
}

Expand Down
Loading

0 comments on commit 452145e

Please sign in to comment.