bundler 4.0.10 → 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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/lib/bundler/build_metadata.rb +1 -1
  4. data/lib/bundler/cli/add.rb +3 -0
  5. data/lib/bundler/cli/common.rb +6 -0
  6. data/lib/bundler/cli/config.rb +8 -3
  7. data/lib/bundler/cli/gem.rb +1 -1
  8. data/lib/bundler/cli/install.rb +3 -0
  9. data/lib/bundler/cli/outdated.rb +42 -2
  10. data/lib/bundler/cli/update.rb +2 -0
  11. data/lib/bundler/cli.rb +16 -12
  12. data/lib/bundler/compact_index_client/parser.rb +4 -1
  13. data/lib/bundler/definition.rb +28 -4
  14. data/lib/bundler/dsl.rb +6 -2
  15. data/lib/bundler/endpoint_specification.rb +11 -1
  16. data/lib/bundler/installer/parallel_installer.rb +11 -19
  17. data/lib/bundler/installer.rb +5 -0
  18. data/lib/bundler/lockfile_generator.rb +16 -1
  19. data/lib/bundler/lockfile_parser.rb +23 -1
  20. data/lib/bundler/man/bundle-add.1 +5 -2
  21. data/lib/bundler/man/bundle-add.1.ronn +6 -1
  22. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  23. data/lib/bundler/man/bundle-cache.1 +1 -1
  24. data/lib/bundler/man/bundle-check.1 +1 -1
  25. data/lib/bundler/man/bundle-clean.1 +1 -1
  26. data/lib/bundler/man/bundle-config.1 +121 -157
  27. data/lib/bundler/man/bundle-config.1.ronn +31 -1
  28. data/lib/bundler/man/bundle-console.1 +1 -1
  29. data/lib/bundler/man/bundle-doctor.1 +1 -1
  30. data/lib/bundler/man/bundle-env.1 +1 -1
  31. data/lib/bundler/man/bundle-exec.1 +1 -1
  32. data/lib/bundler/man/bundle-fund.1 +1 -1
  33. data/lib/bundler/man/bundle-gem.1 +1 -1
  34. data/lib/bundler/man/bundle-help.1 +1 -1
  35. data/lib/bundler/man/bundle-info.1 +1 -1
  36. data/lib/bundler/man/bundle-init.1 +1 -1
  37. data/lib/bundler/man/bundle-install.1 +5 -2
  38. data/lib/bundler/man/bundle-install.1.ronn +9 -1
  39. data/lib/bundler/man/bundle-issue.1 +1 -1
  40. data/lib/bundler/man/bundle-licenses.1 +1 -1
  41. data/lib/bundler/man/bundle-list.1 +1 -1
  42. data/lib/bundler/man/bundle-lock.1 +1 -1
  43. data/lib/bundler/man/bundle-open.1 +1 -1
  44. data/lib/bundler/man/bundle-outdated.1 +17 -14
  45. data/lib/bundler/man/bundle-outdated.1.ronn +19 -12
  46. data/lib/bundler/man/bundle-platform.1 +1 -1
  47. data/lib/bundler/man/bundle-plugin.1 +1 -1
  48. data/lib/bundler/man/bundle-pristine.1 +1 -1
  49. data/lib/bundler/man/bundle-remove.1 +1 -1
  50. data/lib/bundler/man/bundle-show.1 +1 -1
  51. data/lib/bundler/man/bundle-update.1 +5 -2
  52. data/lib/bundler/man/bundle-update.1.ronn +8 -0
  53. data/lib/bundler/man/bundle-version.1 +1 -1
  54. data/lib/bundler/man/bundle.1 +1 -1
  55. data/lib/bundler/man/gemfile.5 +1 -1
  56. data/lib/bundler/remote_specification.rb +1 -1
  57. data/lib/bundler/resolver.rb +58 -1
  58. data/lib/bundler/rubygems_ext.rb +22 -0
  59. data/lib/bundler/rubygems_gem_installer.rb +1 -1
  60. data/lib/bundler/settings.rb +1 -0
  61. data/lib/bundler/source/git/git_proxy.rb +7 -2
  62. data/lib/bundler/source/metadata.rb +4 -0
  63. data/lib/bundler/source/path.rb +3 -2
  64. data/lib/bundler/source/rubygems/remote.rb +12 -2
  65. data/lib/bundler/source/rubygems.rb +53 -5
  66. data/lib/bundler/source_list.rb +10 -2
  67. data/lib/bundler/templates/newgem/newgem.gemspec.tt +7 -1
  68. data/lib/bundler/version.rb +1 -1
  69. data/lib/bundler.rb +10 -0
  70. metadata +2 -2
