Skip to content

Commit 5bd253f

Browse files
hsbtclaude
andcommitted
Plumb per-source cooldown from the Gemfile DSL
Lets `source "https://rubygems.org", cooldown: 7` attach a per-remote value to the global Rubygems source, which the new `Remote#effective_cooldown` reads with CLI > config > Gemfile per-source precedence. The cooldown is stored on Source::Rubygems keyed by URI so that several top-level `source` lines can carry independent values onto the same global source. Non-negative Integer values are required at parse time; everything else (strings, floats, arrays, negative numbers) raises InvalidOption so a typo can't silently disable the filter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 478c3ff commit 5bd253f

7 files changed

Lines changed: 113 additions & 9 deletions

File tree

bundler/lib/bundler/dsl.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ def source(source, *args, &blk)
117117
options = args.last.is_a?(Hash) ? args.pop.dup : {}
118118
options = normalize_hash(options)
119119
source = normalize_source(source)
120+
cooldown = options["cooldown"]
121+
if cooldown && !(cooldown.is_a?(Integer) && cooldown >= 0)
122+
raise InvalidOption, "Expected `cooldown` to be a non-negative integer, got #{cooldown.inspect}"
123+
end
120124

121125
if options.key?("type")
122126
options["type"] = options["type"].to_s
@@ -131,9 +135,9 @@ def source(source, *args, &blk)
131135
source_opts = options.merge("uri" => source)
132136
with_source(@sources.add_plugin_source(options["type"], source_opts), &blk)
133137
elsif block_given?
134-
with_source(@sources.add_rubygems_source("remotes" => source), &blk)
138+
with_source(@sources.add_rubygems_source("remotes" => source, "cooldown" => cooldown), &blk)
135139
else
136-
@sources.add_global_rubygems_remote(source)
140+
@sources.add_global_rubygems_remote(source, cooldown: cooldown)
137141
end
138142
end
139143

bundler/lib/bundler/source/rubygems.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Rubygems < Source
1616
def initialize(options = {})
1717
@options = options
1818
@remotes = []
19+
@remote_cooldowns = {}
1920
@dependency_names = []
2021
@allow_remote = false
2122
@allow_cached = false
@@ -25,7 +26,8 @@ def initialize(options = {})
2526
@gem_installers = {}
2627
@gem_installers_mutex = Mutex.new
2728

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) }
2931

3032
@lockfile_remotes = @remotes if options["from_lockfile"]
3133
end
@@ -243,9 +245,14 @@ def cached_built_in_gem(spec, local: false)
243245
cached_path
244246
end
245247

246-
def add_remote(source)
248+
def add_remote(source, cooldown: nil)
247249
uri = normalize_uri(source)
248250
@remotes.unshift(uri) unless @remotes.include?(uri)
251+
@remote_cooldowns[uri] = cooldown if cooldown
252+
end
253+
254+
def cooldown_for(uri)
255+
@remote_cooldowns[uri]
249256
end
250257

251258
def spec_names
@@ -266,7 +273,7 @@ def unmet_deps
266273

267274
def remote_fetchers
268275
@remote_fetchers ||= remotes.to_h do |uri|
269-
remote = Source::Rubygems::Remote.new(uri)
276+
remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri))
270277
[remote, Bundler::Fetcher.new(remote)]
271278
end.freeze
272279
end

bundler/lib/bundler/source/rubygems/remote.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@ module Bundler
44
class Source
55
class Rubygems
66
class Remote
7-
attr_reader :uri, :anonymized_uri, :original_uri
7+
attr_reader :uri, :anonymized_uri, :original_uri, :cooldown
88

9-
def initialize(uri)
9+
def initialize(uri, cooldown: nil)
1010
orig_uri = uri
1111
uri = Bundler.settings.mirror_for(uri)
1212
@original_uri = orig_uri if orig_uri != uri
1313
fallback_auth = Bundler.settings.credentials_for(uri)
1414

1515
@uri = apply_auth(uri, fallback_auth).freeze
1616
@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
1727
end
1828

1929
MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length

bundler/lib/bundler/source_list.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ def add_plugin_source(source, options = {})
5959
add_source_to_list Plugin.source(source).new(options), @plugin_sources
6060
end
6161

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)
6464
global_rubygems_source
6565
end
6666

spec/bundler/dsl_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,48 @@
367367
end
368368
end
369369

370+
describe "#source with cooldown" do
371+
before do
372+
allow(@rubygems).to receive(:add_remote)
373+
end
374+
375+
it "accepts a non-negative integer" do
376+
expect do
377+
subject.source("https://rubygems.org", cooldown: 7)
378+
end.not_to raise_error
379+
end
380+
381+
it "accepts 0 as an explicit disable" do
382+
expect do
383+
subject.source("https://rubygems.org", cooldown: 0)
384+
end.not_to raise_error
385+
end
386+
387+
it "rejects a string" do
388+
expect do
389+
subject.source("https://rubygems.org", cooldown: "7")
390+
end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
391+
end
392+
393+
it "rejects a float" do
394+
expect do
395+
subject.source("https://rubygems.org", cooldown: 7.5)
396+
end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
397+
end
398+
399+
it "rejects a negative integer" do
400+
expect do
401+
subject.source("https://rubygems.org", cooldown: -7)
402+
end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
403+
end
404+
405+
it "rejects an array" do
406+
expect do
407+
subject.source("https://rubygems.org", cooldown: [7])
408+
end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
409+
end
410+
end
411+
370412
describe "#override" do
371413
it "stores an Override for a gem with a version: operation" do
372414
subject.override("rails", version: ">= 8.0")

spec/bundler/source/rubygems/remote_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,39 @@ def remote(uri)
169169
end
170170
end
171171
end
172+
173+
describe "#cooldown" do
174+
it "is nil by default" do
175+
expect(remote(uri_no_auth).cooldown).to be_nil
176+
end
177+
178+
it "returns the value passed to the constructor" do
179+
r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
180+
expect(r.cooldown).to eq(7)
181+
end
182+
end
183+
184+
describe "#effective_cooldown" do
185+
it "returns the per-remote value when no override is set" do
186+
r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
187+
expect(r.effective_cooldown).to eq(7)
188+
end
189+
190+
it "returns nil when neither override nor per-remote value is set" do
191+
expect(remote(uri_no_auth).effective_cooldown).to be_nil
192+
end
193+
194+
it "settings override per-remote value" do
195+
r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
196+
Bundler.settings.temporary(cooldown: 14) do
197+
expect(r.effective_cooldown).to eq(14)
198+
end
199+
end
200+
201+
it "settings override even when per-remote value is absent" do
202+
Bundler.settings.temporary(cooldown: 14) do
203+
expect(remote(uri_no_auth).effective_cooldown).to eq(14)
204+
end
205+
end
206+
end
172207
end

spec/bundler/source_list_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@
129129
Gem::URI("https://rubygems.org/"),
130130
]
131131
end
132+
133+
it "records the per-remote cooldown when supplied" do
134+
source_list.add_global_rubygems_remote("https://othersource.org", cooldown: 7)
135+
expect(returned_source.cooldown_for(Gem::URI("https://othersource.org/"))).to eq(7)
136+
expect(returned_source.cooldown_for(Gem::URI("https://rubygems.org/"))).to be_nil
137+
end
132138
end
133139

134140
describe "#add_plugin_source" do

0 commit comments

Comments
 (0)