Skip to content

Commit d6fc947

Browse files
Merge pull request #7558 from rubygems/update-until-really-updates
Make sure `bundle update <specific_gems>` can always update to the latest resolvable version of each requested gem (cherry picked from commit d95430f)
1 parent 210a858 commit d6fc947

File tree

2 files changed

+175
-24
lines changed

2 files changed

+175
-24
lines changed

bundler/lib/bundler/definition.rb

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
132132
@sources.merged_gem_lockfile_sections!(locked_gem_sources.first)
133133
end
134134

135-
@unlock[:sources] ||= []
135+
@sources_to_unlock = @unlock.delete(:sources) || []
136136
@unlock[:ruby] ||= if @ruby_version && locked_ruby_version_object
137137
@ruby_version.diff(locked_ruby_version_object)
138138
end
@@ -144,11 +144,13 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
144144
@path_changes = converge_paths
145145
@source_changes = converge_sources
146146

147+
@explicit_unlocks = @unlock.delete(:gems) || []
148+
147149
if @unlock[:conservative]
148-
@unlock[:gems] ||= @dependencies.map(&:name)
150+
@gems_to_unlock = @explicit_unlocks.any? ? @explicit_unlocks : @dependencies.map(&:name)
149151
else
150-
eager_unlock = (@unlock[:gems] || []).map {|name| Dependency.new(name, ">= 0") }
151-
@unlock[:gems] = @locked_specs.for(eager_unlock, false, platforms).map(&:name).uniq
152+
eager_unlock = @explicit_unlocks.map {|name| Dependency.new(name, ">= 0") }
153+
@gems_to_unlock = @locked_specs.for(eager_unlock, false, platforms).map(&:name).uniq
152154
end
153155

154156
@dependency_changes = converge_dependencies
@@ -227,7 +229,6 @@ def missing_specs?
227229
@resolver = nil
228230
@resolution_packages = nil
229231
@specs = nil
230-
@gem_version_promoter = nil
231232

232233
Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})"
233234
true
@@ -568,8 +569,10 @@ def resolution_packages
568569
@resolution_packages ||= begin
569570
last_resolve = converge_locked_specs
570571
remove_invalid_platforms!(current_dependencies)
571-
packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @unlock[:gems], prerelease: gem_version_promoter.pre?)
572-
additional_base_requirements_for_resolve(packages, last_resolve)
572+
packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @gems_to_unlock, prerelease: gem_version_promoter.pre?)
573+
packages = additional_base_requirements_to_prevent_downgrades(packages, last_resolve)
574+
packages = additional_base_requirements_to_force_updates(packages)
575+
packages
573576
end
574577
end
575578

@@ -673,14 +676,18 @@ def add_current_platform
673676