@@ -1,10 +1,10 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-OUTDATED" "1" "March 2026" ""
3
+ .TH "BUNDLE\-OUTDATED" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-outdated\fR \- List installed gems with newer versions available
6
6
  .SH "SYNOPSIS"
7
- \fBbundle outdated\fR [GEM] [\-\-local] [\-\-pre] [\-\-source] [\-\-filter\-strict | \-\-strict] [\-\-update\-strict] [\-\-parseable | \-\-porcelain] [\-\-group=GROUP] [\-\-groups] [\-\-patch|\-\-minor|\-\-major] [\-\-filter\-major] [\-\-filter\-minor] [\-\-filter\-patch] [\-\-only\-explicit]
7
+ \fBbundle outdated\fR [GEM] [\-\-local] [\-\-pre] [\-\-source] [\-\-filter\-strict | \-\-strict] [\-\-update\-strict] [\-\-parseable | \-\-porcelain] [\-\-group=GROUP] [\-\-groups] [\-\-patch|\-\-minor|\-\-major] [\-\-filter\-major] [\-\-filter\-minor] [\-\-filter\-patch] [\-\-only\-explicit] [\-\-cooldown=NUMBER]
8
8
  .SH "DESCRIPTION"
9
9
  Outdated lists the names and versions of gems that have a newer version available in the given source\. Calling outdated with [GEM [GEM]] will only check for newer versions of the given gems\. Prerelease gems are ignored by default\. If your gems are up to date, Bundler will exit with a status of 0\. Otherwise, it will exit 1\.
10
10
  .SH "OPTIONS"
@@ -53,6 +53,9 @@ Only list patch newer versions\.
53
53
  .TP
54
54
  \fB\-\-only\-explicit\fR
55
55
  Only list gems specified in your Gemfile, not their dependencies\.
56
+ .TP
57
+ \fB\-\-cooldown=<number>\fR
58
+ Annotate (rather than hide) versions that are still inside the cooldown window of \fInumber\fR days\. The prose output appends "in cooldown for Nd more days" and the table form adds "(cooldown Nd)" to the Latest column\. See \fBcooldown\fR in bundle\-config(1)\.
56
59
  .SH "PATCH LEVEL OPTIONS"
57
60
  See bundle update(1) \fIbundle\-update\.1\.html\fR for details\.
58
61
  .SH "FILTERING OUTPUT"
@@ -61,42 +64,42 @@ The 3 filtering options do not affect the resolution of versions, merely what ve
61
64
  If the regular output shows the following:
62
65
  .IP "" 4
63
66
  .nf
64
- * Gem Current Latest Requested Groups
65
- * faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test
66
- * hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default
67
- * headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test
67
+ * Gem Current Latest Requested Groups Release Date
68
+ * faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05
69
+ * hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default 2023\-11\-10
70
+ * headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test 2022\-08\-19
68
71
  .fi
69
72
  .IP "" 0
70
73
  .P
71
74
  \fB\-\-filter\-major\fR would only show:
72
75
  .IP "" 4
73
76
  .nf
74
- * Gem Current Latest Requested Groups
75
- * hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default
77
+ * Gem Current Latest Requested Groups Release Date
78
+ * hashie 1\.2\.0 3\.4\.6 = 1\.2\.0 default 2023\-11\-10
76
79
  .fi
