From 0ddf25e5aa478f8f2bbe088123702a6b8e8cee10 Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 16:01:37 +0100 Subject: [PATCH 01/11] Fix typo --- bundler/lib/bundler/resolver/candidate.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundler/lib/bundler/resolver/candidate.rb b/bundler/lib/bundler/resolver/candidate.rb index e695ef08ee0b..9e8b9133358c 100644 --- a/bundler/lib/bundler/resolver/candidate.rb +++ b/bundler/lib/bundler/resolver/candidate.rb @@ -15,7 +15,7 @@ class Resolver # considered separately. # # Some candidates may also keep some information explicitly about the - # package the refer to. These candidates are referred to as "canonical" and + # package they refer to. These candidates are referred to as "canonical" and # are used when materializing resolution results back into RubyGems # specifications that can be installed, written to lock files, and so on. # From 0e612361b81c80ef83f40e7c6a380997faaa92e3 Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 16:02:17 +0100 Subject: [PATCH 02/11] Rename method for clarity And also so that it matches the method used by main PubGrub sample resolver class. --- bundler/lib/bundler/resolver.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index f60069f421ab..d1c3addea2db 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -158,7 +158,7 @@ def parse_dependency(package, dependency) def versions_for(package, range=VersionRange.any) versions = range.select_versions(@sorted_versions[package]) - sort_versions(package, versions) + sort_versions_by_preferred(package, versions) end def no_versions_incompatibility_for(package, unsatisfied_term) @@ -275,7 +275,7 @@ def all_versions_for(package) groups end - sort_versions(package, versions) + sort_versions_by_preferred(package, versions) end def source_for(name) @@ -357,7 +357,7 @@ def requirement_satisfied_by?(requirement, spec) requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec) end - def sort_versions(package, versions) + def sort_versions_by_preferred(package, versions) if versions.size > 1 @gem_version_promoter.sort_versions(package, versions).reverse else From 13294528c4ba2cb7da9001ed182e80ff96ef97b3 Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 16:55:50 +0100 Subject: [PATCH 03/11] No need to sort twice when filling versions --- bundler/lib/bundler/gem_version_promoter.rb | 8 +++++--- bundler/lib/bundler/resolver.rb | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bundler/lib/bundler/gem_version_promoter.rb b/bundler/lib/bundler/gem_version_promoter.rb index c7eacd193049..b666c29d32f8 100644 --- a/bundler/lib/bundler/gem_version_promoter.rb +++ b/bundler/lib/bundler/gem_version_promoter.rb @@ -53,7 +53,7 @@ def level=(value) # @return [Specification] A new instance of the Specification Array sorted and # possibly filtered. def sort_versions(package, specs) - specs = filter_dep_specs(specs, package) if strict + specs = filter_versions(package, specs) sort_dep_specs(specs, package) end @@ -73,9 +73,9 @@ def pre? pre == true end - private + def filter_versions(package, specs) + return specs unless strict - def filter_dep_specs(specs, package) locked_version = package.locked_version return specs if locked_version.nil? || major? @@ -89,6 +89,8 @@ def filter_dep_specs(specs, package) end end + private + def sort_dep_specs(specs, package) locked_version = package.locked_version diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index d1c3addea2db..1cd94ccf50c6 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -275,7 +275,7 @@ def all_versions_for(package) groups end - sort_versions_by_preferred(package, versions) + @gem_version_promoter.filter_versions(package, versions) end def source_for(name) From ede15847dbad9c32e8a3810e3a29dac681456333 Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 17:20:57 +0100 Subject: [PATCH 04/11] Remove unnecessary filtering We do that when first caching versions, and then it's no longer necessary. --- bundler/lib/bundler/gem_version_promoter.rb | 52 ++++++++----------- .../spec/bundler/gem_version_promoter_spec.rb | 8 +-- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/bundler/lib/bundler/gem_version_promoter.rb b/bundler/lib/bundler/gem_version_promoter.rb index b666c29d32f8..66141b7b6361 100644 --- a/bundler/lib/bundler/gem_version_promoter.rb +++ b/bundler/lib/bundler/gem_version_promoter.rb @@ -53,9 +53,30 @@ def level=(value) # @return [Specification] A new instance of the Specification Array sorted and # possibly filtered. def sort_versions(package, specs) - specs = filter_versions(package, specs) + locked_version = package.locked_version - sort_dep_specs(specs, package) + 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? + a <=> b + elsif either_version_older_than_locked?(a, b, locked_version) + a <=> b + elsif segments_do_not_match?(a, b, :major) + b <=> a + elsif !minor? && segments_do_not_match?(a, b, :minor) + b <=> a + else + a <=> b + end + end + post_sort(result, package.unlock?, locked_version) end # @return [bool] Convenience method for testing value of level variable. @@ -91,33 +112,6 @@ def filter_versions(package, specs) private - def sort_dep_specs(specs, package) - 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? - a <=> b - elsif either_version_older_than_locked?(a, b, locked_version) - a <=> b - elsif segments_do_not_match?(a, b, :major) - b <=> a - elsif !minor? && segments_do_not_match?(a, b, :minor) - b <=> a - else - a <=> b - end - end - post_sort(result, package.unlock?, locked_version) - end - def either_version_older_than_locked?(a, b, locked_version) a.version < locked_version || b.version < locked_version end diff --git a/bundler/spec/bundler/gem_version_promoter_spec.rb b/bundler/spec/bundler/gem_version_promoter_spec.rb index fccbb58fea18..23228cd3da0b 100644 --- a/bundler/spec/bundler/gem_version_promoter_spec.rb +++ b/bundler/spec/bundler/gem_version_promoter_spec.rb @@ -58,18 +58,18 @@ def sorted_versions(candidates:, current:, name: "foo", locked: []) context "when level is minor" do before { gvp.level = :minor } - it "removes downgrades and major upgrades" do + it "sorts highest minor within same major in last position" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.3.0 0.3.1 0.9.0] + expect(versions).to eq %w[0.2.0 2.0.1 2.1.0 1.0.0 0.3.0 0.3.1 0.9.0] end end context "when level is patch" do before { gvp.level = :patch } - it "removes downgrades and major and minor upgrades" do + it "sorts highest patch within same minor in last position" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.3.0 0.3.1] + expect(versions).to eq %w[0.2.0 2.1.0 2.0.1 1.0.0 0.9.0 0.3.0 0.3.1] end end end From ac24a684865b657c69a0ce75005d61057ac6ae9c Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 17:27:04 +0100 Subject: [PATCH 05/11] Update docs --- bundler/lib/bundler/gem_version_promoter.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bundler/lib/bundler/gem_version_promoter.rb b/bundler/lib/bundler/gem_version_promoter.rb index 66141b7b6361..c7187654b761 100644 --- a/bundler/lib/bundler/gem_version_promoter.rb +++ b/bundler/lib/bundler/gem_version_promoter.rb @@ -45,13 +45,12 @@ def level=(value) # Given a Resolver::Package and an Array of Specifications of available # versions for a gem, this method will return the Array of Specifications - # 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. + # 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 and - # possibly filtered. + # @return [Specification] A new instance of the Specification Array sorted. def sort_versions(package, specs) locked_version = package.locked_version @@ -94,6 +93,15 @@ def pre? pre == true end + # 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 From aeea5e2e006a30f8964e708574dfa12a31660a48 Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 17:55:14 +0100 Subject: [PATCH 06/11] Let GemVersionPromoter sort in preferred order directly So that we don't need to reverse the Array. --- bundler/lib/bundler/gem_version_promoter.rb | 20 ++++++------ bundler/lib/bundler/resolver.rb | 2 +- .../spec/bundler/gem_version_promoter_spec.rb | 32 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bundler/lib/bundler/gem_version_promoter.rb b/bundler/lib/bundler/gem_version_promoter.rb index c7187654b761..ecc65b495602 100644 --- a/bundler/lib/bundler/gem_version_promoter.rb +++ b/bundler/lib/bundler/gem_version_promoter.rb @@ -59,20 +59,20 @@ def sort_versions(package, specs) a_pre = a.prerelease? b_pre = b.prerelease? - next -1 if a_pre && !b_pre - next 1 if b_pre && !a_pre + next 1 if a_pre && !b_pre + next -1 if b_pre && !a_pre end if major? || locked_version.nil? - a <=> b + b <=> a elsif either_version_older_than_locked?(a, b, locked_version) - a <=> b - elsif segments_do_not_match?(a, b, :major) b <=> a + elsif segments_do_not_match?(a, b, :major) + a <=> b elsif !minor? && segments_do_not_match?(a, b, :minor) - b <=> a - else a <=> b + else + b <=> a end end post_sort(result, package.unlock?, locked_version) @@ -137,13 +137,13 @@ def post_sort(result, unlock, locked_version) 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) + move.concat(keep) end end end diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index 1cd94ccf50c6..329540fd3d5e 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -359,7 +359,7 @@ def requirement_satisfied_by?(requirement, spec) def sort_versions_by_preferred(package, versions) if versions.size > 1 - @gem_version_promoter.sort_versions(package, versions).reverse + @gem_version_promoter.sort_versions(package, versions) else versions end diff --git a/bundler/spec/bundler/gem_version_promoter_spec.rb b/bundler/spec/bundler/gem_version_promoter_spec.rb index 23228cd3da0b..917daba95de5 100644 --- a/bundler/spec/bundler/gem_version_promoter_spec.rb +++ b/bundler/spec/bundler/gem_version_promoter_spec.rb @@ -33,13 +33,13 @@ def sorted_versions(candidates:, current:, name: "foo", locked: []) it "numerically sorts versions" do versions = sorted_versions(candidates: %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0], current: "1.7.8") - expect(versions).to eq %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0] + expect(versions).to eq %w[1.8.0 1.7.15 1.7.9 1.7.8 1.7.7] end context "with no options" do it "defaults to level=:major, strict=false, pre=false" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0] + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] end end @@ -51,25 +51,25 @@ def sorted_versions(candidates:, current:, name: "foo", locked: []) it "keeps downgrades" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0] + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] end end context "when level is minor" do before { gvp.level = :minor } - it "sorts highest minor within same major in last position" do + it "sorts highest minor within same major in first position" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.2.0 2.0.1 2.1.0 1.0.0 0.3.0 0.3.1 0.9.0] + expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0] end end context "when level is patch" do before { gvp.level = :patch } - it "sorts highest patch within same minor in last position" do + it "sorts highest patch within same minor in first position" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.2.0 2.1.0 2.0.1 1.0.0 0.9.0 0.3.0 0.3.1] + expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0] end end end @@ -82,25 +82,25 @@ def sorted_versions(candidates:, current:, name: "foo", locked: []) it "orders by version" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0] + expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0] end end context "when level is minor" do before { gvp.level = :minor } - it "favors downgrades, then upgrades by major descending, minor ascending, patch ascending" do + it "favors minor upgrades, then patch upgrades, then major upgrades, then downgrades" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.2.0 2.0.1 2.1.0 1.0.0 0.3.0 0.3.1 0.9.0] + expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0] end end context "when level is patch" do before { gvp.level = :patch } - it "favors downgrades, then upgrades by major descending, minor descending, patch ascending" do + it "favors patch upgrades, then minor upgrades, then major upgrades, then downgrades" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0") - expect(versions).to eq %w[0.2.0 2.1.0 2.0.1 1.0.0 0.9.0 0.3.0 0.3.1] + expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0] end end end @@ -110,7 +110,7 @@ def sorted_versions(candidates:, current:, name: "foo", locked: []) it "sorts regardless of prerelease status" do versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0") - expect(versions).to eq %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0] + expect(versions).to eq %w[2.0.0 2.0.0.pre 1.8.1 1.8.1.pre 1.8.0 1.7.7.pre] end end @@ -119,16 +119,16 @@ def sorted_versions(candidates:, current:, name: "foo", locked: []) it "deprioritizes prerelease gems" do versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0") - expect(versions).to eq %w[1.7.7.pre 1.8.1.pre 2.0.0.pre 1.8.0 1.8.1 2.0.0] + expect(versions).to eq %w[2.0.0 1.8.1 1.8.0 2.0.0.pre 1.8.1.pre 1.7.7.pre] end end context "when locking and not major" do before { gvp.level = :minor } - it "keeps the current version last" do + it "keeps the current version first" do versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.1.0 2.0.1], current: "0.3.0", locked: ["bar"]) - expect(versions.last).to eq("0.3.0") + expect(versions.first).to eq("0.3.0") end end end From bb5727934c4767ecafb39001b40b86408e4883d1 Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 18:19:13 +0100 Subject: [PATCH 07/11] Make it look more like BasicPackageSource --- bundler/lib/bundler/resolver.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index 329540fd3d5e..30e9a7beaa3e 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -158,7 +158,13 @@ def parse_dependency(package, dependency) def versions_for(package, range=VersionRange.any) versions = range.select_versions(@sorted_versions[package]) - sort_versions_by_preferred(package, versions) + # Conditional avoids (among other things) calling + # sort_versions_by_preferred with the root package + if versions.size > 1 + sort_versions_by_preferred(package, versions) + else + versions + end end def no_versions_incompatibility_for(package, unsatisfied_term) @@ -358,11 +364,7 @@ def requirement_satisfied_by?(requirement, spec) end def sort_versions_by_preferred(package, versions) - if versions.size > 1 - @gem_version_promoter.sort_versions(package, versions) - else - versions - end + @gem_version_promoter.sort_versions(package, versions) end def repository_for(package) From 8355a225d7beb83c15d12442728c5273babd0f5d Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 18:41:31 +0100 Subject: [PATCH 08/11] No need for any version prioritization when building errors We just need to filter versions belonging to the range, but don't need anything else. --- bundler/lib/bundler/resolver.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index 30e9a7beaa3e..2b6022cc2757 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -156,7 +156,7 @@ def parse_dependency(package, dependency) end def versions_for(package, range=VersionRange.any) - versions = range.select_versions(@sorted_versions[package]) + versions = select_sorted_versions(package, range) # Conditional avoids (among other things) calling # sort_versions_by_preferred with the root package @@ -381,11 +381,12 @@ def prepare_dependencies(requirements, packages) next [dep_package, dep_constraint] if name == "bundler" - versions = versions_for(dep_package, dep_constraint.range) + dep_range = dep_constraint.range + versions = select_sorted_versions(dep_package, dep_range) if versions.empty? && dep_package.ignores_prereleases? @sorted_versions.delete(dep_package) dep_package.consider_prereleases! - versions = versions_for(dep_package, dep_constraint.range) + versions = select_sorted_versions(dep_package, dep_range) end next [dep_package, dep_constraint] unless versions.empty? @@ -395,6 +396,10 @@ def prepare_dependencies(requirements, packages) end.compact.to_h end + def select_sorted_versions(package, range) + range.select_versions(@sorted_versions[package]) + end + def other_specs_matching_message(specs, requirement) message = String.new("The source contains the following gems matching '#{requirement}':\n") message << specs.map {|s| " * #{s.full_name}" }.join("\n") From 6ca192649f45509570092785738688bcda7a9d4e Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 21:20:40 +0100 Subject: [PATCH 09/11] No need to check for root package every time --- bundler/lib/bundler/resolver.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index 2b6022cc2757..78beddd7a32b 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -51,25 +51,21 @@ def setup_solver end @sorted_versions = Hash.new do |candidates, package| - candidates[package] = if package.root? - [root_version] - else - all_versions_for(package).sort - end + candidates[package] = all_versions_for(package).sort end + @sorted_versions[root] = [root_version] + root_dependencies = prepare_dependencies(@requirements, @packages) @cached_dependencies = Hash.new do |dependencies, package| - dependencies[package] = if package.root? - { root_version => root_dependencies } - else - Hash.new do |versions, version| - versions[version] = to_dependency_hash(version.dependencies.reject {|d| d.name == package.name }, @packages) - end + dependencies[package] = Hash.new do |versions, version| + versions[version] = to_dependency_hash(version.dependencies.reject {|d| d.name == package.name }, @packages) end end + @cached_dependencies[root] = { root_version => root_dependencies } + logger = Bundler::UI::Shell.new logger.level = debug? ? "debug" : "warn" From 7b5cc51a96afce864dc61a2769a9f5e19df31dac Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 19:19:05 +0100 Subject: [PATCH 10/11] Keep unfiltered versions separately --- bundler/lib/bundler/resolver.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index 78beddd7a32b..c148c79b371f 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -50,8 +50,12 @@ def setup_solver specs[name] = matches.sort_by {|s| [s.version, s.platform.to_s] } end + @all_versions = Hash.new do |candidates, package| + candidates[package] = all_versions_for(package) + end + @sorted_versions = Hash.new do |candidates, package| - candidates[package] = all_versions_for(package).sort + candidates[package] = filtered_versions_for(package).sort end @sorted_versions[root] = [root_version] @@ -249,7 +253,7 @@ def all_versions_for(package) locked_requirement = base_requirements[name] results = filter_matching_specs(results, locked_requirement) if locked_requirement - versions = results.group_by(&:version).reduce([]) do |groups, (version, specs)| + results.group_by(&:version).reduce([]) do |groups, (version, specs)| platform_specs = package.platforms.map {|platform| select_best_platform_match(specs, platform) } # If package is a top-level dependency, @@ -276,8 +280,6 @@ def all_versions_for(package) groups end - - @gem_version_promoter.filter_versions(package, versions) end def source_for(name) @@ -336,6 +338,10 @@ def raise_not_found!(package) private + def filtered_versions_for(package) + @gem_version_promoter.filter_versions(package, @all_versions[package]) + end + def filter_matching_specs(specs, requirements) Array(requirements).flat_map do |requirement| specs.select {| spec| requirement_satisfied_by?(requirement, spec) } @@ -380,6 +386,7 @@ def prepare_dependencies(requirements, packages) dep_range = dep_constraint.range versions = select_sorted_versions(dep_package, dep_range) if versions.empty? && dep_package.ignores_prereleases? + @all_versions.delete(dep_package) @sorted_versions.delete(dep_package) dep_package.consider_prereleases! versions = select_sorted_versions(dep_package, dep_range) From 1ea44b37497a7d225b1588ede4ed5bb621eb200e Mon Sep 17 00:00:00 2001 From: David Rodriguez Date: Thu, 21 Mar 2024 20:59:42 +0100 Subject: [PATCH 11/11] Improve error message when strict resolution filters out everything --- bundler/lib/bundler/resolver.rb | 20 ++++++++++++++++++++ bundler/spec/commands/lock_spec.rb | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index c148c79b371f..1a6711ea6fca 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -342,6 +342,17 @@ def filtered_versions_for(package) @gem_version_promoter.filter_versions(package, @all_versions[package]) end + def raise_all_versions_filtered_out!(package) + level = @gem_version_promoter.level + name = package.name + locked_version = package.locked_version + requirement = package.dependency + + raise GemNotFound, + "#{name} is locked to #{locked_version}, while Gemfile is requesting #{requirement}. " \ + "--strict --#{level} was specified, but there are no #{level} level upgrades from #{locked_version} satisfying #{requirement}, so version solving has failed" + end + def filter_matching_specs(specs, requirements) Array(requirements).flat_map do |requirement| specs.select {| spec| requirement_satisfied_by?(requirement, spec) } @@ -391,6 +402,11 @@ def prepare_dependencies(requirements, packages) dep_package.consider_prereleases! versions = select_sorted_versions(dep_package, dep_range) end + + if versions.empty? && select_all_versions(dep_package, dep_range).any? + raise_all_versions_filtered_out!(dep_package) + end + next [dep_package, dep_constraint] unless versions.empty? next unless dep_package.current_platform? @@ -403,6 +419,10 @@ def select_sorted_versions(package, range) range.select_versions(@sorted_versions[package]) end + def select_all_versions(package, range) + range.select_versions(@all_versions[package]) + end + def other_specs_matching_message(specs, requirement) message = String.new("The source contains the following gems matching '#{requirement}':\n") message << specs.map {|s| " * #{s.full_name}" }.join("\n") diff --git a/bundler/spec/commands/lock_spec.rb b/bundler/spec/commands/lock_spec.rb index 0f1aeef91065..f6793d393ba8 100644 --- a/bundler/spec/commands/lock_spec.rb +++ b/bundler/spec/commands/lock_spec.rb @@ -392,6 +392,22 @@ expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort) end + it "shows proper error when Gemfile changes forbid patch upgrades, and --patch --strict is given" do + # force next minor via Gemfile + gemfile <<-G + source "#{file_uri_for(gem_repo4)}" + gem 'foo', '1.5.0' + gem 'qux' + G + + bundle "lock --update foo --patch --strict", raise_on_error: false + + expect(err).to include( + "foo is locked to 1.4.3, while Gemfile is requesting foo (= 1.5.0). " \ + "--strict --patch was specified, but there are no patch level upgrades from 1.4.3 satisfying foo (= 1.5.0), so version solving has failed" + ) + end + context "pre" do it "defaults to major" do bundle "lock --update --pre"