410 lines
10 KiB
Go
410 lines
10 KiB
Go
package version
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// The compiled regular expression used to test the validity of a version.
|
|
var (
|
|
versionRegexp *regexp.Regexp
|
|
semverRegexp *regexp.Regexp
|
|
)
|
|
|
|
// The raw regular expression string used for testing the validity
|
|
// of a version.
|
|
const (
|
|
VersionRegexpRaw string = `(?:\w+\-)*[vV]?` + // Optional prefixes, will be ignored for parsing
|
|
`([0-9]+(\.[0-9]+)*?)` + // ( MajorNum ( '.' MinorNums ) *? )
|
|
`(-` + // Followed by (optionally): ( '-'
|
|
`([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)` + // Either ( PreNum String ( '.' OtherString ) * )
|
|
`|` +
|
|
`([-\.]?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` + // Or ( ['-' '.' ] ? ( AlphaHyphenTilde String * ( '.' String ) * ))) ?
|
|
`(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` + // and more Optionally: ( '+' String ( '.' String ) * )
|
|
`([\+\.\-~]g[0-9A-Fa-f]{10}$)?` + // Optionally a: ( Punct 'g' Sha )
|
|
`?`
|
|
|
|
// SemverRegexpRaw requires a separator between version and prerelease
|
|
SemverRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` +
|
|
`(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` +
|
|
`(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` +
|
|
`?`
|
|
)
|
|
|
|
// Version represents a single version.
|
|
type Version struct {
|
|
metadata string
|
|
pre string
|
|
segments []int64
|
|
si int
|
|
original string
|
|
}
|
|
|
|
func init() {
|
|
versionRegexp = regexp.MustCompile("^" + VersionRegexpRaw + "$")
|
|
semverRegexp = regexp.MustCompile("^" + SemverRegexpRaw + "$")
|
|
}
|
|
|
|
// NewVersion parses the given version and returns a new
|
|
// Version.
|
|
func NewVersion(v string) (*Version, error) {
|
|
return newVersion(v, versionRegexp)
|
|
}
|
|
|
|
// NewSemver parses the given version and returns a new
|
|
// Version that adheres strictly to SemVer specs
|
|
// https://semver.org/
|
|
func NewSemver(v string) (*Version, error) {
|
|
return newVersion(v, semverRegexp)
|
|
}
|
|
|
|
func newVersion(v string, pattern *regexp.Regexp) (*Version, error) {
|
|
matches := pattern.FindStringSubmatch(v)
|
|
if matches == nil {
|
|
return nil, fmt.Errorf("Malformed version: %s", v)
|
|
}
|
|
segmentsStr := strings.Split(matches[1], ".")
|
|
segments := make([]int64, len(segmentsStr))
|
|
si := 0
|
|
for i, str := range segmentsStr {
|
|
val, err := strconv.ParseInt(str, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Error parsing version: %s", err)
|
|
}
|
|
|
|
segments[i] = int64(val)
|
|
si++
|
|
}
|
|
|
|
// Even though we could support more than three segments, if we
|
|
// got less than three, pad it with 0s. This is to cover the basic
|
|
// default usecase of semver, which is MAJOR.MINOR.PATCH at the minimum
|
|
for i := len(segments); i < 3; i++ {
|
|
segments = append(segments, 0)
|
|
}
|
|
|
|
pre := matches[7]
|
|
if pre == "" {
|
|
pre = matches[4]
|
|
}
|
|
|
|
return &Version{
|
|
metadata: matches[10],
|
|
pre: pre,
|
|
segments: segments,
|
|
si: si,
|
|
original: v,
|
|
}, nil
|
|
}
|
|
|
|
// Must is a helper that wraps a call to a function returning (*Version, error)
|
|
// and panics if error is non-nil.
|
|
func Must(v *Version, err error) *Version {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
// Compare compares this version to another version. This
|
|
// returns -1, 0, or 1 if this version is smaller, equal,
|
|
// or larger than the other version, respectively.
|
|
//
|
|
// If you want boolean results, use the LessThan, Equal,
|
|
// GreaterThan, GreaterThanOrEqual or LessThanOrEqual methods.
|
|
func (v *Version) Compare(other *Version) int {
|
|
// A quick, efficient equality check
|
|
if v.String() == other.String() {
|
|
return 0
|
|
}
|
|
|
|
segmentsSelf := v.Segments64()
|
|
segmentsOther := other.Segments64()
|
|
|
|
// If the segments are the same, we must compare on prerelease info
|
|
if reflect.DeepEqual(segmentsSelf, segmentsOther) {
|
|
preSelf := v.Prerelease()
|
|
preOther := other.Prerelease()
|
|
if preSelf == "" && preOther == "" {
|
|
return 0
|
|
}
|
|
if preSelf == "" {
|
|
return 1
|
|
}
|
|
if preOther == "" {
|
|
return -1
|
|
}
|
|
|
|
return comparePrereleases(preSelf, preOther)
|
|
}
|
|
|
|
// Get the highest specificity (hS), or if they're equal, just use segmentSelf length
|
|
lenSelf := len(segmentsSelf)
|
|
lenOther := len(segmentsOther)
|
|
hS := lenSelf
|
|
if lenSelf < lenOther {
|
|
hS = lenOther
|
|
}
|
|
// Compare the segments
|
|
// Because a constraint could have more/less specificity than the version it's
|
|
// checking, we need to account for a lopsided or jagged comparison
|
|
for i := 0; i < hS; i++ {
|
|
if i > lenSelf-1 {
|
|
// This means Self had the lower specificity
|
|
// Check to see if the remaining segments in Other are all zeros
|
|
if !allZero(segmentsOther[i:]) {
|
|
// if not, it means that Other has to be greater than Self
|
|
return -1
|
|
}
|
|
break
|
|
} else if i > lenOther-1 {
|
|
// this means Other had the lower specificity
|
|
// Check to see if the remaining segments in Self are all zeros -
|
|
if !allZero(segmentsSelf[i:]) {
|
|
// if not, it means that Self has to be greater than Other
|
|
return 1
|
|
}
|
|
break
|
|
}
|
|
lhs := segmentsSelf[i]
|
|
rhs := segmentsOther[i]
|
|
if lhs == rhs {
|
|
continue
|
|
} else if lhs < rhs {
|
|
return -1
|
|
}
|
|
// Otherwis, rhs was > lhs, they're not equal
|
|
return 1
|
|
}
|
|
|
|
// if we got this far, they're equal
|
|
return 0
|
|
}
|
|
|
|
func allZero(segs []int64) bool {
|
|
for _, s := range segs {
|
|
if s != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func comparePart(preSelf string, preOther string) int {
|
|
if preSelf == preOther {
|
|
return 0
|
|
}
|
|
|
|
var selfInt int64
|
|
selfNumeric := true
|
|
selfInt, err := strconv.ParseInt(preSelf, 10, 64)
|
|
if err != nil {
|
|
selfNumeric = false
|
|
}
|
|
|
|
var otherInt int64
|
|
otherNumeric := true
|
|
otherInt, err = strconv.ParseInt(preOther, 10, 64)
|
|
if err != nil {
|
|
otherNumeric = false
|
|
}
|
|
|
|
// if a part is empty, we use the other to decide
|
|
if preSelf == "" {
|
|
if otherNumeric {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
|
|
if preOther == "" {
|
|
if selfNumeric {
|
|
return 1
|
|
}
|
|
return -1
|
|
}
|
|
|
|
if selfNumeric && !otherNumeric {
|
|
return -1
|
|
} else if !selfNumeric && otherNumeric {
|
|
return 1
|
|
} else if !selfNumeric && !otherNumeric && preSelf > preOther {
|
|
return 1
|
|
} else if selfInt > otherInt {
|
|
return 1
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
func comparePrereleases(v string, other string) int {
|
|
// the same pre release!
|
|
if v == other {
|
|
return 0
|
|
}
|
|
|
|
// split both pre releases for analyse their parts
|
|
selfPreReleaseMeta := strings.Split(v, ".")
|
|
otherPreReleaseMeta := strings.Split(other, ".")
|
|
|
|
selfPreReleaseLen := len(selfPreReleaseMeta)
|
|
otherPreReleaseLen := len(otherPreReleaseMeta)
|
|
|
|
biggestLen := otherPreReleaseLen
|
|
if selfPreReleaseLen > otherPreReleaseLen {
|
|
biggestLen = selfPreReleaseLen
|
|
}
|
|
|
|
// loop for parts to find the first difference
|
|
for i := 0; i < biggestLen; i++ {
|
|
partSelfPre := ""
|
|
if i < selfPreReleaseLen {
|
|
partSelfPre = selfPreReleaseMeta[i]
|
|
}
|
|
|
|
partOtherPre := ""
|
|
if i < otherPreReleaseLen {
|
|
partOtherPre = otherPreReleaseMeta[i]
|
|
}
|
|
|
|
compare := comparePart(partSelfPre, partOtherPre)
|
|
// if parts are equals, continue the loop
|
|
if compare != 0 {
|
|
return compare
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// Core returns a new version constructed from only the MAJOR.MINOR.PATCH
|
|
// segments of the version, without prerelease or metadata.
|
|
func (v *Version) Core() *Version {
|
|
segments := v.Segments64()
|
|
segmentsOnly := fmt.Sprintf("%d.%d.%d", segments[0], segments[1], segments[2])
|
|
return Must(NewVersion(segmentsOnly))
|
|
}
|
|
|
|
// Equal tests if two versions are equal.
|
|
func (v *Version) Equal(o *Version) bool {
|
|
if v == nil || o == nil {
|
|
return v == o
|
|
}
|
|
|
|
return v.Compare(o) == 0
|
|
}
|
|
|
|
// GreaterThan tests if this version is greater than another version.
|
|
func (v *Version) GreaterThan(o *Version) bool {
|
|
return v.Compare(o) > 0
|
|
}
|
|
|
|
// GreaterThanOrEqual tests if this version is greater than or equal to another version.
|
|
func (v *Version) GreaterThanOrEqual(o *Version) bool {
|
|
return v.Compare(o) >= 0
|
|
}
|
|
|
|
// LessThan tests if this version is less than another version.
|
|
func (v *Version) LessThan(o *Version) bool {
|
|
return v.Compare(o) < 0
|
|
}
|
|
|
|
// LessThanOrEqual tests if this version is less than or equal to another version.
|
|
func (v *Version) LessThanOrEqual(o *Version) bool {
|
|
return v.Compare(o) <= 0
|
|
}
|
|
|
|
// Metadata returns any metadata that was part of the version
|
|
// string.
|
|
//
|
|
// Metadata is anything that comes after the "+" in the version.
|
|
// For example, with "1.2.3+beta", the metadata is "beta".
|
|
func (v *Version) Metadata() string {
|
|
return v.metadata
|
|
}
|
|
|
|
// Prerelease returns any prerelease data that is part of the version,
|
|
// or blank if there is no prerelease data.
|
|
//
|
|
// Prerelease information is anything that comes after the "-" in the
|
|
// version (but before any metadata). For example, with "1.2.3-beta",
|
|
// the prerelease information is "beta".
|
|
func (v *Version) Prerelease() string {
|
|
return v.pre
|
|
}
|
|
|
|
// Segments returns the numeric segments of the version as a slice of ints.
|
|
//
|
|
// This excludes any metadata or pre-release information. For example,
|
|
// for a version "1.2.3-beta", segments will return a slice of
|
|
// 1, 2, 3.
|
|
func (v *Version) Segments() []int {
|
|
segmentSlice := make([]int, len(v.segments))
|
|
for i, v := range v.segments {
|
|
segmentSlice[i] = int(v)
|
|
}
|
|
return segmentSlice
|
|
}
|
|
|
|
// Segments64 returns the numeric segments of the version as a slice of int64s.
|
|
//
|
|
// This excludes any metadata or pre-release information. For example,
|
|
// for a version "1.2.3-beta", segments will return a slice of
|
|
// 1, 2, 3.
|
|
func (v *Version) Segments64() []int64 {
|
|
result := make([]int64, len(v.segments))
|
|
copy(result, v.segments)
|
|
return result
|
|
}
|
|
|
|
// String returns the full version string included pre-release
|
|
// and metadata information.
|
|
//
|
|
// This value is rebuilt according to the parsed segments and other
|
|
// information. Therefore, ambiguities in the version string such as
|
|
// prefixed zeroes (1.04.0 => 1.4.0), `v` prefix (v1.0.0 => 1.0.0), and
|
|
// missing parts (1.0 => 1.0.0) will be made into a canonicalized form
|
|
// as shown in the parenthesized examples.
|
|
func (v *Version) String() string {
|
|
var buf bytes.Buffer
|
|
fmtParts := make([]string, len(v.segments))
|
|
for i, s := range v.segments {
|
|
// We can ignore err here since we've pre-parsed the values in segments
|
|
str := strconv.FormatInt(s, 10)
|
|
fmtParts[i] = str
|
|
}
|
|
fmt.Fprint(&buf, strings.Join(fmtParts, "."))
|
|
if v.pre != "" {
|
|
fmt.Fprintf(&buf, "-%s", v.pre)
|
|
}
|
|
if v.metadata != "" {
|
|
fmt.Fprintf(&buf, "+%s", v.metadata)
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// Original returns the original parsed version as-is, including any
|
|
// potential whitespace, `v` prefix, etc.
|
|
func (v *Version) Original() string {
|
|
return v.original
|
|
}
|
|
|
|
// RemoveMeta remove metadata
|
|
// original parsed version data is not touched
|
|
func (v *Version) RemoveMeta() {
|
|
v.metadata = ""
|
|
}
|
|
|
|
// RemovePre remove pre-release data
|
|
// original parsed version data is not touched
|
|
func (v *Version) RemovePre() {
|
|
v.pre = ""
|
|
}
|