diff options
Diffstat (limited to 'lib/bundler/gem_version_promoter.rb')
-rw-r--r-- | lib/bundler/gem_version_promoter.rb | 177 |
1 files changed, 68 insertions, 109 deletions
diff --git a/lib/bundler/gem_version_promoter.rb b/lib/bundler/gem_version_promoter.rb index 3cce3f2139..ecc65b4956 100644 --- a/lib/bundler/gem_version_promoter.rb +++ b/lib/bundler/gem_version_promoter.rb @@ -7,14 +7,13 @@ module Bundler # available dependency versions as found in its index, before returning it to # to the resolution engine to select the best version. class GemVersionPromoter - DEBUG = ENV["BUNDLER_DEBUG_RESOLVER"] || ENV["DEBUG_RESOLVER"] - - attr_reader :level, :locked_specs, :unlock_gems + 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 by promoted past the requested level, + # 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 @@ -24,24 +23,13 @@ module Bundler # existing in the referenced source. attr_accessor :strict - attr_accessor :prerelease_specified - - # Given a list of locked_specs and a list of gems to unlock creates a - # GemVersionPromoter instance. + # Creates a GemVersionPromoter instance. # - # @param locked_specs [SpecSet] All current locked specs. Unlike Definition - # where this list is empty if all gems are being updated, this should - # always be populated for all gems so this class can properly function. - # @param unlock_gems [String] List of gem names being unlocked. If empty, - # all gems will be considered unlocked. # @return [GemVersionPromoter] - def initialize(locked_specs = SpecSet.new([]), unlock_gems = []) + def initialize @level = :major @strict = false - @locked_specs = locked_specs - @unlock_gems = unlock_gems - @sort_versions = {} - @prerelease_specified = {} + @pre = false end # @param value [Symbol] One of three Symbols: :major, :minor or :patch. @@ -55,37 +43,39 @@ module Bundler @level = v end - # Given a Dependency and an Array of SpecGroups of available versions for a - # gem, this method will return the Array of SpecGroups sorted (and possibly - # truncated if strict is true) 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 dep [Dependency] The Dependency of the gem. - # @param spec_groups [SpecGroup] An array of SpecGroups for the same gem - # named in the @dep param. - # @return [SpecGroup] A new instance of the SpecGroup Array sorted and - # possibly filtered. - def sort_versions(dep, spec_groups) - before_result = "before sort_versions: #{debug_format_result(dep, spec_groups).inspect}" if DEBUG - - @sort_versions[dep] ||= begin - gem_name = dep.name - - # An Array per version returned, different entries for different platforms. - # We only need the version here so it's ok to hard code this to the first instance. - locked_spec = locked_specs[gem_name].first - - if strict - filter_dep_specs(spec_groups, locked_spec) + # 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 - sort_dep_specs(spec_groups, locked_spec) - end.tap do |specs| - if DEBUG - puts before_result - puts " after sort_versions: #{debug_format_result(dep, specs).inspect}" - end + b <=> a end end + post_sort(result, package.unlock?, locked_version) end # @return [bool] Convenience method for testing value of level variable. @@ -98,93 +88,62 @@ module Bundler level == :minor end - private - - def filter_dep_specs(spec_groups, locked_spec) - res = spec_groups.select do |spec_group| - if locked_spec && !major? - gsv = spec_group.version - lsv = locked_spec.version - - must_match = minor? ? [0] : [0, 1] - - matches = must_match.map {|idx| gsv.segments[idx] == lsv.segments[idx] } - matches.uniq == [true] ? (gsv >= lsv) : false - else - true - end - end - - sort_dep_specs(res, locked_spec) + # @return [bool] Convenience method for testing value of pre variable. + def pre? + pre == true end - def sort_dep_specs(spec_groups, locked_spec) - return spec_groups unless locked_spec - @gem_name = locked_spec.name - @locked_version = locked_spec.version + # 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 - result = spec_groups.sort do |a, b| - @a_ver = a.version - @b_ver = b.version + locked_version = package.locked_version + return specs if locked_version.nil? || major? - unless @prerelease_specified[@gem_name] - a_pre = @a_ver.prerelease? - b_pre = @b_ver.prerelease? + specs.select do |spec| + gsv = spec.version - next -1 if a_pre && !b_pre - next 1 if b_pre && !a_pre - end + must_match = minor? ? [0] : [0, 1] - if major? - @a_ver <=> @b_ver - elsif either_version_older_than_locked - @a_ver <=> @b_ver - elsif segments_do_not_match(:major) - @b_ver <=> @a_ver - elsif !minor? && segments_do_not_match(:minor) - @b_ver <=> @a_ver - else - @a_ver <=> @b_ver - end + all_match = must_match.all? {|idx| gsv.segments[idx] == locked_version.segments[idx] } + all_match && gsv >= locked_version end - post_sort(result) end - def either_version_older_than_locked - @a_ver < @locked_version || @b_ver < @locked_version - end + private - def segments_do_not_match(level) - index = [:major, :minor].index(level) - @a_ver.segments[index] != @b_ver.segments[index] + def either_version_older_than_locked?(a, b, locked_version) + a.version < locked_version || b.version < locked_version end - def unlocking_gem? - unlock_gems.empty? || unlock_gems.include?(@gem_name) + 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) + def post_sort(result, unlock, locked_version) # default :major behavior in Bundler does not do this return result if major? - if unlocking_gem? + if unlock || locked_version.nil? result else - move_version_to_end(result, @locked_version) + move_version_to_beginning(result, locked_version) end end - def move_version_to_end(result, version) + def move_version_to_beginning(result, version) move, keep = result.partition {|s| s.version.to_s == version.to_s } - keep.concat(move) - end - - def debug_format_result(dep, spec_groups) - a = [dep.to_s, - spec_groups.map {|sg| [sg.version, sg.dependencies_for_activated_platforms.map {|dp| [dp.name, dp.requirement.to_s] }] }] - last_map = a.last.map {|sg_data| [sg_data.first.version, sg_data.last.map {|aa| aa.join(" ") }] } - [a.first, last_map, level, strict ? :strict : :not_strict] + move.concat(keep) end end end |