diff options
Diffstat (limited to 'lib/rubygems/version.rb')
| -rw-r--r-- | lib/rubygems/version.rb | 472 |
1 files changed, 276 insertions, 196 deletions
diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index ff5c1c1e72..306733c1d7 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -1,4 +1,10 @@ # frozen_string_literal: true + +#-- +# Workaround for directly loading Gem::Version in some cases +module Gem; end +#++ + ## # The Version class processes string versions into comparable # values. A version string should normally be a series of numbers @@ -24,153 +30,168 @@ # 4. 0.9 # # If you want to specify a version restriction that includes both prereleases -# and regular releases of the 1.x series this is the best way: +# and regular releases of 1.x or later versions: # -# s.add_dependency 'example', '>= 1.0.0.a', '< 2.0.0' +# s.add_dependency 'example', '>= 1.0.0.a' # # == How Software Changes # -# Users expect to be able to specify a version constraint that gives them -# some reasonable expectation that new versions of a library will work with -# their software if the version constraint is true, and not work with their -# software if the version constraint is false. In other words, the perfect -# system will accept all compatible versions of the library and reject all -# incompatible versions. -# -# Libraries change in 3 ways (well, more than 3, but stay focused here!). +# Libraries generally change in 3 ways: # -# 1. The change may be an implementation detail only and have no effect on -# the client software. -# 2. The change may add new features, but do so in a way that client software -# written to an earlier version is still compatible. -# 3. The change may change the public interface of the library in such a way -# that old software is no longer compatible. +# 1. The change is an implementation detail, bug fix, security fix, or +# optimization, and has no behavioral effect on the software using it. # -# Some examples are appropriate at this point. Suppose I have a Stack class -# that supports a <tt>push</tt> and a <tt>pop</tt> method. +# 2. The change adds new features, and software using those new features is +# not compatible with previous versions of the library, but software using +# previous versions of the library is compatible with the change. # -# === Examples of Category 1 changes: +# 3. The change modifies the public interface of some part of the library in +# such a way that software that uses that part of the library must be +# modified to work. # -# * Switch from an array based implementation to a linked-list based -# implementation. -# * Provide an automatic (and transparent) backing store for large stacks. -# -# === Examples of Category 2 changes might be: -# -# * Add a <tt>depth</tt> method to return the current depth of the stack. -# * Add a <tt>top</tt> method that returns the current top of stack (without -# changing the stack). -# * Change <tt>push</tt> so that it returns the item pushed (previously it -# had no usable return value). -# -# === Examples of Category 3 changes might be: -# -# * Changes <tt>pop</tt> so that it no longer returns a value (you must use -# <tt>top</tt> to get the top of the stack). -# * Rename the methods to <tt>push_item</tt> and <tt>pop_item</tt>. -# -# == RubyGems Rational Versioning +# == RubyGems Rational Versioning (the recommended approach) # # * Versions shall be represented by three non-negative integers, separated -# by periods (e.g. 3.1.4). The first integers is the "major" version +# by periods (e.g. 3.1.4). The first integer is the "major" version # number, the second integer is the "minor" version number, and the third -# integer is the "build" number. +# integer is the "patch" version number. # -# * A category 1 change (implementation detail) will increment the build -# number. +# * A category 1 change (implementation detail, bug fix, or security fix) +# will increment the patch number. # # * A category 2 change (backwards compatible) will increment the minor -# version number and reset the build number. -# -# * A category 3 change (incompatible) will increment the major build number -# and reset the minor and build numbers. -# -# * Any "public" release of a gem should have a different version. Normally -# that means incrementing the build number. This means a developer can -# generate builds all day long, but as soon as they make a public release, -# the version must be updated. +# version number and reset the patch number. # -# === Examples +# * A category 3 change (incompatible) will increment the major version number +# and reset the minor and patch numbers. # -# Let's work through a project lifecycle using our Stack example from above. +# * Any "public" release of a gem should have a different version. # -# Version 0.0.1:: The initial Stack class is release. -# Version 0.0.2:: Switched to a linked=list implementation because it is -# cooler. -# Version 0.1.0:: Added a <tt>depth</tt> method. -# Version 1.0.0:: Added <tt>top</tt> and made <tt>pop</tt> return nil -# (<tt>pop</tt> used to return the old top item). -# Version 1.1.0:: <tt>push</tt> now returns the value pushed (it used it -# return nil). -# Version 1.1.1:: Fixed a bug in the linked list implementation. -# Version 1.1.2:: Fixed a bug introduced in the last fix. +# == Optimistic Vs. Pessimistic Dependency Versioning # -# Client A needs a stack with basic push/pop capability. They write to the -# original interface (no <tt>top</tt>), so their version constraint looks like: -# -# gem 'stack', '>= 0.0' -# -# Essentially, any version is OK with Client A. An incompatible change to -# the library will cause them grief, but they are willing to take the chance -# (we call Client A optimistic). -# -# Client B is just like Client A except for two things: (1) They use the -# <tt>depth</tt> method and (2) they are worried about future -# incompatibilities, so they write their version constraint like this: -# -# gem 'stack', '~> 0.1' -# -# The <tt>depth</tt> method was introduced in version 0.1.0, so that version -# or anything later is fine, as long as the version stays below version 1.0 -# where incompatibilities are introduced. We call Client B pessimistic -# because they are worried about incompatible future changes (it is OK to be -# pessimistic!). -# -# == Preventing Version Catastrophe: -# -# From: http://blog.zenspider.com/2008/10/rubygems-howto-preventing-cata.html -# -# Let's say you're depending on the fnord gem version 2.y.z. If you -# specify your dependency as ">= 2.0.0" then, you're good, right? What -# happens if fnord 3.0 comes out and it isn't backwards compatible -# with 2.y.z? Your stuff will break as a result of using ">=". The -# better route is to specify your dependency with an "approximate" version -# specifier ("~>"). They're a tad confusing, so here is how the dependency -# specifiers work: -# -# Specification From ... To (exclusive) -# ">= 3.0" 3.0 ... ∞ -# "~> 3.0" 3.0 ... 4.0 -# "~> 3.0.0" 3.0.0 ... 3.1 -# "~> 3.5" 3.5 ... 4.0 -# "~> 3.5.0" 3.5.0 ... 3.6 -# "~> 3" 3.0 ... 4.0 -# -# For the last example, single-digit versions are automatically extended with -# a zero to give a sensible result. +# Users expect to be able to specify a version constraint that gives them +# a reasonable expectation that new versions of a library will work with +# their software if the version constraint is true, and not work with their +# software if the version constraint is false. In other words, the perfect +# system will accept all compatible versions of the library and reject all +# incompatible versions. Unfortunately, there is no perfect system, as you +# cannot predict the future. You can never know whether a future version of +# a library will contain which type of change. +# +# There are two common outlooks on dependency versioning: +# +# 1. Optimistic. This does not set an upper bound on a dependency. It is +# possible that a future version of a dependency will break the software, +# and in that case, the dependency version will need to be updated and +# changes will need to be made. +# +# 2. Pessimistic. This assumes all major version changes of a dependency will +# break the software, and that patch or minor changes of a dependency will +# not break the software. If there is a major version of a dependency +# released, the dependency version must be updated in order to use it, even +# if no code changes are actually needed. +# +# In general, optimistic versioning is superior to pessimistic versioning. +# Pessimistic versioning is often wrong in both directions. Dependencies can +# release patch or minor versions that contain incompatibilities. One +# common reason is that a security fix may require a backwards-incompatible API +# change. In this case, even though pessimistic versioning was used, it +# didn't even save effort, as you still need to make code changes and adjust +# dependency versions. Similarly, for all but the smallest dependencies, just +# because the dependency made a backwards incompatible change to one interface +# doesn't mean the dependency made a backwards incompatible change to an +# interface that the software is using. It is a common problem that a +# dependency will release a new major version and the software does not require +# any changes in order to use it. In this case, being pessimistic results in +# additional work for no benefit. +# +# When a library uses pessimistic versioning of dependencies, it causes +# significant problems if that library is not diligent about updating +# dependency versions and any library is depending on that library. +# For example: +# +# * Library A is currently on release 1.2.3. +# +# * Library B is at version 2.3.4 and has a pessimistic dependency on +# library A, using ~> 1.0 (>= 1.0, < 2). +# +# * Library C is at version 3.4.5 and has an optimistic dependency on +# library A, using >= 1.0. +# +# * Library D has optimistic dependencies on both libraries B and C. +# +# * Library A releases a new major version, 2.0.0, with new features, which +# is mostly backwards compatible, but does contain some backwards +# incompatible changes. +# +# * Library B would work with A 2.0.0, but cannot use it due to pessimistic +# versioning. +# +# * Library C wants to use the new features in the major release of library +# A to implement its own new features, so it does so, bumps the +# dependency version of A to >= 2.0, and releases version 3.5.0. +# +# * Library D cannot upgrade to the new version of library C, because it +# depends on library B, which has a pessimistic dependency on library A. +# +# * Library C releases a security fix patch version 3.5.1 to fix a +# vulnerability present in all previous versions. +# +# * Library D is now in a terrible situation. It cannot upgrade to library +# C 3.5.1, as that requires library A > 2.0, because it depends on library +# B, which requires library A > 1.0, < 2, even though library B would work +# fine with library A 2.0.0. +# +# This type of situation brought on by pessimistic versioning is unfortunately +# both common and serious in practice. +# +# This is not to say that optimistic versioning never causes a problem. +# However, with optimistic versioning, if there is a problem, it can be solved +# with the addition of a single dependency. For example, continuing the +# previous example: +# +# * Library A releases a new major version, 3.0.0, which makes backwards +# incompatible changes that break library C. +# +# * Until library C releases an updated version with new changes, library +# D only needs to set a specific dependency on library A for > 2.0, < 3, +# until library C is updated to work with the new version of library A. +# +# Both optimistic versioning and pessimistic versioning have problems in +# certain cases. However, it's significantly easier to fix optimistic +# versioning problems than to fix pessimistic versioning problems. +# +# That is not to say that pessimistic versioning is never appropriate. If the +# dependency is a library that adds a single method, where any change resulting +# in a major version bump would probably break a library using it, then using +# pessimistic versioning may be warranted. Additionally, if a dependency has +# already announced or committed backwards incompatible changes that would +# break a library's use of it, then having that library use a pessimistic +# version constraint would likely be warranted. However, outside of +# specific situations, you should avoid using pessimistic versioning, as the +# costs typically exceed the benefits. class Gem::Version - autoload :Requirement, 'rubygems/requirement' - include Comparable VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc: ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc: + RADIX_OPT = [9_500, 3_500, 260_000, 22_227, 24].freeze # :nodoc: ## # A string representation of this Version. def version - @version.dup + @version end - alias to_s version + alias_method :to_s, :version ## # True if the +version+ string matches RubyGems' requirements. - def self.correct? version - !!(version.to_s =~ ANCHORED_VERSION_PATTERN) + def self.correct?(version) + version.nil? || ANCHORED_VERSION_PATTERN.match?(version.to_s) end ## @@ -179,22 +200,21 @@ class Gem::Version # # ver1 = Version.create('1.3.17') # -> (Version object) # ver2 = Version.create(ver1) # -> (ver1) - # ver3 = Version.create(nil) # -> nil - def self.create input - if self === input then # check yourself before you wreck yourself + def self.create(input) + if self === input # check yourself before you wreck yourself input - elsif input.nil? then - nil else new input end end @@all = {} + @@bump = {} + @@release = {} - def self.new version # :nodoc: - return super unless Gem::Version == self + def self.new(version) # :nodoc: + return super unless self == Gem::Version @@all[version] ||= super end @@ -203,16 +223,25 @@ class Gem::Version # Constructs a Version from the +version+ string. A version string is a # series of digits or ASCII letters separated by dots. - def initialize version + def initialize(version) unless self.class.correct?(version) raise ArgumentError, "Malformed version number string #{version}" end # If version is an empty string convert it to 0 - version = 0 if version =~ /\A\s*\Z/ + version = 0 if version.nil? || (version.is_a?(String) && /\A\s*\Z/.match?(version)) - @version = version.to_s.strip.gsub("-",".pre.") + @version = version.to_s + + # optimization to avoid allocation when given an integer, since we know + # it's to_s won't have any spaces or dashes + unless version.is_a?(Integer) + @version = @version.strip + @version.gsub!("-",".pre.") + end + @version = -@version @segments = nil + @sort_key = compute_sort_key end ## @@ -222,29 +251,29 @@ class Gem::Version # Pre-release (alpha) parts, e.g, 5.3.1.b.2 => 5.4, are ignored. def bump - @bump ||= begin - segments = self.segments - segments.pop while segments.any? { |s| String === s } - segments.pop if segments.size > 1 - - segments[-1] = segments[-1].succ - self.class.new segments.join(".") - end + @@bump[self] ||= begin + segments = self.segments + segments.pop while segments.any? {|s| String === s } + segments.pop if segments.size > 1 + + segments[-1] = segments[-1].succ + self.class.new segments.join(".") + end end ## # A Version is only eql? to another version if it's specified to the # same precision. Version "1.0" is not the same as version "1". - def eql? other - self.class === other and @version == other._version + def eql?(other) + self.class === other && @version == other.version end def hash # :nodoc: canonical_segments.hash end - def init_with coder # :nodoc: + def init_with(coder) # :nodoc: yaml_initialize coder.tag, coder.map end @@ -257,29 +286,28 @@ class Gem::Version # string for backwards (RubyGems 1.3.5 and earlier) compatibility. def marshal_dump - [version] + [@version] end ## # Load custom marshal format. It's a string for backwards (RubyGems # 1.3.5 and earlier) compatibility. - def marshal_load array - initialize array[0] + def marshal_load(array) + string = array[0] + raise TypeError, "wrong version string" unless string.is_a?(String) + + initialize string end def yaml_initialize(tag, map) # :nodoc: - @version = map['version'] + @version = -map["version"] @segments = nil @hash = nil end - def to_yaml_properties # :nodoc: - ["@version"] - end - - def encode_with coder # :nodoc: - coder.add 'version', @version + def encode_with(coder) # :nodoc: + coder.add "version", @version end ## @@ -287,12 +315,12 @@ class Gem::Version def prerelease? unless instance_variable_defined? :@prerelease - @prerelease = !!(@version =~ /[a-zA-Z]/) + @prerelease = /[a-zA-Z]/.match?(version) end @prerelease end - def pretty_print q # :nodoc: + def pretty_print(q) # :nodoc: q.text "Gem::Version.new(#{version.inspect})" end @@ -301,13 +329,13 @@ class Gem::Version # Non-prerelease versions return themselves. def release - @release ||= if prerelease? - segments = self.segments - segments.pop while segments.any? { |s| String === s } - self.class.new segments.join('.') - else - self - end + @@release[self] ||= if prerelease? + segments = self.segments + segments.pop while segments.any? {|s| String === s } + self.class.new segments.join(".") + else + self + end end def segments # :nodoc: @@ -315,78 +343,130 @@ class Gem::Version end ## - # A recommended version for use with a ~> Requirement. + # A recommended version for use with a >= Requirement. def approximate_recommendation segments = self.segments - segments.pop while segments.any? { |s| String === s } + segments.pop while segments.any? {|s| String === s } segments.pop while segments.size > 2 segments.push 0 while segments.size < 2 - "~> #{segments.join(".")}" + recommendation = ">= #{segments.join(".")}" + recommendation += ".a" if prerelease? + recommendation end ## # Compares this version with +other+ returning -1, 0, or 1 if the # other version is larger, the same, or smaller than this - # one. Attempts to compare to something that's not a - # <tt>Gem::Version</tt> return +nil+. + # one. +other+ must be an instance of Gem::Version, comparing with + # other types may raise an exception. + + def <=>(other) + if Gem::Version === other + # Fast path for comparison when available. + if @sort_key && other.sort_key + return @sort_key <=> other.sort_key + end + + return 0 if @version == other.version || canonical_segments == other.canonical_segments + + lhsegments = canonical_segments + rhsegments = other.canonical_segments + + lhsize = lhsegments.size + rhsize = rhsegments.size + limit = (lhsize > rhsize ? rhsize : lhsize) + + i = 0 + + while i < limit + lhs = lhsegments[i] + rhs = rhsegments[i] + i += 1 + + next if lhs == rhs + return -1 if String === lhs && Numeric === rhs + return 1 if Numeric === lhs && String === rhs + + return lhs <=> rhs + end + + lhs = lhsegments[i] + + if lhs.nil? + rhs = rhsegments[i] + + while i < rhsize + return 1 if String === rhs + return -1 unless rhs.zero? + rhs = rhsegments[i += 1] + end + else + while i < lhsize + return -1 if String === lhs + return 1 unless lhs.zero? + lhs = lhsegments[i += 1] + end + end + + 0 + elsif String === other + return unless self.class.correct?(other) + self <=> self.class.new(other) + end + end - def <=> other - return unless Gem::Version === other - return 0 if @version == other._version || canonical_segments == other.canonical_segments + # remove trailing zeros segments before first letter or at the end of the version + def canonical_segments + @canonical_segments ||= begin + # remove trailing 0 segments, using dot or letter as anchor + # may leave a trailing dot which will be ignored by partition_segments + canonical_version = @version.sub(/(?<=[a-zA-Z.])[.0]+\z/, "") + # remove 0 segments before the first letter in a prerelease version + canonical_version.sub!(/(?<=\.|\A)[0.]+(?=[a-zA-Z])/, "") if prerelease? + partition_segments(canonical_version) + end + end - lhsegments = _segments - rhsegments = other._segments + def freeze + prerelease? + _segments + canonical_segments + super + end - lhsize = lhsegments.size - rhsize = rhsegments.size - limit = (lhsize > rhsize ? lhsize : rhsize) - 1 + protected - i = 0 + attr_reader :sort_key # :nodoc: - while i <= limit - lhs, rhs = lhsegments[i] || 0, rhsegments[i] || 0 - i += 1 + def compute_sort_key + return if prerelease? - next if lhs == rhs - return -1 if String === lhs && Numeric === rhs - return 1 if Numeric === lhs && String === rhs + segments = canonical_segments + return if segments.size > 5 - return lhs <=> rhs + key = 0 + RADIX_OPT.each_with_index do |radix, i| + seg = segments.fetch(i, 0) + return nil if seg >= radix + key = key * radix + seg end - return 0 - end - - def canonical_segments - @canonical_segments ||= - _split_segments.map! do |segments| - segments.reverse_each.drop_while {|s| s == 0 }.reverse - end.reduce(&:concat) - end - - protected - - def _version - @version + key end def _segments # segments is lazy so it can pick up version values that come from # old marshaled versions, which don't go through marshal_load. # since this version object is cached in @@all, its @segments should be frozen - - @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s| - /^\d+$/ =~ s ? s.to_i : s - end.freeze + @segments ||= partition_segments(@version) end - def _split_segments - string_start = _segments.index {|s| s.is_a?(String) } - string_segments = segments - numeric_segments = string_segments.slice!(0, string_start || string_segments.size) - return numeric_segments, string_segments + def partition_segments(ver) + ver.scan(/\d+|[a-z]+/i).map! do |s| + /\A\d/.match?(s) ? s.to_i : -s + end.freeze end end |
