bundler 4.0.12 → 4.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -184,6 +184,9 @@ module Bundler
184
184
 
185
185
  platforms_explanation = specs_matching_other_platforms.any? ? " for any resolution platforms (#{package.platforms.join(", ")})" : ""
186
186
  custom_explanation = "#{constraint} could not be found in #{repository_for(package)}#{platforms_explanation}"
187
+ if hint = cooldown_hint(specs_matching_other_platforms)
188
+ custom_explanation += " (#{hint})"
189
+ end
187
190
 
188
191
  label = "#{name} (#{constraint_string})"
189
192
  extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any?
@@ -353,6 +356,10 @@ module Bundler
353
356
  message << "\n#{other_specs_matching_message(specs, matching_part)}"
354
357
  end
355
358
 
359
+ if hint = cooldown_hint(specs_matching_requirement)
360
+ message << "\n\n#{hint}."
361
+ end
362
+
356
363
  if specs_matching_requirement.any? && (hint = platform_mismatch_hint)
357
364
  message << "\n\n#{hint}"
358
365
  end
@@ -396,7 +403,7 @@ module Bundler
396
403
  end
397
404
 
398
405
  def filter_specs(specs, package)
399
- filter_remote_specs(filter_prereleases(specs, package), package)
406
+ filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package)
400
407
  end
401
408
 
402
409
  def filter_prereleases(specs, package)
@@ -405,6 +412,56 @@ module Bundler
405
412
  specs.reject {|s| s.version.prerelease? }
406
413
  end
407
414
 
415
+ def filter_cooldown(specs)
416
+ return specs if specs.empty?
417
+ excluded_versions = cooldown_excluded_versions(specs)
418
+ return specs if excluded_versions.empty?
419
+ specs.reject {|s| excluded_versions.include?([s.name, s.version]) }
420
+ end
421
+
422
+ def cooldown_excluded_versions(specs)
423
+ excluded = {}
424
+ specs.each do |spec|
425
+ next unless cooldown_excluded?(spec)
426
+ excluded[[spec.name, spec.version]] = true
427
+ end
428
+ excluded
429
+ end
430
+
431
+ def cooldown_hint(specs)
432
+ excluded_versions = cooldown_excluded_versions(specs)
433
+ return nil if excluded_versions.empty?
434
+ "#{excluded_versions.size} version#{"s" if excluded_versions.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass"
435
+ end
436
+
437
+ def cooldown_excluded?(spec)
438
+ return false unless spec.respond_to?(:created_at) && spec.created_at
439
+ return false unless spec.respond_to?(:remote) && spec.remote
440
+ return false if pinned_by_lockfile_floor?(spec)
441
+ days = spec.remote.effective_cooldown
442
+ return false if days.nil? || days <= 0
443
+ (cooldown_now - spec.created_at) < (days * 86_400)
444
+ end
445
+
446
+ # A spec sitting exactly at a `>= locked_version` prevent-downgrade floor is
447
+ # the version the lockfile currently pins. `bundle update` and `bundle
448
+ # outdated` install that floor so resolution never moves a gem backwards.
449
+ # Filtering it out for cooldown would then make resolution impossible
450
+ # whenever the locked version is itself inside the cooldown window, which is
451
+ # exactly what happens to a lockfile written before cooldown was enabled.
452
+ # Keep it eligible; gems being explicitly updated carry an exact `=`
453
+ # requirement instead and stay subject to the cooldown filter.
454
+ def pinned_by_lockfile_floor?(spec)
455
+ return false unless defined?(@base) && @base
456
+ requirement = base_requirements[spec.name]
457
+ return false unless requirement && !requirement.exact?
458
+ requirement.requirements.any? {|op, version| op == ">=" && version == spec.version }
459
+ end
460
+
461
+ def cooldown_now
462
+ @cooldown_now ||= Time.now
463
+ end
464
+
408
465
  def filter_remote_specs(specs, package)
409
466
  if package.prefer_local?
410
467
  local_specs = specs.select {|s| s.is_a?(StubSpecification) }
@@ -465,6 +465,28 @@ module Gem
465
465
  Resolver::APISet::GemParser.prepend(UnfreezeCompactIndexParsedResponse)
466
466
  end
467
467
 
468
+ # RubyGems before 4.0.13 split compact index dependency/requirement entries
469
+ # on every colon, which mangles metadata values that contain colons such as
470
+ # the `created_at` timestamps the cooldown feature relies on. Split only on
471
+ # the first colon so those values survive on older RubyGems.
472
+ #
473
+ # The module is defined unconditionally so it stays testable on any RubyGems,
474
+ # but only prepended when the host RubyGems still has the buggy behavior.
475
+ module SplitCompactIndexEntryOnFirstColon
476
+ private
477
+
478
+ def parse_dependency(string)
479
+ dependency = string.split(":", 2)
480
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
481
+ dependency[0] = -dependency[0]
482
+ dependency
483
+ end
484
+ end
485
+
486
+ unless Gem.rubygems_version >= Gem::Version.new("4.0.13")
487
+ Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon)
488
+ end
489
+
468
490
  if Gem.rubygems_version < Gem::Version.new("3.6.0")
