bundler 2.6.3 → 2.6.9

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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +119 -6
  3. data/README.md +1 -1
  4. data/lib/bundler/build_metadata.rb +2 -2
  5. data/lib/bundler/checksum.rb +22 -12
  6. data/lib/bundler/cli/console.rb +8 -6
  7. data/lib/bundler/cli/doctor/diagnose.rb +167 -0
  8. data/lib/bundler/cli/doctor/ssl.rb +249 -0
  9. data/lib/bundler/cli/doctor.rb +27 -151
  10. data/lib/bundler/cli/info.rb +4 -4
  11. data/lib/bundler/cli/inject.rb +2 -2
  12. data/lib/bundler/cli/issue.rb +3 -3
  13. data/lib/bundler/cli/lock.rb +2 -1
  14. data/lib/bundler/cli/show.rb +1 -1
  15. data/lib/bundler/cli.rb +2 -11
  16. data/lib/bundler/compact_index_client/cache.rb +1 -1
  17. data/lib/bundler/compact_index_client/parser.rb +1 -1
  18. data/lib/bundler/compact_index_client/updater.rb +2 -1
  19. data/lib/bundler/current_ruby.rb +23 -33
  20. data/lib/bundler/definition.rb +220 -184
  21. data/lib/bundler/dependency.rb +92 -47
  22. data/lib/bundler/dsl.rb +84 -80
  23. data/lib/bundler/endpoint_specification.rb +10 -3
  24. data/lib/bundler/errors.rb +22 -0
  25. data/lib/bundler/friendly_errors.rb +1 -1
  26. data/lib/bundler/gem_helpers.rb +4 -10
  27. data/lib/bundler/gem_version_promoter.rb +0 -2
  28. data/lib/bundler/injector.rb +9 -9
  29. data/lib/bundler/installer.rb +2 -2
  30. data/lib/bundler/lazy_specification.rb +67 -45
  31. data/lib/bundler/lockfile_parser.rb +8 -5
  32. data/lib/bundler/man/bundle-add.1 +1 -1
  33. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  34. data/lib/bundler/man/bundle-cache.1 +1 -1
  35. data/lib/bundler/man/bundle-check.1 +1 -1
  36. data/lib/bundler/man/bundle-clean.1 +1 -1
  37. data/lib/bundler/man/bundle-config.1 +6 -6
  38. data/lib/bundler/man/bundle-config.1.ronn +9 -4
  39. data/lib/bundler/man/bundle-console.1 +1 -1
  40. data/lib/bundler/man/bundle-doctor.1 +1 -1
  41. data/lib/bundler/man/bundle-env.1 +1 -1
  42. data/lib/bundler/man/bundle-exec.1 +3 -3
  43. data/lib/bundler/man/bundle-exec.1.ronn +2 -2
  44. data/lib/bundler/man/bundle-fund.1 +1 -1
  45. data/lib/bundler/man/bundle-gem.1 +1 -1
  46. data/lib/bundler/man/bundle-help.1 +1 -1
  47. data/lib/bundler/man/bundle-info.1 +1 -1
  48. data/lib/bundler/man/bundle-init.1 +1 -1
  49. data/lib/bundler/man/bundle-inject.1 +1 -1
  50. data/lib/bundler/man/bundle-install.1 +1 -1
  51. data/lib/bundler/man/bundle-issue.1 +1 -1
  52. data/lib/bundler/man/bundle-licenses.1 +1 -1
  53. data/lib/bundler/man/bundle-list.1 +1 -1
  54. data/lib/bundler/man/bundle-lock.1 +1 -1
  55. data/lib/bundler/man/bundle-open.1 +1 -1
  56. data/lib/bundler/man/bundle-outdated.1 +1 -1
  57. data/lib/bundler/man/bundle-platform.1 +1 -1
  58. data/lib/bundler/man/bundle-plugin.1 +1 -1
  59. data/lib/bundler/man/bundle-pristine.1 +1 -1
  60. data/lib/bundler/man/bundle-remove.1 +1 -1
  61. data/lib/bundler/man/bundle-show.1 +1 -1
  62. data/lib/bundler/man/bundle-update.1 +1 -1
  63. data/lib/bundler/man/bundle-version.1 +1 -1
  64. data/lib/bundler/man/bundle-viz.1 +1 -1
  65. data/lib/bundler/man/bundle.1 +1 -1
  66. data/lib/bundler/man/gemfile.5 +1 -1
  67. data/lib/bundler/match_metadata.rb +13 -0
  68. data/lib/bundler/plugin/api/source.rb +1 -1
  69. data/lib/bundler/plugin/index.rb +1 -1
  70. data/lib/bundler/plugin/installer/path.rb +8 -0
  71. data/lib/bundler/plugin.rb +1 -1
  72. data/lib/bundler/resolver/candidate.rb +12 -9
  73. data/lib/bundler/resolver/package.rb +7 -3
  74. data/lib/bundler/resolver/spec_group.rb +1 -25
  75. data/lib/bundler/resolver/strategy.rb +40 -0
  76. data/lib/bundler/resolver.rb +29 -27
  77. data/lib/bundler/rubygems_ext.rb +97 -81
  78. data/lib/bundler/rubygems_integration.rb +2 -3
  79. data/lib/bundler/runtime.rb +27 -29
  80. data/lib/bundler/shared_helpers.rb +4 -0
  81. data/lib/bundler/source/gemspec.rb +1 -4
  82. data/lib/bundler/source/git/git_proxy.rb +14 -3
  83. data/lib/bundler/source/git.rb +5 -1
  84. data/lib/bundler/source/path.rb +2 -2
  85. data/lib/bundler/source/rubygems/remote.rb +11 -3
  86. data/lib/bundler/source/rubygems.rb +19 -4
  87. data/lib/bundler/source.rb +2 -0
  88. data/lib/bundler/source_list.rb +33 -11
  89. data/lib/bundler/spec_set.rb +98 -40
  90. data/lib/bundler/templates/newgem/Gemfile.tt +1 -0
  91. data/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +53 -3
  92. data/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +1 -1
  93. data/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +11 -0
  94. data/lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb +4 -24
  95. data/lib/bundler/vendor/pub_grub/lib/pub_grub/strategy.rb +42 -0
  96. data/lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb +20 -8
  97. data/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb +17 -29
  98. data/lib/bundler/vendor/uri/lib/uri/common.rb +7 -3
  99. data/lib/bundler/vendor/uri/lib/uri/generic.rb +12 -11
  100. data/lib/bundler/vendor/uri/lib/uri/rfc2396_parser.rb +6 -6
  101. data/lib/bundler/vendor/uri/lib/uri/version.rb +1 -1
  102. data/lib/bundler/version.rb +1 -1
  103. metadata +7 -4
  104. data/lib/bundler/compact_index_client/gem_parser.rb +0 -32
