module Bundler::Patch class ConservativeResolver < Bundler::Resolver attr_accessor :locked_specs, :gems_to_update, :strict, :minor_preferred, :prefer_minimal def initialize(index, source_requirements, base) case Bundler::Resolver.instance_method(:initialize).arity when 3 # 1.9 1.10 super(index, source_requirements, base) when 4 # 1.11 1.12 super(index, source_requirements, base, nil) end end def search_for(dependency) res = super(dependency) dep = dependency.dep unless dependency.is_a? Gem::Dependency super_result = "super search_for: #{debug_format_result(dep, res).inspect}" @conservative_search_for ||= {} res = @conservative_search_for[dep] ||= begin gem_name = dep.name # An Array per version returned, different entries for different platforms. # We just need the version here so it's ok to hard code this to the first instance. locked_spec = @locked_specs[gem_name].first (@strict ? filter_specs(res, locked_spec) : sort_specs(res, locked_spec)).tap do |result| if ENV['DEBUG_PATCH_RESOLVER'] STDERR.puts super_result STDERR.puts "after search_for: #{debug_format_result(dep, result).inspect}" end end end # dup is important, in weird (large) cases Bundler will empty the result array corrupting the cache. # Bundler itself doesn't have this problem because the super search_for does a select on its cached # search results, effectively duping it. res.dup end def debug_format_result(dep, res) a = [dep.to_s, res.map { |sg| [sg.version, sg.dependencies_for_activated_platforms.map { |dp| [dp.name, dp.requirement.to_s] }] }] [a.first, a.last.map { |sg_data| [sg_data.first.version, sg_data.last.map { |aa| aa.join(' ') }] }] end def filter_specs(specs, locked_spec) res = specs.select do |sg| # SpecGroup is grouped by name/version, multiple entries for multiple platforms. # We only need the name, which will be the same, so hard coding to first is ok. gem_spec = sg.first if locked_spec gsv = gem_spec.version lsv = locked_spec.version must_match = @minor_preferred ? [0] : [0, 1] matches = must_match.map { |idx| gsv.segments[idx] == lsv.segments[idx] } (matches.uniq == [true]) ? gsv.send(:>=, lsv) : false else true end end sort_specs(res, locked_spec) end # reminder: sort still filters anything older than locked version def sort_specs(specs, locked_spec) return specs unless locked_spec @gem_name = locked_spec.name @locked_version = locked_spec.version filtered = specs.select { |s| s.first.version >= @locked_version } @gem_patch = @gems_to_update.gem_patch_for(@gem_name) @new_version = @gem_patch ? @gem_patch.new_version : nil result = filtered.sort do |a, b| @a_ver = a.first.version @b_ver = b.first.version case when segments_do_not_match(:major) @b_ver <=> @a_ver when !@minor_preferred && segments_do_not_match(:minor) @b_ver <=> @a_ver when @prefer_minimal && !unlocking_gem? @b_ver <=> @a_ver when @prefer_minimal && unlocking_gem? && (neither_version_matches(@locked_version) && (!@new_version || both_versions_gt_or_equal_to_version(@new_version))) @b_ver <=> @a_ver else @a_ver <=> @b_ver end end post_sort(result) end def post_sort(result) unless unlocking_gem? result = move_version_to_end(result, @locked_version) end if @new_version && unlocking_gem? && segments_match(:major, @new_version, @locked_version) if @prefer_minimal || (!@prefer_minimal && (result.last.first.version < @new_version)) result = move_version_to_end(result, @new_version) end end result end def unlocking_gem? @gems_to_update.unlocking_gem?(@gem_name) end def either_version_older_than_locked(locked_version) @a_ver < locked_version || @b_ver < locked_version end def segments_match(level, a_ver=@a_ver, b_ver=@b_ver) !segments_do_not_match(level, a_ver, b_ver) end def segments_do_not_match(level, a_ver=@a_ver, b_ver=@b_ver) index = [:major, :minor].index(level) a_ver.segments[index] != b_ver.segments[index] end def neither_version_matches(match_version) !one_version_matches(match_version) end def one_version_matches(match_version) [@a_ver, @b_ver].include?(match_version) end def both_versions_gt_or_equal_to_version(version) version && @a_ver >= version && @b_ver >= version end def move_version_to_end(result, version) move, keep = result.partition { |s| s.first.version.to_s == version.to_s } keep.concat(move) end end end