diff options
Diffstat (limited to 'lib/rubygems/version.rb')
| -rw-r--r-- | lib/rubygems/version.rb | 357 |
1 files changed, 203 insertions, 154 deletions
diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 3ac3676d0a..306733c1d7 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true -require_relative "deprecate" +#-- +# Workaround for directly loading Gem::Version in some cases +module Gem; end +#++ ## # The Version class processes string versions into comparable @@ -27,136 +30,153 @@ require_relative "deprecate" # 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!). -# -# 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. +# Libraries generally change in 3 ways: # -# 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. +# 1. The change is an implementation detail, bug fix, security fix, or +# optimization, and has no behavioral effect on the software using it. # -# === Examples of Category 1 changes: +# 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. # -# * Switch from an array based implementation to a linked-list based -# implementation. -# * Provide an automatic (and transparent) backing store for large stacks. +# 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. # -# === 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: https://www.zenspider.com/ruby/2008/10/rubygems-how-to-preventing-catastrophe.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 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. @@ -171,9 +191,7 @@ class Gem::Version # True if the +version+ string matches RubyGems' requirements. def self.correct?(version) - nil_versions_are_discouraged! if version.nil? - - ANCHORED_VERSION_PATTERN.match?(version.to_s) + version.nil? || ANCHORED_VERSION_PATTERN.match?(version.to_s) end ## @@ -182,15 +200,10 @@ 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 # check yourself before you wreck yourself input - elsif input.nil? - nil_versions_are_discouraged! - - nil else new input end @@ -206,14 +219,6 @@ class Gem::Version @@all[version] ||= super end - def self.nil_versions_are_discouraged! - unless Gem::Deprecate.skip - warn "nil versions are discouraged and will be deprecated in Rubygems 4" - end - end - - private_class_method :nil_versions_are_discouraged! - ## # Constructs a Version from the +version+ string. A version string is a # series of digits or ASCII letters separated by dots. @@ -224,7 +229,7 @@ class Gem::Version end # If version is an empty string convert it to 0 - version = 0 if version.is_a?(String) && /\A\s*\Z/.match?(version) + version = 0 if version.nil? || (version.is_a?(String) && /\A\s*\Z/.match?(version)) @version = version.to_s @@ -236,6 +241,7 @@ class Gem::Version end @version = -@version @segments = nil + @sort_key = compute_sort_key end ## @@ -337,7 +343,7 @@ 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 @@ -346,7 +352,7 @@ class Gem::Version segments.pop while segments.size > 2 segments.push 0 while segments.size < 2 - recommendation = "~> #{segments.join(".")}" + recommendation = ">= #{segments.join(".")}" recommendation += ".a" if prerelease? recommendation end @@ -354,37 +360,62 @@ class Gem::Version ## # 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> or a valid version String return +nil+. + # one. +other+ must be an instance of Gem::Version, comparing with + # other types may raise an exception. def <=>(other) - return self <=> self.class.new(other) if (String === other) && self.class.correct?(other) - - return unless Gem::Version === other - 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 ? lhsize : rhsize) - 1 - - i = 0 - - while i <= limit - lhs = lhsegments[i] || 0 - rhs = rhsegments[i] || 0 - i += 1 - - next if lhs == rhs - return -1 if String === lhs && Numeric === rhs - return 1 if Numeric === lhs && String === rhs - - return lhs <=> rhs + 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 - - 0 end # remove trailing zeros segments before first letter or at the end of the version @@ -408,6 +439,24 @@ class Gem::Version protected + attr_reader :sort_key # :nodoc: + + def compute_sort_key + return if prerelease? + + segments = canonical_segments + return if segments.size > 5 + + 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 + + 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. |