@@ -147,6 +147,12 @@ module Bundler
147
147
  end
148
148
  end
149
149
 
150
+ def installed_to?(destination)
151
+ # if copy_to is interrupted, it may leave a partially installed directory that
152
+ # contains .git but no other files -- consider this not to be installed
153
+ Dir.exist?(destination) && (Dir.children(destination) - [".git"]).any?
154
+ end
155
+
150
156
  private
151
157
 
152
158
  def git_remote_fetch(args)
@@ -179,7 +185,8 @@ module Bundler
179
185
  _, err, status = capture(command, nil)
180
186
  return extra_ref if status.success?
181
187
 
182
- if err.include?("Could not find remote branch")
188
+ if err.include?("Could not find remote branch") || # git up to 2.49
189
+ err.include?("Remote branch #{branch_option} not found") # git 2.49 or higher
183
190
  raise MissingGitRevisionError.new(command_with_no_credentials, nil, explicit_ref, credential_filtered_uri)
184
191
  else
185
192
  idx = command.index("--depth")
@@ -256,7 +263,7 @@ module Bundler
256
263
  end
257
264
 
258
265
  def not_pinned?
259
- branch || tag || ref.nil?
266
+ branch_option || ref.nil?
260
267
  end
261
268
 
262
269
  def pinned_to_full_sha?
@@ -420,7 +427,7 @@ module Bundler
420
427
  # anyways.
421
428
  return args if @revision
422
429
 
423
- args += ["--branch", branch || tag] if branch || tag
430
+ args += ["--branch", branch_option] if branch_option
424
431
  args
425
432
  end
426
433
 
@@ -436,6 +443,10 @@ module Bundler
436
443
  extra_args
