diff options
Diffstat (limited to 'lib/rubygems/version.rb')
-rw-r--r-- | lib/rubygems/version.rb | 357 |
1 files changed, 206 insertions, 151 deletions
diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index f959429846..77403ff32b 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -1,9 +1,3 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - ## # The Version class processes string versions into comparable # values. A version string should normally be a series of numbers @@ -24,72 +18,153 @@ # 2. 1.0.b # 3. 1.0.a # 4. 0.9 +# +# == 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. +# +# 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. +# +# === Examples of Category 1 changes: +# +# * 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 +# +# * Versions shall be represented by three non-negative integers, separated +# by periods (e.g. 3.1.4). The first integers is the "major" version +# number, the second integer is the "minor" version number, and the third +# integer is the "build" number. +# +# * A category 1 change (implementation detail) will increment the build +# 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 for himself, but as soon as he/she makes a +# public release, the version must be updated. +# +# === Examples +# +# Let's work through a project lifecycle using our Stack example from above. +# +# 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. +# +# Client A needs a stack with basic push/pop capability. He writes to the +# original interface (no <tt>top</tt>), so his version constraint looks +# like: +# +# gem 'stack', '~> 0.0' +# +# Essentially, any version is OK with Client A. An incompatible change to +# the library will cause him grief, but he is willing to take the chance (we +# call Client A optimistic). +# +# Client B is just like Client A except for two things: (1) He uses the +# <tt>depth</tt> method and (2) he is worried about future +# incompatibilities, so he writes his 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 he is 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 a "spermy" 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 class Gem::Version - - class Part - include Comparable - - attr_reader :value - - def initialize(value) - @value = (value =~ /\A\d+\z/) ? value.to_i : value - end - - def to_s - self.value.to_s - end - - def inspect - @value - end - - def alpha? - String === value - end - - def numeric? - Fixnum === value - end - - def <=>(other) - if self.numeric? && other.alpha? then - 1 - elsif self.alpha? && other.numeric? then - -1 - else - self.value <=> other.value - end - end - - def succ - self.class.new(self.value.succ) - end - end - include Comparable - VERSION_PATTERN = '[0-9]+(\.[0-9a-z]+)*' + VERSION_PATTERN = '[0-9]+(\.[0-9a-zA-Z]+)*' # :nodoc: + ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})*\s*\z/ # :nodoc: + + ## + # A string representation of this Version. attr_reader :version + alias to_s version - def self.correct?(version) - pattern = /\A\s*(#{VERSION_PATTERN})*\s*\z/ + ## + # True if the +version+ string matches RubyGems' requirements. - version.is_a? Integer or - version =~ pattern or - version.to_s =~ pattern + def self.correct? version + version.to_s =~ ANCHORED_VERSION_PATTERN end ## - # Factory method to create a Version object. Input may be a Version or a - # String. Intended to simplify client code. + # Factory method to create a Version object. Input may be a Version + # or a String. Intended to simplify client code. # # ver1 = Version.create('1.3.17') # -> (Version object) # ver2 = Version.create(ver1) # -> (ver1) # ver3 = Version.create(nil) # -> nil - def self.create(input) + def self.create input if input.respond_to? :version then input elsif input.nil? then @@ -103,149 +178,129 @@ 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 raise ArgumentError, "Malformed version number string #{version}" unless self.class.correct?(version) - self.version = version - end + @version = version.to_s + @version.strip! - def inspect # :nodoc: - "#<#{self.class} #{@version.inspect}>" + segments # prime @segments end ## - # Dump only the raw version string, not the complete object + # Return a new version object where the next to the last revision + # number is one greater (e.g., 5.3.1 => 5.4). + # + # Pre-release (alpha) parts, e.g, 5.3.1.b2 => 5.4, are ignored. - def marshal_dump - [@version] + def bump + segments = self.segments.dup + 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 ## - # Load custom marshal format + # 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 marshal_load(array) - self.version = array[0] + def eql? other + self.class === other and segments == other.segments end - def parts - @parts ||= normalize + def hash # :nodoc: + segments.hash end - ## - # Strip ignored trailing zeros. - - def normalize - parts_arr = parse_parts_from_version_string - if parts_arr.length != 1 - parts_arr.pop while parts_arr.last && parts_arr.last.value == 0 - parts_arr = [Part.new(0)] if parts_arr.empty? - end - parts_arr + def inspect # :nodoc: + "#<#{self.class} #{version.inspect}>" end ## - # Returns the text representation of the version + # Dump only the raw version string, not the complete object. It's a + # string for backwards (RubyGems 1.3.5 and earlier) compatibility. - def to_s - @version + def marshal_dump + [version] end - def to_yaml_properties - ['@version'] - end + ## + # Load custom marshal format. It's a string for backwards (RubyGems + # 1.3.5 and earlier) compatibility. - def version=(version) - @version = version.to_s.strip - normalize + def marshal_load array + initialize array[0] end - ## - # A version is considered a prerelease if any part contains a letter. + # A version is considered a prerelease if it contains a letter. def prerelease? - parts.any? { |part| part.alpha? } - end - - ## - # The release for this version (e.g. 1.2.0.a -> 1.2.0) - # Non-prerelease versions return themselves - def release - return self unless prerelease? - rel_parts = parts.dup - rel_parts.pop while rel_parts.any? { |part| part.alpha? } - self.class.new(rel_parts.join('.')) + @prerelease ||= segments.any? { |s| String === s } end - def yaml_initialize(tag, values) - self.version = values['version'] + def pretty_print q # :nodoc: + q.text "Gem::Version.new(#{version.inspect})" end - ## - # Compares this version with +other+ returning -1, 0, or 1 if the other - # version is larger, the same, or smaller than this one. + # The release for this version (e.g. 1.2.0.a -> 1.2.0). + # Non-prerelease versions return themselves. - def <=>(other) - return nil unless self.class === other - return 1 unless other - mine, theirs = balance(self.parts.dup, other.parts.dup) - mine <=> theirs - end + def release + return self unless prerelease? - def balance(a, b) - a << Part.new(0) while a.size < b.size - b << Part.new(0) while b.size < a.size - [a, b] + segments = self.segments.dup + segments.pop while segments.any? { |s| String === s } + self.class.new segments.join('.') end - ## - # A Version is only eql? to another version if it has the same version - # string. "1.0" is not the same version as "1". + def segments # :nodoc: - def eql?(other) - self.class === other and @version == other.version - end + # @segments is lazy so it can pick up @version values that come + # from old marshaled versions, which don't go through + # marshal_load. +segments+ is called in +initialize+ to "prime + # the pump" in normal cases. - def hash # :nodoc: - @version.hash + @segments ||= @version.scan(/[0-9a-z]+/i).map do |s| + /^\d+$/ =~ s ? s.to_i : s + end end ## - # Return a new version object where the next to the last revision number is - # one greater. (e.g. 5.3.1 => 5.4) - # - # Pre-release (alpha) parts are ignored. (e.g 5.3.1.b2 => 5.4) + # A recommended version for use with a ~> Requirement. - def bump - parts = parse_parts_from_version_string - parts.pop while parts.any? { |part| part.alpha? } - parts.pop if parts.size > 1 - parts[-1] = parts[-1].succ - self.class.new(parts.join(".")) - end + def spermy_recommendation + segments = self.segments.dup - def parse_parts_from_version_string # :nodoc: - @version.to_s.scan(/[0-9a-z]+/i).map { |s| Part.new(s) } - end + segments.pop while segments.any? { |s| String === s } + segments.pop while segments.size > 2 + segments.push 0 while segments.size < 2 - def pretty_print(q) # :nodoc: - q.text "Gem::Version.new(#{@version.inspect})" + "~> #{segments.join(".")}" end - #:stopdoc: + ## + # Compares this version with +other+ returning -1, 0, or 1 if the other + # version is larger, the same, or smaller than this one. - require 'rubygems/requirement' + def <=> other + return 1 unless other # HACK: comparable with nil? why? + return nil unless self.class === other - ## - # Gem::Requirement's original definition is nested in Version. - # Although an inappropriate place, current gems specs reference the nested - # class name explicitly. To remain compatible with old software loading - # gemspecs, we leave a copy of original definition in Version, but define an - # alias Gem::Requirement for use everywhere else. + lhsize = segments.size + rhsize = other.segments.size + limit = (lhsize > rhsize ? lhsize : rhsize) - 1 - Requirement = ::Gem::Requirement + 0.upto(limit) do |i| + lhs, rhs = segments[i] || 0, other.segments[i] || 0 - # :startdoc: + return -1 if String === lhs && Numeric === rhs + return 1 if Numeric === lhs && String === rhs + return lhs <=> rhs if lhs != rhs + end + return 0 + end end - |