469
491
  class Package; end
470
492
  require "rubygems/package/tar_reader"
@@ -20,7 +20,7 @@ module Bundler
20
20
  strict_rm_rf spec.extension_dir
21
21
 
22
22
  SharedHelpers.filesystem_access(gem_dir, :create) do
23
- FileUtils.mkdir_p gem_dir, mode: 0o755
23
+ FileUtils.mkdir_p gem_dir
24
24
  end
25
25
 
26
26
  SharedHelpers.filesystem_access(gem_dir, :write) do
@@ -42,6 +42,7 @@ module Bundler
42
42
  ].freeze
43
43
 
44
44
  NUMBER_KEYS = %w[
45
+ cooldown
45
46
  jobs
46
47
  redirect
47
48
  retry
@@ -432,9 +432,14 @@ module Bundler
432
432
  end
433
433
 
434
434
  def capture3_args_for(cmd, dir)
435
- return ["git", *cmd] unless dir
435
+ # Disable automatic maintenance so a background commit-graph write in
436
+ # the source repo can't race the hardlinking local clone and fail with
437
+ # "hardlink different from source".
438
+ opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"]
436
439
 
437
- ["git", "-C", dir.to_s, *cmd]
440
+ return ["git", *opts, *cmd] unless dir
441
+
442
+ ["git", "-C", dir.to_s, *opts, *cmd]
438
443
  end
439
444
 
440
445
  def extra_clone_args
@@ -4,9 +4,9 @@ module Bundler
4
4
  class Source
5
5
  class Rubygems
6
6
  class Remote
7
- attr_reader :uri, :anonymized_uri, :original_uri
7
+ attr_reader :uri, :anonymized_uri, :original_uri, :cooldown
8
8
 
9
- def initialize(uri)
9
+ def initialize(uri, cooldown: nil)
10
10
  orig_uri = uri
11
11
  uri = Bundler.settings.mirror_for(uri)
12
12
  @original_uri = orig_uri if orig_uri != uri
@@ -14,6 +14,16 @@ module Bundler
14
14
 
15
15
  @uri = apply_auth(uri, fallback_auth).freeze
16
16
  @anonymized_uri = remove_auth(@uri).freeze
17
+ @cooldown = cooldown
18
+ end
19
+
20
+ # Returns the cooldown days that apply to this remote, resolving the
21
+ # precedence CLI > config > Gemfile per-source. Returns nil if no
22
+ # cooldown applies.
23
+ def effective_cooldown
24
+ override = Bundler.settings[:cooldown]
25
+ return override if override
26
+ @cooldown
17
27
  end
18
28
 
19
29
  MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length
@@ -11,11 +11,12 @@ module Bundler
11
11
  API_REQUEST_SIZE = 100
12
12
  REQUIRE_MUTEX = Mutex.new
13
13
 
14
- attr_accessor :remotes
14
+ attr_accessor :remotes, :remote_cooldowns
15
15
 
16
16
  def initialize(options = {})
17
17
  @options = options
18
18
  @remotes = []
19
+ @remote_cooldowns = {}
19
20
  @dependency_names = []
20
21
  @allow_remote = false
21
22
  @allow_cached = false
@@ -25,7 +26,8 @@ module Bundler
25
26
  @gem_installers = {}
26
27
  @gem_installers_mutex = Mutex.new
27
28
 
28
- Array(options["remotes"]).reverse_each {|r| add_remote(r) }
29
+ cooldown = options["cooldown"]
30
+ Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) }
29
31
 
30
32
  @lockfile_remotes = @remotes if options["from_lockfile"]
31
33
  end
@@ -148,6 +150,13 @@ module Bundler
148
150
  # sources, and large_idx.merge! small_idx is way faster than
149
151
  # small_idx.merge! large_idx.
150
152
  index = @allow_remote ? remote_specs.dup : Index.new
153
+
154
+ # Snapshot per-version `created_at` from the remote info before installed
155
+ # / cached specs overwrite the EndpointSpecification objects that carry
156
+ # it. The cooldown filter consults `created_at` on every candidate, so
157
+ # local stubs need the published date back-filled to participate.
158
+ remote_created_at = collect_remote_created_at(index)
159
+
151
160
  index.merge!(cached_specs) if @allow_cached
152
161
  index.merge!(installed_specs) if @allow_local
153
162
 