77
80
  .IP "" 0
78
81
  .P
79
82
  \fB\-\-filter\-minor\fR would only show:
80
83
  .IP "" 4
81
84
  .nf
82
- * Gem Current Latest Requested Groups
83
- * headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test
85
+ * Gem Current Latest Requested Groups Release Date
86
+ * headless 2\.2\.3 2\.3\.1 = 2\.2\.3 test 2022\-08\-19
84
87
  .fi
85
88
  .IP "" 0
86
89
  .P
87
90
  \fB\-\-filter\-patch\fR would only show:
88
91
  .IP "" 4
89
92
  .nf
90
- * Gem Current Latest Requested Groups
91
- * faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test
93
+ * Gem Current Latest Requested Groups Release Date
94
+ * faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05
92
95
  .fi
93
96
  .IP "" 0
94
97
  .P
95
98
  Filter options can be combined\. \fB\-\-filter\-minor\fR and \fB\-\-filter\-patch\fR would show:
96
99
  .IP "" 4
97
100
  .nf
98
- * Gem Current Latest Requested Groups
99
- * faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test
101
+ * Gem Current Latest Requested Groups Release Date
102
+ * faker 1\.6\.5 1\.6\.6 ~> 1\.4 development, test 2024\-02\-05
100
103
  .fi
101
104
  .IP "" 0
102
105
  .P
@@ -16,6 +16,7 @@ bundle-outdated(1) -- List installed gems with newer versions available
16
16
  [--filter-minor]
17
17
  [--filter-patch]
18
18
  [--only-explicit]
19
+ [--cooldown=NUMBER]
19
20
 
20
21
  ## DESCRIPTION
21
22
 
@@ -71,6 +72,12 @@ are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1.
71
72
  * `--only-explicit`:
72
73
  Only list gems specified in your Gemfile, not their dependencies.
73
74
 
75
+ * `--cooldown=<number>`:
76
+ Annotate (rather than hide) versions that are still inside the
77
+ cooldown window of <number> days. The prose output appends "in
78
+ cooldown for Nd more days" and the table form adds "(cooldown Nd)" to
79
+ the Latest column. See `cooldown` in bundle-config(1).
80
+
74
81
  ## PATCH LEVEL OPTIONS
75
82
 
76
83
  See [bundle update(1)](bundle-update.1.html) for details.
@@ -82,29 +89,29 @@ in the output.
82
89
 
83
90
  If the regular output shows the following:
84
91
 
85
- * Gem Current Latest Requested Groups
86
- * faker 1.6.5 1.6.6 ~> 1.4 development, test
87
- * hashie 1.2.0 3.4.6 = 1.2.0 default
88
- * headless 2.2.3 2.3.1 = 2.2.3 test
92
+ * Gem Current Latest Requested Groups Release Date
93
+ * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05
94
+ * hashie 1.2.0 3.4.6 = 1.2.0 default 2023-11-10
95
+ * headless 2.2.3 2.3.1 = 2.2.3 test 2022-08-19
89
96
 
90
97
  `--filter-major` would only show:
91
98
 
92
- * Gem Current Latest Requested Groups
93
- * hashie 1.2.0 3.4.6 = 1.2.0 default
99
+ * Gem Current Latest Requested Groups Release Date
100
+ * hashie 1.2.0 3.4.6 = 1.2.0 default 2023-11-10
94
101
 
95
102
  `--filter-minor` would only show:
96
103
 
97
- * Gem Current Latest Requested Groups
98
- * headless 2.2.3 2.3.1 = 2.2.3 test
104
+ * Gem Current Latest Requested Groups Release Date
105
+ * headless 2.2.3 2.3.1 = 2.2.3 test 2022-08-19
99
106
 
100
107
  `--filter-patch` would only show:
101
108
 
102
- * Gem Current Latest Requested Groups
103
- * faker 1.6.5 1.6.6 ~> 1.4 development, test
109
+ * Gem Current Latest Requested Groups Release Date
110
+ * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05
104
111
 