674677
def change_reason
675678
if unlocking?
676-
unlock_reason = @unlock.reject {|_k, v| Array(v).empty? }.map do |k, v|
677-
if v == true
678-
k.to_s
679-
else
680-
v = Array(v)
681-
"#{k}: (#{v.join(", ")})"
682-
end
683-
end.join(", ")
679+
unlock_targets = if @gems_to_unlock.any?
680+
["gems", @gems_to_unlock]
681+
elsif @sources_to_unlock.any?
682+
["sources", @sources_to_unlock]
683+
end
684+
685+
unlock_reason = if unlock_targets
686+
"#{unlock_targets.first}: (#{unlock_targets.last.join(", ")})"
687+
else
688+
@unlock[:ruby] ? "ruby" : ""
689+
end
690+
684691
return "bundler is unlocking #{unlock_reason}"
685692
end
686693
[
@@ -735,15 +742,15 @@ def converge_locals
735742
spec = @dependencies.find {|s| s.name == k }
736743
source = spec&.source
737744
if source&.respond_to?(:local_override!)
738-
source.unlock! if @unlock[:gems].include?(spec.name)
745+
source.unlock! if @gems_to_unlock.include?(spec.name)
739746
locals << [source, source.local_override!(v)]
740747
end
741748
end
742749

743750
sources_with_changes = locals.select do |source, changed|
744751
changed || specs_changed?(source)
745752
end.map(&:first)
746-
!sources_with_changes.each {|source| @unlock[:sources] << source.name }.empty?
753+
!sources_with_changes.each {|source| @sources_to_unlock << source.name }.empty?
747754
end
748755

749756
def check_lockfile
@@ -820,7 +827,7 @@ def converge_sources
820827
# gem), unlock it. For git sources, this means to unlock the revision, which
821828
# will cause the `ref` used to be the most recent for the branch (or master) if
822829
# an explicit `ref` is not used.
823-
if source.respond_to?(:unlock!) && @unlock[:sources].include?(source.name)
830+
if source.respond_to?(:unlock!) && @sources_to_unlock.include?(source.name)
824831
source.unlock!
825832
changes = true
826833
end
@@ -864,7 +871,7 @@ def converge_dependencies
864871
def converge_locked_specs
865872
converged = converge_specs(@locked_specs)
866873

867-
resolve = SpecSet.new(converged.reject {|s| @unlock[:gems].include?(s.name) })
874+
resolve = SpecSet.new(converged.reject {|s| @gems_to_unlock.include?(s.name) })
868875

869876
diff = nil
870877

@@ -897,7 +904,7 @@ def converge_specs(specs)
897904

898905
@specs_that_changed_sources << s if gemfile_source != lockfile_source
899906
deps << dep if !dep.source || lockfile_source.include?(dep.source)
900-
@unlock[:gems] << name if lockfile_source.include?(dep.source) && lockfile_source != gemfile_source
907+
@gems_to_unlock << name if lockfile_source.include?(dep.source) && lockfile_source != gemfile_source
901908

902909
# Replace the locked dependency's source with the equivalent source from the Gemfile
903910
s.source = gemfile_source
@@ -906,7 +913,7 @@ def converge_specs(specs)
906913
s.source = default_source unless sources.get(lockfile_source)
907914
end
908915

909-
next if @unlock[:sources].include?(s.source.name)
916+
next if @sources_to_unlock.include?(s.source.name)
910917

911918
# Path sources have special logic
912919
if s.source.instance_of?(Source::Path) || s.source.instance_of?(Source::Gemspec)
@@ -928,12 +935,12 @@ def converge_specs(specs)
928935
else
929936
# If the spec is no longer in the path source, unlock it. This
930937
# commonly happens if the version changed in the gemspec
931-
@unlock[:gems] << name
938+
@gems_to_unlock << name
932939
end
933940
end
934941

935942
if dep.nil? && requested_dependencies.find {|d| name == d.name }
936-
@unlock[:gems] << s.name
943+
@gems_to_unlock << s.name
937944
else
938945
converged << s
939946
end
@@ -1010,7 +1017,7 @@ def lockfiles_equal?(current, proposed, preserve_unknown_sections)
10101017
current == proposed
10111018
end
10121019

1013-
def additional_base_requirements_for_resolve(resolution_packages, last_resolve)
1020+
def additional_base_requirements_to_prevent_downgrades(resolution_packages, last_resolve)
10141021
return resolution_packages unless @locked_gems && !sources.expired_sources?(@locked_gems.sources)
10151022
converge_specs(@originally_locked_specs - last_resolve).each do |locked_spec|
10161023
next if locked_spec.source.is_a?(Source::Path)
@@ -1019,6 +1026,28 @@ def additional_base_requirements_for_resolve(resolution_packages, last_resolve)
10191026
resolution_packages
10201027
end
10211028

1029+
def additional_base_requirements_to_force_updates(resolution_packages)
1030+
return resolution_packages if @explicit_unlocks.empty?
1031+
full_update = dup_for_full_unlock.resolve
1032+
@explicit_unlocks.each do |name|
1033+
version = full_update[name].first&.version
1034+
resolution_packages.base_requirements[name] = Gem::Requirement.new("= #{version}") if version
1035+
end
1036+
resolution_packages
1037+
end
1038+
1039+
def dup_for_full_unlock
1040+
unlocked_definition = self.class.new(@lockfile, @dependencies, @sources, true, @ruby_version, @optional_groups, @gemfiles)
1041+
unlocked_definition.resolution_mode = { "local" => !@remote }
1042+
unlocked_definition.setup_sources_for_resolve
1043+
unlocked_definition.gem_version_promoter.tap do |gvp|
1044+
gvp.level = gem_version_promoter.level
1045+
gvp.strict = gem_version_promoter.strict
1046+
gvp.pre = gem_version_promoter.pre
1047+
end
1048+
unlocked_definition
1049+
end
1050+
10221051
def remove_invalid_platforms!(dependencies)
10231052
return if Bundler.frozen_bundle?
10241053

bundler/spec/commands/lock_spec.rb

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,128 @@
252252
expect(read_lockfile).to eq(remove_checksums_from_lockfile(@lockfile, "(2.3.2)", "(#{rake_version})"))
253253
end
254254

255+
it "updates specific gems using --update, even if that requires unlocking other top level gems" do
256+
build_repo4 do
257+
build_gem "prism", "0.15.1"
258+
build_gem "prism", "0.24.0"
259+
260+
build_gem "ruby-lsp", "0.12.0" do |s|
261+
s.add_dependency "prism", "< 0.24.0"
262+
end
263+
264+
build_gem "ruby-lsp", "0.16.1" do |s|
265+
s.add_dependency "prism", ">= 0.24.0"
266+
end
267+
268+
build_gem "tapioca", "0.11.10" do |s|
269+
s.add_dependency "prism", "< 0.24.0"
270+
end
271+
272+
build_gem "tapioca", "0.13.1" do |s|
273+
s.add_dependency "prism", ">= 0.24.0"
274+
end
275+
end
276+
277+
gemfile <<~G
278+
source "#{file_uri_for(gem_repo4)}"
279+
280+
gem "tapioca"
281+
gem "ruby-lsp"
282+
G
283+
284+
lockfile <<~L
285+
GEM
286+
remote: #{file_uri_for(gem_repo4)}
287+
specs:
288+
prism (0.15.1)
289+
ruby-lsp (0.12.0)
290+
prism (< 0.24.0)
291+
tapioca (0.11.10)
292+
prism (< 0.24.0)
293+
294+
PLATFORMS
295+
#{lockfile_platforms}
296+
297+
DEPENDENCIES
298+
ruby-lsp
299+
tapioca
300+
301+
BUNDLED WITH
302+
#{Bundler::VERSION}
303+
L
304+
305+
bundle "lock --update tapioca --verbose"
306+
307+
expect(lockfile).to include("tapioca (0.13.1)")
308+
end
309+
310+
it "updates specific gems using --update, even if that requires unlocking other top level gems, but only as few as possible" do
311+
build_repo4 do
312+
build_gem "prism", "0.15.1"
313+
build_gem "prism", "0.24.0"
314+
315+
build_gem "ruby-lsp", "0.12.0" do |s|
316+
s.add_dependency "prism", "< 0.24.0"
317+
end
318+
319+
build_gem "ruby-lsp", "0.16.1" do |s|
320+
s.add_dependency "prism", ">= 0.24.0"
321+
end
322+
323+
build_gem "tapioca", "0.11.10" do |s|
324+
s.add_dependency "prism", "< 0.24.0"
325+
end
326+
327+
build_gem "tapioca", "0.13.1" do |s|
328+
s.add_dependency "prism", ">= 0.24.0"
329+
end
330+
331+
build_gem "other-prism-dependent", "1.0.0" do |s|
332+
s.add_dependency "prism", ">= 0.15.1"
333+
end
334+
335+
build_gem "other-prism-dependent", "1.1.0" do |s|
336+
s.add_dependency "prism", ">= 0.15.1"
337+
end
338+
end
339+
340+
gemfile <<~G
341+
source "#{file_uri_for(gem_repo4)}"
342+
343+
gem "tapioca"
344+
gem "ruby-lsp"
345+
gem "other-prism-dependent"
346+
G
347+
348+
lockfile <<~L
349+
GEM
350+
remote: #{file_uri_for(gem_repo4)}
351+
specs:
352+
other-prism-dependent (1.0.0)
353+
prism (>= 0.15.1)
354+
prism (0.15.1)
355+
ruby-lsp (0.12.0)
356+
prism (< 0.24.0)
357+
tapioca (0.11.10)
358+
prism (< 0.24.0)
359+
360+
PLATFORMS
361+
#{lockfile_platforms}
362+
363+
DEPENDENCIES
364+
ruby-lsp
365+
tapioca
366+
367+
BUNDLED WITH
368+
#{Bundler::VERSION}
369+
L
370+
371+
bundle "lock --update tapioca"
372+
373+
expect(lockfile).to include("tapioca (0.13.1)")
374+
expect(lockfile).to include("other-prism-dependent (1.0.0)")
375+
end
376+
255377
it "preserves unknown checksum algorithms" do
256378
lockfile @lockfile.gsub(/(sha256=[a-f0-9]+)$/, "constant=true,\\1,xyz=123")
257379

0 commit comments

Comments
 (0)