-
Notifications
You must be signed in to change notification settings - Fork 916
/
requirements_updater.rb
283 lines (231 loc) 路 9.83 KB
/
requirements_updater.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# frozen_string_literal: true
require "dependabot/bundler/update_checker"
module Dependabot
module Bundler
class UpdateChecker
class RequirementsUpdater
class UnfixableRequirement < StandardError; end
ALLOWED_UPDATE_STRATEGIES =
%i(bump_versions bump_versions_if_necessary).freeze
def initialize(requirements:, update_strategy:, updated_source:,
latest_version:, latest_resolvable_version:)
@requirements = requirements
@latest_version = Gem::Version.new(latest_version) if latest_version
@updated_source = updated_source
@update_strategy = update_strategy
check_update_strategy
return unless latest_resolvable_version
@latest_resolvable_version =
Gem::Version.new(latest_resolvable_version)
end
def updated_requirements
requirements.map do |req|
if req[:file].match?(/\.gemspec/)
update_gemspec_requirement(req)
else
# If a requirement doesn't come from a gemspec, it must be from
# a Gemfile.
update_gemfile_requirement(req)
end
end
end
private
attr_reader :requirements, :updated_source,
:latest_version, :latest_resolvable_version,
:update_strategy
def check_update_strategy
return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)
raise "Unknown update strategy: #{update_strategy}"
end
def update_gemfile_requirement(req)
req = req.merge(source: updated_source)
return req unless latest_resolvable_version
case update_strategy
when :bump_versions
update_version_requirement(req)
when :bump_versions_if_necessary
update_version_requirement_if_needed(req)
else raise "Unexpected update strategy: #{update_strategy}"
end
end
def update_version_requirement_if_needed(req)
return req if new_version_satisfies?(req)
update_version_requirement(req)
end
def update_version_requirement(req)
requirements =
req[:requirement].split(",").map { |r| Gem::Requirement.new(r) }
new_requirement =
if requirements.any?(&:exact?) then latest_resolvable_version.to_s
elsif requirements.any? { |r| r.to_s.start_with?("~>") }
tw_req = requirements.find { |r| r.to_s.start_with?("~>") }
update_twiddle_version(tw_req, latest_resolvable_version).to_s
else
update_gemfile_range(requirements).map(&:to_s).join(", ")
end
req.merge(requirement: new_requirement)
end
def new_version_satisfies?(req)
original_req = Gem::Requirement.new(req[:requirement].split(","))
original_req.satisfied_by?(latest_resolvable_version)
end
def update_gemfile_range(requirements)
updated_requirements =
requirements.flat_map do |r|
next r if r.satisfied_by?(latest_resolvable_version)
case op = r.requirements.first.first
when "<", "<="
[update_greatest_version(r, latest_resolvable_version)]
when "!="
[]
else
raise "Unexpected operation for unsatisfied Gemfile "\
"requirement: #{op}"
end
end
binding_requirements(updated_requirements)
end
def at_same_precision(new_version, old_version)
release_precision = old_version.to_s.split(".").
take_while { |i| i.match?(/^\d+$/) }.count
prerelease_precision =
old_version.to_s.split(".").count - release_precision
new_release =
new_version.to_s.split(".").first(release_precision)
new_prerelease =
new_version.to_s.split(".").
drop_while { |i| i.match?(/^\d+$/) }.
first([prerelease_precision, 1].max)
[*new_release, *new_prerelease].join(".")
end
# rubocop:disable Metrics/PerceivedComplexity
def update_gemspec_requirement(req)
req = req.merge(source: updated_source) if req.fetch(:source)
return req unless latest_version && latest_resolvable_version
requirements =
req[:requirement].split(",").map { |r| Gem::Requirement.new(r) }
return req if requirements.all? do |r|
requirement_satisfied?(r, req[:groups])
end
updated_requirements =
requirements.flat_map do |r|
next r if requirement_satisfied?(r, req[:groups])
if req[:groups] == ["development"] then bumped_requirements(r)
else widened_requirements(r)
end
end
updated_requirements = binding_requirements(updated_requirements)
req.merge(requirement: updated_requirements.map(&:to_s).join(", "))
rescue UnfixableRequirement
req.merge(requirement: :unfixable)
end
# rubocop:enable Metrics/PerceivedComplexity
def requirement_satisfied?(req, groups)
if groups == ["development"]
req.satisfied_by?(latest_resolvable_version)
else
req.satisfied_by?(latest_version)
end
end
def binding_requirements(requirements)
grouped_by_operator =
requirements.group_by { |r| r.requirements.first.first }
binding_reqs = grouped_by_operator.flat_map do |operator, reqs|
case operator
when "<", "<=" then reqs.min_by { |r| r.requirements.first.last }
when ">", ">=" then reqs.max_by { |r| r.requirements.first.last }
else requirements
end
end.uniq
binding_reqs << Gem::Requirement.new if binding_reqs.empty?
binding_reqs.sort_by { |r| r.requirements.first.last }
end
def widened_requirements(req)
op, version = req.requirements.first
case op
when "=", nil
if version < latest_resolvable_version
[Gem::Requirement.new("#{op} #{latest_resolvable_version}")]
else
req
end
when "<", "<=" then [update_greatest_version(req, latest_version)]
when "~>" then convert_twidle_to_range(req, latest_version)
when "!=" then []
when ">", ">=" then raise UnfixableRequirement
else raise "Unexpected operation for requirement: #{op}"
end
end
def bumped_requirements(req)
op, version = req.requirements.first
case op
when "=", nil
if version < latest_resolvable_version
[Gem::Requirement.new("#{op} #{latest_resolvable_version}")]
else
req
end
when "~>"
[update_twiddle_version(req, latest_resolvable_version)]
when "<", "<=" then [update_greatest_version(req, latest_version)]
when "!=" then []
when ">", ">=" then raise UnfixableRequirement
else raise "Unexpected operation for requirement: #{op}"
end
end
def convert_twidle_to_range(requirement, version_to_be_permitted)
version = requirement.requirements.first.last
version = version.release if version.prerelease?
index_to_update = [version.segments.count - 2, 0].max
ub_segments = version_to_be_permitted.segments
ub_segments << 0 while ub_segments.count <= index_to_update
ub_segments = ub_segments[0..index_to_update]
ub_segments[index_to_update] += 1
lb_segments = version.segments
lb_segments.pop while lb_segments.any? && lb_segments.last.zero?
if lb_segments.none?
return [Gem::Requirement.new("< #{ub_segments.join('.')}")]
end
# Ensure versions have the same length as each other (cosmetic)
length = [lb_segments.count, ub_segments.count].max
lb_segments.fill(0, lb_segments.count...length)
ub_segments.fill(0, ub_segments.count...length)
[
Gem::Requirement.new(">= #{lb_segments.join('.')}"),
Gem::Requirement.new("< #{ub_segments.join('.')}")
]
end
# Updates the version in a "~>" constraint to allow the given version
def update_twiddle_version(requirement, version_to_be_permitted)
old_version = requirement.requirements.first.last
updated_v = at_same_precision(version_to_be_permitted, old_version)
Gem::Requirement.new("~> #{updated_v}")
end
# Updates the version in a "<" or "<=" constraint to allow the given
# version
def update_greatest_version(requirement, version_to_be_permitted)
if version_to_be_permitted.is_a?(String)
version_to_be_permitted = Gem::Version.new(version_to_be_permitted)
end
op, version = requirement.requirements.first
version = version.release if version.prerelease?
index_to_update = [
version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max,
version_to_be_permitted.segments.count - 1
].min
new_segments = version.segments.map.with_index do |_, index|
if index < index_to_update
version_to_be_permitted.segments[index]
elsif index == index_to_update
version_to_be_permitted.segments[index] + 1
elsif index > version_to_be_permitted.segments.count - 1
nil
else 0
end
end.compact
Gem::Requirement.new("#{op} #{new_segments.join('.')}")
end
end
end
end
end