github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/version/version.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package version
    12  
    13  import (
    14  	"fmt"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"github.com/cockroachdb/errors"
    20  )
    21  
    22  // Version represents a semantic version; see
    23  // https://semver.org/spec/v2.0.0.html.
    24  type Version struct {
    25  	major      int32
    26  	minor      int32
    27  	patch      int32
    28  	preRelease string
    29  	metadata   string
    30  }
    31  
    32  // Major returns the major (first) version number.
    33  func (v *Version) Major() int {
    34  	return int(v.major)
    35  }
    36  
    37  // Minor returns the minor (second) version number.
    38  func (v *Version) Minor() int {
    39  	return int(v.minor)
    40  }
    41  
    42  // Patch returns the patch (third) version number.
    43  func (v *Version) Patch() int {
    44  	return int(v.patch)
    45  }
    46  
    47  // PreRelease returns the pre-release version (if present).
    48  func (v *Version) PreRelease() string {
    49  	return v.preRelease
    50  }
    51  
    52  // Metadata returns the metadata (if present).
    53  func (v *Version) Metadata() string {
    54  	return v.metadata
    55  }
    56  
    57  // String returns the string representation, in the format:
    58  //   "v1.2.3-beta+md"
    59  func (v Version) String() string {
    60  	var b strings.Builder
    61  	fmt.Fprintf(&b, "v%d.%d.%d", v.major, v.minor, v.patch)
    62  	if v.preRelease != "" {
    63  		fmt.Fprintf(&b, "-%s", v.preRelease)
    64  	}
    65  	if v.metadata != "" {
    66  		fmt.Fprintf(&b, "+%s", v.metadata)
    67  	}
    68  	return b.String()
    69  }
    70  
    71  // versionRE is the regexp that is used to verify that a version string is
    72  // of the form "vMAJOR.MINOR.PATCH[-PRERELEASE][+METADATA]". This
    73  // conforms to https://semver.org/spec/v2.0.0.html
    74  var versionRE = regexp.MustCompile(
    75  	`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-.]+)?(\+[0-9A-Za-z-.]+|)?$`,
    76  	// ^major           ^minor           ^patch         ^preRelease       ^metadata
    77  )
    78  
    79  // numericRE is the regexp used to check if an identifier is numeric.
    80  var numericRE = regexp.MustCompile(`^(0|[1-9][0-9]*)$`)
    81  
    82  // Parse creates a version from a string. The string must be a valid semantic
    83  // version (as per https://semver.org/spec/v2.0.0.html) in the format:
    84  //   "vMINOR.MAJOR.PATCH[-PRERELEASE][+METADATA]".
    85  // MINOR, MAJOR, and PATCH are numeric values (without any leading 0s).
    86  // PRERELEASE and METADATA can contain ASCII characters and digits, hyphens and
    87  // dots.
    88  func Parse(str string) (*Version, error) {
    89  	if !versionRE.MatchString(str) {
    90  		return nil, errors.Errorf("invalid version string '%s'", str)
    91  	}
    92  
    93  	var v Version
    94  	r := strings.NewReader(str)
    95  	// Read the major.minor.patch part.
    96  	_, err := fmt.Fscanf(r, "v%d.%d.%d", &v.major, &v.minor, &v.patch)
    97  	if err != nil {
    98  		panic(fmt.Sprintf("invalid version '%s' passed the regex: %s", str, err))
    99  	}
   100  	remaining := str[len(str)-r.Len():]
   101  	// Read the pre-release, if present.
   102  	if len(remaining) > 0 && remaining[0] == '-' {
   103  		p := strings.IndexRune(remaining, '+')
   104  		if p == -1 {
   105  			p = len(remaining)
   106  		}
   107  		v.preRelease = remaining[1:p]
   108  		remaining = remaining[p:]
   109  	}
   110  	// Read the metadata, if present.
   111  	if len(remaining) > 0 {
   112  		if remaining[0] != '+' {
   113  			panic(fmt.Sprintf("invalid version '%s' passed the regex", str))
   114  		}
   115  		v.metadata = remaining[1:]
   116  	}
   117  	return &v, nil
   118  }
   119  
   120  // MustParse is like Parse but panics on any error. Recommended as an
   121  // initializer for global values.
   122  func MustParse(str string) *Version {
   123  	v, err := Parse(str)
   124  	if err != nil {
   125  		panic(err)
   126  	}
   127  	return v
   128  }
   129  
   130  func cmpVal(a, b int32) int {
   131  	if a > b {
   132  		return +1
   133  	}
   134  	if a < b {
   135  		return -1
   136  	}
   137  	return 0
   138  }
   139  
   140  // Compare returns -1, 0, or +1 indicating the relative ordering of versions.
   141  func (v *Version) Compare(w *Version) int {
   142  	if v := cmpVal(v.major, w.major); v != 0 {
   143  		return v
   144  	}
   145  	if v := cmpVal(v.minor, w.minor); v != 0 {
   146  		return v
   147  	}
   148  	if v := cmpVal(v.patch, w.patch); v != 0 {
   149  		return v
   150  	}
   151  	if v.preRelease != w.preRelease {
   152  		if v.preRelease == "" && w.preRelease != "" {
   153  			// 1.0.0 is greater than 1.0.0-alpha.
   154  			return 1
   155  		}
   156  		if v.preRelease != "" && w.preRelease == "" {
   157  			// 1.0.0-alpha is less than 1.0.0.
   158  			return -1
   159  		}
   160  
   161  		// Quoting from https://semver.org/spec/v2.0.0.html:
   162  		//   Precedence for two pre-release versions with the same major, minor, and
   163  		//   patch version MUST be determined by comparing each dot separated
   164  		//   identifier from left to right until a difference is found as follows:
   165  		//    (1) Identifiers consisting of only digits are compared numerically.
   166  		//    (2) identifiers with letters or hyphens are compared lexically in ASCII
   167  		//        sort order.
   168  		//    (3) Numeric identifiers always have lower precedence than non-numeric
   169  		//        identifiers.
   170  		//    (4) A larger set of pre-release fields has a higher precedence than a
   171  		//        smaller set, if all of the preceding identifiers are equal.
   172  		//
   173  		vs := strings.Split(v.preRelease, ".")
   174  		ws := strings.Split(w.preRelease, ".")
   175  		for ; len(vs) > 0 && len(ws) > 0; vs, ws = vs[1:], ws[1:] {
   176  			vStr, wStr := vs[0], ws[0]
   177  			if vStr == wStr {
   178  				continue
   179  			}
   180  			vNumeric := numericRE.MatchString(vStr)
   181  			wNumeric := numericRE.MatchString(wStr)
   182  			switch {
   183  			case vNumeric && wNumeric:
   184  				// Case 1.
   185  				vVal, err := strconv.Atoi(vStr)
   186  				if err != nil {
   187  					panic(err)
   188  				}
   189  				wVal, err := strconv.Atoi(wStr)
   190  				if err != nil {
   191  					panic(err)
   192  				}
   193  				if vVal == wVal {
   194  					panic("different strings yield the same numbers")
   195  				}
   196  				if vVal < wVal {
   197  					return -1
   198  				}
   199  				return 1
   200  
   201  			case vNumeric:
   202  				// Case 3.
   203  				return -1
   204  
   205  			case wNumeric:
   206  				// Case 3.
   207  				return 1
   208  
   209  			default:
   210  				// Case 2.
   211  				if vStr < wStr {
   212  					return -1
   213  				}
   214  				return 1
   215  			}
   216  		}
   217  
   218  		if len(vs) > 0 {
   219  			// Case 4.
   220  			return +1
   221  		}
   222  		if len(ws) > 0 {
   223  			// Case 4.
   224  			return -1
   225  		}
   226  	}
   227  
   228  	return 0
   229  }
   230  
   231  // AtLeast returns true if v >= w.
   232  func (v *Version) AtLeast(w *Version) bool {
   233  	return v.Compare(w) >= 0
   234  }