105
112
  Filter options can be combined. `--filter-minor` and `--filter-patch` would show:
106
113
 
107
- * Gem Current Latest Requested Groups
108
- * faker 1.6.5 1.6.6 ~> 1.4 development, test
114
+ * Gem Current Latest Requested Groups Release Date
115
+ * faker 1.6.5 1.6.6 ~> 1.4 development, test 2024-02-05
109
116
 
110
117
  Combining all three `filter` options would be the same result as providing none of them.
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-PLATFORM" "1" "March 2026" ""
3
+ .TH "BUNDLE\-PLATFORM" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-platform\fR \- Displays platform compatibility information
6
6
  .SH "SYNOPSIS"
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-PLUGIN" "1" "March 2026" ""
3
+ .TH "BUNDLE\-PLUGIN" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-plugin\fR \- Manage Bundler plugins
6
6
  .SH "SYNOPSIS"
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-PRISTINE" "1" "March 2026" ""
3
+ .TH "BUNDLE\-PRISTINE" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-pristine\fR \- Restores installed gems to their pristine condition
6
6
  .SH "SYNOPSIS"
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-REMOVE" "1" "March 2026" ""
3
+ .TH "BUNDLE\-REMOVE" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-remove\fR \- Removes gems from the Gemfile
6
6
  .SH "SYNOPSIS"
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-SHOW" "1" "March 2026" ""
3
+ .TH "BUNDLE\-SHOW" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem
6
6
  .SH "SYNOPSIS"
@@ -1,10 +1,10 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-UPDATE" "1" "March 2026" ""
3
+ .TH "BUNDLE\-UPDATE" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-update\fR \- Update your gems to the latest available versions
6
6
  .SH "SYNOPSIS"
7
- \fBbundle update\fR \fI*gems\fR [\-\-all] [\-\-group=NAME] [\-\-source=NAME] [\-\-local] [\-\-ruby] [\-\-bundler[=VERSION]] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative]
7
+ \fBbundle update\fR \fI*gems\fR [\-\-all] [\-\-group=NAME] [\-\-source=NAME] [\-\-local] [\-\-ruby] [\-\-bundler[=VERSION]] [\-\-cooldown=NUMBER] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative]
8
8
  .SH "DESCRIPTION"
9
9
  Update the gems specified (all gems, if \fB\-\-all\fR flag is used), ignoring the previously installed gems specified in the \fBGemfile\.lock\fR\. In general, you should use bundle install(1) \fIbundle\-install\.1\.html\fR to install the same exact gems and versions across machines\.
10
10
  .P
@@ -64,6 +64,9 @@ Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR
64
64
  .TP
65
65
  \fB\-\-conservative\fR
66
66
  Use bundle install conservative update behavior and do not allow indirect dependencies to be updated\.
67
+ .TP
68
+ \fB\-\-cooldown=<number>\fR
69
+ Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. Combine with \fB\-\-conservative\fR to minimize transitive churn when bypassing cooldown for an urgent update\. See \fBcooldown\fR in bundle\-config(1)\.
67
70
  .SH "UPDATING ALL GEMS"
68
71
  If you run \fBbundle update \-\-all\fR, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\.
69
72
  .P
@@ -9,6 +9,7 @@ bundle-update(1) -- Update your gems to the latest available versions
9
9
  [--local]
10
10
  [--ruby]
11
11
  [--bundler[=VERSION]]
12
+ [--cooldown=NUMBER]
12
13
  [--force]
13
14
  [--full-index]
14
15
  [--gemfile=GEMFILE]
@@ -91,6 +92,13 @@ gem.
91
92
  * `--conservative`:
92
93
  Use bundle install conservative update behavior and do not allow indirect dependencies to be updated.
93
94
 
95
+ * `--cooldown=<number>`:
96
+ Only consider gem versions published at least <number> days ago when
97
+ resolving. Pass `0` to disable cooldown for this run, overriding any
98
+ per-source or global configuration. Combine with `--conservative` to
99
+ minimize transitive churn when bypassing cooldown for an urgent
100
+ update. See `cooldown` in bundle-config(1).
101
+
94
102
  ## UPDATING ALL GEMS
