This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
# This class contains all of the logic for determining the next version of a
|
||||
# Gem to update to based on the requested level (patch, minor, major).
|
||||
# Primarily designed to work with Resolver which will provide it the list of
|
||||
# available dependency versions as found in its index, before returning it to
|
||||
# to the resolution engine to select the best version.
|
||||
class GemVersionPromoter
|
||||
attr_reader :level
|
||||
attr_accessor :pre
|
||||
|
||||
# By default, strict is false, meaning every available version of a gem
|
||||
# is returned from sort_versions. The order gives preference to the
|
||||
# requested level (:patch, :minor, :major) but in complicated requirement
|
||||
# cases some gems will by necessity be promoted past the requested level,
|
||||
# or even reverted to older versions.
|
||||
#
|
||||
# If strict is set to true, the results from sort_versions will be
|
||||
# truncated, eliminating any version outside the current level scope.
|
||||
# This can lead to unexpected outcomes or even VersionConflict exceptions
|
||||
# that report a version of a gem not existing for versions that indeed do
|
||||
# existing in the referenced source.
|
||||
attr_accessor :strict
|
||||
|
||||
# Creates a GemVersionPromoter instance.
|
||||
#
|
||||
# @return [GemVersionPromoter]
|
||||
def initialize
|
||||
@level = :major
|
||||
@strict = false
|
||||
@pre = false
|
||||
end
|
||||
|
||||
# @param value [Symbol] One of three Symbols: :major, :minor or :patch.
|
||||
def level=(value)
|
||||
v = case value
|
||||
when String, Symbol
|
||||
value.to_sym
|
||||
end
|
||||
|
||||
raise ArgumentError, "Unexpected level #{v}. Must be :major, :minor or :patch" unless [:major, :minor, :patch].include?(v)
|
||||
@level = v
|
||||
end
|
||||
|
||||
# Given a Resolver::Package and an Array of Specifications of available
|
||||
# versions for a gem, this method will return the Array of Specifications
|
||||
# sorted in an order to give preference to the current level (:major, :minor
|
||||
# or :patch) when resolution is deciding what versions best resolve all
|
||||
# dependencies in the bundle.
|
||||
# @param package [Resolver::Package] The package being resolved.
|
||||
# @param specs [Specification] An array of Specifications for the package.
|
||||
# @return [Specification] A new instance of the Specification Array sorted.
|
||||
def sort_versions(package, specs)
|
||||
locked_version = package.locked_version
|
||||
|
||||
result = specs.sort do |a, b|
|
||||
unless package.prerelease_specified? || pre?
|
||||
a_pre = a.prerelease?
|
||||
b_pre = b.prerelease?
|
||||
|
||||
next 1 if a_pre && !b_pre
|
||||
next -1 if b_pre && !a_pre
|
||||
end
|
||||
|
||||
if major? || locked_version.nil?
|
||||
b <=> a
|
||||
elsif either_version_older_than_locked?(a, b, locked_version)
|
||||
b <=> a
|
||||
elsif segments_do_not_match?(a, b, :major)
|
||||
a <=> b
|
||||
elsif !minor? && segments_do_not_match?(a, b, :minor)
|
||||
a <=> b
|
||||
else
|
||||
b <=> a
|
||||
end
|
||||
end
|
||||
post_sort(result, package.unlock?, locked_version)
|
||||
end
|
||||
|
||||
# @return [bool] Convenience method for testing value of level variable.
|
||||
def major?
|
||||
level == :major
|
||||
end
|
||||
|
||||
# @return [bool] Convenience method for testing value of level variable.
|
||||
def minor?
|
||||
level == :minor
|
||||
end
|
||||
|
||||
# @return [bool] Convenience method for testing value of pre variable.
|
||||
def pre?
|
||||
pre == true
|
||||
end
|
||||
|
||||
# Given a Resolver::Package and an Array of Specifications of available
|
||||
# versions for a gem, this method will truncate the Array if strict
|
||||
# is true. That means filtering out downgrades from the version currently
|
||||
# locked, and filtering out upgrades that go past the selected level (major,
|
||||
# minor, or patch).
|
||||
# @param package [Resolver::Package] The package being resolved.
|
||||
# @param specs [Specification] An array of Specifications for the package.
|
||||
# @return [Specification] A new instance of the Specification Array
|
||||
# truncated.
|
||||
def filter_versions(package, specs)
|
||||
return specs unless strict
|
||||
|
||||
locked_version = package.locked_version
|
||||
return specs if locked_version.nil? || major?
|
||||
|
||||
specs.select do |spec|
|
||||
gsv = spec.version
|
||||
|
||||
must_match = minor? ? [0] : [0, 1]
|
||||
|
||||
all_match = must_match.all? {|idx| gsv.segments[idx] == locked_version.segments[idx] }
|
||||
all_match && gsv >= locked_version
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def either_version_older_than_locked?(a, b, locked_version)
|
||||
a.version < locked_version || b.version < locked_version
|
||||
end
|
||||
|
||||
def segments_do_not_match?(a, b, level)
|
||||
index = [:major, :minor].index(level)
|
||||
a.segments[index] != b.segments[index]
|
||||
end
|
||||
|
||||
# Specific version moves can't always reliably be done during sorting
|
||||
# as not all elements are compared against each other.
|
||||
def post_sort(result, unlock, locked_version)
|
||||
if unlock || locked_version.nil?
|
||||
result
|
||||
else
|
||||
move_version_to_beginning(result, locked_version)
|
||||
end
|
||||
end
|
||||
|
||||
def move_version_to_beginning(result, version)
|
||||
move, keep = result.partition {|s| s.version.to_s == version.to_s }
|
||||
move.concat(keep)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user