437
444
  end
438
445
 
446
+ def branch_option
447
+ branch || tag
448
+ end
449
+
439
450
  def full_clone?
440
451
  depth.nil?
441
452
  end
@@ -360,7 +360,11 @@ module Bundler
360
360
  end
361
361
 
362
362
  def locked_revision_checked_out?
363
- locked_revision && locked_revision == revision && install_path.exist?
363
+ locked_revision && locked_revision == revision && installed?
364
+ end
365
+
366
+ def installed?
367
+ git_proxy.installed_to?(install_path)
364
368
  end
365
369
 
366
370
  def base_name
@@ -60,8 +60,8 @@ module Bundler
60
60
  end
61
61
 
62
62
  def eql?(other)
63
- return unless other.class == self.class
64
- expanded_original_path == other.expanded_original_path &&
63
+ [Gemspec, Path].include?(other.class) &&
64
+ expanded_original_path == other.expanded_original_path &&
65
65
  version == other.version
66
66
  end
67
67
 
@@ -16,6 +16,9 @@ module Bundler
16
16
  @anonymized_uri = remove_auth(@uri).freeze
17
17
  end
18
18
 
19
+ MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length
20
+ private_constant :MAX_CACHE_SLUG_HOST_SIZE
21
+
19
22
  # @return [String] A slug suitable for use as a cache key for this
20
23
  # remote.
21
24
  #
@@ -28,10 +31,15 @@ module Bundler
28
31
  host = cache_uri.to_s.start_with?("file://") ? nil : cache_uri.host
29
32
 
30
33
  uri_parts = [host, cache_uri.user, cache_uri.port, cache_uri.path]
31
- uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.compact.join("."))
34
+ uri_parts.compact!
35
+ uri_digest = SharedHelpers.digest(:MD5).hexdigest(uri_parts.join("."))
36
+
37
+ uri_parts.pop
38
+ host_parts = uri_parts.join(".")
39
+ return uri_digest if host_parts.empty?
32
40
 
33
- uri_parts[-1] = uri_digest
34
- uri_parts.compact.join(".")
41
+ shortened_host_parts = host_parts[0...MAX_CACHE_SLUG_HOST_SIZE]
42
+ [shortened_host_parts, uri_digest].join(".")
35
43
  end
36
44
  end
37
45
 
@@ -19,6 +19,7 @@ module Bundler
19
19
  @allow_remote = false
20
20
  @allow_cached = false
21
21
  @allow_local = options["allow_local"] || false
22
+ @prefer_local = false
22
23
  @checksum_store = Checksum::Store.new
23
24
 
24
25
  Array(options["remotes"]).reverse_each {|r| add_remote(r) }
@@ -30,6 +31,10 @@ module Bundler
30
31
  @caches ||= [cache_path, *Bundler.rubygems.gem_cache]
31
32
  end
32
33
 
34
+ def prefer_local!
35
+ @prefer_local = true
36
+ end
37
+
33
38
  def local_only!
34
39
  @specs = nil
35
40
  @allow_local = true
@@ -37,6 +42,10 @@ module Bundler
37
42
  @allow_remote = false
38
43
  end
39
44
 
45
+ def local_only?
46
+ @allow_local && !@allow_remote
47
+ end
48
+
40
49
  def local!
41
50
  return if @allow_local
42
51
 
@@ -139,9 +148,15 @@ module Bundler
139
148
  index.merge!(cached_specs) if @allow_cached
140
149
  index.merge!(installed_specs) if @allow_local
141
150
 
142
- # complete with default specs, only if not already available in the
143
- # index through remote, cached, or installed specs
144
- index.use(default_specs) if @allow_local
151
+ if @allow_local
152
+ if @prefer_local
153
+ index.merge!(default_specs)
154
+ else
155
+ # complete with default specs, only if not already available in the
156
+ # index through remote, cached, or installed specs
157
+ index.use(default_specs)
158
+ end
159
+ end
145
160
 
146
161
  index
147
162
  end
@@ -439,7 +454,7 @@ module Bundler
439
454
  end
440
455
 
441
456
  def installed?(spec)
442
- installed_specs[spec].any? && !spec.deleted_gem?
457
+ installed_specs[spec].any? && !spec.installation_missing?
443
458
  end
444
459
 
445
460
  def rubygems_dir