95
103
 
96
104
  If you run `bundle update --all`, bundler will ignore
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE\-VERSION" "1" "March 2026" ""
3
+ .TH "BUNDLE\-VERSION" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\-version\fR \- Prints Bundler version information
6
6
  .SH "SYNOPSIS"
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "BUNDLE" "1" "March 2026" ""
3
+ .TH "BUNDLE" "1" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBbundle\fR \- Ruby Dependency Management
6
6
  .SH "SYNOPSIS"
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.10.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.10.1
3
- .TH "GEMFILE" "5" "March 2026" ""
3
+ .TH "GEMFILE" "5" "April 2026" ""
4
4
  .SH "NAME"
5
5
  \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs
6
6
  .SH "SYNOPSIS"
@@ -12,7 +12,7 @@ module Bundler
12
12
 
13
13
  attr_reader :name, :version, :platform
14
14
  attr_writer :dependencies
15
- attr_accessor :source, :remote, :locked_platform
15
+ attr_accessor :source, :remote, :locked_platform, :created_at
16
16
 
17
17
  def initialize(name, version, platform, spec_fetcher)
18
18
  @name = name
@@ -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
@@ -58,6 +58,10 @@ module Bundler
58
58
  def version_message(spec)
59
59
  "#{spec.name} #{spec.version}"
60
60
  end
61
+
62
+ def checksum_store
63
+ @checksum_store ||= Checksum::Store.new
64
+ end
61
65
  end
62
66
  end
63
67
  end
@@ -220,10 +220,11 @@ module Bundler
220
220
  # Some gem authors put absolute paths in their gemspec
221
221
  # and we have to save them from themselves
222
222
  spec.files = spec.files.filter_map do |path|
223
- next path unless /\A#{Pathname::SEPARATOR_PAT}/o.match?(path)
223
+ pathname = Pathname.new(path)
224
+ next path unless pathname.absolute?
224
225
  next if File.directory?(path)
225
226
  begin
226
- Pathname.new(path).relative_path_from(gem_dir).to_s
227
+ pathname.relative_path_from(gem_dir).to_s
227
228
  rescue ArgumentError
228
229
  path
229
230
  end
@@ -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
@@ -508,7 +556,7 @@ module Bundler
508
556
 
509
557
  # We are using a mutex to reaed and write from/to the hash.
510
558
  # The reason this double synchronization was added is for performance
511
- # and lock the mutex for the shortest possible amount of time. Otherwise,
559
+ # and to lock the mutex for the shortest possible amount of time. Otherwise,
512
560
  # all threads are fighting over this mutex and when it gets acquired it gets locked
513
561
  # until a threads finishes downloading a gem, leaving the other threads waiting
514
562
  # doing nothing.
@@ -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
@@ -22,6 +22,12 @@ Gem::Specification.new do |spec|
22
22
  spec.metadata["changelog_uri"] = "<%= config[:changelog_uri] %>"
23
23
  <%- end -%>
24
24
 
25
+ # Uncomment the line below to require MFA for gem pushes.
26
+ # This helps protect your gem from supply chain attacks by ensuring
27
+ # no one can publish a new version without multi-factor authentication.
28
+ # See: https://guides.rubygems.org/mfa-requirement-opt-in/
29
+ # spec.metadata["rubygems_mfa_required"] = "true"
30
+
25
31
  # Specify which files should be added to the gem when it is released.
26
32
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
33
  gemspec = File.basename(__FILE__)
@@ -48,5 +54,5 @@ Gem::Specification.new do |spec|
48
54
  <%- end -%>
49
55
 
50
56
  # For more information and examples about making a new gem, check out our
51
- # guide at: https://bundler.io/guides/creating_gem.html
57
+ # guide at: https://guides.rubygems.org/make-your-own-gem/
52
58
  end