dependabot-bundler 0.95.6 → 0.95.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/bundler/file_fetcher/child_gemfile_finder.rb +68 -0
  3. data/lib/dependabot/bundler/file_fetcher/gemspec_finder.rb +96 -0
  4. data/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb +112 -0
  5. data/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb +65 -0
  6. data/lib/dependabot/bundler/file_fetcher.rb +216 -0
  7. data/lib/dependabot/bundler/file_parser/file_preparer.rb +84 -0
  8. data/lib/dependabot/bundler/file_parser/gemfile_checker.rb +46 -0
  9. data/lib/dependabot/bundler/file_parser.rb +297 -0
  10. data/lib/dependabot/bundler/file_updater/gemfile_updater.rb +114 -0
  11. data/lib/dependabot/bundler/file_updater/gemspec_dependency_name_finder.rb +50 -0
  12. data/lib/dependabot/bundler/file_updater/gemspec_sanitizer.rb +298 -0
  13. data/lib/dependabot/bundler/file_updater/gemspec_updater.rb +62 -0
  14. data/lib/dependabot/bundler/file_updater/git_pin_replacer.rb +78 -0
  15. data/lib/dependabot/bundler/file_updater/git_source_remover.rb +100 -0
  16. data/lib/dependabot/bundler/file_updater/lockfile_updater.rb +387 -0
  17. data/lib/dependabot/bundler/file_updater/requirement_replacer.rb +221 -0
  18. data/lib/dependabot/bundler/file_updater.rb +125 -0
  19. data/lib/dependabot/bundler/metadata_finder.rb +204 -0
  20. data/lib/dependabot/bundler/requirement.rb +29 -0
  21. data/lib/dependabot/bundler/update_checker/file_preparer.rb +279 -0
  22. data/lib/dependabot/bundler/update_checker/force_updater.rb +259 -0
  23. data/lib/dependabot/bundler/update_checker/latest_version_finder.rb +165 -0
  24. data/lib/dependabot/bundler/update_checker/requirements_updater.rb +281 -0
  25. data/lib/dependabot/bundler/update_checker/ruby_requirement_setter.rb +113 -0
  26. data/lib/dependabot/bundler/update_checker/shared_bundler_helpers.rb +244 -0
  27. data/lib/dependabot/bundler/update_checker/version_resolver.rb +272 -0
  28. data/lib/dependabot/bundler/update_checker.rb +334 -0
  29. data/lib/dependabot/bundler/version.rb +13 -0
  30. data/lib/dependabot/bundler.rb +27 -0
  31. data/lib/dependabot/monkey_patches/bundler/definition_bundler_version_patch.rb +15 -0
  32. data/lib/dependabot/monkey_patches/bundler/definition_ruby_version_patch.rb +14 -0
  33. data/lib/dependabot/monkey_patches/bundler/git_source_patch.rb +27 -0
  34. metadata +37 -5
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/monkey_patches/bundler/definition_ruby_version_patch"
4
+ require "dependabot/monkey_patches/bundler/definition_bundler_version_patch"
5
+ require "dependabot/monkey_patches/bundler/git_source_patch"
6
+
7
+ require "dependabot/bundler/update_checker"
8
+ require "dependabot/bundler/update_checker/requirements_updater"
9
+ require "dependabot/bundler/file_updater/lockfile_updater"
10
+ require "dependabot/bundler/file_parser"
11
+ require "dependabot/shared_helpers"
12
+ require "dependabot/errors"
13
+
14
+ module Dependabot
15
+ module Bundler
16
+ class UpdateChecker
17
+ class ForceUpdater
18
+ def initialize(dependency:, dependency_files:, credentials:,
19
+ target_version:, requirements_update_strategy:)
20
+ @dependency = dependency
21
+ @dependency_files = dependency_files
22
+ @credentials = credentials
23
+ @target_version = target_version
24
+ @requirements_update_strategy = requirements_update_strategy
25
+ end
26
+
27
+ def updated_dependencies
28
+ @updated_dependencies ||= force_update
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :dependency, :dependency_files, :credentials,
34
+ :target_version, :requirements_update_strategy
35
+
36
+ def force_update
37
+ in_a_temporary_bundler_context do
38
+ other_updates = []
39
+
40
+ begin
41
+ definition = build_definition(other_updates: other_updates)
42
+ definition.resolve_remotely!
43
+ specs = definition.resolve
44
+ dependencies_from([dependency] + other_updates, specs)
45
+ rescue ::Bundler::VersionConflict => error
46
+ # TODO: Not sure this won't unlock way too many things...
47
+ new_dependencies_to_unlock =
48
+ new_dependencies_to_unlock_from(
49
+ error: error,
50
+ already_unlocked: other_updates
51
+ )
52
+
53
+ raise if new_dependencies_to_unlock.none?
54
+
55
+ other_updates += new_dependencies_to_unlock
56
+ retry
57
+ end
58
+ end
59
+ rescue SharedHelpers::ChildProcessFailed => error
60
+ raise_unresolvable_error(error)
61
+ end
62
+
63
+ #########################
64
+ # Bundler context setup #
65
+ #########################
66
+
67
+ def in_a_temporary_bundler_context
68
+ SharedHelpers.in_a_temporary_directory do
69
+ write_temporary_dependency_files
70
+
71
+ SharedHelpers.in_a_forked_process do
72
+ # Remove installed gems from the default Rubygems index
73
+ ::Gem::Specification.all = []
74
+
75
+ # Set auth details
76
+ relevant_credentials.each do |cred|
77
+ token = cred["token"] ||
78
+ "#{cred['username']}:#{cred['password']}"
79
+
80
+ ::Bundler.settings.set_command_option(
81
+ cred.fetch("host"),
82
+ token.gsub("@", "%40F").gsub("?", "%3F")
83
+ )
84
+ end
85
+
86
+ # Only allow upgrades. Othewise it's unlikely that this
87
+ # resolution will be found by the FileUpdater
88
+ ::Bundler.settings.set_command_option(
89
+ "only_update_to_newer_versions",
90
+ true
91
+ )
92
+
93
+ yield
94
+ end
95
+ end
96
+ end
97
+
98
+ def new_dependencies_to_unlock_from(error:, already_unlocked:)
99
+ potentials_deps =
100
+ error.cause.conflicts.values.
101
+ flat_map(&:requirement_trees).
102
+ reject do |tree|
103
+ next true unless tree.last.requirement.specific?
104
+ next false unless tree.last.name == dependency.name
105
+
106
+ tree.last.requirement.satisfied_by?(
107
+ Gem::Version.new(target_version)
108
+ )
109
+ end.map(&:first)
110
+
111
+ potentials_deps.
112
+ reject { |dep| already_unlocked.map(&:name).include?(dep.name) }.
113
+ reject { |dep| [dependency.name, "ruby\0"].include?(dep.name) }.
114
+ uniq
115
+ end
116
+
117
+ def raise_unresolvable_error(error)
118
+ msg = error.error_class + " with message: " + error.error_message
119
+ raise Dependabot::DependencyFileNotResolvable, msg
120
+ end
121
+
122
+ def build_definition(other_updates:)
123
+ gems_to_unlock = other_updates.map(&:name) + [dependency.name]
124
+ definition = ::Bundler::Definition.build(
125
+ gemfile.name,
126
+ lockfile&.name,
127
+ gems: gems_to_unlock + subdependencies,
128
+ lock_shared_dependencies: true
129
+ )
130
+
131
+ # Remove the Gemfile / gemspec requirements on the gems we're
132
+ # unlocking (i.e., completely unlock them)
133
+ gems_to_unlock.each do |gem_name|
134
+ unlock_gem(definition: definition, gem_name: gem_name)
135
+ end
136
+
137
+ # Set the requirement for the gem we're forcing an update of
138
+ new_req = Gem::Requirement.create("= #{target_version}")
139
+ definition.dependencies.
140
+ find { |d| d.name == dependency.name }.
141
+ instance_variable_set(:@requirement, new_req)
142
+
143
+ definition
144
+ end
145
+
146
+ def subdependencies
147
+ # If there's no lockfile we don't need to worry about
148
+ # subdependencies
149
+ return [] unless lockfile
150
+
151
+ all_deps = ::Bundler::LockfileParser.new(sanitized_lockfile_body).
152
+ specs.map(&:name).map(&:to_s)
153
+ top_level = ::Bundler::Definition.
154
+ build(gemfile.name, lockfile.name, {}).
155
+ dependencies.map(&:name).map(&:to_s)
156
+
157
+ all_deps - top_level
158
+ end
159
+
160
+ def unlock_gem(definition:, gem_name:)
161
+ dep = definition.dependencies.find { |d| d.name == gem_name }
162
+ version = definition.locked_gems.specs.
163
+ find { |d| d.name == gem_name }.version
164
+
165
+ dep&.instance_variable_set(
166
+ :@requirement,
167
+ Gem::Requirement.create(">= #{version}")
168
+ )
169
+ end
170
+
171
+ def original_dependencies
172
+ @original_dependencies ||=
173
+ FileParser.new(
174
+ dependency_files: dependency_files,
175
+ credentials: credentials,
176
+ source: nil
177
+ ).parse
178
+ end
179
+
180
+ def dependencies_from(updated_deps, specs)
181
+ # You might think we'd want to remove dependencies whose version
182
+ # hadn't changed from this array. We don't. We still need to unlock
183
+ # them to get Bundler to resolve, because unlocking them is what
184
+ # updates their subdependencies.
185
+ #
186
+ # This is kind of a bug in Bundler, and we should try to fix it,
187
+ # but resolving it won't necessarily be easy.
188
+ updated_deps.map do |dep|
189
+ original_dep =
190
+ original_dependencies.find { |d| d.name == dep.name }
191
+ spec = specs.find { |d| d.name == dep.name }
192
+
193
+ next if spec.version.to_s == original_dep.version
194
+
195
+ build_dependency(original_dep, spec)
196
+ end.compact
197
+ end
198
+
199
+ def build_dependency(original_dep, updated_spec)
200
+ Dependency.new(
201
+ name: updated_spec.name,
202
+ version: updated_spec.version.to_s,
203
+ requirements:
204
+ RequirementsUpdater.new(
205
+ requirements: original_dep.requirements,
206
+ update_strategy: requirements_update_strategy,
207
+ updated_source: source_for(original_dep),
208
+ latest_version: updated_spec.version.to_s,
209
+ latest_resolvable_version: updated_spec.version.to_s
210
+ ).updated_requirements,
211
+ previous_version: original_dep.version,
212
+ previous_requirements: original_dep.requirements,
213
+ package_manager: original_dep.package_manager
214
+ )
215
+ end
216
+
217
+ def source_for(dependency)
218
+ dependency.requirements.
219
+ find { |r| r.fetch(:source) }&.
220
+ fetch(:source)
221
+ end
222
+
223
+ def gemfile
224
+ dependency_files.find { |f| f.name == "Gemfile" } ||
225
+ dependency_files.find { |f| f.name == "gems.rb" }
226
+ end
227
+
228
+ def lockfile
229
+ dependency_files.find { |f| f.name == "Gemfile.lock" } ||
230
+ dependency_files.find { |f| f.name == "gems.locked" }
231
+ end
232
+
233
+ def sanitized_lockfile_body
234
+ re = FileUpdater::LockfileUpdater::LOCKFILE_ENDING
235
+ lockfile.content.gsub(re, "")
236
+ end
237
+
238
+ def write_temporary_dependency_files
239
+ dependency_files.each do |file|
240
+ path = file.name
241
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
242
+ File.write(path, file.content)
243
+ end
244
+
245
+ File.write(lockfile.name, sanitized_lockfile_body) if lockfile
246
+ end
247
+
248
+ def relevant_credentials
249
+ credentials.select do |cred|
250
+ next true if cred["type"] == "git_source"
251
+ next true if cred["type"] == "rubygems_server"
252
+
253
+ false
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/monkey_patches/bundler/definition_ruby_version_patch"
4
+ require "dependabot/monkey_patches/bundler/definition_bundler_version_patch"
5
+ require "dependabot/monkey_patches/bundler/git_source_patch"
6
+
7
+ require "excon"
8
+
9
+ require "dependabot/bundler/update_checker"
10
+ require "dependabot/bundler/requirement"
11
+ require "dependabot/shared_helpers"
12
+ require "dependabot/errors"
13
+
14
+ module Dependabot
15
+ module Bundler
16
+ class UpdateChecker
17
+ class LatestVersionFinder
18
+ require_relative "shared_bundler_helpers"
19
+ include SharedBundlerHelpers
20
+
21
+ def initialize(dependency:, dependency_files:, credentials:,
22
+ ignored_versions:)
23
+ @dependency = dependency
24
+ @dependency_files = dependency_files
25
+ @credentials = credentials
26
+ @ignored_versions = ignored_versions
27
+ end
28
+
29
+ def latest_version_details
30
+ @latest_version_details ||= fetch_latest_version_details
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :dependency, :dependency_files, :credentials,
36
+ :ignored_versions
37
+
38
+ def fetch_latest_version_details
39
+ return latest_rubygems_version_details if dependency.name == "bundler"
40
+
41
+ case dependency_source
42
+ when NilClass then latest_rubygems_version_details
43
+ when ::Bundler::Source::Rubygems
44
+ if dependency_source.remotes.none? ||
45
+ dependency_source.remotes.first.to_s == "https://rubygems.org/"
46
+ latest_rubygems_version_details
47
+ else
48
+ latest_private_version_details
49
+ end
50
+ when ::Bundler::Source::Git then latest_git_version_details
51
+ end
52
+ end
53
+
54
+ def latest_rubygems_version_details
55
+ response = Excon.get(
56
+ "https://rubygems.org/api/v1/versions/#{dependency.name}.json",
57
+ idempotent: true,
58
+ **SharedHelpers.excon_defaults
59
+ )
60
+
61
+ relevant_versions =
62
+ JSON.parse(response.body).
63
+ reject do |d|
64
+ version = Gem::Version.new(d["number"])
65
+ next true if version.prerelease? && !wants_prerelease?
66
+ next true if ignore_reqs.any? { |r| r.satisfied_by?(version) }
67
+
68
+ false
69
+ end
70
+
71
+ dep = relevant_versions.max_by { |d| Gem::Version.new(d["number"]) }
72
+ return unless dep
73
+
74
+ {
75
+ version: Gem::Version.new(dep["number"]),
76
+ sha: dep["sha"]
77
+ }
78
+ rescue JSON::ParserError, Excon::Error::Timeout
79
+ nil
80
+ end
81
+
82
+ def latest_private_version_details
83
+ in_a_temporary_bundler_context do
84
+ spec =
85
+ dependency_source.
86
+ fetchers.flat_map do |fetcher|
87
+ fetcher.
88
+ specs_with_retry([dependency.name], dependency_source).
89
+ search_all(dependency.name).
90
+ reject { |s| s.version.prerelease? && !wants_prerelease? }.
91
+ reject do |s|
92
+ ignore_reqs.any? { |r| r.satisfied_by?(s.version) }
93
+ end
94
+ end.
95
+ max_by(&:version)
96
+ spec.nil? ? nil : { version: spec.version }
97
+ end
98
+ end
99
+
100
+ def latest_git_version_details
101
+ dependency_source_details =
102
+ dependency.requirements.map { |r| r.fetch(:source) }.
103
+ uniq.compact.first
104
+
105
+ in_a_temporary_bundler_context do
106
+ SharedHelpers.with_git_configured(credentials: credentials) do
107
+ # Note: we don't set `ref`, as we want to unpin the dependency
108
+ source = ::Bundler::Source::Git.new(
109
+ "uri" => dependency_source_details[:url],
110
+ "branch" => dependency_source_details[:branch],
111
+ "name" => dependency.name,
112
+ "submodules" => true
113
+ )
114
+
115
+ # Tell Bundler we're fine with fetching the source remotely
116
+ source.instance_variable_set(:@allow_remote, true)
117
+
118
+ spec = source.specs.first
119
+ { version: spec.version, commit_sha: spec.source.revision }
120
+ end
121
+ end
122
+ end
123
+
124
+ def wants_prerelease?
125
+ @wants_prerelease ||=
126
+ begin
127
+ current_version = dependency.version
128
+ if current_version && Gem::Version.correct?(current_version) &&
129
+ Gem::Version.new(current_version).prerelease?
130
+ return true
131
+ end
132
+
133
+ dependency.requirements.any? do |req|
134
+ req[:requirement].match?(/[a-z]/i)
135
+ end
136
+ end
137
+ end
138
+
139
+ def dependency_source
140
+ return nil unless gemfile
141
+
142
+ @dependency_source ||=
143
+ in_a_temporary_bundler_context do
144
+ definition = ::Bundler::Definition.build(gemfile.name, nil, {})
145
+
146
+ specified_source =
147
+ definition.dependencies.
148
+ find { |dep| dep.name == dependency.name }&.source
149
+
150
+ specified_source || definition.send(:sources).default_source
151
+ end
152
+ end
153
+
154
+ def ignore_reqs
155
+ ignored_versions.map { |req| Gem::Requirement.new(req.split(",")) }
156
+ end
157
+
158
+ def gemfile
159
+ dependency_files.find { |f| f.name == "Gemfile" } ||
160
+ dependency_files.find { |f| f.name == "gems.rb" }
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/bundler/update_checker"
4
+
5
+ module Dependabot
6
+ module Bundler
7
+ class UpdateChecker
8
+ class RequirementsUpdater
9
+ class UnfixableRequirement < StandardError; end
10
+
11
+ ALLOWED_UPDATE_STRATEGIES =
12
+ %i(bump_versions bump_versions_if_necessary).freeze
13
+
14
+ def initialize(requirements:, update_strategy:, updated_source:,
15
+ latest_version:, latest_resolvable_version:)
16
+ @requirements = requirements
17
+ @latest_version = Gem::Version.new(latest_version) if latest_version
18
+ @updated_source = updated_source
19
+ @update_strategy = update_strategy
20
+
21
+ check_update_strategy
22
+
23
+ return unless latest_resolvable_version
24
+
25
+ @latest_resolvable_version =
26
+ Gem::Version.new(latest_resolvable_version)
27
+ end
28
+
29
+ def updated_requirements
30
+ requirements.map do |req|
31
+ if req[:file].match?(/\.gemspec/)
32
+ update_gemspec_requirement(req)
33
+ else
34
+ # If a requirement doesn't come from a gemspec, it must be from
35
+ # a Gemfile.
36
+ update_gemfile_requirement(req)
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :requirements, :updated_source,
44
+ :latest_version, :latest_resolvable_version,
45
+ :update_strategy
46
+
47
+ def check_update_strategy
48
+ return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)
49
+
50
+ raise "Unknown update strategy: #{update_strategy}"
51
+ end
52
+
53
+ def update_gemfile_requirement(req)
54
+ req = req.merge(source: updated_source)
55
+ return req unless latest_resolvable_version
56
+
57
+ case update_strategy
58
+ when :bump_versions
59
+ update_version_requirement(req)
60
+ when :bump_versions_if_necessary
61
+ update_version_requirement_if_needed(req)
62
+ else raise "Unexpected update strategy: #{update_strategy}"
63
+ end
64
+ end
65
+
66
+ def update_version_requirement_if_needed(req)
67
+ return req if new_version_satisfies?(req)
68
+
69
+ update_version_requirement(req)
70
+ end
71
+
72
+ def update_version_requirement(req)
73
+ requirements =
74
+ req[:requirement].split(",").map { |r| Gem::Requirement.new(r) }
75
+
76
+ new_requirement =
77
+ if requirements.any?(&:exact?) then latest_resolvable_version.to_s
78
+ elsif requirements.any? { |r| r.to_s.start_with?("~>") }
79
+ tw_req = requirements.find { |r| r.to_s.start_with?("~>") }
80
+ update_twiddle_version(tw_req, latest_resolvable_version).to_s
81
+ else
82
+ update_gemfile_range(requirements).map(&:to_s).join(", ")
83
+ end
84
+
85
+ req.merge(requirement: new_requirement)
86
+ end
87
+
88
+ def new_version_satisfies?(req)
89
+ original_req = Gem::Requirement.new(req[:requirement].split(","))
90
+ original_req.satisfied_by?(latest_resolvable_version)
91
+ end
92
+
93
+ def update_gemfile_range(requirements)
94
+ updated_requirements =
95
+ requirements.flat_map do |r|
96
+ next r if r.satisfied_by?(latest_resolvable_version)
97
+
98
+ case op = r.requirements.first.first
99
+ when "<", "<="
100
+ [update_greatest_version(r, latest_resolvable_version)]
101
+ when "!="
102
+ []
103
+ else
104
+ raise "Unexpected operation for unsatisfied Gemfile "\
105
+ "requirement: #{op}"
106
+ end
107
+ end
108
+
109
+ binding_requirements(updated_requirements)
110
+ end
111
+
112
+ def at_same_precision(new_version, old_version)
113
+ release_precision =
114
+ old_version.to_s.split(".").select { |i| i.match?(/^\d+$/) }.count
115
+ prerelease_precision =
116
+ old_version.to_s.split(".").count - release_precision
117
+
118
+ new_release =
119
+ new_version.to_s.split(".").first(release_precision)
120
+ new_prerelease =
121
+ new_version.to_s.split(".").
122
+ drop_while { |i| i.match?(/^\d+$/) }.
123
+ first([prerelease_precision, 1].max)
124
+
125
+ [*new_release, *new_prerelease].join(".")
126
+ end
127
+
128
+ # rubocop:disable Metrics/PerceivedComplexity
129
+ def update_gemspec_requirement(req)
130
+ return req unless latest_version && latest_resolvable_version
131
+
132
+ requirements =
133
+ req[:requirement].split(",").map { |r| Gem::Requirement.new(r) }
134
+
135
+ return req if requirements.all? do |r|
136
+ requirement_satisfied?(r, req[:groups])
137
+ end
138
+
139
+ updated_requirements =
140
+ requirements.flat_map do |r|
141
+ next r if requirement_satisfied?(r, req[:groups])
142
+
143
+ if req[:groups] == ["development"] then bumped_requirements(r)
144
+ else widened_requirements(r)
145
+ end
146
+ end
147
+
148
+ updated_requirements = binding_requirements(updated_requirements)
149
+ req.merge(requirement: updated_requirements.map(&:to_s).join(", "))
150
+ rescue UnfixableRequirement
151
+ req.merge(requirement: :unfixable)
152
+ end
153
+ # rubocop:enable Metrics/PerceivedComplexity
154
+
155
+ def requirement_satisfied?(req, groups)
156
+ if groups == ["development"]
157
+ req.satisfied_by?(latest_resolvable_version)
158
+ else
159
+ req.satisfied_by?(latest_version)
160
+ end
161
+ end
162
+
163
+ def binding_requirements(requirements)
164
+ grouped_by_operator =
165
+ requirements.group_by { |r| r.requirements.first.first }
166
+
167
+ binding_reqs = grouped_by_operator.flat_map do |operator, reqs|
168
+ case operator
169
+ when "<", "<=" then reqs.min_by { |r| r.requirements.first.last }
170
+ when ">", ">=" then reqs.max_by { |r| r.requirements.first.last }
171
+ else requirements
172
+ end
173
+ end.uniq
174
+
175
+ binding_reqs << Gem::Requirement.new if binding_reqs.empty?
176
+ binding_reqs.sort_by { |r| r.requirements.first.last }
177
+ end
178
+
179
+ def widened_requirements(req)
180
+ op, version = req.requirements.first
181
+
182
+ case op
183
+ when "=", nil
184
+ if version < latest_resolvable_version
185
+ [Gem::Requirement.new("#{op} #{latest_resolvable_version}")]
186
+ else
187
+ req
188
+ end
189
+ when "<", "<=" then [update_greatest_version(req, latest_version)]
190
+ when "~>" then convert_twidle_to_range(req, latest_version)
191
+ when "!=" then []
192
+ when ">", ">=" then raise UnfixableRequirement
193
+ else raise "Unexpected operation for requirement: #{op}"
194
+ end
195
+ end
196
+
197
+ def bumped_requirements(req)
198
+ op, version = req.requirements.first
199
+
200
+ case op
201
+ when "=", nil
202
+ if version < latest_resolvable_version
203
+ [Gem::Requirement.new("#{op} #{latest_resolvable_version}")]
204
+ else
205
+ req
206
+ end
207
+ when "~>"
208
+ [update_twiddle_version(req, latest_resolvable_version)]
209
+ when "<", "<=" then [update_greatest_version(req, latest_version)]
210
+ when "!=" then []
211
+ when ">", ">=" then raise UnfixableRequirement
212
+ else raise "Unexpected operation for requirement: #{op}"
213
+ end
214
+ end
215
+
216
+ # rubocop:disable Metrics/AbcSize
217
+ def convert_twidle_to_range(requirement, version_to_be_permitted)
218
+ version = requirement.requirements.first.last
219
+ version = version.release if version.prerelease?
220
+
221
+ index_to_update = [version.segments.count - 2, 0].max
222
+
223
+ ub_segments = version_to_be_permitted.segments
224
+ ub_segments << 0 while ub_segments.count <= index_to_update
225
+ ub_segments = ub_segments[0..index_to_update]
226
+ ub_segments[index_to_update] += 1
227
+
228
+ lb_segments = version.segments
229
+ lb_segments.pop while lb_segments.any? && lb_segments.last.zero?
230
+
231
+ if lb_segments.none?
232
+ return [Gem::Requirement.new("< #{ub_segments.join('.')}")]
233
+ end
234
+
235
+ # Ensure versions have the same length as each other (cosmetic)
236
+ length = [lb_segments.count, ub_segments.count].max
237
+ lb_segments.fill(0, lb_segments.count...length)
238
+ ub_segments.fill(0, ub_segments.count...length)
239
+
240
+ [
241
+ Gem::Requirement.new(">= #{lb_segments.join('.')}"),
242
+ Gem::Requirement.new("< #{ub_segments.join('.')}")
243
+ ]
244
+ end
245
+ # rubocop:enable Metrics/AbcSize
246
+
247
+ # Updates the version in a "~>" constraint to allow the given version
248
+ def update_twiddle_version(requirement, version_to_be_permitted)
249
+ old_version = requirement.requirements.first.last
250
+ updated_v = at_same_precision(version_to_be_permitted, old_version)
251
+ Gem::Requirement.new("~> #{updated_v}")
252
+ end
253
+
254
+ # Updates the version in a "<" or "<=" constraint to allow the given
255
+ # version
256
+ def update_greatest_version(requirement, version_to_be_permitted)
257
+ if version_to_be_permitted.is_a?(String)
258
+ version_to_be_permitted =
259
+ Gem::Version.new(version_to_be_permitted)
260
+ end
261
+ op, version = requirement.requirements.first
262
+ version = version.release if version.prerelease?
263
+
264
+ index_to_update =
265
+ version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max
266
+
267
+ new_segments = version.segments.map.with_index do |_, index|
268
+ if index < index_to_update
269
+ version_to_be_permitted.segments[index]
270
+ elsif index == index_to_update
271
+ version_to_be_permitted.segments[index] + 1
272
+ else 0
273
+ end
274
+ end
275
+
276
+ Gem::Requirement.new("#{op} #{new_segments.join('.')}")
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end