@@ -35,6 +35,8 @@ module Bundler
35
35
  spec.source == self
36
36
  end
37
37
 
38
+ def prefer_local!; end
39
+
38
40
  def local!; end
39
41
 
40
42
  def local_only!; end
@@ -141,6 +141,10 @@ module Bundler
141
141
  different_sources?(lock_sources, replacement_sources)
142
142
  end
143
143
 
144
+ def prefer_local!
145
+ all_sources.each(&:prefer_local!)
146
+ end
147
+
144
148
  def local_only!
145
149
  all_sources.each(&:local_only!)
146
150
  end
@@ -169,39 +173,57 @@ module Bundler
169
173
 
170
174
  def map_sources(replacement_sources)
171
175
  rubygems = @rubygems_sources.map do |source|
172
- replace_rubygems_source(replacement_sources, source) || source
176
+ replace_rubygems_source(replacement_sources, source)
173
177
  end
174
178
 
175
179
  git, plugin = [@git_sources, @plugin_sources].map do |sources|
176
180
  sources.map do |source|
177
- replacement_sources.find {|s| s == source } || source
181
+ replace_source(replacement_sources, source)
178
182
  end
179
183
  end
180
184
 
181
185
  path = @path_sources.map do |source|
182
- replacement_sources.find {|s| s == (source.is_a?(Source::Gemspec) ? source.as_path_source : source) } || source
186
+ replace_path_source(replacement_sources, source)
183
187
  end
184
188
 
185
189
  [rubygems, path, git, plugin]
186
190
  end
187
191
 
188
192
  def global_replacement_source(replacement_sources)
189
- replacement_source = replace_rubygems_source(replacement_sources, global_rubygems_source)
190
- return global_rubygems_source unless replacement_source
191
-
192
- replacement_source.local!
193
- replacement_source
193
+ replace_rubygems_source(replacement_sources, global_rubygems_source, &:local!)
194
194
  end
195
195
 
196
196
  def replace_rubygems_source(replacement_sources, gemfile_source)
197
+ replace_source(replacement_sources, gemfile_source) do |replacement_source|
198
+ # locked sources never include credentials so always prefer remotes from the gemfile
199
+ replacement_source.remotes = gemfile_source.remotes
200
+
201
+ yield replacement_source if block_given?
202
+
203
+ replacement_source
204
+ end
205
+ end
206
+
207
+ def replace_source(replacement_sources, gemfile_source)
197
208
  replacement_source = replacement_sources.find {|s| s == gemfile_source }
198
- return unless replacement_source
209
+ return gemfile_source unless replacement_source
210
+
211
+ replacement_source = yield(replacement_source) if block_given?
199
212
 
200
- # locked sources never include credentials so always prefer remotes from the gemfile
201
- replacement_source.remotes = gemfile_source.remotes
202
213
  replacement_source
203
214
  end
204
215
 
216
+ def replace_path_source(replacement_sources, gemfile_source)
217
+ replace_source(replacement_sources, gemfile_source) do |replacement_source|
218
+ if gemfile_source.is_a?(Source::Gemspec)
219
+ gemfile_source.checksum_store = replacement_source.checksum_store
220
+ gemfile_source
221
+ else
222
+ replacement_source
223
+ end
224
+ end
225
+ end
226
+
205
227
  def different_sources?(lock_sources, replacement_sources)
206
228
  !equivalent_sources?(lock_sources, replacement_sources)
207
229
  end
@@ -29,9 +29,10 @@ module Bundler
29
29
  end
30
30
 
31
31
  def normalize_platforms!(deps, platforms)
32
- complete_platforms = add_extra_platforms!(platforms)
32
+ remove_invalid_platforms!(deps, platforms)
33
+ add_extra_platforms!(platforms)
33
34
 
34
- complete_platforms.map do |platform|
35
+ platforms.map! do |platform|
35
36
  next platform if platform == Gem::Platform::RUBY
36
37
 
37
38
  begin
@@ -44,11 +45,34 @@ module Bundler
44
45
  next platform if incomplete_for_platform?(deps, less_specific_platform)
45
46
 
46
47
  less_specific_platform
