# frozen_string_literal: truemoduleBundler# 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.classGemVersionPromoterDEBUG=ENV["BUNDLER_DEBUG_RESOLVER"]||ENV["DEBUG_RESOLVER"]attr_reader:level,:locked_specs,:unlock_gems# 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,# 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:strictattr_accessor:prerelease_specified# Given a list of locked_specs and a list of gems to unlock 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]definitialize(locked_specs=SpecSet.new([]),unlock_gems=[])@level=:major@strict=false@locked_specs=locked_specs@unlock_gems=unlock_gems@sort_versions={}@prerelease_specified={}end# @param value [Symbol] One of three Symbols: :major, :minor or :patch.deflevel=(value)v=casevaluewhenString,Symbolvalue.to_symendraiseArgumentError,"Unexpected level #{v}. Must be :major, :minor or :patch"unless[:major,:minor,:patch].include?(v)@level=vend# 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.defsort_versions(dep,spec_groups)before_result="before sort_versions: #{debug_format_result(dep,spec_groups).inspect}"ifDEBUG@sort_versions[dep]||=begingem_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].firstifstrictfilter_dep_specs(spec_groups,locked_spec)elsesort_dep_specs(spec_groups,locked_spec)end.tapdo|specs|ifDEBUGputsbefore_resultputs" after sort_versions: #{debug_format_result(dep,specs).inspect}"endendendend# @return [bool] Convenience method for testing value of level variable.defmajor?level==:majorend# @return [bool] Convenience method for testing value of level variable.defminor?level==:minorendprivatedeffilter_dep_specs(spec_groups,locked_spec)res=spec_groups.selectdo|spec_group|iflocked_spec&&!major?gsv=spec_group.versionlsv=locked_spec.versionmust_match=minor??[0]:[0,1]matches=must_match.map{|idx|gsv.segments[idx]==lsv.segments[idx]}matches.uniq==[true]?(gsv>=lsv):falseelsetrueendendsort_dep_specs(res,locked_spec)enddefsort_dep_specs(spec_groups,locked_spec)returnspec_groupsunlesslocked_spec@gem_name=locked_spec.name@locked_version=locked_spec.versionresult=spec_groups.sortdo|a,b|@a_ver=a.version@b_ver=b.versionunless@prerelease_specified[@gem_name]a_pre=@a_ver.prerelease?b_pre=@b_ver.prerelease?next-1ifa_pre&&!b_prenext1ifb_pre&&!a_preendifmajor?@a_ver<=>@b_verelsifeither_version_older_than_locked@a_ver<=>@b_verelsifsegments_do_not_match(:major)@b_ver<=>@a_verelsif!minor?&&segments_do_not_match(:minor)@b_ver<=>@a_verelse@a_ver<=>@b_verendendpost_sort(result)enddefeither_version_older_than_locked@a_ver<@locked_version||@b_ver<@locked_versionenddefsegments_do_not_match(level)index=[:major,:minor].index(level)@a_ver.segments[index]!=@b_ver.segments[index]enddefunlocking_gem?unlock_gems.empty?||unlock_gems.include?(@gem_name)end# Specific version moves can't always reliably be done during sorting# as not all elements are compared against each other.defpost_sort(result)# default :major behavior in Bundler does not do thisreturnresultifmajor?ifunlocking_gem?resultelsemove_version_to_end(result,@locked_version)endenddefmove_version_to_end(result,version)move,keep=result.partition{|s|s.version.to_s==version.to_s}keep.concat(move)enddefdebug_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]endendend