Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
version.rb 6.77 KiB
class Version
  include Comparable

  class Token
    include Comparable

    attr_reader :value

    def initialize(value)
      @value = value
    end

    def inspect
      "#<#{self.class} #{value.inspect}>"
    end

    def to_s
      value.to_s
    end
  end

  class NullToken < Token
    def initialize(value=nil)
      super
    end

    def <=>(other)
      case other
      when NumericToken
        other.value == 0 ? 0 : -1
      when AlphaToken, BetaToken, RCToken
        1
      else
        -1
      end
    end

    def inspect
      "#<#{self.class}>"
    end
  end

  NULL_TOKEN = NullToken.new

  class StringToken < Token
    PATTERN = /[a-z]+[0-9]+/i

    def initialize(value)
      @value = value.to_s
    end

    def <=>(other)
      case other
      when StringToken
        value <=> other.value
      when NumericToken, NullToken
        -Integer(other <=> self)
      end
    end
  end

  class NumericToken < Token
    PATTERN = /[0-9]+/i

    def initialize(value)
      @value = value.to_i
    end

    def <=>(other)
      case other
      when NumericToken
        value <=> other.value
      when StringToken
        1
      when NullToken
        -Integer(other <=> self)
      end
    end
  end

  class CompositeToken < StringToken
    def rev
      value[/([0-9]+)/, 1]
    end
  end

  class AlphaToken < CompositeToken
    PATTERN = /a(?:lpha)?[0-9]+/i

    def <=>(other)
      case other
      when AlphaToken
        rev <=> other.rev
      else
        super
      end
    end
  end

  class BetaToken < CompositeToken
    PATTERN = /b(?:eta)?[0-9]+/i

    def <=>(other)
      case other
      when BetaToken
        rev <=> other.rev
      when AlphaToken
        1
      when RCToken, PatchToken
        -1
      else
        super
      end
    end
  end

  class RCToken < CompositeToken
    PATTERN = /rc[0-9]+/i

    def <=>(other)
      case other
      when RCToken
        rev <=> other.rev
      when AlphaToken, BetaToken
        1
      when PatchToken
        -1
      else
        super
      end
    end
  end

  class PatchToken < CompositeToken
    PATTERN = /p[0-9]+/i

    def <=>(other)
      case other
      when PatchToken
        rev <=> other.rev
      when AlphaToken, BetaToken, RCToken
        1
      else
        super
      end
    end
  end

  def self.new_with_scheme(value, scheme)
    if Class === scheme && scheme.ancestors.include?(Version)
      scheme.new(value)
    else
      raise TypeError, "Unknown version scheme #{scheme.inspect}"
    end
  end

  def self.detect(url, specs={})
    if specs.has_key?(:tag)
      new(specs[:tag][/((?:\d+\.)*\d+)/, 1], true)
    else
      parse(url)
    end
  end

  def initialize(val, detected=false)
    @version = val.to_s
    @detected_from_url = detected
  end

  def detected_from_url?
    @detected_from_url
  end

  def head?
    @version == 'HEAD'
  end

  def <=>(other)
    return unless Version === other
    return 0 if head? && other.head?
    return 1 if head? && !other.head?
    return -1 if !head? && other.head?

    max = [tokens.length, other.tokens.length].max
    pad_to(max) <=> other.pad_to(max)
  end

  def to_s
    @version.dup
  end
  alias_method :to_str, :to_s

  protected

  def pad_to(length)
    nums, rest = tokens.partition { |t| NumericToken === t }
    nums.concat([NULL_TOKEN]*(length - tokens.length)).concat(rest)
  end

  def tokens
    @tokens ||= tokenize
  end

  def tokenize
    @version.scan(
      Regexp.union(
        AlphaToken::PATTERN,
        BetaToken::PATTERN,
        RCToken::PATTERN,
        PatchToken::PATTERN,
        NumericToken::PATTERN,
        StringToken::PATTERN
      )
    ).map! do |token|
      case token
      when /\A#{AlphaToken::PATTERN}\z/o   then AlphaToken
      when /\A#{BetaToken::PATTERN}\z/o    then BetaToken
      when /\A#{RCToken::PATTERN}\z/o      then RCToken
      when /\A#{PatchToken::PATTERN}\z/o   then PatchToken
      when /\A#{NumericToken::PATTERN}\z/o then NumericToken
      when /\A#{StringToken::PATTERN}\z/o  then StringToken
      end.new(token)
    end
  end

  def self.parse spec
    version = _parse(spec)
    Version.new(version, true) unless version.nil?
  end

  def self._parse spec
    spec = Pathname.new(spec) unless spec.is_a? Pathname

    spec_s = spec.to_s

    stem = if spec.directory?
      spec.basename.to_s
    elsif %r[((?:sourceforge.net|sf.net)/.*)/download$].match(spec_s)
      Pathname.new(spec.dirname).stem
    else
      spec.stem
    end

    # GitHub tarballs
    # e.g. https://github.com/foo/bar/tarball/v1.2.3
    # e.g. https://github.com/sam-github/libnet/tarball/libnet-1.1.4
    # e.g. https://github.com/isaacs/npm/tarball/v0.2.5-1
    # e.g. https://github.com/petdance/ack/tarball/1.93_02
    m = %r[github.com/.+/(?:zip|tar)ball/(?:v|\w+-)?((?:\d+[-._])+\d*)$].match(spec_s)
    return m.captures.first unless m.nil?

    # e.g. https://github.com/erlang/otp/tarball/OTP_R15B01 (erlang style)
    m = /[-_]([Rr]\d+[AaBb]\d*(?:-\d+)?)/.match(spec_s)
    return m.captures.first unless m.nil?

    # e.g. perforce-2013.1.610569-x86_64
    m = /-([\d\.]+-x86(_64)?)/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. boost_1_39_0
    m = /((?:\d+_)+\d+)$/.match(stem)
    return m.captures.first.gsub('_', '.') unless m.nil?

    # e.g. foobar-4.5.1-1
    # e.g. ruby-1.9.1-p243
    m = /-((?:\d+\.)*\d\.\d+-(?:p|rc|RC)?\d+)(?:[-._](?:bin|dist|stable|src|sources))?$/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. lame-398-1
    m = /-((?:\d)+-\d)/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. foobar-4.5.1
    m = /-((?:\d+\.)*\d+)$/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. foobar-4.5.1b
    m = /-((?:\d+\.)*\d+(?:[abc]|rc|RC)\d*)$/.match(stem)
    return m.captures.first unless m.nil?
    # e.g. foobar-4.5.0-beta1, or foobar-4.50-beta
    m = /-((?:\d+\.)*\d+-beta\d*)$/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. foobar4.5.1
    m = /((?:\d+\.)*\d+)$/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. foobar-4.5.0-bin
    m = /-((?:\d+\.)+\d+[abc]?)[-._](?:bin|dist|stable|src|sources?)$/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. dash_0.5.5.1.orig.tar.gz (Debian style)
    m = /_((?:\d+\.)+\d+[abc]?)[.]orig$/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. http://www.openssl.org/source/openssl-0.9.8s.tar.gz
    m = /-v?([^-]+)/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. astyle_1.23_macosx.tar.gz
    m = /_([^_]+)/.match(stem)
    return m.captures.first unless m.nil?

    # e.g. http://mirrors.jenkins-ci.org/war/1.486/jenkins.war
    m = /\/(\d\.\d+)\//.match(spec_s)
    return m.captures.first unless m.nil?

    # e.g. http://www.ijg.org/files/jpegsrc.v8d.tar.gz
    m = /\.v(\d+[a-z]?)/.match(stem)
    return m.captures.first unless m.nil?
  end
end