Skip to content

Commit

Permalink
install: add the ability to flexibly define the version
Browse files Browse the repository at this point in the history
Allow specify version like:
 - 2.11 - Chooses last release patch for specified major.minor set
 - 3 - get last available release version with required major==3
 - 2.11-rc - will select in major.minor version with last release candidate mark
 And many other combination matched with version regular expression.

Closes #737
  • Loading branch information
dmyger authored and oleg-jukovec committed Sep 25, 2024
1 parent 48df9f5 commit 0c4094e
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 67 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
cluster config (3.0) or cartridge orchestrator.
- `tt replicaset roles remove`: command to remove roles in the tarantool replicaset with
cluster config (3.0) or cartridge orchestrator.
- `tt install tt|tarantool <version>` - allow <version> be incomplete. So `2.3` will install
the the last available release with specified <major=2>.<minor=3> in <version>.

### Fixed
- Command `\set delimiter [marker]` works correctly and don't hangs `tt` console.
Expand Down
64 changes: 25 additions & 39 deletions cli/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ const (
// We need to give permission for all to execute
// read,write for user and only read for others.
defaultDirPermissions = 0755

MajorMinorPatchRegexp = `^[0-9]+\.[0-9]+\.[0-9]+`
)

// programGitRepoUrls contains URLs of programs git repositories.
Expand Down Expand Up @@ -206,20 +204,11 @@ func detectOsName() (string, error) {
// getVersionsFromRepo returns all available versions from github repository.
func getVersionsFromRepo(local bool, distfiles string, program string,
repolink string) ([]version.Version, error) {
versions := []version.Version{}
var err error

if local {
versions, err = search.GetVersionsFromGitLocal(filepath.Join(distfiles, program))
} else {
versions, err = search.GetVersionsFromGitRemote(repolink)
}

if err != nil {
return nil, err
return search.GetVersionsFromGitLocal(filepath.Join(distfiles, program))
}

return versions, nil
return search.GetVersionsFromGitRemote(repolink)
}

// getCommit returns all available commits from repository.
Expand Down Expand Up @@ -502,30 +491,25 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error {
// The tag format in tt is vX.Y.Z, but the user can use the X.Y.Z format
// and this option needs to be supported.
if ttVersion != "master" {
_, err := version.Parse(ttVersion)
if err == nil {
if version.IsVersion(ttVersion, false) {
log.Infof("Searching in versions...")
versions, err := getVersionsFromRepo(installCtx.Local, distfiles,
"tt", search.GitRepoTT)
if err != nil {
return err
}

versionMatches, err := regexp.Match(MajorMinorPatchRegexp, []byte(ttVersion))
match, err := version.MatchVersion(ttVersion, versions)
if err != nil {
return err
}
for _, ver := range versions {
if ttVersion == ver.Str || (versionMatches && "v"+ttVersion == ver.Str) {
versionFound = true
ttVersion = ver.Str
break
var errNotFound version.NotFoundError
if !errors.As(err, &errNotFound) {
return err
}
} else {
versionFound = true
ttVersion = match
}
if !versionFound {
return fmt.Errorf("%s version of tt doesn't exist", ttVersion)
}
} else {
}
if !versionFound {
isPullRequest, pullRequestID = util.IsPullRequest(ttVersion)

if isPullRequest {
Expand All @@ -534,13 +518,13 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error {
log.Infof("Searching in commits...")
}

var err error
ttVersion, pullRequestHash, err = checkCommit(
ttVersion, "tt", installCtx, distfiles)
if err != nil {
return err
}
}

}

versionStr := ""
Expand Down Expand Up @@ -1018,24 +1002,25 @@ func installTarantool(binDir string, incDir string, installCtx InstallCtx,

// Check that the version exists.
if tarVersion != "master" {
_, err := version.Parse(tarVersion)
if err == nil {
if version.IsVersion(tarVersion, false) {
log.Infof("Searching in versions...")
versions, err := getVersionsFromRepo(installCtx.Local, distfiles, "tarantool",
search.GitRepoTarantool)
if err != nil {
return err
}
for _, ver := range versions {
if tarVersion == ver.Str {
versionFound = true
break
match, err := version.MatchVersion(tarVersion, versions)
if err != nil {
var errNotFound version.NotFoundError
if !errors.As(err, &errNotFound) {
return err
}
} else {
versionFound = true
tarVersion = match
}
if !versionFound {
return fmt.Errorf("%s version of tarantool doesn't exist", tarVersion)
}
} else {
}
if !versionFound {
isPullRequest, pullRequestID = util.IsPullRequest(tarVersion)

if isPullRequest {
Expand All @@ -1044,6 +1029,7 @@ func installTarantool(binDir string, incDir string, installCtx InstallCtx,
log.Infof("Searching in commits...")
}

var err error
tarVersion, pullRequestHash, err = checkCommit(
tarVersion, "tarantool", installCtx, distfiles)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion cli/uninstall/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const (
search.ProgramCe + "|" +
search.ProgramEe + ")"
verRegexp = "(?P<ver>.*)"

MajorMinorPatchRegexp = `^[0-9]+\.[0-9]+\.[0-9]+`
)

var errNotInstalled = errors.New("program is not installed")
Expand Down Expand Up @@ -157,7 +159,7 @@ func getAllTtVersionFormats(programName, ttVersion string) ([]string, error) {
if programName == search.ProgramTt {
// Need to determine if we have x.y.z format in tt uninstall argument
// to make sure we add version prefix.
versionMatches, err := regexp.Match(install.MajorMinorPatchRegexp, []byte(ttVersion))
versionMatches, err := regexp.Match(MajorMinorPatchRegexp, []byte(ttVersion))
if err != nil {
return versionsToDelete, err
}
Expand Down
2 changes: 1 addition & 1 deletion cli/uninstall/uninstall_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func TestGetAllVersionFormats(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
ttVersions, err := getAllTtVersionFormats(tc.programName, tc.ttVersion)
assert.NoError(t, err)
assert.Equal(t, ttVersions, tc.expectedVersions)
assert.Equal(t, tc.expectedVersions, ttVersions)
})
}
}
175 changes: 175 additions & 0 deletions cli/version/version_match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package version

import (
"fmt"

"github.com/tarantool/tt/cli/util"
)

type requiredField uint8
type requiredFields []requiredField

const (
// Values according order of Fields in regex from `matchVersionParts()`.
requiredFieldBuildName requiredField = iota
requiredFieldMajor
requiredFieldMinor
requiredFieldPatch
requiredFieldReleaseType
requiredFieldReleaseNum
requiredFieldAdditional
requiredFieldHash
requiredFieldRevision

countOfRequiredFields
)

// NotFoundError is sentinel error for case when required version not found.
type NotFoundError struct {
expected string
}

// Error implements the [error] interface.
func (e NotFoundError) Error() string {
return fmt.Sprintf("not matched with: %q", e.expected)
}

// exploreMatchVersion examines the given string and retrieves the version components,
// that define the rules for checking for version consistency.
func exploreMatchVersion(verStr string) (Version, requiredFields, error) {
var version Version
fields := make(requiredFields, 0, countOfRequiredFields)

matches, err := matchVersionParts(verStr, false)
if err != nil {
return version, fields, err
}

version.BuildName = matches["buildname"]
if version.BuildName != "" {
fields = append(fields, requiredFieldBuildName)
}

if matches["major"] != "" {
if version.Major, err = util.AtoiUint64(matches["major"]); err != nil {
return version, fields, fmt.Errorf("can't parse Major: %w", err)
}
fields = append(fields, requiredFieldMajor)
}

if matches["minor"] != "" {
if matches["major"] == "" {
return version, fields, fmt.Errorf("minor version requires major to be specified")
}
if version.Minor, err = util.AtoiUint64(matches["minor"]); err != nil {
return version, fields, fmt.Errorf("can't parse Minor: %w", err)
}
fields = append(fields, requiredFieldMinor)
}

if matches["patch"] != "" {
if matches["minor"] == "" {
return version, fields, fmt.Errorf("patch version requires minor to be specified")
}
if version.Patch, err = util.AtoiUint64(matches["patch"]); err != nil {
return version, fields, fmt.Errorf("can't parse Patch version: %w", err)
}
fields = append(fields, requiredFieldPatch)
}

if matches["release"] != "" {
version.Release, err = newRelease(matches["release"], matches["releaseNum"])
if err != nil {
return version, fields, fmt.Errorf("can't parse Release: %w", err)
}
fields = append(fields, requiredFieldReleaseType)
if matches["releaseNum"] != "" {
fields = append(fields, requiredFieldReleaseNum)
}
} else if len(fields) > 0 {
// By default require 'release' version.
version.Release.Type = TypeRelease
fields = append(fields, requiredFieldReleaseType)
}

if matches["additional"] != "" {
if version.Additional, err = util.AtoiUint64(matches["additional"]); err != nil {
return version, fields, fmt.Errorf("can't parse Additional: %w", err)
}
fields = append(fields, requiredFieldAdditional)
}

version.Hash = matches["hash"]
if version.Hash != "" {
fields = append(fields, requiredFieldHash)
}

if matches["revision"] != "" {
if version.Revision, err = util.AtoiUint64(matches["revision"]); err != nil {
return version, fields, fmt.Errorf("can't parse Revision: %w", err)
}
fields = append(fields, requiredFieldRevision)
}

version.Str = verStr

return version, fields, nil
}

// compareVersions compares with two Version according list of fields.
//
// Return `true` if all required version components the same.
func compareVersions(ref Version, other Version, fields requiredFields) bool {
for _, m := range fields {
isMatch := false
switch m {
case requiredFieldBuildName:
isMatch = ref.BuildName == other.BuildName
case requiredFieldMajor:
isMatch = ref.Major == other.Major
case requiredFieldMinor:
isMatch = ref.Minor == other.Minor
case requiredFieldPatch:
isMatch = ref.Patch == other.Patch
case requiredFieldReleaseType:
isMatch = ref.Release.Type == other.Release.Type
case requiredFieldReleaseNum:
isMatch = ref.Release.Num == other.Release.Num
case requiredFieldAdditional:
isMatch = ref.Additional == other.Additional
case requiredFieldHash:
isMatch = ref.Hash == other.Hash
case requiredFieldRevision:
isMatch = ref.Revision == other.Revision
}
if !isMatch {
return false
}
}
return true
}

// IsVersion checks is string looks like version.
// If isStrict is true, then version <major.minor.patch> components are required.
func IsVersion(version string, isStrict bool) bool {
re := createVersionRegexp(isStrict)
return re.MatchString(version)
}

// MatchVersion takes as input an ascending sorted list of versions.
// In which it finds the latest version matching the search mask.
//
// Return string of found version or `NotFoundVersion` with error if just not found.
func MatchVersion(expected string, sortedVersions []Version) (string, error) {
reference, fields, err := exploreMatchVersion(expected)
if err != nil {
return "", err
}
for i := len(sortedVersions) - 1; i >= 0; i-- {
ver := sortedVersions[i]
if compareVersions(reference, ver, fields) {
return ver.Str, nil
}
}
return "", NotFoundError{expected}
}
Loading

0 comments on commit 0c4094e

Please sign in to comment.