@@ -161,6 +170,8 @@ module Bundler
161
170
  end
162
171
  end
163
172
 
173
+ backfill_created_at(index, remote_created_at) unless remote_created_at.empty?
174
+
164
175
  index
165
176
  end
166
177
  end
@@ -243,9 +254,14 @@ module Bundler
243
254
  cached_path
244
255
  end
245
256
 
246
- def add_remote(source)
257
+ def add_remote(source, cooldown: nil)
247
258
  uri = normalize_uri(source)
248
259
  @remotes.unshift(uri) unless @remotes.include?(uri)
260
+ @remote_cooldowns[uri] = cooldown if cooldown
261
+ end
262
+
263
+ def cooldown_for(uri)
264
+ @remote_cooldowns[uri]
249
265
  end
250
266
 
251
267
  def spec_names
@@ -266,7 +282,7 @@ module Bundler
266
282
 
267
283
  def remote_fetchers
268
284
  @remote_fetchers ||= remotes.to_h do |uri|
269
- remote = Source::Rubygems::Remote.new(uri)
285
+ remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri))
270
286
  [remote, Bundler::Fetcher.new(remote)]
271
287
  end.freeze
272
288
  end
@@ -314,6 +330,13 @@ module Bundler
314
330
  @allow_remote && api_fetchers.any?
315
331
  end
316
332
 
333
+ def clear_cache
334
+ @specs = nil
335
+ @installed_specs = nil
336
+ @default_specs = nil
337
+ @cached_specs = nil
338
+ end
339
+
317
340
  protected
318
341
 
319
342
  def remote_names
@@ -456,6 +479,31 @@ module Bundler
456
479
 
457
480
  private
458
481
 
482
+ def collect_remote_created_at(index)
483
+ return {} unless @allow_remote
484
+
485
+ snapshot = {}
486
+ index.each do |spec|
487
+ next unless spec.respond_to?(:created_at) && spec.created_at
488
+ # Remember the remote that supplied the date too: when a source has
489
+ # several remotes with different per-URI cooldown settings we must
490
+ # restore the same one during backfill so `effective_cooldown` agrees.
491
+ snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote]
492
+ end
493
+ snapshot
494
+ end
495
+
496
+ def backfill_created_at(index, snapshot)
497
+ index.each do |spec|
498
+ next unless spec.respond_to?(:created_at=)
499
+ next if spec.created_at
500
+ remote_created_at, remote = snapshot[[spec.name, spec.version]]
501
+ next unless remote_created_at
502
+ spec.created_at = remote_created_at
503
+ spec.remote ||= remote if remote && spec.respond_to?(:remote=)
504
+ end
505
+ end
506
+
459
507
  def lockfile_remotes
460
508
  @lockfile_remotes || credless_remotes
461
509
  end
@@ -59,8 +59,8 @@ module Bundler
59
59
  add_source_to_list Plugin.source(source).new(options), @plugin_sources
60
60
  end
61
61
 
62
- def add_global_rubygems_remote(uri)
63
- global_rubygems_source.add_remote(uri)
62
+ def add_global_rubygems_remote(uri, cooldown: nil)
63
+ global_rubygems_source.add_remote(uri, cooldown: cooldown)
64
64
  global_rubygems_source
65
65
  end
66
66
 
@@ -136,6 +136,10 @@ module Bundler
136
136
  all_sources.each(&:remote!)
137
137
  end
138
138
 
139
+ def clear_cache
140
+ rubygems_sources.each(&:clear_cache)
141
+ end
142
+
139
143
  private
140
144
 
141
145
  def map_sources(replacement_sources)
@@ -165,6 +169,10 @@ module Bundler
165
169
  # locked sources never include credentials so always prefer remotes from the gemfile
166
170
  replacement_source.remotes = gemfile_source.remotes
167
171
 
172
+ # cooldowns are only ever declared in the Gemfile, so carry them over
173
+ # along with the remotes they apply to
174
+ replacement_source.remote_cooldowns = gemfile_source.remote_cooldowns
175
+
168
176
  yield replacement_source if block_given?
169
177
 
170
178
  replacement_source
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  module Bundler
4
- VERSION = "4.0.12".freeze
4
+ VERSION = "4.0.14".freeze
5
5
 
6
6
  def self.bundler_major_version
7
7
  @bundler_major_version ||= gem_version.segments.first
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.12
4
+ version: 4.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - André Arko
@@ -402,7 +402,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
402
402
  - !ruby/object:Gem::Version
403
403
  version: 3.4.1
404
404
  requirements: []
405
- rubygems_version: 4.0.10
405
+ rubygems_version: 4.0.13
406
406
  specification_version: 4
407
407
  summary: The best way to manage your application's dependencies
408
408
  test_files: []