47
- end.uniq
48
+ end.uniq!
49
+ end
50
+
51
+ def add_originally_invalid_platforms!(platforms, originally_invalid_platforms)
52
+ originally_invalid_platforms.each do |originally_invalid_platform|
53
+ platforms << originally_invalid_platform if complete_platform(originally_invalid_platform)
54
+ end
55
+ end
56
+
57
+ def remove_invalid_platforms!(deps, platforms, skips: [])
58
+ invalid_platforms = []
59
+
60
+ platforms.reject! do |platform|
61
+ next false if skips.include?(platform)
62
+
63
+ invalid = incomplete_for_platform?(deps, platform)
64
+ invalid_platforms << platform if invalid
65
+ invalid
66
+ end
67
+
68
+ invalid_platforms
48
69
  end
49
70
 
50
71
  def add_extra_platforms!(platforms)
51
- return platforms.concat([Gem::Platform::RUBY]).uniq if @specs.empty?
72
+ if @specs.empty?
73
+ platforms.concat([Gem::Platform::RUBY]).uniq
74
+ return
75
+ end
52
76
 
53
77
  new_platforms = all_platforms.select do |platform|
54
78
  next if platforms.include?(platform)
@@ -56,14 +80,13 @@ module Bundler
56
80
 
57
81
  complete_platform(platform)
58
82
  end
59
- return platforms if new_platforms.empty?
83
+ return if new_platforms.empty?
60
84
 
61
85
  platforms.concat(new_platforms)
86
+ return if new_platforms.include?(Bundler.local_platform)
62
87
 
63
88
  less_specific_platform = new_platforms.find {|platform| platform != Gem::Platform::RUBY && Bundler.local_platform === platform && platform === Bundler.local_platform }
64
89
  platforms.delete(Bundler.local_platform) if less_specific_platform
65
-
66
- platforms
67
90
  end
68
91
 
69
92
  def validate_deps(s)
@@ -83,15 +106,13 @@ module Bundler
83
106
  end
84
107
 
85
108
  def []=(key, value)
86
- @specs << value
109
+ delete_by_name(key)
87
110
 
88
- reset!
111
+ add_spec(value)
89
112
  end
90
113
 
91
114
  def delete(specs)
92
- Array(specs).each {|spec| @specs.delete(spec) }
93
-
94
- reset!
115
+ Array(specs).each {|spec| remove_spec(spec) }
95
116
  end
96
117
 
97
118
  def sort!
@@ -117,20 +138,22 @@ module Bundler
117
138
  def materialized_for_all_platforms
118
139
  @specs.map do |s|
119
140
  next s unless s.is_a?(LazySpecification)
120
- s.source.remote!
121
- spec = s.materialize_strictly
141
+ spec = s.materialize_for_cache
122
142
  raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec
123
143
  spec
124
144
  end
125
145
  end
126
146
 
127
147
  def incomplete_for_platform?(deps, platform)
128
- return false if @specs.empty?
148
+ incomplete_specs_for_platform(deps, platform).any?
149
+ end
150
+
151
+ def incomplete_specs_for_platform(deps, platform)
152
+ return [] if @specs.empty?
129
153
 
130
154
  validation_set = self.class.new(@specs)
131
155
  validation_set.for(deps, [platform])
132
-
133
- validation_set.incomplete_specs.any?
156
+ validation_set.incomplete_specs
134
157
  end
135
158
 
136
159
  def missing_specs_for(deps)
@@ -169,12 +192,14 @@ module Bundler
169
192
 
170
193
  def delete_by_name(name)
171
194
  @specs.reject! {|spec| spec.name == name }
195
+ @sorted&.reject! {|spec| spec.name == name }
196
+ return if @lookup.nil?
172
197
 
173
- reset!
198
+ @lookup[name] = nil
174
199
  end
175
200
 
176
201
  def version_for(name)
177
- self[name].first&.version
202
+ exemplary_spec(name)&.version
178
203
  end
179
204
 
180
205
  def what_required(spec)
@@ -212,6 +237,10 @@ module Bundler
212
237
  s.matches_current_metadata? && valid_dependencies?(s)
213
238
  end
214
239
 
240
+ def to_s
241
+ map(&:full_name).to_s
242
+ end
243
+
215
244
  private
216
245
 
217
246
  def materialize_dependencies(dependencies, platforms = [nil], skips: [])
@@ -245,11 +274,6 @@ module Bundler
245
274
  @materializations.filter_map(&:materialized_spec)
