-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Conservative updates #4676
Conservative updates #4676
Changes from all commits
b57b1df
c821411
a404dad
100c3bb
80e1b17
0ccdb21
949dea2
5f63cad
b26b54c
20426a8
bdc1df5
3630f18
3d3aeda
93fac82
a444014
d88ef5c
6af8ef7
cb2f8c8
8cbe425
861eb2b
00b8064
d025055
f37c76f
3d1e6e3
57d70be
257c8b2
60f64fd
3e56e8f
f16f129
9022339
263c187
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
# 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since this is a new class, it would be nice to add API docs for it |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will do. I need to revisit that flag too, perhaps re-use |
||
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 | ||
|
||
spec_groups.sort do |a, b| | ||
a_ver = a.version | ||
b_ver = b.version | ||
case | ||
when major? | ||
a_ver <=> b_ver | ||
when either_version_older_than_locked(locked_version, a_ver, b_ver) | ||
a_ver <=> b_ver | ||
when segments_do_not_match(:major, a_ver, b_ver) | ||
b_ver <=> a_ver | ||
when !minor? && segments_do_not_match(:minor, a_ver, b_ver) | ||
b_ver <=> a_ver | ||
else | ||
a_ver <=> b_ver | ||
end | ||
end.tap do |result| | ||
# default :major behavior in Bundler does not do this | ||
unless major? | ||
unless unlocking_gem?(gem_name) | ||
move_version_to_end(spec_groups, locked_version, result) | ||
end | ||
end | ||
end | ||
end | ||
|
||
def either_version_older_than_locked(locked_version, a_ver, b_ver) | ||
a_ver < locked_version || b_ver < locked_version | ||
end | ||
|
||
def segments_do_not_match(level, a_ver, b_ver) | ||
index = [:major, :minor].index(level) | ||
a_ver.segments[index] != b_ver.segments[index] | ||
end | ||
|
||
def unlocking_gem?(gem_name) | ||
unlock_gems.empty? || unlock_gems.include?(gem_name) | ||
end | ||
|
||
def move_version_to_end(spec_groups, version, result) | ||
spec_group = spec_groups.detect {|s| s.version.to_s == version.to_s } | ||
return unless spec_group | ||
result.reject! {|s| s.version.to_s == version.to_s } | ||
result << spec_group | ||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -172,14 +172,14 @@ def __dependencies | |
# ==== Returns | ||
# <GemBundle>,nil:: If the list of dependencies can be resolved, a | ||
# collection of gemspecs is returned. Otherwise, nil is returned. | ||
def self.resolve(requirements, index, source_requirements = {}, base = [], ruby_version = nil) | ||
def self.resolve(requirements, index, source_requirements = {}, base = [], ruby_version = nil, gem_version_promoter = GemVersionPromoter.new) | ||
base = SpecSet.new(base) unless base.is_a?(SpecSet) | ||
resolver = new(index, source_requirements, base, ruby_version) | ||
resolver = new(index, source_requirements, base, ruby_version, gem_version_promoter) | ||
result = resolver.start(requirements) | ||
SpecSet.new(result) | ||
end | ||
|
||
def initialize(index, source_requirements, base, ruby_version) | ||
def initialize(index, source_requirements, base, ruby_version, gem_version_promoter) | ||
@index = index | ||
@source_requirements = source_requirements | ||
@base = base | ||
|
@@ -188,6 +188,7 @@ def initialize(index, source_requirements, base, ruby_version) | |
@base_dg = Molinillo::DependencyGraph.new | ||
@base.each {|ls| @base_dg.add_vertex(ls.name, Dependency.new(ls.name, ls.version), true) } | ||
@ruby_version = ruby_version | ||
@gem_version_promoter = gem_version_promoter | ||
end | ||
|
||
def start(requirements) | ||
|
@@ -249,7 +250,7 @@ def search_for(dependency) | |
if vertex = @base_dg.vertex_named(dependency.name) | ||
locked_requirement = vertex.payload.requirement | ||
end | ||
if results.any? | ||
spec_groups = if results.any? | ||
nested = [] | ||
results.each do |spec| | ||
version, specs = nested.last | ||
|
@@ -266,6 +267,13 @@ def search_for(dependency) | |
else | ||
[] | ||
end | ||
# GVP handles major itself, but it's still a bit risky to trust it with it | ||
# until we get it settled with new behavior. For 2.x it can take over all cases. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also a performance concern. also, looking from the code, the resorting might mess with source ordering? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree on perf concern, can you say moar words on source ordering? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @indirect would know better than I, but I'm fairly certain that the order things are returned from the index search can be more than just by version |
||
if @gem_version_promoter.major? | ||
spec_groups | ||
else | ||
@gem_version_promoter.sort_versions(dependency, spec_groups) | ||
end | ||
end | ||
search.select {|sg| sg.for?(platform, @ruby_version) }.each {|sg| sg.activate_platform!(platform) } | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this
only allow updating gems' patch versions
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a
--strict
flag I hadn't added here yet I just realized ...If
--strict
is also used, then the code will actually remove versions fed back to Molinillo, but that can result in no updates happening or VersionConflict cases that report a version can't be found that actually does exist in the index, causing a very weird "looks like a bug" experience. (hmmm ... while I did see VersionConflict cases early on, I haven't seen one in a while ... I may need to try and recreate that in a spec -- maybe that was due to an early bug and shouldn't be possible).So, not using
--strict
is the default and it means the code will only sort to the front (not filter out) the patch versions first - which favors getting something done, but it's potentially not as conservative.One challenge still existing is trying to give better output in these conservative cases, because sometimes no change will happen at all and there's no feedback to the user trying to explain "well, you wanted to go up to 1.8.3 from 1.8.2, but 1.8.3 needs a minor increment and you fed it --strict so it couldn't do anything" - but I've no idea if that's even plausible. It wouldn't be too unlike the Conflict output ... but these aren't cases of a Conflict.
I did think also there could be a way to detect increments beyond what was requested and warn those somehow.
#pascalsapology
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AHHH, this was the whole piece I was missing, sorry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no worries - option isn't listed there yet.