summaryrefslogtreecommitdiff
path: root/lib/rubygems/version.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/version.rb')
-rw-r--r--lib/rubygems/version.rb472
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 ... &infin;
-# "~> 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