246
275
  end
247
276
 
248
- def reset!
249
- @sorted = nil
250
- @lookup = nil
251
- end
252
-
253
277
  def complete_platform(platform)
254
278
  new_specs = []
255
279
 
@@ -269,9 +293,7 @@ module Bundler
269
293
  end
270
294
 
271
295
  if valid_platform && new_specs.any?
272
- @specs.concat(new_specs)
273
-
274
- reset!
296
+ new_specs.each {|spec| add_spec(spec) }
275
297
  end
276
298
 
277
299
  valid_platform
@@ -282,8 +304,13 @@ module Bundler
282
304
  end
283
305
 
284
306
  def additional_variants_from(other)
285
- other.select do |spec|
286
- version_for(spec.name) == spec.version && valid_dependencies?(spec)
307
+ other.select do |other_spec|
308
+ spec = exemplary_spec(other_spec.name)
309
+ next unless spec
310
+
311
+ selected = spec.version == other_spec.version && valid_dependencies?(other_spec)
312
+ other_spec.source = spec.source if selected
313
+ selected
287
314
  end
288
315
  end
289
316
 
@@ -292,15 +319,12 @@ module Bundler
292
319
  end
293
320
 
294
321
  def sorted
295
- rake = @specs.find {|s| s.name == "rake" }
296
- begin
297
- @sorted ||= ([rake] + tsort).compact.uniq
298
- rescue TSort::Cyclic => error
299
- cgems = extract_circular_gems(error)
300
- raise CyclicDependencyError, "Your bundle requires gems that depend" \
301
- " on each other, creating an infinite loop. Please remove either" \
302
- " gem '#{cgems[0]}' or gem '#{cgems[1]}' and try again."
303
- end
322
+ @sorted ||= ([@specs.find {|s| s.name == "rake" }] + tsort).compact.uniq
323
+ rescue TSort::Cyclic => error
324
+ cgems = extract_circular_gems(error)
325
+ raise CyclicDependencyError, "Your bundle requires gems that depend" \
326
+ " on each other, creating an infinite loop. Please remove either" \
327
+ " gem '#{cgems[0]}' or gem '#{cgems[1]}' and try again."
304
328
  end
305
329
 
306
330
  def extract_circular_gems(error)
@@ -311,8 +335,7 @@ module Bundler
311
335
  @lookup ||= begin
312
336
  lookup = {}
313
337
  @specs.each do |s|
314
- lookup[s.name] ||= []
315
- lookup[s.name] << s
338
+ index_spec(lookup, s.name, s)
316
339
  end
317
340
  lookup
318
341
  end
@@ -333,5 +356,40 @@ module Bundler
333
356
  specs_for_name.each {|s2| yield s2 }
334
357
  end
335
358
  end
359
+
360
+ def add_spec(spec)
361
+ @specs << spec
362
+
363
+ name = spec.name
364
+
365
+ @sorted&.insert(@sorted.bsearch_index {|s| s.name >= name } || @sorted.size, spec)
366
+ return if @lookup.nil?
367
+
368
+ index_spec(@lookup, name, spec)
369
+ end
370
+
371
+ def remove_spec(spec)
372
+ @specs.delete(spec)
373
+ @sorted&.delete(spec)
374
+ return if @lookup.nil?
375
+
376
+ indexed_specs = @lookup[spec.name]
377
+ return unless indexed_specs
378
+
379
+ if indexed_specs.size > 1
380
+ @lookup[spec.name].delete(spec)
381
+ else
382
+ @lookup[spec.name] = nil
383
+ end
384
+ end
385
+
386
+ def index_spec(hash, key, value)
387
+ hash[key] ||= []
388
+ hash[key] << value
389
+ end
390
+
391
+ def exemplary_spec(name)
392
+ self[name].first
393
+ end
336
394
  end
337
395
  end
@@ -5,6 +5,7 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in <%= config[:name] %>.gemspec
6
6
  gemspec
7
7
 
8
+ gem "irb"
8
9
  gem "rake", "~> 13.0"
9
10
  <%- if config[:ext] -%>
10
11
 
@@ -41,6 +41,7 @@ class Bundler::ConnectionPool::TimedStack
41
41
  def push(obj, options = {})
42
42
  @mutex.synchronize do
43
43
  if @shutdown_block
