diff options
author | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-08 08:45:41 +0000 |
---|---|---|
committer | hsbt <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2017-09-08 08:45:41 +0000 |
commit | 8598f8c2dc78c6d1ae87cb6ae19c34ba2cb29241 (patch) | |
tree | 0bbd28f684e745cb212761b7c74fe08668f85cc8 /lib/bundler/gem_version_promoter.rb | |
parent | f2e04b77aa8a363d7e36ce5a9cdb60714a537a3c (diff) | |
download | ruby-8598f8c2dc78c6d1ae87cb6ae19c34ba2cb29241.tar.gz |
Merge bundler to standard libraries.
rubygems 2.7.x depends bundler-1.15.x. This is preparation for
rubygems and bundler migration.
* lib/bundler.rb, lib/bundler/*: files of bundler-1.15.4
* spec/bundler/*: rspec examples of bundler-1.15.4. I applied patches.
* https://github.com/bundler/bundler/pull/6007
* Exclude not working examples on ruby repository.
* Fake ruby interpriter instead of installed ruby.
* Makefile.in: Added test task named `test-bundler`. This task is only
working macOS/linux yet. I'm going to support Windows environment later.
* tool/sync_default_gems.rb: Added sync task for bundler.
[Feature #12733][ruby-core:77172]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@59779 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/bundler/gem_version_promoter.rb')
-rw-r--r-- | lib/bundler/gem_version_promoter.rb | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/lib/bundler/gem_version_promoter.rb b/lib/bundler/gem_version_promoter.rb new file mode 100644 index 0000000000..d60d823d9c --- /dev/null +++ b/lib/bundler/gem_version_promoter.rb @@ -0,0 +1,175 @@ +# 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, :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 :strict + + # 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] + def initialize(locked_specs = SpecSet.new([]), unlock_gems = []) + @level = :major + @strict = false + @locked_specs = locked_specs + @unlock_gems = unlock_gems + @sort_versions = {} + 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 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 ENV["DEBUG_RESOLVER"] + + @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) + else + sort_dep_specs(spec_groups, locked_spec) + end.tap do |specs| + if ENV["DEBUG_RESOLVER"] + STDERR.puts before_result + STDERR.puts " after sort_versions: #{debug_format_result(dep, specs).inspect}" + end + end + end + 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 + + 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) + 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 + + result = spec_groups.sort do |a, b| + @a_ver = a.version + @b_ver = b.version + 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 + end + post_sort(result) + end + + def either_version_older_than_locked + @a_ver < @locked_version || @b_ver < @locked_version + end + + def segments_do_not_match(level) + index = [:major, :minor].index(level) + @a_ver.segments[index] != @b_ver.segments[index] + end + + def unlocking_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. + def post_sort(result) + # default :major behavior in Bundler does not do this + return result if major? + if unlocking_gem? + result + else + move_version_to_end(result, @locked_version) + end + end + + def move_version_to_end(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] + end + end +end |