44
+ @created -= 1 unless @created == 0
44
45
  @shutdown_block.call(obj)
45
46
  else
46
47
  store_connection obj, options
@@ -98,6 +99,26 @@ class Bundler::ConnectionPool::TimedStack
98
99
  end
99
100
  end
100
101
 
102
+ ##
103
+ # Reaps connections that were checked in more than +idle_seconds+ ago.
104
+ def reap(idle_seconds, &block)
105
+ raise ArgumentError, "reap must receive a block" unless block
106
+ raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric)
107
+ raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
108
+
109
+ idle.times do
110
+ conn =
111
+ @mutex.synchronize do
112
+ raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
113
+
114
+ reserve_idle_connection(idle_seconds)
115
+ end
116
+ break unless conn
117
+
118
+ block.call(conn)
119
+ end
120
+ end
121
+
101
122
  ##
102
123
  # Returns +true+ if there are no available connections.
103
124
 
@@ -112,6 +133,12 @@ class Bundler::ConnectionPool::TimedStack
112
133
  @max - @created + @que.length
113
134
  end
114
135
 
136
+ ##
137
+ # The number of connections created and available on the stack.
138
+ def idle
139
+ @que.length
140
+ end
141
+
115
142
  private
116
143
 
117
144
  def current_time
@@ -133,7 +160,7 @@ class Bundler::ConnectionPool::TimedStack
133
160
  # This method must return a connection from the stack.
134
161
 
135
162
  def fetch_connection(options = nil)
136
- @que.pop
163
+ @que.pop&.first
137
164
  end
138
165
 
139
166
  ##
@@ -144,9 +171,32 @@ class Bundler::ConnectionPool::TimedStack
144
171
  def shutdown_connections(options = nil)
145
172
  while connection_stored?(options)
146
173
  conn = fetch_connection(options)
174
+ @created -= 1 unless @created == 0
147
175
  @shutdown_block.call(conn)
148
176
  end
149
- @created = 0
177
+ end
178
+
179
+ ##
180
+ # This is an extension point for TimedStack and is called with a mutex.
181
+ #
182
+ # This method returns the oldest idle connection if it has been idle for more than idle_seconds.
183
+ # This requires that the stack is kept in order of checked in time (oldest first).
184
+
185
+ def reserve_idle_connection(idle_seconds)
186
+ return unless idle_connections?(idle_seconds)
187
+
188
+ @created -= 1 unless @created == 0
189
+
190
+ @que.shift.first
191
+ end
192
+
193
+ ##
194
+ # This is an extension point for TimedStack and is called with a mutex.
195
+ #
196
+ # Returns true if the first connection in the stack has been idle for more than idle_seconds
197
+
198
+ def idle_connections?(idle_seconds)
199
+ connection_stored? && (current_time - @que.first.last > idle_seconds)
150
200
  end
151
201
 
152
202
  ##
@@ -155,7 +205,7 @@ class Bundler::ConnectionPool::TimedStack
155
205
  # This method must return +obj+ to the stack.
156
206
 
157
207
  def store_connection(obj, options = nil)
158
- @que.push obj
208
+ @que.push [obj, current_time]
159
209
  end
160
210
 
161
211
  ##
@@ -1,3 +1,3 @@
1
1
  class Bundler::ConnectionPool
2
- VERSION = "2.4.1"
2
+ VERSION = "2.5.0"
3
3
  end
@@ -160,6 +160,12 @@ class Bundler::ConnectionPool
160
160
  @available.shutdown(reload: true, &block)
161
161
  end
162
162
 
163
+ ## Reaps idle connections that have been idle for over +idle_seconds+.
164
+ # +idle_seconds+ defaults to 60.
165
+ def reap(idle_seconds = 60, &block)
166
+ @available.reap(idle_seconds, &block)
167
+ end
168
+
163
169
  # Size of this connection pool
164
170
  attr_reader :size
165
171
  # Automatically drop all connections after fork
@@ -169,6 +175,11 @@ class Bundler::ConnectionPool
169
175
  def available
170
176
  @available.length
171
177
  end
178
+
179
+ # Number of pool entries created and idle in the pool.
180
+ def idle
181
+ @available.idle
182
+ end
172
183
  end
173
184
 
174
185
  require_relative "connection_pool/timed_stack"